375bcfb0fa
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.
1377 lines
43 KiB
TypeScript
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);
|
|
}
|
|
}
|
|
});
|
|
});
|