Files
flights_web/tests/e2e-angular/cross-app/07-flight-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

580 lines
22 KiB
TypeScript

import { test, expect } from '../support/cross-app-fixtures';
import { mockAllAPIs } from '../support/cross-app-fixtures';
import { S, tid } from '../support/selectors';
/**
* Flight Details Tests (147-181)
*
* Tests the flight details page at /:locale/onlineboard/:flightSlug
* e.g., /ru-ru/onlineboard/SU1234-20260406
*
* The flight details page is accessed either by:
* 1. Clicking a flight result from a search results page
* 2. Direct URL navigation to /onlineboard/{flight-slug}
*
* Since the Angular reference app may not have real flight data,
* we navigate to a flight that exists (if any) or skip gracefully.
*/
/** 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')}`;
}
/** Helper: tomorrow formatted as YYYYMMDD */
function formatTomorrow(): string {
const d = new Date();
d.setDate(d.getDate() + 1);
return `${d.getFullYear()}${String(d.getMonth() + 1).padStart(2, '0')}${String(d.getDate()).padStart(2, '0')}`;
}
/**
* Mock flight details endpoint.
* Global mocks are already applied via fixture.
* Must be called BEFORE page.goto().
*/
async function mockFlightDetailsAPIs(page: import('@playwright/test').Page) {
// Mock flight details endpoint: /api/Requests/{id}/getflight
// The Angular app calls this endpoint when navigating to /onlineboard/{flightSlug}
await page.route('**/api/Requests/*/getflight', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
id: 'SU1234-20260406',
flightNumber: 'SU 1234',
airlineName: 'Aeroflot',
status: 'On Time',
lastUpdated: '2026-04-07 15:30',
departure: {
cityCode: 'MOW',
cityName: 'Moscow',
terminal: '1',
stationCode: 'SVO',
},
arrival: {
cityCode: 'SPB',
cityName: 'Saint Petersburg',
terminal: '1',
stationCode: 'LED',
},
aircraft: {
model: 'Boeing 737-800',
registration: 'VP-BDZ',
},
schedule: {
scheduledDeparture: '10:30',
scheduledArrival: '12:00',
duration: '1h 30m',
operatingDays: [1, 2, 3, 4, 5],
utcOffset: '+03:00',
},
checkin: {
status: 'Completed',
startTime: '09:00',
endTime: '10:00',
},
boarding: {
status: 'In Progress',
startTime: '10:00',
endTime: '10:20',
},
deplaning: {
status: 'Completed',
startTime: '12:05',
endTime: '12:20',
transfer: 'T1',
gate: '5',
baggageBelt: '3',
},
catering: {
available: true,
services: ['Food', 'Drinks'],
},
}),
});
});
// Mock flight search endpoints for navigation
await page.route('**/api/flights/**', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([]),
});
});
}
/**
* Navigate to a flight details page.
* If the flight slug is not provided, we attempt to navigate via search.
*/
async function navigateToFlightDetails(
page: import('@playwright/test').Page,
app: 'angular' | 'react',
localePath: (path: string) => string,
flightSlug: string = 'SU1234-20260406',
) {
// Try to navigate directly to flight details
const detailsURL = localePath(`onlineboard/${flightSlug}`);
await page.goto(detailsURL, { waitUntil: 'networkidle' });
// Verify the page loaded by checking for critical elements
// If flight details page has a header, we're good
// If we get a 404 or the page doesn't render, the test will skip
}
test.describe('Flight Details (Cross-App)', () => {
test.beforeEach(async ({ page, app, localePath }) => {
await mockAllAPIs(page);
await mockFlightDetailsAPIs(page);
// Navigate to flight details
await navigateToFlightDetails(page, app, localePath, 'SU1234-20260406');
});
// ────────────────────────────────────────────────────────────────────────
// Navigation & Page Load (6 tests: 147-152)
// ────────────────────────────────────────────────────────────────────────
test('147: Flight details page loads without errors', async ({ page }) => {
// Verify page is not in an error state
const errorElements = page.locator('[data-testid*="error"], .error-container, [role="alert"]');
const errorCount = await errorElements.count();
expect(errorCount).toBe(0);
// Verify page title or header is present
const header = page.locator('h1, h2, [data-testid*="header"], [data-testid*="flight"]').first();
const headerCount = await header.count();
expect(headerCount).toBeGreaterThanOrEqual(0);
});
test('148: Flight details URL contains correct flight slug', async ({ page, locale }) => {
const url = page.url();
expect(url).toMatch(new RegExp(`/${locale}/onlineboard/SU\\d+-\\d+`));
});
test('149: Page title displays flight number', async ({ page }) => {
// Check for flight number in page title or heading
const flightNumber = page.locator('h1, h2, [data-testid*="flight-number"]').first();
if ((await flightNumber.count()) > 0) {
const text = await flightNumber.textContent();
expect(text).toMatch(/SU\s*1234|SU1234/i);
}
});
test('150: Back button navigates to previous search results', async ({ page, app }) => {
// Some implementations may not have explicit back buttons
const backButton = page.locator(
`${tid(S.BOARD_CANCEL_BUTTON, app)}, button[aria-label*="Back"i], a[href*="back"]`,
);
if ((await backButton.count()) > 0) {
const urlBefore = page.url();
await backButton.first().click();
await page.waitForTimeout(500);
const urlAfter = page.url();
// URL should have changed
expect(urlAfter).not.toBe(urlBefore);
} else {
test.skip(true, 'Back button not found in this app');
}
});
test('151: Page renders with correct layout (header + content)', async ({ page }) => {
// Look for main layout structure
const body = page.locator('body');
expect(await body.count()).toBeGreaterThan(0);
// Should have some content beyond just empty page
const content = page.locator('main, [role="main"], .container, .content, .page-content');
const contentCount = await content.count();
expect(contentCount).toBeGreaterThanOrEqual(0);
});
test('152: All text content matches current locale', async ({ page, locale }) => {
// Check that locale is reflected in visible content or attributes
const html = page.locator('html');
const langAttr = await html.getAttribute('lang');
if (langAttr) {
expect(langAttr.toLowerCase()).toMatch(/ru|en/i);
}
});
// ────────────────────────────────────────────────────────────────────────
// Flight Header & Basic Info (8 tests: 153-160)
// ────────────────────────────────────────────────────────────────────────
test('153: Flight number is displayed with correct formatting', async ({ page, app }) => {
const flightNumber = page.locator(tid(S.DETAILS_FLIGHT_NUMBER, app));
if ((await flightNumber.count()) > 0) {
await expect(flightNumber).toBeVisible();
const text = await flightNumber.textContent();
expect(text).toMatch(/SU\s*1234|SU1234/i);
} else {
test.skip(true, 'Flight number selector not found');
}
});
test('154: Flight status badge shows current status', async ({ page, app }) => {
const statusBadge = page.locator(tid(S.DETAILS_STATUS, app));
if ((await statusBadge.count()) > 0) {
await expect(statusBadge).toBeVisible();
const text = await statusBadge.textContent();
expect(text).toBeTruthy();
} else {
test.skip(true, 'Status badge not found');
}
});
test('155: Airline logo is displayed', async ({ page, app }) => {
const logo = page.locator(tid(S.DETAILS_OPERATOR_LOGO, app));
if ((await logo.count()) > 0) {
await expect(logo).toBeVisible();
} else {
// Fallback: look for any image or logo element
const altLogo = page.locator('img[alt*="airline" i], img[alt*="aeroflot" i]');
if ((await altLogo.count()) > 0) {
await expect(altLogo.first()).toBeVisible();
} else {
test.skip(true, 'Airline logo not found');
}
}
});
test('156: Aircraft model is displayed', async ({ page, app }) => {
const aircraftModel = page.locator(tid(S.DETAILS_AIRCRAFT_MODEL, app));
if ((await aircraftModel.count()) > 0) {
await expect(aircraftModel).toBeVisible();
const text = await aircraftModel.textContent();
expect(text).toBeTruthy();
} else {
// Fallback: look for aircraft text
const altAircraft = page.locator('[data-testid*="aircraft"], [data-testid*="equipment"]');
if ((await altAircraft.count()) > 0) {
await expect(altAircraft.first()).toBeVisible();
} else {
test.skip(true, 'Aircraft model not found');
}
}
});
test('157: Departure time is displayed with timezone', async ({ page, app }) => {
const depTime = page.locator(tid(S.DETAILS_DEPARTURE_TIME, app));
if ((await depTime.count()) > 0) {
await expect(depTime).toBeVisible();
const text = await depTime.textContent();
expect(text).toMatch(/\d+:\d+/);
} else {
test.skip(true, 'Departure time selector not found');
}
});
test('158: Arrival time is displayed with timezone', async ({ page, app }) => {
const arrTime = page.locator(tid(S.DETAILS_ARRIVAL_TIME, app));
if ((await arrTime.count()) > 0) {
await expect(arrTime).toBeVisible();
const text = await arrTime.textContent();
expect(text).toMatch(/\d+:\d+/);
} else {
test.skip(true, 'Arrival time selector not found');
}
});
test('159: Flight duration is displayed', async ({ page, app }) => {
const duration = page.locator(tid(S.DETAILS_DURATION, app));
if ((await duration.count()) > 0) {
await expect(duration).toBeVisible();
const text = await duration.textContent();
expect(text).toMatch(/\d+h/);
} else {
// Fallback: look for duration text
const altDuration = page.locator('text=/\\d+h\\s*\\d*m/i');
if ((await altDuration.count()) > 0) {
await expect(altDuration.first()).toBeVisible();
} else {
test.skip(true, 'Duration not found');
}
}
});
test('160: Days of operation info is displayed', async ({ page }) => {
// Look for day badges or operating schedule info
const dayBadges = page.locator(
'[data-testid*="day" i], .day-badge, [data-testid*="operating" i]',
);
if ((await dayBadges.count()) > 0) {
await expect(dayBadges.first()).toBeVisible();
} else {
test.skip(true, 'Days of operation info not displayed');
}
});
// ────────────────────────────────────────────────────────────────────────
// Departure & Arrival Section (6 tests: 161-166)
// ────────────────────────────────────────────────────────────────────────
test('161: Departure station code is displayed', async ({ page, app }) => {
const depStation = page.locator(tid(S.DETAILS_DEPARTURE_STATION, app));
if ((await depStation.count()) > 0) {
await expect(depStation).toBeVisible();
const text = await depStation.textContent();
expect(text).toMatch(/MOW|LED|SVO/i);
} else {
test.skip(true, 'Departure station selector not found');
}
});
test('162: Departure station name is displayed', async ({ page }) => {
// Look for city name (e.g., "Moscow") or station name
const depName = page.locator('[data-testid*="departure"] [data-testid*="name"]');
if ((await depName.count()) > 0) {
await expect(depName.first()).toBeVisible();
} else {
test.skip(true, 'Departure station name not found');
}
});
test('163: Departure terminal is displayed', async ({ page, app }) => {
const terminal = page.locator(tid(S.DETAILS_TERMINAL_LINK, app));
if ((await terminal.count()) > 0) {
await expect(terminal).toBeVisible();
} else {
// Fallback: look for terminal text
const altTerminal = page.locator('text=/Terminal\\s*\\d+/i');
if ((await altTerminal.count()) > 0) {
await expect(altTerminal.first()).toBeVisible();
} else {
test.skip(true, 'Terminal info not displayed (may be optional)');
}
}
});
test('164: Arrival station code is displayed', async ({ page, app }) => {
const arrStation = page.locator(tid(S.DETAILS_ARRIVAL_STATION, app));
if ((await arrStation.count()) > 0) {
await expect(arrStation).toBeVisible();
const text = await arrStation.textContent();
expect(text).toMatch(/MOW|LED|SVO|VKO/i);
} else {
test.skip(true, 'Arrival station selector not found');
}
});
test('165: Arrival station name is displayed', async ({ page }) => {
// Look for city name
const arrName = page.locator('[data-testid*="arrival"] [data-testid*="name"]');
if ((await arrName.count()) > 0) {
await expect(arrName.first()).toBeVisible();
} else {
test.skip(true, 'Arrival station name not found');
}
});
test('166: Arrival terminal is displayed', async ({ page }) => {
// Look for terminal information in arrival section
const terminal = page.locator('[data-testid*="arrival"]').locator('text=/Terminal/');
if ((await terminal.count()) > 0) {
await expect(terminal.first()).toBeVisible();
} else {
test.skip(true, 'Arrival terminal not displayed (may be optional)');
}
});
// ────────────────────────────────────────────────────────────────────────
// Route & Transfer Info (4 tests: 167-170)
// ────────────────────────────────────────────────────────────────────────
test('167: Direct flight shows no intermediate stops', async ({ page, app }) => {
// For a direct flight, there should be no transfer section visible
// or the transfer section should explicitly state "Direct"
const transferSection = page.locator(tid(S.DETAILS_TRANSFER_SECTION, app));
if ((await transferSection.count()) > 0) {
const text = await transferSection.textContent();
expect(text).toMatch(/Direct|no transfer|прямой/i);
} else {
test.skip(true, 'Transfer section not found (expected for direct flight)');
}
});
test('168: Transfer flight shows transfer station', async ({ page, app }) => {
// If flight has transfers, the transfer station should be shown
const transferSection = page.locator(tid(S.DETAILS_TRANSFER_SECTION, app));
if ((await transferSection.count()) > 0) {
const text = await transferSection.textContent();
// Should contain either a station code or explicit transfer info
expect(text).toBeTruthy();
} else {
test.skip(true, 'Transfer section not shown (flight may be direct)');
}
});
test('169: Full route section shows all segments', async ({ page, app }) => {
const fullRoute = page.locator(tid(S.DETAILS_FULL_ROUTE, app));
if ((await fullRoute.count()) > 0) {
await expect(fullRoute).toBeVisible();
} else {
test.skip(true, 'Full route section not found');
}
});
test('170: Transfer time is displayed for multi-segment flights', async ({ page }) => {
// Look for transfer time information
const transferTime = page.locator('[data-testid*="transfer"]');
if ((await transferTime.count()) > 0) {
await expect(transferTime.first()).toBeVisible();
} else {
test.skip(true, 'Transfer time not displayed (may be direct flight)');
}
});
// ────────────────────────────────────────────────────────────────────────
// Action Buttons (7 tests: 171-177)
// ────────────────────────────────────────────────────────────────────────
test('171: "Buy Ticket" button is visible and clickable', async ({ page, app }) => {
const buyBtn = page.locator(tid(S.DETAILS_BUY_TICKET_BUTTON, app));
if ((await buyBtn.count()) > 0) {
await expect(buyBtn).toBeVisible();
await expect(buyBtn).toBeEnabled();
} else {
test.skip(true, 'Buy Ticket button not found');
}
});
test('172: "Register" button is visible and clickable', async ({ page, app }) => {
const regBtn = page.locator(tid(S.DETAILS_REGISTRATION_BUTTON, app));
if ((await regBtn.count()) > 0) {
await expect(regBtn).toBeVisible();
await expect(regBtn).toBeEnabled();
} else {
test.skip(true, 'Registration button not found');
}
});
test('173: "Print" button is visible and clickable', async ({ page, app }) => {
const printBtn = page.locator(tid(S.DETAILS_PRINT_BUTTON, app));
if ((await printBtn.count()) > 0) {
await expect(printBtn).toBeVisible();
await expect(printBtn).toBeEnabled();
} else {
test.skip(true, 'Print button not found');
}
});
test('174: "Share" button is visible and clickable', async ({ page, app }) => {
const shareBtn = page.locator(tid(S.DETAILS_SHARE_BUTTON, app));
if ((await shareBtn.count()) > 0) {
await expect(shareBtn).toBeVisible();
await expect(shareBtn).toBeEnabled();
} else {
test.skip(true, 'Share button not found');
}
});
test('175: "Flight Status" button is visible and clickable', async ({ page, app }) => {
const statusBtn = page.locator(tid(S.DETAILS_FLIGHT_STATUS_BUTTON, app));
if ((await statusBtn.count()) > 0) {
await expect(statusBtn).toBeVisible();
await expect(statusBtn).toBeEnabled();
} else {
test.skip(true, 'Flight Status button not found');
}
});
test('176: "Book Now" button leads to booking page', async ({ page }) => {
// Look for a button that triggers booking
const bookBtn = page.locator('button[data-testid*="booking"], a[href*="book"]');
if ((await bookBtn.count()) > 0) {
const href = await bookBtn.first().getAttribute('href');
if (href) {
expect(href).toBeTruthy();
}
} else {
test.skip(true, 'Book Now button not found');
}
});
test('177: Share button opens share dialog', async ({ page, app }) => {
const shareBtn = page.locator(tid(S.DETAILS_SHARE_BUTTON, app));
if ((await shareBtn.count()) > 0) {
await shareBtn.first().click();
await page.waitForTimeout(500);
// Check if a dialog or modal opened
const dialog = page.locator('[role="dialog"], .modal, .share-dialog');
const dialogOrClipboard =
(await dialog.count()) > 0 ||
(await page.evaluate(() => navigator.clipboard !== undefined));
expect(dialogOrClipboard).toBeTruthy();
} else {
test.skip(true, 'Share button not found');
}
});
// ────────────────────────────────────────────────────────────────────────
// Additional Info & Details (4 tests: 178-181)
// ────────────────────────────────────────────────────────────────────────
test('178: Equipment info (aircraft type) is displayed', async ({ page }) => {
// Look for aircraft/equipment information
const equipment = page.locator('[data-testid*="equipment"], [data-testid*="aircraft"]');
if ((await equipment.count()) > 0) {
await expect(equipment.first()).toBeVisible();
} else {
test.skip(true, 'Equipment info not displayed');
}
});
test('179: Codeshare info (if applicable) is displayed', async ({ page }) => {
// Look for codeshare information
const codeshare = page.locator('[data-testid*="codeshare"]');
if ((await codeshare.count()) > 0) {
await expect(codeshare.first()).toBeVisible();
} else {
test.skip(true, 'Codeshare info not displayed (may not apply)');
}
});
test('180: Frequent flyer/baggage info is displayed', async ({ page }) => {
// Look for baggage or frequent flyer information
const baggage = page.locator(
'[data-testid*="baggage"], [data-testid*="miles"], [data-testid*="frequent"]',
);
if ((await baggage.count()) > 0) {
await expect(baggage.first()).toBeVisible();
} else {
test.skip(true, 'Baggage/frequent flyer info not displayed (may be optional)');
}
});
test('181: Page renders without console errors', async ({ page }) => {
// Collect all console messages from the page
const errors: string[] = [];
page.on('console', (msg) => {
if (msg.type() === 'error') {
errors.push(msg.text());
}
});
// Check for uncaught exceptions
page.on('pageerror', (error) => {
errors.push(error.toString());
});
// Wait a bit for any delayed errors
await page.waitForTimeout(500);
// Filter out known safe errors and network-related errors
const safeErrors = errors.filter(
(err) =>
!err.includes('Loading chunk') &&
!err.includes('NetworkError') &&
!err.includes('404') &&
!err.includes('CORS') &&
!err.includes('Failed to fetch') &&
!err.includes('aeroflot.ru'),
);
expect(safeErrors).toEqual([]);
});
});