Files
flights_web/tests/e2e-angular/cross-app/11-flights-map.spec.ts
T
gnezim 375bcfb0fa Add e2e test suite from flights-front with Angular API mocks
Copies Playwright e2e tests (58 specs, 300+ tests) designed for cross-app
testing. Adapts API mocks to match real Aeroflot dictionary format (title
objects with multilingual keys), adds board/schedule/days endpoint mocks,
and provides Angular-specific Playwright config on port 4203.
2026-04-15 23:07:44 +03:00

1377 lines
43 KiB
TypeScript

import { test, expect } from '../support/cross-app-fixtures';
import { mockAllAPIs } from '../support/cross-app-fixtures';
import { S, tid } from '../support/selectors';
import { waitForLocatorExtended } from '../support/test-utilities';
// Flights Map — tests 260-287
/**
* Angular dictionary data in the format the app expects.
* Cities use {code, title: {ru, en}, country_code, has_afl_flights}.
*/
const MOCK_CITIES = [
{ code: 'MOW', title: { ru: 'Москва', en: 'Moscow' }, country_code: 'RU', has_afl_flights: true },
{
code: 'LED',
title: { ru: 'Санкт-Петербург', en: 'Saint Petersburg' },
country_code: 'RU',
has_afl_flights: true,
},
{
code: 'AER',
title: { ru: 'Сочи', en: 'Sochi' },
country_code: 'RU',
has_afl_flights: true,
},
{
code: 'KRR',
title: { ru: 'Краснодар', en: 'Krasnodar' },
country_code: 'RU',
has_afl_flights: true,
},
{
code: 'SVX',
title: { ru: 'Екатеринбург', en: 'Yekaterinburg' },
country_code: 'RU',
has_afl_flights: true,
},
{
code: 'JFK',
title: { ru: 'Нью-Йорк', en: 'New York' },
country_code: 'US',
has_afl_flights: true,
},
];
const MOCK_AIRPORTS = [
{
code: 'SVO',
title: { ru: 'Шереметьево', en: 'Sheremetyevo' },
city_code: 'MOW',
country_code: 'RU',
has_afl_flights: true,
},
{
code: 'DME',
title: { ru: 'Домодедово', en: 'Domodedovo' },
city_code: 'MOW',
country_code: 'RU',
has_afl_flights: true,
},
{
code: 'VKO',
title: { ru: 'Внуково', en: 'Vnukovo' },
city_code: 'MOW',
country_code: 'RU',
has_afl_flights: true,
},
{
code: 'LED',
title: { ru: 'Пулково', en: 'Pulkovo' },
city_code: 'LED',
country_code: 'RU',
has_afl_flights: true,
},
{
code: 'AER',
title: { ru: 'Сочи', en: 'Sochi' },
city_code: 'AER',
country_code: 'RU',
has_afl_flights: true,
},
{
code: 'KRR',
title: { ru: 'Пашковский', en: 'Pashkovsky' },
city_code: 'KRR',
country_code: 'RU',
has_afl_flights: true,
},
{
code: 'SVX',
title: { ru: 'Кольцово', en: 'Koltsovo' },
city_code: 'SVX',
country_code: 'RU',
has_afl_flights: true,
},
{
code: 'JFK',
title: { ru: 'Нью-Йорк Кеннеди', en: 'New York Kennedy' },
city_code: 'JFK',
country_code: 'US',
has_afl_flights: true,
},
];
const MOCK_COUNTRIES = [
{ code: 'RU', title: { ru: 'Россия', en: 'Russia' } },
{ code: 'US', title: { ru: 'США', en: 'United States' } },
];
const MOCK_REGIONS = [
{ code: 'EUR', title: { ru: 'Европа', en: 'Europe' } },
{ code: 'NAM', title: { ru: 'Северная Америка', en: 'North America' } },
];
/** Helper: today formatted as YYYYMMDD */
function formatToday(): string {
const d = new Date();
return `${d.getFullYear()}${String(d.getMonth() + 1).padStart(2, '0')}${String(d.getDate()).padStart(2, '0')}`;
}
/**
* Setup API mocks for flights map tests.
* Must be called BEFORE page.goto().
*/
async function mockFlightsMapAPIs(page: import('@playwright/test').Page) {
await page.route('**/api/appSettings', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
showDebugVersion: 'False',
uiOptions: {
filter: {
onlineboard: { searchFrom: '2d', searchTo: '2d' },
schedule: { searchFrom: '30d', searchTo: '30d' },
},
buttons: {
flightStatus: { availableFrom: '24h' },
buyTicket: { period: { min: '2h', max: '72h' } },
},
},
}),
});
});
await page.route('**/api/Requests/*/getpopular', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{ requestType: 'FlightNumber', carrierCode: 'SU', flightNumber: '0654' },
{ requestType: 'Route', departureCity: 'MOW', arrivalCity: 'AER' },
]),
});
});
// Dictionary endpoints with proper Angular model format
await page.route('**/api/dictionary/**', (route) => {
const url = route.request().url();
if (url.includes('cities')) {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(MOCK_CITIES),
});
} else if (url.includes('airports')) {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(MOCK_AIRPORTS),
});
} else if (url.includes('countries')) {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(MOCK_COUNTRIES),
});
} else if (url.includes('world_regions')) {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(MOCK_REGIONS),
});
} else {
route.fulfill({ status: 200, contentType: 'application/json', body: '[]' });
}
});
await page.route('**/api/version', (route) => {
route.fulfill({ status: 200, contentType: 'application/json', body: '{"version":"1.0"}' });
});
// Block external calls to avoid CORS errors
await page.route('**/*.aeroflot.ru/**', (route) => route.abort());
// Mock flights map routes/markers data
await page.route('**/api/Requests/*/getroutes', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
routes: [
{
departure: 'SVO',
arrival: 'AER',
frequency: 10,
distance: 1600,
domestic: true,
connecting: false,
},
{
departure: 'SVO',
arrival: 'LED',
frequency: 15,
distance: 700,
domestic: true,
connecting: false,
},
{
departure: 'SVO',
arrival: 'SVX',
frequency: 8,
distance: 2300,
domestic: true,
connecting: false,
},
{
departure: 'SVO',
arrival: 'JFK',
frequency: 5,
distance: 9200,
domestic: false,
connecting: false,
},
{
departure: 'SVO',
arrival: 'KRR',
frequency: 3,
distance: 1900,
domestic: false,
connecting: true,
},
],
markers: [
{ lat: 55.97, lng: 37.42, city: 'MOW', flightCount: 20 },
{ lat: 59.8, lng: 30.26, city: 'LED', flightCount: 15 },
{ lat: 43.45, lng: 39.95, city: 'AER', flightCount: 10 },
{ lat: 56.48, lng: 84.97, city: 'SVX', flightCount: 8 },
{ lat: 40.64, lng: -73.78, city: 'JFK', flightCount: 5 },
],
}),
});
});
}
/** Get the departure city autocomplete input element from map page. */
function getMapDepartureInput(page: import('@playwright/test').Page, app: 'angular' | 'react') {
// For React: map-departure-input; For Angular: route-departure-city-input
const testidName = app === 'react' ? 'map-departure-input' : 'route-departure-city-input';
const container = page.locator(`[data-testid="${testidName}"]`);
// Try to find input element inside, otherwise use the container
const input = container.locator('input').first();
return input;
}
/** Get the arrival city autocomplete input element from map page. */
function getMapArrivalInput(page: import('@playwright/test').Page, app: 'angular' | 'react') {
// For React: map-arrival-input; For Angular: route-arrival-city-input
const testidName = app === 'react' ? 'map-arrival-input' : 'route-arrival-city-input';
const container = page.locator(`[data-testid="${testidName}"]`);
// Try to find input element inside, otherwise use the container
const input = container.locator('input').first();
return input;
}
/** PrimeNG autocomplete suggestion options selector. */
const OPTION_SEL =
'p-autocomplete-panel li[role="option"], .p-autocomplete-panel li, .p-autocomplete-items li';
test.describe('Flights Map (Cross-App)', () => {
test.beforeEach(async ({ page, localePath, app }) => {
try {
// Mock all common APIs via the shared fixture
await mockAllAPIs(page);
// Set up map-specific API mocks quickly without race conditions
// This will be called in parallel with the navigation below
mockFlightsMapAPIs(page).catch(() => {
// If additional mocking fails, continue anyway - mockAllAPIs covers most cases
});
// Navigate to flights map page with reasonable timeout
await page
.goto(localePath('flights-map'), {
waitUntil: 'domcontentloaded',
timeout: 8000,
})
.catch(() => {
// If navigation fails, the page likely doesn't exist for this app
test.skip();
});
// Skip if 404 or error
try {
const status = await page.evaluate(() => {
const w = window as unknown as Record<string, number>;
return w.__pageStatus || 200;
});
if (status === 404) {
test.skip();
}
} catch {
// Ignore evaluation errors
}
// Minimal wait for map to start initializing
await page.waitForTimeout(200);
} catch (e) {
// Any uncaught errors cause all tests to skip
test.skip();
}
});
// ===== Map Page Navigation Tests (3 tests) =====
test('260: Flights Map tab is visible in navigation', async ({ page, app }) => {
const tab = page.locator(tid(S.NAV_FLIGHTS_MAP_TAB, app));
await expect(tab).toBeVisible();
});
test('261: Clicking Flights Map tab navigates to /locale/flights-map', async ({
page,
app,
locale,
}) => {
const tab = page.locator(tid(S.NAV_FLIGHTS_MAP_TAB, app));
await expect(tab).toHaveClass(/active|selected/);
await expect(page).toHaveURL(new RegExp(`/${locale}/flights-map`));
});
test('262: Map page loads without errors', async ({ page }) => {
// Check page title contains expected text
const title = await page.title();
expect(title.length).toBeGreaterThan(0);
// Verify no console errors during navigation
const errors: string[] = [];
page.on('console', (msg) => {
if (msg.type() === 'error') {
errors.push(msg.text());
}
});
// Wait a bit for any deferred errors
await page.waitForTimeout(500);
// Allow some errors but not critical ones
const criticalErrors = errors.filter((e) => !e.includes('favicon'));
expect(criticalErrors.length).toBe(0);
});
// ===== Map Display & Initialization Tests (4 tests) =====
test('263: Map container is visible', async ({ page, app }) => {
const mapContainer = page.locator(tid(S.MAP_CONTAINER, app));
// Use extended wait for map initialization (Leaflet can be slow)
await waitForLocatorExtended(mapContainer, 20000);
await expect(mapContainer).toBeVisible({ timeout: 10000 });
});
test('264: Map displays flight routes as lines/arrows', async ({ page, app }) => {
const mapContainer = page.locator(tid(S.MAP_CONTAINER, app));
// Wait for map to fully render
await page.waitForTimeout(1000);
// Check if map container has SVG (lines) or canvas (map library specific)
const svgLines = mapContainer.locator('svg');
const hasMapLibraryContainer =
(await mapContainer.locator('div[class*="leaflet"]').count()) > 0 ||
(await mapContainer.locator('canvas').count()) > 0;
expect(hasMapLibraryContainer || (await svgLines.count()) > 0).toBeTruthy();
});
test('265: Map shows departure city marker', async ({ page, app }) => {
const departureInput = getMapDepartureInput(page, app);
// Select a departure city
await departureInput.click();
await departureInput.fill('Москва');
await page.waitForTimeout(500);
// Find and click first suggestion
const options = page.locator(OPTION_SEL);
if ((await options.count()) > 0) {
await options.first().click();
await page.waitForTimeout(800);
}
// Verify marker appears on map
const mapContainer = page.locator(tid(S.MAP_CONTAINER, app));
const markers = mapContainer.locator(`[data-testid="${S.MAP_MARKER}"], .leaflet-marker-icon`);
const markerCount = await markers.count();
// Should have at least one marker for departure
expect(markerCount).toBeGreaterThanOrEqual(1);
});
test('266: Map shows arrival city markers/points', async ({ page, app }) => {
const departureInput = getMapDepartureInput(page, app);
const arrivalInput = getMapArrivalInput(page, app);
// Select departure city
await departureInput.click();
await departureInput.fill('Москва');
await page.waitForTimeout(500);
const options = page.locator(OPTION_SEL);
if ((await options.count()) > 0) {
await options.first().click();
await page.waitForTimeout(500);
}
// Select arrival city
await arrivalInput.click();
await arrivalInput.fill('Сочи');
await page.waitForTimeout(500);
const arrivalOptions = page.locator(OPTION_SEL);
if ((await arrivalOptions.count()) > 0) {
await arrivalOptions.first().click();
await page.waitForTimeout(800);
}
// Verify multiple markers appear
const mapContainer = page.locator(tid(S.MAP_CONTAINER, app));
const markers = mapContainer.locator(`[data-testid="${S.MAP_MARKER}"], .leaflet-marker-icon`);
const markerCount = await markers.count();
// Should have at least 2 markers (departure + arrival)
expect(markerCount).toBeGreaterThanOrEqual(2);
});
// ===== Departure City Selection Tests (3 tests) =====
test('267: Departure city input is visible', async ({ page, app }) => {
const departureContainer = page.locator(tid(S.MAP_DEPARTURE_INPUT, app));
await expect(departureContainer).toBeVisible();
});
test('268: Typing departure shows autocomplete suggestions', async ({ page, app }) => {
const departureInput = getMapDepartureInput(page, app);
await departureInput.click();
await departureInput.fill('Мос');
await page.waitForTimeout(500);
const options = page.locator(OPTION_SEL);
const optionCount = await options.count();
// Should show at least one suggestion
expect(optionCount).toBeGreaterThan(0);
});
test('269: Selecting departure city updates map display', async ({ page, app }) => {
const departureInput = getMapDepartureInput(page, app);
await departureInput.click();
await departureInput.fill('Мос');
await page.waitForTimeout(500);
const options = page.locator(OPTION_SEL);
if ((await options.count()) > 0) {
await options.first().click();
await page.waitForTimeout(800);
}
// Input should contain the selected city
const inputValue = await departureInput.inputValue();
expect(inputValue.length).toBeGreaterThan(0);
});
// ===== Arrival City Selection Tests (3 tests) =====
test('270: Arrival city input is visible', async ({ page, app }) => {
const arrivalContainer = page.locator(tid(S.MAP_ARRIVAL_INPUT, app));
await expect(arrivalContainer).toBeVisible();
});
test('271: Typing arrival shows autocomplete suggestions', async ({ page, app }) => {
const arrivalInput = getMapArrivalInput(page, app);
await arrivalInput.click();
await arrivalInput.fill('Соч');
await page.waitForTimeout(500);
const options = page.locator(OPTION_SEL);
const optionCount = await options.count();
// Should show at least one suggestion
expect(optionCount).toBeGreaterThan(0);
});
test('272: Selecting arrival city updates map display', async ({ page, app }) => {
const arrivalInput = getMapArrivalInput(page, app);
await arrivalInput.click();
await arrivalInput.fill('Соч');
await page.waitForTimeout(500);
const options = page.locator(OPTION_SEL);
if ((await options.count()) > 0) {
await options.first().click();
await page.waitForTimeout(800);
}
// Input should contain the selected city
const inputValue = await arrivalInput.inputValue();
expect(inputValue.length).toBeGreaterThan(0);
});
// ===== Swap Functionality Tests (2 tests) =====
test('273: Swap button swaps departure and arrival cities', async ({ page, app }) => {
const departureInput = getMapDepartureInput(page, app);
const arrivalInput = getMapArrivalInput(page, app);
const swapButton = page.locator(tid(S.MAP_SWAP_BUTTON, app));
// Fill departure
await departureInput.click();
await departureInput.fill('Москва');
await page.waitForTimeout(300);
let options = page.locator(OPTION_SEL);
if ((await options.count()) > 0) {
await options.first().click();
await page.waitForTimeout(300);
}
const firstDeptValue = await departureInput.inputValue();
// Fill arrival
await arrivalInput.click();
await arrivalInput.fill('Сочи');
await page.waitForTimeout(300);
options = page.locator(OPTION_SEL);
if ((await options.count()) > 0) {
await options.first().click();
await page.waitForTimeout(300);
}
const firstArrValue = await arrivalInput.inputValue();
// Verify both have values before swap
expect(firstDeptValue.length).toBeGreaterThan(0);
expect(firstArrValue.length).toBeGreaterThan(0);
// Click swap
if ((await swapButton.count()) > 0) {
await swapButton.click();
await page.waitForTimeout(500);
const newDeptValue = await departureInput.inputValue();
const newArrValue = await arrivalInput.inputValue();
// Values should be swapped
expect(newDeptValue).toContain(firstArrValue.substring(0, 3));
expect(newArrValue).toContain(firstDeptValue.substring(0, 3));
} else {
test.skip();
}
});
test('274: Swap button is disabled when either city empty', async ({ page, app }) => {
const departureInput = getMapDepartureInput(page, app);
const swapButton = page.locator(tid(S.MAP_SWAP_BUTTON, app));
if ((await swapButton.count()) === 0) {
test.skip();
}
// Check button state with empty departure
const isDisabledEmpty = await swapButton.evaluate((el: HTMLElement) => {
return el.hasAttribute('disabled') || el.getAttribute('aria-disabled') === 'true';
});
// Fill departure only
await departureInput.click();
await departureInput.fill('Москва');
await page.waitForTimeout(300);
const options = page.locator(OPTION_SEL);
if ((await options.count()) > 0) {
await options.first().click();
await page.waitForTimeout(300);
}
// With only departure filled, button should be in expected state
// (may be enabled or disabled depending on implementation)
// This test mainly verifies the button responds appropriately
const hasSwapButton = (await swapButton.count()) > 0;
expect(hasSwapButton).toBeTruthy();
});
// ===== Date Selection Tests (2 tests) =====
test('275: Date picker input is visible', async ({ page, app }) => {
const dateInput = page.locator(tid(S.MAP_CALENDAR, app));
if ((await dateInput.count()) === 0) {
test.skip();
}
await expect(dateInput).toBeVisible();
});
test('276: Selecting date updates flight routes on map', async ({ page, app }) => {
const dateInput = page.locator(tid(S.MAP_CALENDAR, app));
if ((await dateInput.count()) === 0) {
test.skip();
}
const today = formatToday();
// Try to set date (exact interaction depends on calendar component)
await dateInput.click();
await page.waitForTimeout(300);
// Try typing date
const inputField = dateInput.locator('input').first();
if ((await inputField.count()) > 0) {
await inputField.fill(today);
await page.waitForTimeout(500);
}
// Verify map is still visible and responsive
const mapContainer = page.locator(tid(S.MAP_CONTAINER, app));
await expect(mapContainer).toBeVisible();
});
// ===== Filter Toggle Tests (6 tests) =====
test('277: Domestic flights toggle is visible', async ({ page, app }) => {
const toggle = page.locator(tid(S.MAP_DOMESTIC_TOGGLE, app));
if ((await toggle.count()) === 0) {
test.skip();
}
await expect(toggle).toBeVisible();
});
test('278: International flights toggle is visible', async ({ page, app }) => {
const toggle = page.locator(tid(S.MAP_INTERNATIONAL_TOGGLE, app));
if ((await toggle.count()) === 0) {
test.skip();
}
await expect(toggle).toBeVisible();
});
test('279: Connecting flights toggle is visible', async ({ page, app }) => {
const toggle = page.locator(tid(S.MAP_CONNECTING_TOGGLE, app));
if ((await toggle.count()) === 0) {
test.skip();
}
await expect(toggle).toBeVisible();
});
test('280: All toggles are checked by default', async ({ page, app }) => {
const domesticToggle = page.locator(tid(S.MAP_DOMESTIC_TOGGLE, app));
const internationalToggle = page.locator(tid(S.MAP_INTERNATIONAL_TOGGLE, app));
const connectingToggle = page.locator(tid(S.MAP_CONNECTING_TOGGLE, app));
// Skip if toggles don't exist
const hasDomestic = (await domesticToggle.count()) > 0;
const hasInternational = (await internationalToggle.count()) > 0;
if (!hasDomestic && !hasInternational) {
test.skip();
}
// Check default state of visible toggles
if (hasDomestic) {
const isChecked = await domesticToggle.evaluate((el: HTMLElement) => {
const input = el.querySelector('input') || el;
return (input as HTMLInputElement).checked || input.getAttribute('aria-checked') === 'true';
});
expect(isChecked).toBeTruthy();
}
if (hasInternational) {
const isChecked = await internationalToggle.evaluate((el: HTMLElement) => {
const input = el.querySelector('input') || el;
return (input as HTMLInputElement).checked || input.getAttribute('aria-checked') === 'true';
});
expect(isChecked).toBeTruthy();
}
});
test('281: Unchecking domestic toggle hides domestic routes', async ({ page, app }) => {
const domesticToggle = page.locator(tid(S.MAP_DOMESTIC_TOGGLE, app));
if ((await domesticToggle.count()) === 0) {
test.skip();
}
// Get initial marker count
const mapContainer = page.locator(tid(S.MAP_CONTAINER, app));
const initialMarkers = await mapContainer
.locator(`[data-testid="${S.MAP_MARKER}"], .leaflet-marker-icon`)
.count();
// Uncheck domestic toggle
const checkbox = domesticToggle.locator('input').first();
if ((await checkbox.count()) > 0) {
await checkbox.click();
await page.waitForTimeout(800);
}
// Marker count may change or stay same depending on which routes are shown
const finalMarkers = await mapContainer
.locator(`[data-testid="${S.MAP_MARKER}"], .leaflet-marker-icon`)
.count();
// Verify map is still responsive
await expect(mapContainer).toBeVisible();
});
test('282: Unchecking international toggle hides international routes', async ({ page, app }) => {
const internationalToggle = page.locator(tid(S.MAP_INTERNATIONAL_TOGGLE, app));
if ((await internationalToggle.count()) === 0) {
test.skip();
}
const mapContainer = page.locator(tid(S.MAP_CONTAINER, app));
// Uncheck international toggle
const checkbox = internationalToggle.locator('input').first();
if ((await checkbox.count()) > 0) {
await checkbox.click();
await page.waitForTimeout(800);
}
// Verify map is still responsive
await expect(mapContainer).toBeVisible();
});
test('283: Unchecking connecting toggle hides connecting routes', async ({ page, app }) => {
const connectingToggle = page.locator(tid(S.MAP_CONNECTING_TOGGLE, app));
if ((await connectingToggle.count()) === 0) {
test.skip();
}
const mapContainer = page.locator(tid(S.MAP_CONTAINER, app));
// Uncheck connecting toggle
const checkbox = connectingToggle.locator('input').first();
if ((await checkbox.count()) > 0) {
await checkbox.click();
await page.waitForTimeout(800);
}
// Verify map is still responsive
await expect(mapContainer).toBeVisible();
});
// ===== Map Markers & Clustering Tests (4 tests) =====
test('284: Map markers show flight frequency/count', async ({ page, app }) => {
const mapContainer = page.locator(tid(S.MAP_CONTAINER, app));
const markers = mapContainer.locator(`[data-testid="${S.MAP_MARKER}"], .leaflet-marker-icon`);
// Markers should be visible or have count indicators
const markerCount = await markers.count();
// Map markers may have titles or aria-labels with flight count info
let hasFlightInfo = false;
if (markerCount > 0) {
for (let i = 0; i < Math.min(markerCount, 3); i++) {
const marker = markers.nth(i);
const title = await marker.getAttribute('title');
const ariaLabel = await marker.getAttribute('aria-label');
const text = await marker.textContent();
if (
(title && title.match(/\d+/)) ||
(ariaLabel && ariaLabel.match(/\d+/)) ||
(text && text.match(/\d+/))
) {
hasFlightInfo = true;
break;
}
}
}
// Either has markers or visual indication of counts
expect(markerCount > 0 || hasFlightInfo).toBeTruthy();
});
test('285: Clicking marker shows popup with flight info', async ({ page, app }) => {
// Select a departure city first to ensure markers
const departureInput = getMapDepartureInput(page, app);
await departureInput.click();
await departureInput.fill('Москва');
await page.waitForTimeout(500);
const options = page.locator(OPTION_SEL);
if ((await options.count()) > 0) {
await options.first().click();
await page.waitForTimeout(800);
}
const mapContainer = page.locator(tid(S.MAP_CONTAINER, app));
const markers = mapContainer.locator(`[data-testid="${S.MAP_MARKER}"], .leaflet-marker-icon`);
if ((await markers.count()) > 0) {
// Click first marker
await markers.first().click();
await page.waitForTimeout(500);
// Check for popup or tooltip
const popup = page.locator('.leaflet-popup, [class*="popup"], [role="tooltip"]');
const popupExists = (await popup.count()) > 0;
// If no popup, that's OK - some implementations use different UI
expect(popupExists || (await markers.count()) > 0).toBeTruthy();
}
});
test('286: Multiple destinations cluster when zoomed out', async ({ page, app }) => {
const mapContainer = page.locator(tid(S.MAP_CONTAINER, app));
// Map may use clustering at certain zoom levels
const clusters = mapContainer.locator(
`[data-testid="${S.MAP_MARKER_CLUSTER}"], [class*="cluster"]`,
);
// Wait for map to load fully
await page.waitForTimeout(1000);
// Check if clustering UI exists or if markers are visible
const hasMarkerUI =
(await mapContainer.locator(`[data-testid="${S.MAP_MARKER}"]`).count()) > 0 ||
(await mapContainer.locator('.leaflet-marker-icon').count()) > 0;
expect(hasMarkerUI).toBeTruthy();
});
test('287: Map zooms when searching new route', async ({ page, app }) => {
const departureInput = getMapDepartureInput(page, app);
const arrivalInput = getMapArrivalInput(page, app);
const mapContainer = page.locator(tid(S.MAP_CONTAINER, app));
// Select departure
await departureInput.click();
await departureInput.fill('Москва');
await page.waitForTimeout(500);
let options = page.locator(OPTION_SEL);
if ((await options.count()) > 0) {
await options.first().click();
await page.waitForTimeout(500);
}
// Get initial map state/container bounds
const initialBounds = await mapContainer.boundingBox();
// Select arrival
await arrivalInput.click();
await arrivalInput.fill('Сочи');
await page.waitForTimeout(500);
options = page.locator(OPTION_SEL);
if ((await options.count()) > 0) {
await options.first().click();
await page.waitForTimeout(800);
}
// Map should still be visible and responsive after route selection
await expect(mapContainer).toBeVisible();
// Container should still exist (zoom/pan happens within map)
const finalBounds = await mapContainer.boundingBox();
expect(finalBounds).toBeTruthy();
});
// US-76: Enhanced Route Popup with Details
test('288: Popup displays departure and arrival airport codes', async ({ page, app }) => {
const departureInput = getMapDepartureInput(page, app);
const arrivalInput = getMapArrivalInput(page, app);
// Select departure city
await departureInput.click();
await departureInput.fill('Москва');
await page.waitForTimeout(500);
let options = page.locator(OPTION_SEL);
if ((await options.count()) > 0) {
await options.first().click();
await page.waitForTimeout(500);
}
// Select arrival city
await arrivalInput.click();
await arrivalInput.fill('Сочи');
await page.waitForTimeout(500);
options = page.locator(OPTION_SEL);
if ((await options.count()) > 0) {
await options.first().click();
await page.waitForTimeout(800);
}
// Look for popup with airport codes
const popup = page.locator('[data-testid="route-popup"]');
const depCode = page.locator('[data-testid="popup-dep-code"]');
const arrCode = page.locator('[data-testid="popup-arr-code"]');
if ((await popup.count()) > 0) {
expect(await depCode.count()).toBeGreaterThan(0);
expect(await arrCode.count()).toBeGreaterThan(0);
}
});
test('289: Popup displays departure and arrival city names', async ({ page, app }) => {
const departureInput = getMapDepartureInput(page, app);
const arrivalInput = getMapArrivalInput(page, app);
// Select cities
await departureInput.click();
await departureInput.fill('Москва');
await page.waitForTimeout(500);
let options = page.locator(OPTION_SEL);
if ((await options.count()) > 0) {
await options.first().click();
await page.waitForTimeout(500);
}
await arrivalInput.click();
await arrivalInput.fill('Сочи');
await page.waitForTimeout(500);
options = page.locator(OPTION_SEL);
if ((await options.count()) > 0) {
await options.first().click();
await page.waitForTimeout(800);
}
// Check for city names in popup
const depName = page.locator('[data-testid="popup-departure"]');
const arrName = page.locator('[data-testid="popup-arrival"]');
if ((await depName.count()) > 0) {
expect(await depName.isVisible()).toBeTruthy();
}
if ((await arrName.count()) > 0) {
expect(await arrName.isVisible()).toBeTruthy();
}
});
test('290: Popup displays flight count for route', async ({ page, app }) => {
const departureInput = getMapDepartureInput(page, app);
const arrivalInput = getMapArrivalInput(page, app);
// Select route
await departureInput.click();
await departureInput.fill('Москва');
await page.waitForTimeout(500);
let options = page.locator(OPTION_SEL);
if ((await options.count()) > 0) {
await options.first().click();
await page.waitForTimeout(500);
}
await arrivalInput.click();
await arrivalInput.fill('Сочи');
await page.waitForTimeout(500);
options = page.locator(OPTION_SEL);
if ((await options.count()) > 0) {
await options.first().click();
await page.waitForTimeout(800);
}
// Check for flight count display
const flightCount = page.locator('[data-testid="popup-flight-count"]');
if ((await flightCount.count()) > 0) {
const text = await flightCount.textContent();
expect(text).toMatch(/\d+/);
}
});
test('291: Popup displays aircraft type when available', async ({ page, app }) => {
const departureInput = getMapDepartureInput(page, app);
const arrivalInput = getMapArrivalInput(page, app);
// Select route
await departureInput.click();
await departureInput.fill('Москва');
await page.waitForTimeout(500);
let options = page.locator(OPTION_SEL);
if ((await options.count()) > 0) {
await options.first().click();
await page.waitForTimeout(500);
}
await arrivalInput.click();
await arrivalInput.fill('Сочи');
await page.waitForTimeout(500);
options = page.locator(OPTION_SEL);
if ((await options.count()) > 0) {
await options.first().click();
await page.waitForTimeout(800);
}
// Check for aircraft type (optional field)
const aircraft = page.locator('[data-testid="popup-aircraft"]');
if ((await aircraft.count()) > 0) {
expect(await aircraft.isVisible()).toBeTruthy();
}
});
test('292: Popup displays estimated duration when available', async ({ page, app }) => {
const departureInput = getMapDepartureInput(page, app);
const arrivalInput = getMapArrivalInput(page, app);
// Select route
await departureInput.click();
await departureInput.fill('Москва');
await page.waitForTimeout(500);
let options = page.locator(OPTION_SEL);
if ((await options.count()) > 0) {
await options.first().click();
await page.waitForTimeout(500);
}
await arrivalInput.click();
await arrivalInput.fill('Сочи');
await page.waitForTimeout(500);
options = page.locator(OPTION_SEL);
if ((await options.count()) > 0) {
await options.first().click();
await page.waitForTimeout(800);
}
// Check for duration (optional field)
const duration = page.locator('[data-testid="popup-duration"]');
if ((await duration.count()) > 0) {
expect(await duration.isVisible()).toBeTruthy();
}
});
test('293: Popup displays status indicator when available', async ({ page, app }) => {
const departureInput = getMapDepartureInput(page, app);
const arrivalInput = getMapArrivalInput(page, app);
// Select route
await departureInput.click();
await departureInput.fill('Москва');
await page.waitForTimeout(500);
let options = page.locator(OPTION_SEL);
if ((await options.count()) > 0) {
await options.first().click();
await page.waitForTimeout(500);
}
await arrivalInput.click();
await arrivalInput.fill('Сочи');
await page.waitForTimeout(500);
options = page.locator(OPTION_SEL);
if ((await options.count()) > 0) {
await options.first().click();
await page.waitForTimeout(800);
}
// Check for status indicator (optional field)
const status = page.locator('[data-testid="popup-status"]');
if ((await status.count()) > 0) {
expect(await status.isVisible()).toBeTruthy();
}
});
test('294: Popup has responsive layout on mobile viewport', async ({ page, app }) => {
// Set mobile viewport
await page.setViewportSize({ width: 375, height: 667 });
await page.waitForTimeout(500);
const departureInput = getMapDepartureInput(page, app);
const arrivalInput = getMapArrivalInput(page, app);
// Select route
await departureInput.click();
await departureInput.fill('Москва');
await page.waitForTimeout(500);
let options = page.locator(OPTION_SEL);
if ((await options.count()) > 0) {
await options.first().click();
await page.waitForTimeout(500);
}
await arrivalInput.click();
await arrivalInput.fill('Сочи');
await page.waitForTimeout(500);
options = page.locator(OPTION_SEL);
if ((await options.count()) > 0) {
await options.first().click();
await page.waitForTimeout(800);
}
// Verify popup is still visible on mobile
const popup = page.locator('[data-testid="route-popup"]');
if ((await popup.count()) > 0) {
expect(await popup.isVisible()).toBeTruthy();
}
});
// US-79: Buy Ticket Link
test('295: Buy Ticket button appears in popup', async ({ page, app }) => {
const departureInput = getMapDepartureInput(page, app);
const arrivalInput = getMapArrivalInput(page, app);
// Select route
await departureInput.click();
await departureInput.fill('Москва');
await page.waitForTimeout(500);
let options = page.locator(OPTION_SEL);
if ((await options.count()) > 0) {
await options.first().click();
await page.waitForTimeout(500);
}
await arrivalInput.click();
await arrivalInput.fill('Сочи');
await page.waitForTimeout(500);
options = page.locator(OPTION_SEL);
if ((await options.count()) > 0) {
await options.first().click();
await page.waitForTimeout(800);
}
// Look for Buy Ticket button
const buyButton = page.locator('[data-testid="buy-ticket-button"]');
if ((await buyButton.count()) > 0) {
expect(await buyButton.isVisible()).toBeTruthy();
}
});
test('296: Buy Ticket button has proper styling', async ({ page, app }) => {
const departureInput = getMapDepartureInput(page, app);
const arrivalInput = getMapArrivalInput(page, app);
// Select route
await departureInput.click();
await departureInput.fill('Москва');
await page.waitForTimeout(500);
let options = page.locator(OPTION_SEL);
if ((await options.count()) > 0) {
await options.first().click();
await page.waitForTimeout(500);
}
await arrivalInput.click();
await arrivalInput.fill('Сочи');
await page.waitForTimeout(500);
options = page.locator(OPTION_SEL);
if ((await options.count()) > 0) {
await options.first().click();
await page.waitForTimeout(800);
}
// Check button styling
const buyButton = page.locator('[data-testid="buy-ticket-button"]');
if ((await buyButton.count()) > 0) {
const isVisible = await buyButton.isVisible();
expect(isVisible).toBeTruthy();
// Check button has proper size for touch targets
const boundingBox = await buyButton.boundingBox();
if (boundingBox) {
expect(boundingBox.height).toBeGreaterThanOrEqual(40);
}
}
});
test('297: Buy Ticket button opens Aeroflot booking link', async ({ page, app, context }) => {
const departureInput = getMapDepartureInput(page, app);
const arrivalInput = getMapArrivalInput(page, app);
// Select route
await departureInput.click();
await departureInput.fill('Москва');
await page.waitForTimeout(500);
let options = page.locator(OPTION_SEL);
if ((await options.count()) > 0) {
await options.first().click();
await page.waitForTimeout(500);
}
await arrivalInput.click();
await arrivalInput.fill('Сочи');
await page.waitForTimeout(500);
options = page.locator(OPTION_SEL);
if ((await options.count()) > 0) {
await options.first().click();
await page.waitForTimeout(800);
}
// Listen for new page
const newPagePromise = context.waitForEvent('page');
// Click button
const buyButton = page.locator('[data-testid="buy-ticket-button"]');
if ((await buyButton.count()) > 0) {
// Handle popup blocker
const pagePromise = Promise.race([
newPagePromise,
new Promise((resolve) => setTimeout(() => resolve(null), 3000)),
]);
await buyButton.click();
const newPage = await pagePromise;
if (newPage) {
// Verify new page has Aeroflot booking URL
const url = newPage.url();
expect(url).toContain('aeroflot');
await newPage.close();
}
}
});
test('298: Buy Ticket button encodes route parameters correctly', async ({ page, app }) => {
const departureInput = getMapDepartureInput(page, app);
const arrivalInput = getMapArrivalInput(page, app);
// Select route
await departureInput.click();
await departureInput.fill('Москва');
await page.waitForTimeout(500);
let options = page.locator(OPTION_SEL);
if ((await options.count()) > 0) {
await options.first().click();
await page.waitForTimeout(500);
}
await arrivalInput.click();
await arrivalInput.fill('Сочи');
await page.waitForTimeout(500);
options = page.locator(OPTION_SEL);
if ((await options.count()) > 0) {
await options.first().click();
await page.waitForTimeout(800);
}
// Check button href/data attributes
const buyButton = page.locator('[data-testid="buy-ticket-button"]');
if ((await buyButton.count()) > 0) {
// Button should be a proper button element
const isButton = await buyButton.evaluate((el) => el.tagName === 'BUTTON');
expect(isButton).toBeTruthy();
}
});
test('299: Buy Ticket button is accessible with proper ARIA labels', async ({ page, app }) => {
const departureInput = getMapDepartureInput(page, app);
const arrivalInput = getMapArrivalInput(page, app);
// Select route
await departureInput.click();
await departureInput.fill('Москва');
await page.waitForTimeout(500);
let options = page.locator(OPTION_SEL);
if ((await options.count()) > 0) {
await options.first().click();
await page.waitForTimeout(500);
}
await arrivalInput.click();
await arrivalInput.fill('Сочи');
await page.waitForTimeout(500);
options = page.locator(OPTION_SEL);
if ((await options.count()) > 0) {
await options.first().click();
await page.waitForTimeout(800);
}
// Check accessibility attributes
const buyButton = page.locator('[data-testid="buy-ticket-button"]');
if ((await buyButton.count()) > 0) {
const hasAriaLabel = await buyButton.evaluate((el) => el.hasAttribute('aria-label'));
if (hasAriaLabel) {
expect(hasAriaLabel).toBeTruthy();
}
// Button should be keyboard accessible
expect(await buyButton.isEnabled()).toBeTruthy();
}
});
test('300: Buy Ticket button maintains proper touch target size on mobile', async ({
page,
app,
}) => {
// Set mobile viewport
await page.setViewportSize({ width: 375, height: 667 });
await page.waitForTimeout(500);
const departureInput = getMapDepartureInput(page, app);
const arrivalInput = getMapArrivalInput(page, app);
// Select route
await departureInput.click();
await departureInput.fill('Москва');
await page.waitForTimeout(500);
let options = page.locator(OPTION_SEL);
if ((await options.count()) > 0) {
await options.first().click();
await page.waitForTimeout(500);
}
await arrivalInput.click();
await arrivalInput.fill('Сочи');
await page.waitForTimeout(500);
options = page.locator(OPTION_SEL);
if ((await options.count()) > 0) {
await options.first().click();
await page.waitForTimeout(800);
}
// Check touch target size
const buyButton = page.locator('[data-testid="buy-ticket-button"]');
if ((await buyButton.count()) > 0) {
const boundingBox = await buyButton.boundingBox();
if (boundingBox) {
// Minimum 44px touch target (accessibility standard)
expect(boundingBox.height).toBeGreaterThanOrEqual(40);
}
}
});
});