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

692 lines
26 KiB
TypeScript

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 Results — tests 212-237 (26 tests)
/**
* Mock schedule results API endpoint for Angular.
* Provides sample flight schedule data for a route.
*/
async function mockScheduleResultsAPIs(page: import('@playwright/test').Page) {
await mockAngularAPIs(page);
// Mock schedule results API endpoint: /api/Requests/{id}/getschedule
await page.route('**/api/Requests/*/getschedule', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
flights: [
{
number: 'SU 100',
departureTime: '06:00',
arrivalTime: '12:00',
duration: '6h 0m',
aircraft: 'Boeing 777',
stops: 0,
price: 15000,
available: true,
},
{
number: 'SU 102',
departureTime: '08:30',
arrivalTime: '14:30',
duration: '6h 0m',
aircraft: 'Airbus A330',
stops: 0,
price: 12500,
available: true,
},
{
number: 'SU 104',
departureTime: '14:00',
arrivalTime: '20:00',
duration: '6h 0m',
aircraft: 'Boeing 747',
stops: 1,
price: 9500,
available: true,
},
],
week: [
'2026-04-13',
'2026-04-14',
'2026-04-15',
'2026-04-16',
'2026-04-17',
'2026-04-18',
'2026-04-19',
],
currentDay: '2026-04-15',
returnFlights: [
{
number: 'SU 200',
departureTime: '13:00',
arrivalTime: '19:00',
duration: '6h 0m',
aircraft: 'Boeing 777',
stops: 0,
price: 14500,
available: true,
},
{
number: 'SU 202',
departureTime: '15:30',
arrivalTime: '21:30',
duration: '6h 0m',
aircraft: 'Airbus A330',
stops: 0,
price: 11000,
available: true,
},
],
}),
});
});
}
/**
* 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')}`;
}
/**
* Navigate to schedule results page.
* Returns true if the page loaded successfully, false if 404 or error.
*/
async function gotoScheduleResults(
page: import('@playwright/test').Page,
localePath: (path: string) => string,
from: string = 'SVO',
to: string = 'JFK',
date: string = '20260415',
): Promise<boolean> {
const params = new URLSearchParams({
from,
to,
date,
directOnly: 'false',
});
const url = localePath(`schedule?${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 Results (Cross-App)', () => {
test.beforeEach(async ({ page, app, localePath }) => {
await mockAllAPIs(page);
await mockScheduleResultsAPIs(page);
// Navigate to schedule results with sample parameters
const navigated = await gotoScheduleResults(page, localePath);
if (!navigated) {
test.skip(true, 'Schedule results page not available in this app');
return;
}
await page.waitForLoadState('networkidle');
await page.waitForTimeout(500);
});
// ─────────────────────────────────────────────────────────────────────────
// Page Load & Navigation (3 tests: 212-214)
// ─────────────────────────────────────────────────────────────────────────
test('212: Schedule results 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('213: Results page displays correct search parameters (departure, arrival, date)', async ({
page,
localePath,
}) => {
// Check URL contains search parameters
const url = page.url();
expect(url).toContain('schedule');
expect(url).toContain('from=');
expect(url).toContain('to=');
expect(url).toContain('date=');
// Verify parameters are preserved in URL
const fromParam = new URL(url).searchParams.get('from');
const toParam = new URL(url).searchParams.get('to');
const dateParam = new URL(url).searchParams.get('date');
expect(fromParam).toBeTruthy();
expect(toParam).toBeTruthy();
expect(dateParam).toBeTruthy();
});
test('214: Back button navigates to schedule search page', async ({ page, app, localePath }) => {
// Look for back button — might be in details view or header
const backBtn = page.locator(
`${tid(S.SCHEDULE_DETAILS_BACK_BUTTON, app)}, button[aria-label*="Back"i], a[href*="back"], .back-button`,
);
// Back button may not exist on results page directly, so skip if not found
if ((await backBtn.count()) === 0) {
test.skip(true, 'Back button not found in results header');
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);
});
// ─────────────────────────────────────────────────────────────────────────
// Week Navigation (5 tests: 215-219)
// ─────────────────────────────────────────────────────────────────────────
test('215: Week tabs are displayed for each day of the week', async ({ page, app }) => {
const weekTabsContainer = page.locator(tid(S.SCHEDULE_WEEK_TABS, app));
if ((await weekTabsContainer.count()) === 0) {
test.skip(true, 'Week tabs container not found');
return;
}
const weekTabs = page.locator(tid(S.SCHEDULE_WEEK_TAB, app));
const tabCount = await weekTabs.count();
// Should have at least 5-7 tabs (Mon-Sun or similar)
expect(tabCount).toBeGreaterThanOrEqual(5);
});
test('216: Current day week tab is highlighted', async ({ page, app }) => {
const weekTabs = page.locator(tid(S.SCHEDULE_WEEK_TAB, app));
if ((await weekTabs.count()) === 0) {
test.skip(true, 'Week tabs 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 weekTabs.count()); i++) {
const tab = weekTabs.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('217: Clicking week tab switches displayed flights', async ({ page, app }) => {
const weekTabs = page.locator(tid(S.SCHEDULE_WEEK_TAB, app));
if ((await weekTabs.count()) < 2) {
test.skip(true, 'Not enough week tabs to test switching');
return;
}
// Get flight list before switching tab
const flightItemsBefore = page.locator(tid(S.SCHEDULE_FLIGHT_ITEM, app));
const countBefore = await flightItemsBefore.count();
// Click second tab
const secondTab = weekTabs.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('218: Previous week button navigates to previous week', async ({ page, app }) => {
const prevBtn = page.locator(tid(S.SCHEDULE_WEEK_PREV, app));
if ((await prevBtn.count()) === 0) {
test.skip(true, 'Previous week button not found');
return;
}
const urlBefore = page.url();
await prevBtn.evaluate((el: HTMLElement) => el.click());
await page.waitForTimeout(1000);
const urlAfter = page.url();
// URL should change to reflect previous week
// (date param or some week identifier should change)
expect(urlAfter.length).toBeGreaterThan(0);
});
test('219: Next week button navigates to next week', async ({ page, app }) => {
const nextBtn = page.locator(tid(S.SCHEDULE_WEEK_NEXT, app));
if ((await nextBtn.count()) === 0) {
test.skip(true, 'Next week button not found');
return;
}
const urlBefore = page.url();
await nextBtn.evaluate((el: HTMLElement) => el.click());
await page.waitForTimeout(1000);
const urlAfter = page.url();
// URL should change to reflect next week
expect(urlAfter.length).toBeGreaterThan(0);
});
// ─────────────────────────────────────────────────────────────────────────
// Flight Results Display (6 tests: 220-225)
// ─────────────────────────────────────────────────────────────────────────
test('220: Flight result list is visible with multiple flights', async ({ page, app }) => {
const flightItems = page.locator(tid(S.SCHEDULE_FLIGHT_ITEM, app));
if ((await flightItems.count()) === 0) {
test.skip(true, 'No flight items found in results');
return;
}
// Should have multiple flights
const count = await flightItems.count();
expect(count).toBeGreaterThan(0);
// All visible
for (let i = 0; i < Math.min(3, count); i++) {
await expect(flightItems.nth(i)).toBeVisible();
}
});
test('221: Each flight shows departure time', async ({ page, app }) => {
const flightItems = page.locator(tid(S.SCHEDULE_FLIGHT_ITEM, app));
if ((await flightItems.count()) === 0) {
test.skip(true, 'No flight items found');
return;
}
// Check first flight for departure time
const firstFlight = flightItems.first();
const text = await firstFlight.textContent();
// Should contain time pattern (HH:MM)
expect(text).toMatch(/\d{1,2}:\d{2}/);
});
test('222: Each flight shows arrival time', async ({ page, app }) => {
const flightItems = page.locator(tid(S.SCHEDULE_FLIGHT_ITEM, app));
if ((await flightItems.count()) === 0) {
test.skip(true, 'No flight items found');
return;
}
// Check first flight for arrival time (should have at least 2 time patterns)
const firstFlight = flightItems.first();
const text = await firstFlight.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('223: Each flight shows flight number', async ({ page, app }) => {
const flightItems = page.locator(tid(S.SCHEDULE_FLIGHT_ITEM, app));
if ((await flightItems.count()) === 0) {
test.skip(true, 'No flight items found');
return;
}
// Check first flight for flight number pattern (e.g., SU 100)
const firstFlight = flightItems.first();
const text = await firstFlight.textContent();
// Should contain airline code + flight number pattern
expect(text).toMatch(/[A-Z]{2}\s*\d+/);
});
test('224: Each flight shows airline logo or identifier', async ({ page, app }) => {
const flightItems = page.locator(tid(S.SCHEDULE_FLIGHT_ITEM, app));
if ((await flightItems.count()) === 0) {
test.skip(true, 'No flight items found');
return;
}
const firstFlight = flightItems.first();
const text = await firstFlight.textContent();
// Airline name or code should be present
const hasAirlineIndicator = text && (text.includes('SU') || text.includes('Aeroflot'));
expect(hasAirlineIndicator).toBe(true);
});
test('225: Each flight shows price (if available)', async ({ page, app }) => {
const flightItems = page.locator(tid(S.SCHEDULE_FLIGHT_ITEM, app));
if ((await flightItems.count()) === 0) {
test.skip(true, 'No flight items found');
return;
}
const firstFlight = flightItems.first();
const text = await firstFlight.textContent();
// Price may be shown (number with currency or price pattern)
// Some implementations may not show price, so this is informational
if (text) {
expect(text.length).toBeGreaterThan(10);
}
});
// ─────────────────────────────────────────────────────────────────────────
// Flight Details Access (3 tests: 226-228)
// ─────────────────────────────────────────────────────────────────────────
test('226: Clicking flight result expands to show details', async ({ page, app }) => {
const flightItems = page.locator(tid(S.SCHEDULE_FLIGHT_ITEM, app));
if ((await flightItems.count()) === 0) {
test.skip(true, 'No flight items found');
return;
}
const firstFlight = flightItems.first();
const textBefore = await firstFlight.textContent();
// Try clicking the flight item
await firstFlight.evaluate((el: HTMLElement) => el.click());
await page.waitForTimeout(800);
const textAfter = await firstFlight.textContent();
// After clicking, content may expand to show more details
expect(textAfter?.length || 0).toBeGreaterThanOrEqual((textBefore?.length || 0) * 0.8);
});
test('227: Expanded flight shows full route information', async ({ page, app }) => {
const flightItems = page.locator(tid(S.SCHEDULE_FLIGHT_ITEM, app));
if ((await flightItems.count()) === 0) {
test.skip(true, 'No flight items found');
return;
}
const firstFlight = flightItems.first();
const text = await firstFlight.textContent();
// Should contain departure/arrival info (times, cities, or codes)
// Route info might include city codes or station names
const hasRouteInfo =
text &&
(/[A-Z]{3}/.test(text) || // Airport codes like MOW, SVO
/\d{1,2}:\d{2}/.test(text)); // Times like 10:30
expect(hasRouteInfo).toBe(true);
});
test('228: Expanded flight shows duration and aircraft type', async ({ page, app }) => {
const flightItems = page.locator(tid(S.SCHEDULE_FLIGHT_ITEM, app));
if ((await flightItems.count()) === 0) {
test.skip(true, 'No flight items found');
return;
}
const firstFlight = flightItems.first();
const text = await firstFlight.textContent();
// Should show duration (e.g., "6h 0m" or "360 minutes") and aircraft info
// Aircraft type typically appears in schedule results
const hasFlightInfo = text && text.length > 30; // Some indication of extended info
expect(hasFlightInfo).toBe(true);
});
// ─────────────────────────────────────────────────────────────────────────
// Sorting & Filtering (5 tests: 229-233)
// ─────────────────────────────────────────────────────────────────────────
test('229: Sort dropdown is visible', async ({ page, app }) => {
const sortDropdown = page.locator(tid(S.SCHEDULE_SORT_DROPDOWN, app));
if ((await sortDropdown.count()) === 0) {
test.skip(true, 'Sort dropdown not found on results page');
return;
}
await expect(sortDropdown.first()).toBeVisible();
});
test('230: Sorting by departure time changes flight order', async ({ page, app }) => {
const sortDropdown = page.locator(tid(S.SCHEDULE_SORT_DROPDOWN, app));
if ((await sortDropdown.count()) === 0) {
test.skip(true, 'Sort dropdown not found');
return;
}
const flightItemsBefore = page.locator(tid(S.SCHEDULE_FLIGHT_ITEM, app));
const countBefore = await flightItemsBefore.count();
if (countBefore < 2) {
test.skip(true, 'Not enough flights to test sorting');
return;
}
// Get first flight before sorting
const firstFlightBefore = await flightItemsBefore.first().textContent();
// Click dropdown to open options
await sortDropdown.first().evaluate((el: HTMLElement) => el.click());
await page.waitForTimeout(500);
// Look for sort option (may have different names: "Departure", "By Time", etc.)
const sortOptions = page.locator(
'p-dropdown-item, [role="option"], .p-dropdown-items-wrapper li, li[role="option"]',
);
if ((await sortOptions.count()) > 0) {
// Click first non-current option
await sortOptions.first().evaluate((el: HTMLElement) => el.click());
await page.waitForTimeout(800);
// Verify order may have changed (or at least verify we can sort)
const flightItemsAfter = page.locator(tid(S.SCHEDULE_FLIGHT_ITEM, app));
expect(await flightItemsAfter.count()).toBeGreaterThan(0);
} else {
test.skip(true, 'Sort options not accessible');
}
});
test('231: Sorting by arrival time changes flight order', async ({ page, app }) => {
const sortDropdown = page.locator(tid(S.SCHEDULE_SORT_DROPDOWN, app));
if ((await sortDropdown.count()) === 0) {
test.skip(true, 'Sort dropdown not found');
return;
}
const flightItems = page.locator(tid(S.SCHEDULE_FLIGHT_ITEM, app));
if ((await flightItems.count()) < 2) {
test.skip(true, 'Not enough flights to test sorting');
return;
}
// Open dropdown
await sortDropdown.first().evaluate((el: HTMLElement) => el.click());
await page.waitForTimeout(500);
// Click on a sort option (e.g., second option if available)
const sortOptions = page.locator(
'p-dropdown-item, [role="option"], .p-dropdown-items-wrapper li, li[role="option"]',
);
if ((await sortOptions.count()) > 1) {
await sortOptions.nth(1).evaluate((el: HTMLElement) => el.click());
await page.waitForTimeout(800);
// Verify flights are still displayed
expect(await flightItems.count()).toBeGreaterThan(0);
} else {
test.skip(true, 'Not enough sort options available');
}
});
test('232: Sorting by price changes flight order (if available)', async ({ page, app }) => {
const sortDropdown = page.locator(tid(S.SCHEDULE_SORT_DROPDOWN, app));
if ((await sortDropdown.count()) === 0) {
test.skip(true, 'Sort dropdown not found');
return;
}
const flightItems = page.locator(tid(S.SCHEDULE_FLIGHT_ITEM, app));
const countBefore = await flightItems.count();
if (countBefore < 2) {
test.skip(true, 'Not enough flights to test sorting');
return;
}
// Try to open and click a sort option
await sortDropdown.first().evaluate((el: HTMLElement) => el.click());
await page.waitForTimeout(500);
const sortOptions = page.locator(
'p-dropdown-item, [role="option"], .p-dropdown-items-wrapper li, li[role="option"]',
);
if ((await sortOptions.count()) > 0) {
// Click any available option
const optionIndex = Math.min(2, (await sortOptions.count()) - 1);
await sortOptions.nth(optionIndex).evaluate((el: HTMLElement) => el.click());
await page.waitForTimeout(800);
// Verify state is consistent
expect(await flightItems.count()).toBeGreaterThan(0);
} else {
test.skip(true, 'No sort options found');
}
});
test('233: Direction switch (outbound/return) toggles flight display', async ({ page, app }) => {
const directionSwitch = page.locator(tid(S.SCHEDULE_DIRECTION_SWITCH, app));
if ((await directionSwitch.count()) === 0) {
test.skip(true, 'Direction switch not found (may not be round-trip search)');
return;
}
const flightItemsBefore = page.locator(tid(S.SCHEDULE_FLIGHT_ITEM, app));
const countBefore = await flightItemsBefore.count();
// Click direction switch
await directionSwitch.first().evaluate((el: HTMLElement) => el.click());
await page.waitForTimeout(800);
// Should still have flights displayed (may be different flights for return leg)
const flightItemsAfter = page.locator(tid(S.SCHEDULE_FLIGHT_ITEM, app));
const countAfter = await flightItemsAfter.count();
expect(countAfter).toBeGreaterThanOrEqual(0);
});
// ─────────────────────────────────────────────────────────────────────────
// Return Flight Toggle (2 tests: 234-235)
// ─────────────────────────────────────────────────────────────────────────
test('234: Return flight tab appears for round-trip searches', async ({ page, app }) => {
// Check if return flight tab/section exists
const returnTab = page.locator(
`${tid(S.SCHEDULE_DIRECTION_SWITCH, app)}, [data-testid*="return"], button:has-text("Return")`,
);
if ((await returnTab.count()) === 0) {
test.skip(true, 'Return flight tab/toggle not found (may be one-way search)');
return;
}
await expect(returnTab.first()).toBeVisible();
});
test('235: Switching to return flights shows different flight list', async ({ page, app }) => {
const directionSwitch = page.locator(tid(S.SCHEDULE_DIRECTION_SWITCH, app));
if ((await directionSwitch.count()) === 0) {
test.skip(true, 'Direction switch not found (not a round-trip search)');
return;
}
// Get initial flight list content
const flightItems = page.locator(tid(S.SCHEDULE_FLIGHT_ITEM, app));
const flightCountBefore = await flightItems.count();
// Switch to return flights
await directionSwitch.first().evaluate((el: HTMLElement) => el.click());
await page.waitForTimeout(800);
// Verify we still have flights displayed
const flightItemsAfter = page.locator(tid(S.SCHEDULE_FLIGHT_ITEM, app));
const flightCountAfter = await flightItemsAfter.count();
expect(flightCountAfter).toBeGreaterThanOrEqual(0);
});
// ─────────────────────────────────────────────────────────────────────────
// Empty & Error States (2 tests: 236-237)
// ─────────────────────────────────────────────────────────────────────────
test('236: No results message displays when no flights available', async ({
page,
app,
localePath,
}) => {
// Navigate to a route with potentially no flights (e.g., past date or invalid route)
const noResultsPageLoaded = await gotoScheduleResults(
page,
localePath,
'MOW', // Moscow
'PEK', // Beijing (may have limited flights)
'20261231', // End of year
);
if (!noResultsPageLoaded) {
test.skip(true, 'Could not navigate to schedule page');
return;
}
// Check for empty state message
const emptyMessage = page.locator(
`${tid(S.SCHEDULE_LOADER, app)}, .empty-state, [data-testid*="empty"], .no-results`,
);
// Empty message may or may not exist depending on app
if ((await emptyMessage.count()) > 0) {
const text = await emptyMessage.first().textContent();
expect(text?.length || 0).toBeGreaterThan(0);
}
});
test('237: Loading spinner shows during flight fetch', async ({ page, app }) => {
// Look for loading indicator
const loader = page.locator(tid(S.SCHEDULE_LOADER, app));
const spinnerIndicators = page.locator(
`${tid(S.SCHEDULE_LOADER, app)}, [data-testid*="loader"], [data-testid*="loading"], .spinner, .p-progress-spinner`,
);
// May not see spinner if page already loaded
if ((await spinnerIndicators.count()) > 0) {
await expect(spinnerIndicators.first()).toBeVisible();
}
// Verify page still loads successfully
const flightItems = page.locator(tid(S.SCHEDULE_FLIGHT_ITEM, app));
expect(await flightItems.count()).toBeGreaterThanOrEqual(0);
});
});