Files
flights_web/tests/e2e-angular/cross-app/10-schedule-details.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

662 lines
24 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test, expect } from '../support/cross-app-fixtures';
import { mockAllAPIs } from '../support/cross-app-fixtures';
import { S, tid } from '../support/selectors';
import { mockAngularAPIs } from '../support/angular-api-mock';
// Schedule Details — tests 238-259 (22 tests)
/**
* Mock schedule details API endpoint for Angular.
* Provides multi-day flight itinerary with transfer information.
*/
async function mockScheduleDetailsAPIs(page: import('@playwright/test').Page) {
await mockAngularAPIs(page);
// Mock schedule details API endpoint: /api/Requests/{id}/getflightdetails
// This endpoint returns detailed flight information for a selected flight
// with all flights in the itinerary across multiple days
await page.route('**/api/Requests/*/getflightdetails', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
route: {
departure: {
code: 'SVO',
title: { ru: 'Москва', en: 'Moscow' },
},
arrival: {
code: 'JFK',
title: { ru: 'Нью-Йорк', en: 'New York' },
},
},
flights: [
{
date: '2026-04-15',
flights: [
{
number: 'SU 100',
departureTime: '06:00',
arrivalTime: '14:00',
duration: '8h 0m',
aircraft: 'Boeing 777-300ER',
transfers: 0,
},
{
number: 'SU 102',
departureTime: '08:30',
arrivalTime: '16:30',
duration: '8h 0m',
aircraft: 'Airbus A330-300',
transfers: 0,
},
{
number: 'SU 104',
departureTime: '14:00',
arrivalTime: '23:00',
duration: '9h 0m',
aircraft: 'Boeing 747-400',
transfers: 1,
transferCity: 'London',
transferTime: '2h 30m',
},
],
},
{
date: '2026-04-16',
flights: [
{
number: 'SU 106',
departureTime: '07:00',
arrivalTime: '15:00',
duration: '8h 0m',
aircraft: 'Boeing 777-300ER',
transfers: 0,
},
{
number: 'SU 108',
departureTime: '10:00',
arrivalTime: '18:00',
duration: '8h 0m',
aircraft: 'Airbus A350-900',
transfers: 0,
},
],
},
{
date: '2026-04-17',
flights: [
{
number: 'SU 110',
departureTime: '05:30',
arrivalTime: '13:30',
duration: '8h 0m',
aircraft: 'Boeing 777-300ER',
transfers: 0,
},
],
},
],
}),
});
});
}
/**
* Navigate to schedule details page.
* Returns true if the page loaded successfully, false if 404 or error.
*/
async function gotoScheduleDetails(
page: import('@playwright/test').Page,
localePath: (path: string) => string,
from: string = 'SVO',
to: string = 'JFK',
date: string = '20260415',
flight: string = 'SU100',
): Promise<boolean> {
const params = new URLSearchParams({
from,
to,
date,
flight,
});
const url = localePath(`schedule/details?${params.toString()}`);
const response = await page.goto(url, { waitUntil: 'networkidle' });
// Check if page loaded successfully
if (!response || response.status() === 404) {
return false;
}
// Check for error page indicators
const errorIndicators = page.locator('[data-testid*="error"], .error-container, [role="alert"]');
const errorCount = await errorIndicators.count();
if (errorCount > 0) {
return false;
}
return true;
}
// ---------------------------------------------------------------------------
test.describe('Schedule Details (Cross-App)', () => {
test.beforeEach(async ({ page, app, localePath }) => {
await mockAllAPIs(page);
await mockScheduleDetailsAPIs(page);
// Navigate to schedule details with sample parameters
const navigated = await gotoScheduleDetails(page, localePath);
if (!navigated) {
test.skip(true, 'Schedule details page not available in this app');
return;
}
await page.waitForLoadState('networkidle');
await page.waitForTimeout(500);
});
// ─────────────────────────────────────────────────────────────────────────
// Page Load & Navigation (4 tests: 238-241)
// ─────────────────────────────────────────────────────────────────────────
test('238: Schedule details page loads without errors', async ({ page }) => {
// Verify page is not in error state
const errorElements = page.locator('[data-testid*="error"], .error-container, [role="alert"]');
const errorCount = await errorElements.count();
expect(errorCount).toBe(0);
// Verify page has content (not empty)
const body = page.locator('body');
await expect(body).toBeVisible();
});
test('239: Back button navigates back to schedule results', async ({ page, app }) => {
const backBtn = page.locator(tid(S.SCHEDULE_DETAILS_BACK_BUTTON, app));
if ((await backBtn.count()) === 0) {
test.skip(true, 'Back button not found on schedule details page');
return;
}
const urlBefore = page.url();
await backBtn.first().click();
await page.waitForTimeout(1000);
const urlAfter = page.url();
// Should navigate away from current page
expect(urlAfter).not.toBe(urlBefore);
});
test('240: Page title shows correct route (departure → arrival)', async ({ page }) => {
// Look for route information in page title or header
// Route should show "SVO → JFK" or "Moscow → New York"
const pageTitle = await page.title();
const pageContent = await page.content();
// Check if route codes or city names are present in page
const hasRouteInfo =
pageContent.includes('SVO') ||
pageContent.includes('JFK') ||
pageContent.includes('Moscow') ||
pageContent.includes('New York');
// If no explicit route info, check if page at least loads (graceful fallback)
if (!hasRouteInfo) {
test.skip(true, 'Schedule details route information not available in this implementation');
return;
}
expect(hasRouteInfo).toBe(true);
});
test('241: Breadcrumbs show correct path', async ({ page, app }) => {
const breadcrumbs = page.locator(tid(S.LAYOUT_BREADCRUMBS, app));
if ((await breadcrumbs.count()) === 0) {
test.skip(true, 'Breadcrumbs not found on page');
return;
}
const breadcrumbText = await breadcrumbs.first().textContent();
// Breadcrumbs should contain navigation path info
expect(breadcrumbText).toBeTruthy();
expect((breadcrumbText?.length || 0) > 0).toBe(true);
});
// ─────────────────────────────────────────────────────────────────────────
// Day Tabs & Navigation (4 tests: 242-245)
// ─────────────────────────────────────────────────────────────────────────
test('242: Day tabs are displayed for each day in selected week', async ({ page, app }) => {
const dayTabsContainer = page.locator(tid(S.SCHEDULE_DETAILS_DAY_TABS, app));
if ((await dayTabsContainer.count()) === 0) {
test.skip(true, 'Day tabs container not found');
return;
}
// Look for individual day tabs - use a flexible selector
const dayTabs = page.locator(
`${tid(S.SCHEDULE_DETAILS_DAY_TABS, app)} button, ${tid(S.SCHEDULE_DETAILS_DAY_TABS, app)} [role="tab"]`,
);
const tabCount = await dayTabs.count();
// Should have at least 3-7 tabs (different days in schedule)
expect(tabCount).toBeGreaterThanOrEqual(1);
});
test('243: Current day tab is highlighted by default', async ({ page, app }) => {
const dayTabsContainer = page.locator(tid(S.SCHEDULE_DETAILS_DAY_TABS, app));
if ((await dayTabsContainer.count()) === 0) {
test.skip(true, 'Day tabs not found');
return;
}
const dayTabs = page.locator(
`${tid(S.SCHEDULE_DETAILS_DAY_TABS, app)} button, ${tid(S.SCHEDULE_DETAILS_DAY_TABS, app)} [role="tab"]`,
);
if ((await dayTabs.count()) === 0) {
test.skip(true, 'Day tab elements not found');
return;
}
// At least one tab should have 'active' or 'selected' class/state
let foundActive = false;
for (let i = 0; i < Math.min(7, await dayTabs.count()); i++) {
const tab = dayTabs.nth(i);
const classes = await tab.getAttribute('class');
const ariaSelected = await tab.getAttribute('aria-selected');
if (
(classes && (classes.includes('active') || classes.includes('selected'))) ||
ariaSelected === 'true'
) {
foundActive = true;
break;
}
}
expect(foundActive).toBe(true);
});
test('244: Clicking day tab switches displayed flights', async ({ page, app }) => {
const dayTabsContainer = page.locator(tid(S.SCHEDULE_DETAILS_DAY_TABS, app));
if ((await dayTabsContainer.count()) === 0) {
test.skip(true, 'Day tabs not found');
return;
}
const dayTabs = page.locator(
`${tid(S.SCHEDULE_DETAILS_DAY_TABS, app)} button, ${tid(S.SCHEDULE_DETAILS_DAY_TABS, app)} [role="tab"]`,
);
if ((await dayTabs.count()) < 2) {
test.skip(true, 'Not enough day tabs to test switching');
return;
}
// Get flight list before switching tab
const flightCardsBefore = page.locator(tid(S.SCHEDULE_DETAILS_FLIGHT_MINI, app));
const countBefore = await flightCardsBefore.count();
// Click second tab
const secondTab = dayTabs.nth(1);
await secondTab.evaluate((el: HTMLElement) => el.click());
await page.waitForTimeout(1000);
// Verify tab switched (some indication should show)
const classes = await secondTab.getAttribute('class');
const ariaSelected = await secondTab.getAttribute('aria-selected');
expect(
(classes && (classes.includes('active') || classes.includes('selected'))) ||
ariaSelected === 'true',
).toBe(true);
});
test('245: Day tab shows date and day of week', async ({ page, app }) => {
const dayTabsContainer = page.locator(tid(S.SCHEDULE_DETAILS_DAY_TABS, app));
if ((await dayTabsContainer.count()) === 0) {
test.skip(true, 'Day tabs not found');
return;
}
const dayTabs = page.locator(
`${tid(S.SCHEDULE_DETAILS_DAY_TABS, app)} button, ${tid(S.SCHEDULE_DETAILS_DAY_TABS, app)} [role="tab"]`,
);
if ((await dayTabs.count()) === 0) {
test.skip(true, 'Day tab elements not found');
return;
}
// Check first tab for date and day of week
const firstTab = dayTabs.first();
const tabText = await firstTab.textContent();
// Should contain some date-like content (numbers or day names)
const hasDateInfo =
tabText &&
(/\d{1,2}/.test(tabText) ||
/Mon|Tue|Wed|Thu|Fri|Sat|Sun|пн|вт|ср|чт|пт|сб|вс/i.test(tabText));
expect(hasDateInfo).toBe(true);
});
// ─────────────────────────────────────────────────────────────────────────
// Flight Mini Cards (6 tests: 246-251)
// ─────────────────────────────────────────────────────────────────────────
test('246: Mini flight card shows departure time', async ({ page, app }) => {
const flightCards = page.locator(tid(S.SCHEDULE_DETAILS_FLIGHT_MINI, app));
if ((await flightCards.count()) === 0) {
test.skip(true, 'No flight mini cards found');
return;
}
const firstCard = flightCards.first();
const text = await firstCard.textContent();
// Should contain time pattern (HH:MM)
expect(text).toMatch(/\d{1,2}:\d{2}/);
});
test('247: Mini flight card shows arrival time', async ({ page, app }) => {
const flightCards = page.locator(tid(S.SCHEDULE_DETAILS_FLIGHT_MINI, app));
if ((await flightCards.count()) === 0) {
test.skip(true, 'No flight mini cards found');
return;
}
const firstCard = flightCards.first();
const text = await firstCard.textContent();
const timeMatches = text?.match(/\d{1,2}:\d{2}/g);
// Should have at least departure and arrival times
expect((timeMatches || []).length).toBeGreaterThanOrEqual(2);
});
test('248: Mini flight card shows flight number', async ({ page, app }) => {
const flightCards = page.locator(tid(S.SCHEDULE_DETAILS_FLIGHT_MINI, app));
if ((await flightCards.count()) === 0) {
test.skip(true, 'No flight mini cards found');
return;
}
const firstCard = flightCards.first();
const text = await firstCard.textContent();
// Should contain airline code + flight number pattern
expect(text).toMatch(/[A-Z]{2}\s*\d+/);
});
test('249: Mini flight card shows airline logo', async ({ page, app }) => {
const flightCards = page.locator(tid(S.SCHEDULE_DETAILS_FLIGHT_MINI, app));
if ((await flightCards.count()) === 0) {
test.skip(true, 'No flight mini cards found');
return;
}
const firstCard = flightCards.first();
const text = await firstCard.textContent();
// Airline name or code should be present
const hasAirlineIndicator = text && (text.includes('SU') || text.includes('Aeroflot'));
expect(hasAirlineIndicator).toBe(true);
});
test('250: Mini flight card shows duration', async ({ page, app }) => {
const flightCards = page.locator(tid(S.SCHEDULE_DETAILS_FLIGHT_MINI, app));
if ((await flightCards.count()) === 0) {
test.skip(true, 'No flight mini cards found');
return;
}
const firstCard = flightCards.first();
const text = await firstCard.textContent();
// Should contain duration pattern (e.g., "8h 0m" or "8h")
expect(text).toMatch(/\d+h(\s*\d+m)?/);
});
test('251: Mini flight card is clickable (expands to full details)', async ({ page, app }) => {
const flightCards = page.locator(tid(S.SCHEDULE_DETAILS_FLIGHT_MINI, app));
if ((await flightCards.count()) === 0) {
test.skip(true, 'No flight mini cards found');
return;
}
const firstCard = flightCards.first();
const textBefore = await firstCard.textContent();
// Try clicking the card
await firstCard.evaluate((el: HTMLElement) => el.click());
await page.waitForTimeout(500);
const textAfter = await firstCard.textContent();
// After clicking, content may expand or change
expect(textAfter).toBeTruthy();
});
// ─────────────────────────────────────────────────────────────────────────
// Transfer & Route Information (4 tests: 252-255)
// ─────────────────────────────────────────────────────────────────────────
test('252: Direct flights show "Non-stop" indicator', async ({ page, app }) => {
const flightCards = page.locator(tid(S.SCHEDULE_DETAILS_FLIGHT_MINI, app));
if ((await flightCards.count()) === 0) {
test.skip(true, 'No flight mini cards found');
return;
}
// Look for direct/non-stop flights (flights with 0 transfers)
// The first few flights in our mock data are direct
const firstCard = flightCards.first();
const text = await firstCard.textContent();
// May show "Direct", "Non-stop", or similar indicator
// Or may just not show transfer info
if (text?.toLowerCase().includes('direct') || text?.toLowerCase().includes('non-stop')) {
expect(true).toBe(true);
} else {
// If no explicit indicator, just verify flight card renders
expect(text).toBeTruthy();
}
});
test('253: Transfer flights show transfer point (intermediate city)', async ({ page, app }) => {
const flightCards = page.locator(tid(S.SCHEDULE_DETAILS_FLIGHT_MINI, app));
if ((await flightCards.count()) < 3) {
test.skip(true, 'Not enough flights to find transfer flight');
return;
}
// Mock data has transfer flight at index 2 (SU 104 with transfer to London)
let foundTransferInfo = false;
for (let i = 0; i < (await flightCards.count()); i++) {
const card = flightCards.nth(i);
const text = await card.textContent();
// Look for transfer indicator: "London", "transfer", "via", "intermediate", etc.
if (text && /London|transfer|via|intermediate|промежуточный|пересадка/i.test(text)) {
foundTransferInfo = true;
break;
}
}
// If no explicit transfer info found, skip (may depend on implementation)
if (!foundTransferInfo) {
test.skip(true, 'Transfer information not displayed in cards');
} else {
expect(foundTransferInfo).toBe(true);
}
});
test('254: Transfer flights show transfer time/layover', async ({ page, app }) => {
const flightCards = page.locator(tid(S.SCHEDULE_DETAILS_FLIGHT_MINI, app));
if ((await flightCards.count()) < 3) {
test.skip(true, 'Not enough flights to find transfer flight');
return;
}
// Look for transfer time in flight cards
let foundTransferTime = false;
for (let i = 0; i < (await flightCards.count()); i++) {
const card = flightCards.nth(i);
const text = await card.textContent();
// Look for time pattern in context of transfer (e.g., "2h 30m", "layover")
if (text && (/\d+h\s*\d+m/.test(text) || /layover|stopover|стыковка/i.test(text))) {
foundTransferTime = true;
break;
}
}
if (!foundTransferTime) {
test.skip(true, 'Transfer time not displayed in cards');
} else {
expect(foundTransferTime).toBe(true);
}
});
test('255: Full routing information is displayed for each flight', async ({ page, app }) => {
const flightCards = page.locator(tid(S.SCHEDULE_DETAILS_FLIGHT_MINI, app));
if ((await flightCards.count()) === 0) {
test.skip(true, 'No flight mini cards found');
return;
}
// Check first flight for route info (departure/arrival codes or cities)
const firstCard = flightCards.first();
const text = await firstCard.textContent();
// Should contain airport codes (3-letter) or route indicators
const hasRouteInfo =
text && /[A-Z]{3}|departure|arrival|from|to|из|в|вылет|прибытие/i.test(text);
expect(hasRouteInfo).toBe(true);
});
// ─────────────────────────────────────────────────────────────────────────
// Flight Expansion & Details (2 tests: 256-257)
// ─────────────────────────────────────────────────────────────────────────
test('256: Clicking flight card expands to show full details', async ({ page, app }) => {
const flightCards = page.locator(tid(S.SCHEDULE_DETAILS_FLIGHT_MINI, app));
if ((await flightCards.count()) === 0) {
test.skip(true, 'No flight mini cards found');
return;
}
const firstCard = flightCards.first();
// Try to find an expand button or click the card itself
const expandBtn = firstCard.locator('button, [role="button"]');
if ((await expandBtn.count()) > 0) {
await expandBtn.first().click();
} else {
await firstCard.evaluate((el: HTMLElement) => el.click());
}
await page.waitForTimeout(500);
// After expansion, additional details should be visible
const detailsVisible = await firstCard.isVisible();
expect(detailsVisible).toBe(true);
});
test('257: Expanded details show additional aircraft information', async ({ page, app }) => {
const flightCards = page.locator(tid(S.SCHEDULE_DETAILS_FLIGHT_MINI, app));
if ((await flightCards.count()) === 0) {
test.skip(true, 'No flight mini cards found');
return;
}
const firstCard = flightCards.first();
const text = await firstCard.textContent();
// Should contain aircraft type info (Boeing, Airbus, etc.)
const hasAircraftInfo =
text && /Boeing|Airbus|Embraer|aircraft|aircraft|самолет|тип судна/i.test(text);
if (!hasAircraftInfo) {
test.skip(true, 'Aircraft information not displayed in this view');
} else {
expect(hasAircraftInfo).toBe(true);
}
});
// ─────────────────────────────────────────────────────────────────────────
// Locale & UI (2 tests: 258-259)
// ─────────────────────────────────────────────────────────────────────────
test('258: All text content matches current locale', async ({ page, locale }) => {
const pageContent = await page.content();
// Simple check: if locale is Russian, should have some Russian text
// if locale is English, should have English text
// This is a basic sanity check
if (locale.startsWith('ru')) {
// Check for Russian characters (Cyrillic)
const hasRussian = /[а-яА-ЯёЁ]/.test(pageContent);
expect(hasRussian).toBe(true);
} else if (locale.startsWith('en')) {
// Check for English content (should be present)
const hasContent = pageContent.length > 100;
expect(hasContent).toBe(true);
}
});
test('259: Page renders without console errors', async ({ page }) => {
// Check if page is 404 - if so, skip this test
const url = page.url();
const pageContent = await page.content();
if (pageContent.includes('404') || pageContent.includes('Страница не найдена')) {
test.skip(true, 'Schedule details page not available (404)');
return;
}
// Capture console error messages only (not warnings)
const consoleErrors: string[] = [];
page.on('console', (msg) => {
if (msg.type() === 'error') {
consoleErrors.push(`${msg.type()}: ${msg.text()}`);
}
});
// Re-navigate to page to capture any errors on load
await page.reload();
await page.waitForLoadState('networkidle');
await page.waitForTimeout(500);
// Filter out known third-party or non-critical errors
const relevantErrors = consoleErrors.filter(
(err) =>
!err.includes('external') &&
!err.includes('google') &&
!err.includes('aeroflot.ru') &&
!err.includes('third-party') &&
!err.includes('favicon') &&
!err.includes('Loading chunk'),
);
// Should not have critical application errors
expect(relevantErrors.length).toBeLessThanOrEqual(0);
});
});