From 2caa5c81fe42733627c298520b48083a8e7b67ff Mon Sep 17 00:00:00 2001 From: gnezim Date: Sat, 4 Apr 2026 12:20:03 +0300 Subject: [PATCH] feat: add flights map e2e tests (74 tests for map rendering, list, interactions, clustering, geolocation, responsive design, api, and state) --- .../integration/features/flights-map.cy.ts | 662 ++++++++++++++++++ 1 file changed, 662 insertions(+) create mode 100644 ClientApp/cypress/integration/features/flights-map.cy.ts diff --git a/ClientApp/cypress/integration/features/flights-map.cy.ts b/ClientApp/cypress/integration/features/flights-map.cy.ts new file mode 100644 index 00000000..2a905a97 --- /dev/null +++ b/ClientApp/cypress/integration/features/flights-map.cy.ts @@ -0,0 +1,662 @@ +/// + +import { CITIES } from '../../support/fixtures'; + +describe('Flights Map Feature', () => { + // Mock data for destinations + const mockDestinations = { + data: { + routes: [ + { + arrivalCity: { + name: 'Москва', + code: 'MOW', + location: { + lat: 55.7558, + lon: 37.6173, + }, + }, + flightCount: 12, + directFlightCount: 8, + }, + { + arrivalCity: { + name: 'Санкт-Петербург', + code: 'LED', + location: { + lat: 59.8011, + lon: 30.2642, + }, + }, + flightCount: 5, + directFlightCount: 3, + }, + { + arrivalCity: { + name: 'Анапа', + code: 'AAQ', + location: { + lat: 44.8972, + lon: 37.3426, + }, + }, + flightCount: 7, + directFlightCount: 5, + }, + { + arrivalCity: { + name: 'Екатеринбург', + code: 'SVX', + location: { + lat: 56.7365, + lon: 60.8025, + }, + }, + flightCount: 3, + directFlightCount: 2, + }, + { + arrivalCity: { + name: 'Новосибирск', + code: 'OVB', + location: { + lat: 55.0077, + lon: 82.9484, + }, + }, + flightCount: 4, + directFlightCount: 1, + }, + ], + }, + }; + + const mockNearbyAirports = { + data: { + airports: [ + { + code: 'SVO', + name: 'Шереметьево', + location: { + lat: 55.9728, + lon: 37.4146, + }, + }, + { + code: 'VKO', + name: 'Внуково', + location: { + lat: 55.5917, + lon: 37.2656, + }, + }, + ], + }, + }; + + beforeEach(() => { + cy.intercept( + 'GET', + '**/api/flights/destinations/**', + mockDestinations + ).as('getDestinations'); + + cy.intercept( + 'GET', + '**/api/flights/nearby/**', + mockNearbyAirports + ).as('getNearby'); + + cy.forbidGeolocation(); + cy.visit('/flights-map'); + cy.wait('@getDestinations'); + }); + + // ====================================== + // MAP RENDERING TESTS (~15 tests) + // ====================================== + describe('Map Rendering', () => { + it('should load map and be interactive', () => { + cy.get('#map').should('be.visible'); + cy.get('.leaflet-container').should('be.visible'); + }); + + it('should display map with correct base tile layer', () => { + cy.get('.leaflet-tile-pane').should('be.visible'); + cy.get('.leaflet-tile').should('have.length.greaterThan', 0); + }); + + it('should render flight destination markers on map', () => { + cy.get('[data-testid="map-marker"]').should('have.length.greaterThan', 0); + }); + + it('should display markers for all destination routes', () => { + const expectedMarkerCount = mockDestinations.data.routes.length; + cy.get('[data-testid="map-marker"]').should('have.length', expectedMarkerCount); + }); + + it('should have correct marker positions based on destination coordinates', () => { + cy.get('[data-testid="map-marker"]').first().should('have.attr', 'data-lat'); + cy.get('[data-testid="map-marker"]').first().should('have.attr', 'data-lon'); + }); + + it('should support map pan functionality', () => { + cy.get('.leaflet-container') + .trigger('mousedown', { x: 400, y: 300 }) + .trigger('mousemove', { x: 300, y: 300 }) + .trigger('mouseup'); + + // Verify map content changed (panned) + cy.get('.leaflet-tile').should('exist'); + }); + + it('should support map zoom in', () => { + cy.get('[data-testid="leaflet-map"]') + .trigger('wheel', { deltaY: -100 }); + + cy.get('[data-testid="leaflet-map"]') + .invoke('getZoom') + .should('be.greaterThan', 5); + }); + + it('should support map zoom out', () => { + cy.get('[data-testid="leaflet-map"]') + .trigger('wheel', { deltaY: 100 }); + + cy.get('[data-testid="leaflet-map"]') + .invoke('getZoom') + .should('be.lessThan', 6); + }); + + it('should respect min and max zoom levels', () => { + cy.get('[data-testid="leaflet-map"]') + .invoke('getMinZoom') + .should('equal', 3); + + cy.get('[data-testid="leaflet-map"]') + .invoke('getMaxZoom') + .should('equal', 6); + }); + + it('should display geolocation button (if available)', () => { + cy.get('[data-testid="geolocation-button"]').should('exist'); + }); + + it('should have geolocation button disabled when geolocation is forbidden', () => { + cy.get('[data-testid="geolocation-button"]').should('be.disabled'); + }); + + it('should render map container with correct CSS classes', () => { + cy.get('[data-testid="flights-map-container"]').should('have.class', 'map-wrapper'); + }); + + it('should display map with proper sizing', () => { + cy.get('[data-testid="flights-map-container"]').should('be.visible'); + cy.get('#map').should('have.css', 'position', 'relative'); + }); + + it('should not show loader after map loads', () => { + cy.get('[data-testid="loader"]').should('not.exist'); + }); + + it('should display destination markers with distinct styling', () => { + cy.get('[data-testid="map-marker"]').each(($marker) => { + cy.wrap($marker).should('have.css', 'opacity', '1'); + }); + }); + }); + + // ====================================== + // DESTINATION LIST TESTS (~15 tests) + // ====================================== + describe('Destination List', () => { + it('should render destination list panel', () => { + cy.get('[data-testid="destination-list"]').should('be.visible'); + }); + + it('should display all destinations in the list', () => { + const expectedCount = mockDestinations.data.routes.length; + cy.get('[data-testid="destination-list-item"]').should('have.length', expectedCount); + }); + + it('should display destination name in list items', () => { + cy.get('[data-testid="destination-list-item"]').first() + .should('contain', mockDestinations.data.routes[0].arrivalCity.name); + }); + + it('should display destination code in list items', () => { + cy.get('[data-testid="destination-list-item"]').first() + .should('contain', mockDestinations.data.routes[0].arrivalCity.code); + }); + + it('should display flight count in list items', () => { + cy.get('[data-testid="destination-list-item"]').first() + .should('contain', mockDestinations.data.routes[0].flightCount); + }); + + it('should display direct flight count in list items', () => { + cy.get('[data-testid="destination-list-item"]').first() + .should('contain', mockDestinations.data.routes[0].directFlightCount); + }); + + it('should render search/filter input for destinations', () => { + cy.get('[data-testid="destination-search-input"]').should('be.visible'); + }); + + it('should filter destination list by city name', () => { + cy.get('[data-testid="destination-search-input"]') + .type('Москва'); + + cy.get('[data-testid="destination-list-item"]').should('have.length', 1); + cy.get('[data-testid="destination-list-item"]').should('contain', 'Москва'); + }); + + it('should filter destination list by city code', () => { + cy.get('[data-testid="destination-search-input"]') + .type('MOW'); + + cy.get('[data-testid="destination-list-item"]').should('have.length', 1); + cy.get('[data-testid="destination-list-item"]').should('contain', 'MOW'); + }); + + it('should show empty state when no destinations match filter', () => { + cy.get('[data-testid="destination-search-input"]') + .type('NONEXISTENT'); + + cy.get('[data-testid="destination-list-empty"]').should('be.visible'); + cy.get('[data-testid="destination-list-item"]').should('have.length', 0); + }); + + it('should clear filter when search input is cleared', () => { + cy.get('[data-testid="destination-search-input"]') + .type('Москва'); + + cy.get('[data-testid="destination-list-item"]').should('have.length', 1); + + cy.get('[data-testid="destination-search-input"]').clear(); + + cy.get('[data-testid="destination-list-item"]').should('have.length', 5); + }); + + it('should have list items with proper styling', () => { + cy.get('[data-testid="destination-list-item"]').first() + .should('have.css', 'cursor', 'pointer'); + }); + + it('should show list item hover effect', () => { + cy.get('[data-testid="destination-list-item"]').first() + .trigger('mouseenter') + .should('have.class', 'hover'); + }); + + it('should render list with scrollable container if needed', () => { + cy.get('[data-testid="destination-list"]').should('exist'); + cy.get('[data-testid="destination-list"]').invoke('attr', 'class') + .should('include', 'scrollable'); + }); + + it('should display destination icons in list items', () => { + cy.get('[data-testid="destination-list-item"]').first() + .find('[data-testid="destination-icon"]').should('exist'); + }); + }); + + // ====================================== + // MAP INTERACTIONS TESTS (~10 tests) + // ====================================== + describe('Map Interactions', () => { + it('should show popup when clicking on marker', () => { + cy.get('[data-testid="map-marker"]').first().click(); + + cy.get('[data-testid="map-popup"]').should('be.visible'); + }); + + it('should display destination name in popup', () => { + cy.get('[data-testid="map-marker"]').first().click(); + + cy.get('[data-testid="map-popup"]') + .should('contain', mockDestinations.data.routes[0].arrivalCity.name); + }); + + it('should display flight count in popup', () => { + cy.get('[data-testid="map-marker"]').first().click(); + + cy.get('[data-testid="map-popup"]') + .should('contain', mockDestinations.data.routes[0].flightCount); + }); + + it('should display link to search flights in popup', () => { + cy.get('[data-testid="map-marker"]').first().click(); + + cy.get('[data-testid="map-popup"]') + .find('[data-testid="popup-search-link"]') + .should('exist'); + }); + + it('should close popup when clicking outside', () => { + cy.get('[data-testid="map-marker"]').first().click(); + + cy.get('[data-testid="map-popup"]').should('be.visible'); + + cy.get('#map').click(100, 100); + + cy.get('[data-testid="map-popup"]').should('not.exist'); + }); + + it('should highlight destination when clicking list item', () => { + cy.get('[data-testid="destination-list-item"]').first().click(); + + cy.get('[data-testid="map-marker"]').first() + .should('have.class', 'highlighted'); + }); + + it('should center map on marker when clicking destination list item', () => { + const targetCity = mockDestinations.data.routes[0].arrivalCity; + const expectedLat = targetCity.location.lat; + const expectedLon = targetCity.location.lon; + + cy.get('[data-testid="destination-list-item"]').first().click(); + + cy.get('[data-testid="leaflet-map"]') + .invoke('getCenter') + .then((center) => { + expect(Math.round(center.lat)).to.equal(Math.round(expectedLat)); + expect(Math.round(center.lng)).to.equal(Math.round(expectedLon)); + }); + }); + + it('should highlight marker when hovering over list item', () => { + cy.get('[data-testid="destination-list-item"]').first() + .trigger('mouseenter'); + + cy.get('[data-testid="map-marker"]').first() + .should('have.class', 'hovered'); + }); + + it('should remove highlight when leaving list item hover', () => { + cy.get('[data-testid="destination-list-item"]').first() + .trigger('mouseenter') + .trigger('mouseleave'); + + cy.get('[data-testid="map-marker"]').first() + .should('not.have.class', 'hovered'); + }); + + it('should allow clicking popup search link to navigate to search', () => { + cy.get('[data-testid="map-marker"]').first().click(); + + cy.get('[data-testid="map-popup"]') + .find('[data-testid="popup-search-link"]') + .should('have.attr', 'href'); + }); + }); + + // ====================================== + // MARKER CLUSTERING TESTS (~5 tests) + // ====================================== + describe('Marker Clustering', () => { + it('should not cluster markers at default zoom level', () => { + cy.get('[data-testid="map-marker"]').should('have.length', 5); + }); + + it('should cluster markers when zooming out below threshold', () => { + // Zoom out significantly + cy.get('[data-testid="leaflet-map"]') + .trigger('wheel', { deltaY: 200 }); + + // Verify zoom is at minimum + cy.get('[data-testid="leaflet-map"]') + .invoke('getZoom') + .should('equal', 3); + }); + + it('should uncluster markers when zooming in', () => { + // First zoom out + cy.get('[data-testid="leaflet-map"]') + .trigger('wheel', { deltaY: 200 }); + + // Then zoom in + cy.get('[data-testid="leaflet-map"]') + .trigger('wheel', { deltaY: -100 }); + + cy.get('[data-testid="map-marker"]').should('have.length.greaterThan', 0); + }); + + it('should display cluster count when markers are grouped', () => { + // Zoom out to trigger clustering + cy.get('[data-testid="leaflet-map"]') + .trigger('wheel', { deltaY: 200 }); + + // Check for cluster elements + cy.get('[data-testid="marker-cluster"]').should('exist'); + }); + + it('should expand cluster on click', () => { + cy.get('[data-testid="leaflet-map"]') + .trigger('wheel', { deltaY: 200 }); + + cy.get('[data-testid="marker-cluster"]').first().click(); + + // Verify map zoomed in + cy.get('[data-testid="leaflet-map"]') + .invoke('getZoom') + .should('be.greaterThan', 3); + }); + }); + + // ====================================== + // GEOLOCATION TESTS (~5 tests) + // ====================================== + describe('Geolocation Feature', () => { + it('should disable geolocation button when permission denied', () => { + cy.get('[data-testid="geolocation-button"]').should('be.disabled'); + }); + + it('should show tooltip on geolocation button', () => { + cy.get('[data-testid="geolocation-button"]') + .should('have.attr', 'title'); + }); + + it('should enable geolocation button with valid coordinates', () => { + cy.mockGeolocation({ latitude: 55.7558, longitude: 37.6173 }); + cy.reload(); + cy.wait('@getDestinations'); + + cy.get('[data-testid="geolocation-button"]').should('not.be.disabled'); + }); + + it('should center map on user location when geolocation is enabled', () => { + cy.mockGeolocation({ latitude: 55.7558, longitude: 37.6173 }); + cy.reload(); + cy.wait('@getDestinations'); + + cy.get('[data-testid="geolocation-button"]').click(); + + cy.get('[data-testid="leaflet-map"]') + .invoke('getCenter') + .then((center) => { + expect(Math.round(center.lat)).to.equal(56); + expect(Math.round(center.lng)).to.equal(38); + }); + }); + + it('should show user location marker on map', () => { + cy.mockGeolocation({ latitude: 55.7558, longitude: 37.6173 }); + cy.reload(); + cy.wait('@getDestinations'); + + cy.get('[data-testid="geolocation-button"]').click(); + + cy.get('[data-testid="user-location-marker"]').should('exist'); + }); + }); + + // ====================================== + // RESPONSIVE DESIGN TESTS (~5 tests) + // ====================================== + describe('Responsive Design', () => { + it('should display map in desktop viewport', () => { + cy.viewport(1280, 720); + cy.get('[data-testid="flights-map-container"]').should('be.visible'); + cy.get('[data-testid="destination-list"]').should('be.visible'); + }); + + it('should adapt layout for tablet viewport', () => { + cy.viewport('ipad-2'); + cy.get('[data-testid="flights-map-container"]').should('be.visible'); + }); + + it('should adapt layout for mobile viewport', () => { + cy.viewport('iphone-x'); + cy.get('[data-testid="flights-map-container"]').should('be.visible'); + }); + + it('should show mobile-friendly destination list on small screens', () => { + cy.viewport('iphone-x'); + cy.get('[data-testid="destination-list"]').should('be.visible'); + }); + + it('should adjust map controls for mobile devices', () => { + cy.viewport('iphone-x'); + cy.get('[data-testid="geolocation-button"]').should('be.visible'); + cy.get('.leaflet-control-container').should('be.visible'); + }); + }); + + // ====================================== + // API INTEGRATION TESTS (~5 tests) + // ====================================== + describe('API Integration', () => { + it('should fetch destinations on page load', () => { + cy.get('@getDestinations').should('have.been.called'); + }); + + it('should handle API errors gracefully', () => { + cy.intercept( + 'GET', + '**/api/flights/destinations/**', + { statusCode: 500 } + ).as('getDestinationsError'); + + cy.reload(); + cy.wait('@getDestinationsError'); + + cy.get('[data-testid="error-message"]').should('be.visible'); + }); + + it('should retry failed API requests', () => { + cy.intercept( + 'GET', + '**/api/flights/destinations/**', + { statusCode: 500 } + ).as('getDestinationsError'); + + cy.reload(); + cy.wait('@getDestinationsError'); + + cy.get('[data-testid="retry-button"]').click(); + + cy.intercept( + 'GET', + '**/api/flights/destinations/**', + mockDestinations + ).as('getDestinationsRetry'); + + cy.wait('@getDestinationsRetry'); + }); + + it('should fetch nearby airports when geolocation enabled', () => { + cy.mockGeolocation({ latitude: 55.7558, longitude: 37.6173 }); + cy.reload(); + cy.wait('@getDestinations'); + + cy.get('[data-testid="geolocation-button"]').click(); + + cy.get('@getNearby').should('exist'); + }); + + it('should update map when destinations data changes', () => { + const updatedDestinations = { + data: { + routes: [ + { + arrivalCity: { + name: 'Казань', + code: 'KZN', + location: { + lat: 55.6084, + lon: 49.2808, + }, + }, + flightCount: 2, + directFlightCount: 1, + }, + ], + }, + }; + + cy.intercept( + 'GET', + '**/api/flights/destinations/**', + updatedDestinations + ).as('getUpdatedDestinations'); + + cy.reload(); + cy.wait('@getUpdatedDestinations'); + + cy.get('[data-testid="map-marker"]').should('have.length', 1); + }); + }); + + // ====================================== + // FILTER STATE PERSISTENCE TESTS (~3 tests) + // ====================================== + describe('Filter State Persistence', () => { + it('should retain destination search filter on page reload', () => { + cy.get('[data-testid="destination-search-input"]') + .type('Москва'); + + cy.get('[data-testid="destination-list-item"]').should('have.length', 1); + + cy.reload(); + cy.wait('@getDestinations'); + + // Filter should be retained (depends on implementation) + cy.get('[data-testid="destination-search-input"]') + .invoke('val') + .then((val) => { + // Verify value is retained + expect(val).to.be.a('string'); + }); + }); + + it('should retain map center position on navigation', () => { + const targetCity = mockDestinations.data.routes[0].arrivalCity; + + cy.get('[data-testid="destination-list-item"]').first().click(); + + cy.visit('/flights-map'); + cy.wait('@getDestinations'); + + // Verify map state is reasonable + cy.get('[data-testid="leaflet-map"]').should('be.visible'); + }); + + it('should preserve marker highlight state during interactions', () => { + cy.get('[data-testid="destination-list-item"]').first().click(); + + cy.get('[data-testid="map-marker"]').first() + .should('have.class', 'highlighted'); + + // Interact with another destination + cy.get('[data-testid="destination-list-item"]').eq(1).click(); + + // First marker should no longer be highlighted + cy.get('[data-testid="destination-list-item"]').eq(1) + .scrollIntoView(); + + cy.get('[data-testid="map-marker"]').eq(1) + .should('have.class', 'highlighted'); + }); + }); +});