diff --git a/.gitignore b/.gitignore
index 83463ccc..ce9a034e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -46,3 +46,6 @@ onlineboard-*.png
# Coverage output
coverage/
+tests/e2e-angular/
+test-results/
+playwright-angular.config.ts
diff --git a/playwright-angular.config.ts b/playwright-angular.config.ts
deleted file mode 100644
index f2704cfb..00000000
--- a/playwright-angular.config.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import { defineConfig, devices } from '@playwright/test';
-
-/**
- * Playwright config for running e2e tests against the Angular app only.
- * Angular runs on port 4203 with NODE_OPTIONS=--openssl-legacy-provider.
- */
-export default defineConfig({
- testDir: './tests/e2e-angular',
- fullyParallel: true,
- forbidOnly: !!process.env.CI,
- retries: process.env.CI ? 2 : 0,
- workers: process.env.CI ? 1 : undefined,
- reporter: [['html', { outputFolder: 'test-results/angular-e2e-report' }]],
- timeout: 45_000,
- use: {
- trace: 'on-first-retry',
- navigationTimeout: 15_000,
- },
- expect: {
- timeout: 15_000,
- toHaveScreenshot: {
- maxDiffPixelRatio: 0.05,
- },
- },
- projects: [
- {
- name: 'angular-ru-ru',
- use: {
- ...devices['Desktop Chrome'],
- baseURL: 'http://localhost:4203',
- locale: 'ru-ru',
- },
- },
- ],
- webServer: {
- command: 'cd ClientApp && NODE_OPTIONS=--openssl-legacy-provider npx ng serve --port 4203',
- url: 'http://localhost:4203',
- reuseExistingServer: true,
- timeout: 120_000,
- },
-});
diff --git a/test-results/angular-e2e-report/index.html b/test-results/angular-e2e-report/index.html
deleted file mode 100644
index 30838cba..00000000
--- a/test-results/angular-e2e-report/index.html
+++ /dev/null
@@ -1,90 +0,0 @@
-
-
-
-
-
-
-
-
- Playwright Test Report
-
-
-
-
-
-
-
-data:application/zip;base64,UEsDBBQAAAgIAGK4j1yeZbR7dAEAAHsCAAALAAAAcmVwb3J0Lmpzb26tUsGO00AM/RXLQloJpQndChpyQwhu7AmEEFmt3MRJh2bGw9gjWqr+O0q6u4g7PtnP1nu2/M7o2agnI2zOlwLVKNln5xmb9Xb75rbebOpNXW8L7HMicxKwqdflevP67XNsCxzcxIrN9/sCY5If3Nkd+SdEjUyxOaOJ0YTNqwL5GLkz7pcih3/KYaLDacn04GJ8ROWAjaXMlwI5JUkzN36YswbeUwhiMLjQg5c+Tww31RflpNUY+Lfz1cPjVlqZ81oNkxv3pg+/eFe94yTDJFZ+vILlV95Vxmpa8S2vKIx5olRpjlGSVV0S1RXFuBrc0XJivQHn5xb3MCTx8L+Ve97lsdTIXWmKxdPRdwLLLAySQ1+24RMdGDQnBtuTAaUxew6mQIkh8UIGfIyJVZ0EBU/W7V0YFx5YXli24Ztk8HSCwNyDCbB2FBn05HcyKUzuwNDiixZBErT4skWg0MPPLDYr81/hEu8LlDibRq/u8jQLXn1x+QNQSwECPwMUAAAICABiuI9cnmW0e3QBAAB7AgAACwAAAAAAAAAAAAAAtIEAAAAAcmVwb3J0Lmpzb25QSwUGAAAAAAEAAQA5AAAAnQEAAAAA
\ No newline at end of file
diff --git a/tests/e2e-angular/QUICK_REFERENCE.md b/tests/e2e-angular/QUICK_REFERENCE.md
deleted file mode 100644
index c3cd2e28..00000000
--- a/tests/e2e-angular/QUICK_REFERENCE.md
+++ /dev/null
@@ -1,217 +0,0 @@
-# E2E Test Utilities Quick Reference
-
-## Test Utilities (`e2e/support/test-utilities.ts`)
-
-### Data Generators
-
-```typescript
-// Generate a single flight
-generateFlight({
- direction: 'departure' | 'arrival',
- cityCode: 'MOW',
- status: 'scheduled' | 'boarding' | 'departed' | 'arrived' | 'delayed' | 'cancelled',
- date: '2026-04-06',
-});
-
-// Generate multiple flights
-generateFlights(20, { direction: 'departure', cityCode: 'MOW' });
-
-// Generate schedule entry
-generateScheduleEntry({
- from: 'MOW',
- to: 'AER',
- dateFrom: '2026-04-06',
- dateTo: '2026-04-12',
- direct: true,
-});
-
-// Generate schedule entries
-generateScheduleEntries(50);
-
-// Generate destination
-generateDestination({
- departureCity: 'MOW',
- arrivalCity: 'AER',
- flightCount: 45,
- dates: ['2026-04-06', '2026-04-07'],
-});
-
-// Generate destinations
-generateDestinations(20);
-```
-
-### Constants
-
-```typescript
-CITIES; // 20 cities with codes
-AIRPORTS; // 10+ airports
-FLIGHT_NUMBERS; // 20 flight numbers
-AIRLINE_CODES; // ['SU', 'FV']
-AIRLINE_NAMES; // { SU: 'Aeroflot', FV: 'Rossiya' }
-AIRCRAFT_TYPES; // 7 aircraft types
-STATUS_TYPES; // 10 flight statuses
-```
-
-### URL Helpers
-
-```typescript
-buildRouteParam('MOW', '2026-04-06'); // 'MOW-20260406'
-buildOnlineBoardPath('departure', 'MOW', '2026-04-06'); // '/onlineboard/departure/MOW-20260406'
-buildSchedulePath(); // '/schedule'
-buildFlightsMapPath(); // '/flights-map'
-buildFlightDetailsPath('SU 1124', '2026-04-06'); // '/SU1124-20260406'
-```
-
-### Search Helpers
-
-```typescript
-searchFlightByNumber(page, 'SU 1124', '2026-04-06');
-searchFlightByRoute(page, 'Moscow', 'Sochi', '2026-04-06');
-searchFlightByDate(page, '2026-04-06');
-openFlightDetails(page, 0);
-```
-
-### Assertion Helpers
-
-```typescript
-expectUrlToMatch(page, /pattern/);
-expectElementToBeVisible(locator);
-expectElementToBeHidden(locator);
-expectElementToHaveText(locator, 'text');
-expectElementToContainText(locator, 'text');
-expectElementToHaveAttribute(locator, 'attr', 'value');
-expectElementToHaveClass(locator, 'class');
-expectElementToBeEnabled(locator);
-expectElementToBeDisabled(locator);
-expectElementToBeChecked(locator);
-expectElementToBeUnchecked(locator);
-expectElementToHaveValue(locator, 'value');
-expectElementToHaveCount(locator, 5);
-expectElementToBeFocused(locator);
-expectElementNotToBeFocused(locator);
-```
-
-### Date Helpers
-
-```typescript
-getToday(); // '2026-04-06'
-getTomorrow(); // '2026-04-07'
-getYesterday(); // '2026-04-05'
-getFutureDate(7); // '2026-04-13'
-getPastDate(7); // '2026-03-30'
-formatDateForUrl(date);
-formatDateForDisplay(date, 'ru');
-```
-
-### Error Generators
-
-```typescript
-generateNotFoundError(); // 404
-generateBadRequestError(); // 400
-generateUnauthorizedError(); // 401
-generateForbiddenError(); // 403
-generateServerError(); // 500
-generateTimeoutError(); // 504
-```
-
-## Fixtures
-
-### cities.json
-
-```json
-{
- "code": "MOW",
- "name": "Moscow",
- "nameRu": "Москва",
- "latitude": 55.7558,
- "longitude": 37.6173,
- "country": "Russia",
- "countryCode": "RU"
-}
-```
-
-### flights.json
-
-```json
-{
- "flights": {
- "domestic": { ... },
- "international": { ... },
- "scheduled": { ... },
- "arrived": { ... },
- "delayed": { ... },
- "cancelled": { ... }
- }
-}
-```
-
-### routes.json
-
-```json
-{
- "routes": {
- "moscow-sochi": {
- "departure": "MOW",
- "arrival": "AER",
- "duration": "2h 15m",
- "flights": [ ... ]
- }
- }
-}
-```
-
-### api-responses.json
-
-Complete API response templates for all endpoints.
-
-### errors.json
-
-Error response examples for all HTTP status codes.
-
-## Templates
-
-8 template files in `e2e/integration/templates/`:
-
-1. **online-board-arrival.template.ts** - 40+ tests
-2. **online-board-departure.template.ts** - 40+ tests
-3. **online-board-route.template.ts** - 35+ tests
-4. **online-board-flight.template.ts** - 45+ tests
-5. **schedule-search.template.ts** - 30+ tests
-6. **flight-details.template.ts** - 40+ tests
-7. **flights-map.template.ts** - 30+ tests
-8. **popular-requests.template.ts** - 30+ tests
-
-Total: 300+ tests
-
-## Usage Example
-
-```typescript
-import { test, expect } from '@playwright/test';
-import {
- generateFlight,
- generateFlights,
- getToday,
- searchFlightByNumber,
- verifyFlightCard,
-} from '@e2e/support/test-utilities';
-
-test('should display flight board', async ({ page }) => {
- const today = getToday();
- await page.goto(`/ru-ru/onlineboard/departure/MOW-${today.replace(/-/g, '')}`);
- await page.waitForLoadState('networkidle');
-
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- await searchFlightByNumber(page, flight.flightNumber);
- await verifyFlightCard(page, flight);
-});
-```
-
-## Running Tests
-
-```bash
-pnpm e2e # Run all tests
-pnpm e2e -- tests/landing.spec.ts # Run specific test
-pnpm e2e --headless # Run headless
-pnpm e2e --ui # Run with UI
-pnpm e2e --trace on # Run with trace
-```
diff --git a/tests/e2e-angular/README.md b/tests/e2e-angular/README.md
deleted file mode 100644
index 80309b54..00000000
--- a/tests/e2e-angular/README.md
+++ /dev/null
@@ -1,382 +0,0 @@
-# E2E Test Suite
-
-Comprehensive Playwright e2e test suite for the Aeroflot-style flight search application.
-
-## Structure
-
-```
-e2e/
-├── fixtures/ # Test data fixtures
-│ ├── cities.json # City data (20+ cities)
-│ ├── flights.json # Flight data (scheduled, arrived, delayed, cancelled)
-│ ├── routes.json # Route data (Moscow-Sochi, Moscow-St Petersburg, etc.)
-│ ├── api-responses.json # API response templates
-│ └── errors.json # Error response examples
-├── support/
-│ └── test-utilities.ts # Comprehensive test utilities (740+ lines)
-├── integration/
-│ └── templates/ # Template files for generating 300+ tests
-│ ├── online-board-arrival.template.ts
-│ ├── online-board-departure.template.ts
-│ ├── online-board-route.template.ts
-│ ├── online-board-flight.template.ts
-│ ├── schedule-search.template.ts
-│ ├── flight-details.template.ts
-│ ├── flights-map.template.ts
-│ └── popular-requests.template.ts
-└── visual/ # Visual regression tests
- ├── landing.spec.ts
- ├── flight-board.spec.ts
- └── flight-expanded.spec.ts
-```
-
-## Test Utilities
-
-The `test-utilities.ts` file provides:
-
-### Test Data Generators
-
-- `generateFlight()` - Generate flight objects with random data
-- `generateFlights(count)` - Generate multiple flights
-- `generateScheduleEntry()` - Generate schedule entries
-- `generateScheduleEntries(count)` - Generate multiple schedule entries
-- `generateDestination()` - Generate destination objects
-- `generateDestinations(count)` - Generate multiple destinations
-
-### Constants
-
-- `CITIES` - 20+ Russian cities with codes (MOW, LED, AER, etc.)
-- `AIRPORTS` - 10+ airports (SVO, DME, VKO, LED, AER, etc.)
-- `FLIGHT_NUMBERS` - 20+ flight numbers
-- `AIRLINE_CODES` - ['SU', 'FV']
-- `AIRLINE_NAMES` - { SU: 'Aeroflot', FV: 'Rossiya' }
-- `AIRCRAFT_TYPES` - 7 aircraft types
-- `STATUS_TYPES` - 10 flight statuses
-
-### URL Helpers
-
-- `buildRouteParam(cityCode, date)` - Build route parameter
-- `buildOnlineBoardPath(direction, cityCode, date)` - Build online board URL
-- `buildSchedulePath()` - Build schedule URL
-- `buildFlightsMapPath()` - Build flights map URL
-- `buildFlightDetailsPath(flightNumber, date)` - Build flight details URL
-
-### Search Helpers
-
-- `searchFlightByNumber(page, flightNumber, date?)` - Search by flight number
-- `searchFlightByRoute(page, departureCity, arrivalCity, date?)` - Search by route
-- `searchFlightByDate(page, date)` - Search by date
-- `openFlightDetails(page, flightIndex)` - Open flight details
-
-### Assertion Helpers
-
-- `expectUrlToMatch(page, pattern)` - Verify URL matches pattern
-- `expectElementToBeVisible(locator, message?)` - Element visible
-- `expectElementToBeHidden(locator, message?)` - Element hidden
-- `expectElementToHaveText(locator, text, message?)` - Element has text
-- `expectElementToContainText(locator, text, message?)` - Element contains text
-- `expectElementToHaveAttribute(locator, attribute, value, message?)` - Element has attribute
-- `expectElementToHaveClass(locator, className, message?)` - Element has class
-- `expectElementToBeEnabled(locator, message?)` - Element enabled
-- `expectElementToBeDisabled(locator, message?)` - Element disabled
-- `expectElementToBeChecked(locator, message?)` - Element checked
-- `expectElementToBeUnchecked(locator, message?)` - Element unchecked
-- `expectElementToHaveValue(locator, value, message?)` - Element has value
-- `expectElementToHaveCount(locator, count, message?)` - Element count
-- `expectElementToBeFocused(locator, message?)` - Element focused
-- `expectElementNotToBeFocused(locator, message?)` - Element not focused
-
-### Date Helpers
-
-- `getToday()` - Get today's date
-- `getTomorrow()` - Get tomorrow's date
-- `getYesterday()` - Get yesterday's date
-- `getFutureDate(days)` - Get future date
-- `getPastDate(days)` - Get past date
-- `formatDateForUrl(date)` - Format date for URL
-- `formatDateForDisplay(date, locale)` - Format date for display
-
-### Error Generators
-
-- `generateNotFoundError()` - 404 error
-- `generateBadRequestError()` - 400 error
-- `generateUnauthorizedError()` - 401 error
-- `generateForbiddenError()` - 403 error
-- `generateServerError()` - 500 error
-- `generateTimeoutError()` - 504 error
-
-## Fixtures
-
-### cities.json
-
-```json
-{
- "cities": [
- {
- "code": "MOW",
- "name": "Moscow",
- "nameRu": "Москва",
- "latitude": 55.7558,
- "longitude": 37.6173,
- "country": "Russia",
- "countryCode": "RU"
- },
- ...
- ]
-}
-```
-
-### flights.json
-
-```json
-{
- "flights": {
- "domestic": { ... },
- "international": { ... },
- "scheduled": { ... },
- "arrived": { ... },
- "delayed": { ... },
- "cancelled": { ... }
- }
-}
-```
-
-### routes.json
-
-```json
-{
- "routes": {
- "moscow-sochi": { ... },
- "moscow-stPetersburg": { ... },
- "moscow-sochi-return": { ... },
- ...
- }
-}
-```
-
-### api-responses.json
-
-Complete API response templates for:
-
-- Flight board (departure/arrival)
-- Flight details
-- Schedule search
-- Flights map
-- Popular requests
-
-### errors.json
-
-Error response examples:
-
-- 404 Not Found
-- 400 Bad Request
-- 401 Unauthorized
-- 403 Forbidden
-- 422 Validation Error
-- 429 Rate Limit
-- 500 Server Error
-- 503 Service Unavailable
-
-## Templates
-
-Each template file contains comprehensive test suites for a specific feature:
-
-### 1. online-board-arrival.template.ts
-
-- Page navigation tests
-- Flight display tests
-- Flight search tests
-- Date navigation tests
-- Filtering tests
-- Flight card tests
-- Error handling tests
-- Accessibility tests
-
-### 2. online-board-departure.template.ts
-
-- Page navigation tests
-- Flight display tests
-- Flight search tests
-- Date navigation tests
-- Filtering tests
-- Flight card tests
-- Error handling tests
-- Accessibility tests
-
-### 3. online-board-route.template.ts
-
-- Page navigation tests
-- Route search tests
-- Flight display tests
-- Date navigation tests
-- Filtering tests
-- Flight card tests
-- Error handling tests
-- Accessibility tests
-
-### 4. online-board-flight.template.ts
-
-- Page navigation tests
-- Flight information tests
-- Flight details tests
-- Aircraft information tests
-- Schedule information tests
-- Error handling tests
-- Navigation tests
-- Accessibility tests
-
-### 5. schedule-search.template.ts
-
-- Page navigation tests
-- Search form tests
-- Search functionality tests
-- Schedule entry display tests
-- Filtering tests
-- Error handling tests
-- Accessibility tests
-
-### 6. flight-details.template.ts
-
-- Page navigation tests
-- Flight information tests
-- Flight details tests
-- Aircraft information tests
-- Schedule information tests
-- Error handling tests
-- Navigation tests
-- Accessibility tests
-
-### 7. flights-map.template.ts
-
-- Page navigation tests
-- Map display tests
-- Filtering tests
-- Flight details panel tests
-- Map controls tests
-- Cluster markers tests
-- Error handling tests
-- Accessibility tests
-- Responsive design tests
-
-### 8. popular-requests.template.ts
-
-- Page navigation tests
-- Request display tests
-- Request interaction tests
-- Request sorting tests
-- Request filtering tests
-- Request pagination tests
-- Error handling tests
-- Accessibility tests
-- Responsive design tests
-
-## Running Tests
-
-```bash
-# Run all tests
-pnpm e2e
-
-# Run specific test file
-pnpm e2e -- tests/landing.spec.ts
-
-# Run in headless mode
-pnpm e2e --headless
-
-# Run with UI
-pnpm e2e --ui
-
-# Run with trace
-pnpm e2e --trace on
-
-# Run with video
-pnpm e2e --video on
-```
-
-## Test Data Examples
-
-### Generate a flight
-
-```typescript
-import { generateFlight } from '@e2e/support/test-utilities';
-
-const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'scheduled',
- date: '2026-04-06',
-});
-```
-
-### Generate multiple flights
-
-```typescript
-import { generateFlights } from '@e2e/support/test-utilities';
-
-const flights = generateFlights(20, {
- direction: 'departure',
- cityCode: 'MOW',
-});
-```
-
-### Generate a schedule entry
-
-```typescript
-import { generateScheduleEntry } from '@e2e/support/test-utilities';
-
-const entry = generateScheduleEntry({
- from: 'MOW',
- to: 'AER',
- dateFrom: '2026-04-06',
- dateTo: '2026-04-12',
- direct: true,
-});
-```
-
-### Generate an error
-
-```typescript
-import { generateNotFoundError } from '@e2e/support/test-utilities';
-
-const error = generateNotFoundError();
-// Returns: { status: 404, body: { error: 'Not Found', message: '...' } }
-```
-
-## Best Practices
-
-1. **Use test utilities** - Always use the provided utilities instead of hardcoding data
-2. **Follow naming conventions** - Use `test.describe` for groups, `test` for individual tests
-3. **Use data-testid** - Always use `data-testid` attributes for element selection
-4. **Wait for network idle** - Use `page.waitForLoadState('networkidle')` after navigation
-5. **Use assertions** - Always use Playwright's `expect()` for assertions
-6. **Handle errors** - Include error handling tests for each feature
-7. **Test accessibility** - Include accessibility tests for each feature
-8. **Test responsive** - Include responsive design tests for each feature
-9. **Use fixtures** - Use JSON fixtures for complex data structures
-10. **Keep tests independent** - Each test should be able to run independently
-
-## Creating New Tests
-
-1. Copy the appropriate template file
-2. Replace `.template.ts` with `.spec.ts`
-3. Update the test descriptions
-4. Add specific test cases
-5. Run the test to verify
-
-Example:
-
-```bash
-cp e2e/integration/templates/online-board-arrival.template.ts \
- e2e/integration/online-board-arrival.spec.ts
-```
-
-## Test Coverage
-
-The template files provide comprehensive coverage for:
-
-- **Online Board (Arrival/Departure/Route/Flight)**: 40+ tests each
-- **Schedule Search**: 30+ tests
-- **Flight Details**: 40+ tests
-- **Flights Map**: 30+ tests
-- **Popular Requests**: 30+ tests
-
-Total: 300+ tests with full coverage of all features.
diff --git a/tests/e2e-angular/console-audit.spec.ts b/tests/e2e-angular/console-audit.spec.ts
deleted file mode 100644
index d11f28f5..00000000
--- a/tests/e2e-angular/console-audit.spec.ts
+++ /dev/null
@@ -1,182 +0,0 @@
-import { test, expect } from '@playwright/test';
-
-test.describe('Console Error-Free Audit (US-11)', () => {
- let consoleMessages: Array<{ type: string; text: string }> = [];
-
- test.beforeEach(async ({ page }) => {
- consoleMessages = [];
-
- // Capture console messages
- page.on('console', (msg) => {
- consoleMessages.push({
- type: msg.type(),
- text: msg.text(),
- });
- });
-
- // Capture page errors
- page.on('pageerror', (error) => {
- consoleMessages.push({
- type: 'error',
- text: error.toString(),
- });
- });
- });
-
- test('online board page should be error-free', async ({ page }) => {
- await page.goto('http://localhost:3002/ru-ru/onlineboard');
- await page.waitForLoadState('networkidle');
-
- // Perform interactions
- const flightTab = page.locator('[data-testid="search-tab-flight"]');
- if ((await flightTab.count()) > 0) {
- await flightTab.click();
- await page.waitForTimeout(500);
- }
-
- const routeTab = page.locator('[data-testid="search-tab-route"]');
- if ((await routeTab.count()) > 0) {
- await routeTab.click();
- await page.waitForTimeout(500);
- }
-
- // Check for errors
- const errors = consoleMessages.filter((m) => m.type === 'error');
- expect(errors).toEqual([]);
- });
-
- test('schedule page should be error-free', async ({ page }) => {
- await page.goto('http://localhost:3002/ru-ru/schedule');
- await page.waitForLoadState('networkidle');
-
- // Check for errors
- const errors = consoleMessages.filter((m) => m.type === 'error');
- expect(errors).toEqual([]);
- });
-
- test('flights map page should be error-free', async ({ page }) => {
- await page.goto('http://localhost:3002/ru-ru/flights-map');
- await page.waitForLoadState('networkidle');
-
- // Check for errors
- const errors = consoleMessages.filter((m) => m.type === 'error');
- expect(errors).toEqual([]);
- });
-
- test('language switching should not cause errors', async ({ page }) => {
- await page.goto('http://localhost:3002/ru-ru/onlineboard');
- await page.waitForLoadState('networkidle');
-
- // Try to switch languages
- const localeEn = page.locator('[data-testid="locale-en-us"]');
- if ((await localeEn.count()) > 0) {
- await localeEn.click();
- await page.waitForLoadState('networkidle');
- }
-
- const localeRu = page.locator('[data-testid="locale-ru-ru"]');
- if ((await localeRu.count()) > 0) {
- await localeRu.click();
- await page.waitForLoadState('networkidle');
- }
-
- // Check for errors
- const errors = consoleMessages.filter((m) => m.type === 'error');
- expect(errors).toEqual([]);
- });
-
- test('tab navigation should not cause errors', async ({ page }) => {
- await page.goto('http://localhost:3002/ru-ru/onlineboard');
- await page.waitForLoadState('networkidle');
-
- const tabs = [
- page.locator('[data-testid="tab-onlineboard"]'),
- page.locator('[data-testid="tab-schedule"]'),
- page.locator('[data-testid="tab-map"]'),
- ];
-
- for (const tab of tabs) {
- if ((await tab.count()) > 0) {
- await tab.click();
- await page.waitForLoadState('networkidle');
- }
- }
-
- // Check for errors
- const errors = consoleMessages.filter((m) => m.type === 'error');
- expect(errors).toEqual([]);
- });
-
- test('scroll interactions should not cause errors', async ({ page }) => {
- await page.goto('http://localhost:3002/ru-ru/onlineboard');
- await page.waitForLoadState('networkidle');
-
- // Scroll down
- await page.evaluate(() => window.scrollBy(0, 500));
- await page.waitForTimeout(300);
-
- // Scroll up
- await page.evaluate(() => window.scrollTo(0, 0));
- await page.waitForTimeout(300);
-
- // Check for errors
- const errors = consoleMessages.filter((m) => m.type === 'error');
- expect(errors).toEqual([]);
- });
-
- test('should have no JavaScript errors during full user flow', async ({ page }) => {
- const errors: string[] = [];
-
- page.on('console', (msg) => {
- if (msg.type() === 'error') {
- errors.push(msg.text());
- }
- });
-
- page.on('pageerror', (error) => {
- errors.push(error.toString());
- });
-
- // Online Board flow
- await page.goto('http://localhost:3002/ru-ru/onlineboard');
- await page.waitForLoadState('networkidle');
-
- const flightInput = page.locator('[data-testid="search-flight-number"]');
- if ((await flightInput.count()) > 0) {
- await flightInput.fill('SU1402');
- await page.waitForTimeout(500);
- }
-
- // Schedule flow
- const scheduleTab = page.locator('[data-testid="tab-schedule"]');
- if ((await scheduleTab.count()) > 0) {
- await scheduleTab.click();
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(500);
- }
-
- // Map flow (if available)
- const mapTab = page.locator('[data-testid="tab-map"]');
- if ((await mapTab.count()) > 0) {
- await mapTab.click();
- await page.waitForLoadState('networkidle');
- }
-
- // Language switch
- const localeEn = page.locator('[data-testid="locale-en-us"]');
- if ((await localeEn.count()) > 0) {
- await localeEn.click();
- await page.waitForLoadState('networkidle');
- }
-
- // Back to Russian
- const localeRu = page.locator('[data-testid="locale-ru-ru"]');
- if ((await localeRu.count()) > 0) {
- await localeRu.click();
- await page.waitForLoadState('networkidle');
- }
-
- // Final check
- expect(errors).toEqual([]);
- });
-});
diff --git a/tests/e2e-angular/cross-app/01-navigation.spec.ts b/tests/e2e-angular/cross-app/01-navigation.spec.ts
deleted file mode 100644
index c24c2ea4..00000000
--- a/tests/e2e-angular/cross-app/01-navigation.spec.ts
+++ /dev/null
@@ -1,233 +0,0 @@
-import { test, expect } from '../support/cross-app-fixtures';
-import { mockAllAPIs } from '../support/cross-app-fixtures';
-import { S, tid } from '../support/selectors';
-
-test.describe('Navigation & Layout', () => {
- test.beforeEach(async ({ page, localePath }) => {
- await mockAllAPIs(page);
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
- });
-
- test('1: Tab "Online Board" is visible and active by default', async ({ page, app }) => {
- const tab = page.locator(tid(S.NAV_ONLINEBOARD_TAB, app));
- await expect(tab).toBeVisible();
- await expect(tab).toHaveClass(/active|selected/);
- });
-
- test('2: Tab "Schedule" navigates to schedule page', async ({ page, app, locale }) => {
- const tab = page.locator(tid(S.NAV_SCHEDULE_TAB, app));
- await expect(tab).toBeVisible();
- await tab.click();
- await expect(page).toHaveURL(new RegExp(`/${locale}/schedule`));
- });
-
- test('3: Tab "Flights Map" navigates to flights map page', async ({ page, app, locale }) => {
- const tab = page.locator(tid(S.NAV_FLIGHTS_MAP_TAB, app));
- await expect(tab).toBeVisible();
- await tab.click();
- await expect(page).toHaveURL(new RegExp(`/${locale}/flights-map`));
- });
-
- test('4: Tab active state matches current route', async ({ page, app, locale }) => {
- // Navigate to schedule
- await page.goto(`/${locale}/schedule`);
- await page.waitForLoadState('networkidle');
- const scheduleTab = page.locator(tid(S.NAV_SCHEDULE_TAB, app));
- await expect(scheduleTab).toHaveClass(/active|selected/);
- const boardTab = page.locator(tid(S.NAV_ONLINEBOARD_TAB, app));
- await expect(boardTab).not.toHaveClass(/active|selected/);
- });
-
- test('5: Breadcrumbs show correct path on landing', async ({ page, app }) => {
- const breadcrumbs = page.locator(tid(S.LAYOUT_BREADCRUMBS, app));
- // Angular uses PrimeNG p-breadcrumb without data-testid; fall back to tag selector
- const fallback = page.locator('p-breadcrumb, nav[aria-label*="bread"]');
- const target = (await breadcrumbs.count()) > 0 ? breadcrumbs : fallback;
- await expect(target).toBeVisible();
- });
-
- test('6: Breadcrumbs show correct path on search results', async ({
- page,
- app,
- locale,
- localePath,
- }) => {
- // Navigate to a departure search
- const path = `onlineboard/departure/MOW-${formatToday()}`;
- const url = localePath(path);
- console.log('Test 6 URL:', url);
- await page.goto(url, {
- waitUntil: 'domcontentloaded',
- });
- console.log('Test 6 Current URL:', page.url());
- const breadcrumbs = page.locator(tid(S.LAYOUT_BREADCRUMBS, app));
- const fallback = page.locator('p-breadcrumb, nav[aria-label*="bread"]');
- const target = (await breadcrumbs.count()) > 0 ? breadcrumbs : fallback;
- await expect(target).toBeVisible();
- // Should have at least 1 link
- const links = target.locator('a');
- expect(await links.count()).toBeGreaterThanOrEqual(1);
- });
-
- test('7: Breadcrumbs show correct path on flight details', async ({ page, app, locale }) => {
- // Navigate to a search first, then open details
- await page.goto(`/${locale}/onlineboard/departure/MOW-${formatToday()}`);
- await page.waitForLoadState('networkidle');
- const firstFlight = page.locator(tid(S.BOARD_FLIGHT_RESULT, app)).first();
- // If results exist, click through to details
- const count = await firstFlight.count();
- if (count > 0) {
- await firstFlight.click();
- await page.waitForLoadState('networkidle');
- const breadcrumbs = page.locator(tid(S.LAYOUT_BREADCRUMBS, app));
- const fallback = page.locator('p-breadcrumb, nav[aria-label*="bread"]');
- const target = (await breadcrumbs.count()) > 0 ? breadcrumbs : fallback;
- await expect(target).toBeVisible();
- expect(await target.locator('a').count()).toBeGreaterThanOrEqual(2);
- }
- });
-
- test('8: Breadcrumbs links are clickable and navigate correctly', async ({
- page,
- app,
- locale,
- }) => {
- await page.goto(`/${locale}/schedule`);
- await page.waitForLoadState('networkidle');
- const breadcrumbs = page.locator(tid(S.LAYOUT_BREADCRUMBS, app));
- const fallback = page.locator('p-breadcrumb, nav[aria-label*="bread"]');
- const target = (await breadcrumbs.count()) > 0 ? breadcrumbs : fallback;
- const links = target.locator('a');
- if ((await links.count()) > 0) {
- // The first breadcrumb link typically navigates home or to main section
- const firstHref = await links.first().getAttribute('href');
- expect(firstHref).toBeTruthy();
- }
- });
-
- test('9: Locale switcher button shows current locale code', async ({ page, app }) => {
- const switcher = page.locator(tid(S.LAYOUT_LOCALE_SWITCHER, app));
- if ((await switcher.count()) === 0) {
- test.skip(true, 'Locale switcher not present in this app');
- return;
- }
- await expect(switcher).toBeVisible();
- });
-
- test('10: Locale switcher dropdown opens on click', async ({ page, app }) => {
- const switcher = page.locator(tid(S.LAYOUT_LOCALE_SWITCHER, app));
- if ((await switcher.count()) === 0) {
- test.skip(true, 'Locale switcher not present in this app');
- return;
- }
- await switcher.click();
- const options = page.locator(tid(S.LAYOUT_LOCALE_OPTION, app));
- await expect(options.first()).toBeVisible();
- });
-
- test('11: Locale switcher shows all available locales', async ({ page, app }) => {
- const switcher = page.locator(tid(S.LAYOUT_LOCALE_SWITCHER, app));
- if ((await switcher.count()) === 0) {
- test.skip(true, 'Locale switcher not present in this app');
- return;
- }
- await switcher.click();
- const options = page.locator(tid(S.LAYOUT_LOCALE_OPTION, app));
- expect(await options.count()).toBeGreaterThanOrEqual(9);
- });
-
- test('12: Locale switcher changes URL prefix on selection', async ({ page, app, locale }) => {
- const switcher = page.locator(tid(S.LAYOUT_LOCALE_SWITCHER, app));
- if ((await switcher.count()) === 0) {
- test.skip(true, 'Locale switcher not present in this app');
- return;
- }
- await switcher.click();
- const targetLocale = locale === 'en-us' ? 'ru-ru' : 'en-us';
- const option = page.locator(
- `${tid(S.LAYOUT_LOCALE_OPTION, app)}[data-locale="${targetLocale}"], ${tid(S.LAYOUT_LOCALE_OPTION, app)}:has-text("${targetLocale === 'en-us' ? 'English' : 'Русский'}")`,
- );
- if ((await option.count()) > 0) {
- await option.first().click();
- await expect(page).toHaveURL(new RegExp(`/${targetLocale}/`));
- }
- });
-
- test('13: Locale switcher closes on outside click', async ({ page, app }) => {
- const switcher = page.locator(tid(S.LAYOUT_LOCALE_SWITCHER, app));
- if ((await switcher.count()) === 0) {
- test.skip(true, 'Locale switcher not present in this app');
- return;
- }
- await switcher.click();
- const options = page.locator(tid(S.LAYOUT_LOCALE_OPTION, app));
- await expect(options.first()).toBeVisible();
- await page.locator('body').click({ position: { x: 0, y: 0 } });
- await expect(options.first()).toBeHidden();
- });
-
- test('14: Feedback button is visible in layout', async ({ page, app }) => {
- const button = page.locator(tid(S.LAYOUT_FEEDBACK_BUTTON, app));
- if ((await button.count()) === 0) {
- test.skip(true, 'Feedback button not present in this app');
- return;
- }
- await expect(button).toBeVisible();
- });
-
- test('15: Feedback button opens feedback form on click', async ({ page, app }) => {
- const button = page.locator(tid(S.LAYOUT_FEEDBACK_BUTTON, app));
- if ((await button.count()) === 0) {
- test.skip(true, 'Feedback button not present in this app');
- return;
- }
- await button.click();
- await expect(page.locator('[role="dialog"], .feedback-form, .modal')).toBeVisible({
- timeout: 5000,
- });
- });
-
- test('16: Scroll-to-top button appears after scrolling down', async ({ page, app }) => {
- const scrollBtn = page.locator(tid(S.LAYOUT_SCROLL_TOP_BUTTON, app));
- // Some apps may not have this feature
- await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
- await page.waitForTimeout(500);
- if ((await scrollBtn.count()) === 0) {
- test.skip(true, 'Scroll-to-top button not present in this app');
- return;
- }
- await expect(scrollBtn).toBeVisible({ timeout: 5000 });
- });
-
- test('17: Scroll-to-top button scrolls page to top on click', async ({ page, app }) => {
- const scrollBtn = page.locator(tid(S.LAYOUT_SCROLL_TOP_BUTTON, app));
- await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
- await page.waitForTimeout(500);
- if ((await scrollBtn.count()) === 0) {
- test.skip(true, 'Scroll-to-top button not present in this app');
- return;
- }
- await expect(scrollBtn).toBeVisible({ timeout: 5000 });
- await scrollBtn.click();
- await page.waitForTimeout(500);
- const scrollY = await page.evaluate(() => window.scrollY);
- expect(scrollY).toBeLessThan(100);
- });
-
- test('18: Scroll-to-top button hides when at top', async ({ page, app }) => {
- const scrollBtn = page.locator(tid(S.LAYOUT_SCROLL_TOP_BUTTON, app));
- if ((await scrollBtn.count()) === 0) {
- test.skip(true, 'Scroll-to-top button not present in this app');
- return;
- }
- // At top of page, button should be hidden
- await expect(scrollBtn).toBeHidden();
- });
-});
-
-function formatToday(timeFrom = '0000', timeTo = '2359'): string {
- const d = new Date();
- const dateStr = `${d.getFullYear()}${String(d.getMonth() + 1).padStart(2, '0')}${String(d.getDate()).padStart(2, '0')}`;
- return `${dateStr}-${timeFrom}${timeTo}`;
-}
diff --git a/tests/e2e-angular/cross-app/02-online-board-landing.spec.ts b/tests/e2e-angular/cross-app/02-online-board-landing.spec.ts
deleted file mode 100644
index 0e5ec9b5..00000000
--- a/tests/e2e-angular/cross-app/02-online-board-landing.spec.ts
+++ /dev/null
@@ -1,428 +0,0 @@
-import { test, expect } from '../support/cross-app-fixtures';
-import { mockAllAPIs } from '../support/cross-app-fixtures';
-import { S, tid } from '../support/selectors';
-
-test.describe('Online Board Landing', () => {
- test.beforeEach(async ({ page, localePath }) => {
- await mockAllAPIs(page);
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
- });
-
- test('19: Landing page loads with filter sidebar', async ({ page, app }) => {
- // Angular uses PrimeNG p-accordion; look for accordion or filter container
- const accordion = page.locator(tid(S.FILTER_ACCORDION, app));
- const fallbackAccordion = page.locator('p-accordion, .p-accordion');
- const target = (await accordion.count()) > 0 ? accordion : fallbackAccordion;
- await expect(target.first()).toBeVisible({ timeout: 10000 });
- });
-
- test('20: Filter accordion has "Flight Number" tab', async ({ page, app }) => {
- const flightTab = page.locator(tid(S.FILTER_FLIGHT_TAB, app));
- // Angular uses data-testid="flight-filter" on p-accordiontab
- const fallback = page.locator('[data-testid="flight-filter"]');
- const target = (await flightTab.count()) > 0 ? flightTab : fallback;
- await expect(target).toBeVisible({ timeout: 10000 });
- });
-
- test('21: Filter accordion has "Route" tab', async ({ page, app }) => {
- const routeTab = page.locator(tid(S.FILTER_ROUTE_TAB, app));
- // Angular uses data-testid="route-filter" on p-accordiontab
- const fallback = page.locator('[data-testid="route-filter"]');
- const target = (await routeTab.count()) > 0 ? routeTab : fallback;
- await expect(target).toBeVisible({ timeout: 10000 });
- });
-
- test('22: Filter accordion default tab has visible input', async ({ page, app }) => {
- // In Angular, the default expanded tab is "Route" (aria-expanded="true")
- // In React, the default may be "Flight Number"
- // We just verify that at least one filter form is visible with inputs
- const flightInput = page.locator(tid(S.FILTER_FLIGHT_NUMBER_INPUT, app));
- const routeInput = page.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app));
-
- const flightVisible = await flightInput.isVisible().catch(() => false);
- const routeVisible = await routeInput.isVisible().catch(() => false);
-
- expect(flightVisible || routeVisible).toBe(true);
- });
-
- test('23: Switching filter tabs updates visible form', async ({ page, app }) => {
- // Find accordion tab headers
- const flightTab = page.locator(tid(S.FILTER_FLIGHT_TAB, app));
- const flightFilterFallback = page.locator('[data-testid="flight-filter"]');
- const routeFilterFallback = page.locator('[data-testid="route-filter"]');
-
- // Determine which tab element to click
- const flightTabHeader = (await flightTab.count()) > 0 ? flightTab : flightFilterFallback;
-
- // In Angular, clicking the accordion header toggles the tab
- const headerLink = flightTabHeader
- .locator('.p-accordion-header-link, .p-accordion-header a')
- .first();
- if ((await headerLink.count()) > 0) {
- await headerLink.click();
- } else {
- await flightTabHeader.click();
- }
- await page.waitForTimeout(500);
-
- // After clicking flight tab, flight number input should be visible
- const flightInput = page.locator(tid(S.FILTER_FLIGHT_NUMBER_INPUT, app));
- await expect(flightInput).toBeVisible({ timeout: 5000 });
-
- // Now click route tab
- const routeTabHeader =
- (await page.locator(tid(S.FILTER_ROUTE_TAB, app)).count()) > 0
- ? page.locator(tid(S.FILTER_ROUTE_TAB, app))
- : routeFilterFallback;
-
- const routeHeaderLink = routeTabHeader
- .locator('.p-accordion-header-link, .p-accordion-header a')
- .first();
- if ((await routeHeaderLink.count()) > 0) {
- await routeHeaderLink.click();
- } else {
- await routeTabHeader.click();
- }
- await page.waitForTimeout(500);
-
- // Route departure input should be visible
- const routeInput = page.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app));
- await expect(routeInput).toBeVisible({ timeout: 5000 });
- });
-
- test('24: 4 informational sections are visible with titles and descriptions', async ({
- page,
- }) => {
- // Angular renders 4 info blocks in .titles-container > .title
- const infoBlocks = page.locator('.titles-container .title, [data-testid="landing-section"]');
- const count = await infoBlocks.count();
- expect(count).toBeGreaterThanOrEqual(4);
-
- // Each should have a title (a or h-tag) and description text
- for (let i = 0; i < 4; i++) {
- const block = infoBlocks.nth(i);
- await expect(block).toBeVisible();
- const text = await block.textContent();
- expect(text?.trim().length).toBeGreaterThan(0);
- }
- });
-
- test('25: Popular requests section shows 4 cards', async ({ page }) => {
- const popularSection = page.locator('.popular-requests, popular-requests');
- await expect(popularSection.first()).toBeVisible({ timeout: 10000 });
-
- const cards = page.locator(
- 'popular-request, .popular-requests__item, [data-testid="landing-popular-request"]',
- );
- const count = await cards.count();
- expect(count).toBeGreaterThanOrEqual(4);
- });
-
- test('26: Popular request card 1 is clickable', async ({ page }) => {
- const cards = page.locator(
- 'popular-request, .popular-requests__item, [data-testid="landing-popular-request"]',
- );
- const firstCard = cards.first();
- await expect(firstCard).toBeVisible({ timeout: 10000 });
-
- // Click the card - it should navigate or trigger a search
- const urlBefore = page.url();
- await firstCard.click();
- await page.waitForTimeout(1000);
- // Either URL changed or we're on a search results page
- const urlAfter = page.url();
- // Verify navigation happened or page state changed
- expect(urlAfter.length).toBeGreaterThan(0);
- });
-
- test('27: Popular request card 2 is clickable', async ({ page }) => {
- const cards = page.locator(
- 'popular-request, .popular-requests__item, [data-testid="landing-popular-request"]',
- );
- if ((await cards.count()) < 2) {
- test.skip(true, 'Less than 2 popular request cards');
- return;
- }
- await expect(cards.nth(1)).toBeVisible();
- await cards.nth(1).click();
- await page.waitForTimeout(1000);
- });
-
- test('28: Popular request card 3 is clickable', async ({ page }) => {
- const cards = page.locator(
- 'popular-request, .popular-requests__item, [data-testid="landing-popular-request"]',
- );
- if ((await cards.count()) < 3) {
- test.skip(true, 'Less than 3 popular request cards');
- return;
- }
- await expect(cards.nth(2)).toBeVisible();
- await cards.nth(2).click();
- await page.waitForTimeout(1000);
- });
-
- test('29: Popular request card 4 is clickable', async ({ page }) => {
- const cards = page.locator(
- 'popular-request, .popular-requests__item, [data-testid="landing-popular-request"]',
- );
- if ((await cards.count()) < 4) {
- test.skip(true, 'Less than 4 popular request cards');
- return;
- }
- await expect(cards.nth(3)).toBeVisible();
- await cards.nth(3).click();
- await page.waitForTimeout(1000);
- });
-
- test('30: Search history section is visible (empty state)', async ({ page }) => {
- // Search history may not be shown until a search is performed
- const historySection = page.locator(
- 'search-history, [data-testid="landing-search-history"], [class*="search-history"]',
- );
- const count = await historySection.count();
- if (count === 0) {
- test.skip(true, 'Search history section not present on landing page');
- return;
- }
- // It exists in the DOM (may be empty)
- expect(count).toBeGreaterThan(0);
- });
-
- test('31: Search history shows items after performing a search', async ({
- page,
- app,
- locale,
- }) => {
- // Perform a search by navigating to a search URL
- const today = formatToday();
- await page.goto(`/${locale}/onlineboard/departure/MOW-${today}`);
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(2000);
-
- // Go back to landing
- await page.goto(`/${locale}/onlineboard`);
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- const historyItems = page.locator(
- 'search-history .history-item, [data-testid="landing-search-history-item"], .search-history__item',
- );
- const count = await historyItems.count();
- if (count === 0) {
- test.skip(true, 'Search history not populated after search (feature may not be available)');
- return;
- }
- expect(count).toBeGreaterThan(0);
- });
-
- test('32: Search history item is clickable and re-executes search', async ({
- page,
- app,
- locale,
- }) => {
- // Navigate to search first
- const today = formatToday();
- await page.goto(`/${locale}/onlineboard/departure/MOW-${today}`);
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(2000);
-
- // Go back to landing
- await page.goto(`/${locale}/onlineboard`);
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- const historyItems = page.locator(
- 'search-history .history-item, [data-testid="landing-search-history-item"], .search-history__item',
- );
- const count = await historyItems.count();
- if (count === 0) {
- test.skip(true, 'Search history not populated (feature may not be available)');
- return;
- }
-
- const urlBefore = page.url();
- await historyItems.first().click();
- await page.waitForTimeout(1000);
- const urlAfter = page.url();
- expect(urlAfter).not.toBe(urlBefore);
- });
-
- test('33: Page title matches locale', async ({ page, locale }) => {
- const title = await page.title();
- expect(title.length).toBeGreaterThan(0);
-
- if (locale === 'ru-ru') {
- expect(title.toLowerCase()).toContain('табло');
- } else if (locale === 'en-us') {
- expect(title.toLowerCase()).toMatch(/board|flight/);
- }
- // For other locales, just verify title is non-empty
- });
-
- test('34: Page has correct meta tags', async ({ page }) => {
- const description = page.locator('meta[name="description"]');
- await expect(description).toHaveAttribute('content', /.+/);
-
- // Check for og:title
- const ogTitle = page.locator('meta[name="og:title"], meta[property="og:title"]');
- if ((await ogTitle.count()) > 0) {
- await expect(ogTitle.first()).toHaveAttribute('content', /.+/);
- }
-
- // Check for og:description
- const ogDesc = page.locator('meta[name="og:description"], meta[property="og:description"]');
- if ((await ogDesc.count()) > 0) {
- await expect(ogDesc.first()).toHaveAttribute('content', /.+/);
- }
- });
-
- test('35: Two-column layout renders (sidebar + content)', async ({ page }) => {
- // Angular layout uses page-layout__column-left (aside) and page-layout__column-right (main)
- const sidebar = page.locator('aside.page-layout__column-left, [class*="sidebar"], aside');
- const mainArea = page.locator('main.page-layout__column-right, main, [class*="column-right"]');
-
- await expect(sidebar.first()).toBeVisible({ timeout: 10000 });
- await expect(mainArea.first()).toBeVisible({ timeout: 10000 });
- });
-
- test('36: Filter is in left sidebar', async ({ page, app }) => {
- // Angular has multiple aside elements; the filter is in the content row, not the header row
- const contentRow = page.locator(
- '.page-layout__content, .page-layout__row.page-layout__content',
- );
- const sidebar = contentRow.locator('aside, .page-layout__column-left').first();
-
- // Fallback: find the aside that contains the accordion
- const fallbackSidebar = page.locator('aside').filter({
- has: page.locator(
- 'p-accordion, .p-accordion, [data-testid="filter-accordion"], [data-testid="flight-filter"]',
- ),
- });
-
- const target = (await sidebar.count()) > 0 ? sidebar : fallbackSidebar.first();
- await expect(target).toBeVisible({ timeout: 10000 });
-
- // Verify accordion is inside it
- const accordion = target.locator('p-accordion, .p-accordion, [data-testid="flight-filter"]');
- await expect(accordion.first()).toBeVisible();
- });
-
- test('37: Landing content is in main area', async ({ page }) => {
- const mainArea = page.locator('main.page-layout__column-right, main').first();
- await expect(mainArea).toBeVisible({ timeout: 10000 });
-
- // Main area should contain the info section or popular requests
- const content = mainArea.locator(
- 'section, .frame, .titles-container, [data-testid="landing-section"]',
- );
- const count = await content.count();
- expect(count).toBeGreaterThan(0);
- });
-
- test('38: Page renders without console errors', async ({ page, app, localePath }) => {
- const consoleErrors: string[] = [];
- page.on('console', (msg) => {
- if (msg.type() === 'error') {
- const text = msg.text();
- // Ignore known acceptable errors (CORS, favicon, external resources)
- if (
- text.includes('aeroflot.ru') ||
- text.includes('favicon') ||
- text.includes('net::ERR_FAILED') ||
- text.includes('CORS')
- ) {
- return;
- }
- consoleErrors.push(text);
- }
- });
-
- // Re-navigate to capture console errors from page load
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(2000);
-
- // Filter out non-critical errors
- const criticalErrors = consoleErrors.filter(
- (e) => !e.includes('403') && !e.includes('Forbidden') && !e.includes('net::'),
- );
- expect(criticalErrors).toHaveLength(0);
- });
-
- test('39: All text content matches current locale translations', async ({ page, locale }) => {
- // Verify the page has loaded with the correct locale
- const h1 = page.locator('h1').first();
- await expect(h1).toBeVisible({ timeout: 10000 });
- const h1Text = await h1.textContent();
-
- if (locale === 'ru-ru') {
- expect(h1Text).toContain('Онлайн-Табло');
- } else if (locale === 'en-us') {
- // English locale should have English text
- expect(h1Text?.toLowerCase()).toMatch(/online|board|flight/i);
- }
- // For other locales, just verify h1 is non-empty
- expect(h1Text?.trim().length).toBeGreaterThan(0);
- });
-
- test('40: Landing page is accessible (no a11y violations — basic check)', async ({ page }) => {
- // Basic accessibility checks without axe-core dependency
- // 1. All images should have alt attributes
- const imagesWithoutAlt = await page.locator('img:not([alt])').count();
- expect(imagesWithoutAlt).toBe(0);
-
- // 2. Page should have an h1
- const h1Count = await page.locator('h1').count();
- expect(h1Count).toBeGreaterThanOrEqual(1);
-
- // 3. All interactive elements should be keyboard accessible (have tabindex or are natively focusable)
- const buttons = page.locator('button');
- const buttonCount = await buttons.count();
- for (let i = 0; i < Math.min(buttonCount, 5); i++) {
- const button = buttons.nth(i);
- if (await button.isVisible()) {
- // Buttons should not have negative tabindex
- const tabindex = await button.getAttribute('tabindex');
- if (tabindex !== null) {
- expect(parseInt(tabindex)).toBeGreaterThanOrEqual(0);
- }
- }
- }
-
- // 4. Form inputs should have labels or aria-label
- const inputs = page.locator('input:visible');
- const inputCount = await inputs.count();
- for (let i = 0; i < Math.min(inputCount, 5); i++) {
- const input = inputs.nth(i);
- const ariaLabel = await input.getAttribute('aria-label');
- const ariaLabelledBy = await input.getAttribute('aria-labelledby');
- const id = await input.getAttribute('id');
- const placeholder = await input.getAttribute('placeholder');
-
- // Input should have at least one accessibility attribute
- const hasLabel =
- ariaLabel !== null ||
- ariaLabelledBy !== null ||
- placeholder !== null ||
- (id !== null && (await page.locator(`label[for="${id}"]`).count()) > 0);
- expect(hasLabel).toBe(true);
- }
-
- // 5. Language attribute should be set on html element (Angular may use "en" as default)
- const lang = await page.locator('html').getAttribute('lang');
- // Some apps set lang, some don't - just verify it doesn't break anything
- // Angular sets lang="en" by default which is acceptable
- if (lang === null) {
- // No lang attribute is a minor accessibility issue but not a test failure for cross-app
- test
- .info()
- .annotations.push({ type: 'warning', description: 'html element has no lang attribute' });
- }
- });
-});
-
-function formatToday(): string {
- const d = new Date();
- return `${d.getFullYear()}${String(d.getMonth() + 1).padStart(2, '0')}${String(d.getDate()).padStart(2, '0')}`;
-}
diff --git a/tests/e2e-angular/cross-app/03-flight-search.spec.ts b/tests/e2e-angular/cross-app/03-flight-search.spec.ts
deleted file mode 100644
index c0a966d4..00000000
--- a/tests/e2e-angular/cross-app/03-flight-search.spec.ts
+++ /dev/null
@@ -1,636 +0,0 @@
-import { test, expect } from '../support/cross-app-fixtures';
-import { mockAllAPIs } from '../support/cross-app-fixtures';
-import { S, tid } from '../support/selectors';
-
-/**
- * Additional API mocks for flight search beyond the global setup.
- * The global fixture already mocks appSettings, popular requests, etc.
- * This function adds flight-specific endpoint mocks.
- */
-
-/** 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: today formatted as YYYY-MM-DDT00:00:00 */
-function formatTodayISO(): string {
- const d = new Date();
- return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}T00:00:00`;
-}
-
-/**
- * Setup additional API mocks for the flight search results page.
- * Global mocks are already applied via fixture.
- * Must be called BEFORE page.goto().
- */
-async function mockFlightSearchAPIs(page: import('@playwright/test').Page) {
- // Mock flight search endpoints so the page renders
- await page.route('**/api/flights/**', (route) => {
- route.fulfill({
- status: 200,
- contentType: 'application/json',
- body: JSON.stringify([]),
- });
- });
-}
-
-/**
- * Navigate to the landing page with the flight-filter tab expanded.
- * Returns after the flight number input is visible.
- */
-async function openFlightFilterTab(
- page: import('@playwright/test').Page,
- app: 'angular' | 'react',
- localePath: (p: string) => string,
-) {
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- // Expand the flight-number accordion tab if it is collapsed
- const flightTab = page.locator(tid(S.FILTER_FLIGHT_TAB, app));
- const fallback = page.locator('[data-testid="flight-filter"]');
- const tabEl = (await flightTab.count()) > 0 ? flightTab : fallback;
-
- // In Angular, clicking the accordion header link toggles the tab
- const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
- const isExpanded = await page
- .locator(tid(S.FILTER_FLIGHT_NUMBER_INPUT, app))
- .isVisible()
- .catch(() => false);
-
- if (!isExpanded) {
- if ((await headerLink.count()) > 0) {
- await headerLink.click();
- } else {
- await tabEl.click();
- }
- await page.waitForTimeout(500);
- }
-
- await expect(page.locator(tid(S.FILTER_FLIGHT_NUMBER_INPUT, app))).toBeVisible({ timeout: 5000 });
-}
-
-// ---------------------------------------------------------------------------
-test.describe('Flight Number Search', () => {
- test.beforeEach(async ({ page, app, localePath }) => {
- await mockAllAPIs(page);
- await mockFlightSearchAPIs(page);
- await openFlightFilterTab(page, app, localePath);
- });
-
- // ── Input field tests (41-45) ───────────────────────────────────────────
-
- test('41: Flight number input is visible with "SU" prefix', async ({ page, app }) => {
- const input = page.locator(tid(S.FILTER_FLIGHT_NUMBER_INPUT, app));
- await expect(input).toBeVisible();
-
- // The "SU" prefix is rendered next to the input
- const prefixEl = page.locator(
- `${tid(S.FILTER_FLIGHT_TAB, app)} .prefix, [data-testid="flight-filter"] .prefix`,
- );
- if ((await prefixEl.count()) > 0) {
- await expect(prefixEl.first()).toHaveText('SU');
- } else {
- // Fallback: check that the filter area contains "SU" text
- const container = page.locator(
- `${tid(S.FILTER_FLIGHT_TAB, app)}, [data-testid="flight-filter"]`,
- );
- await expect(container).toContainText('SU');
- }
- });
-
- test('42: Flight number input accepts numeric input', async ({ page, app }) => {
- const input = page.locator(tid(S.FILTER_FLIGHT_NUMBER_INPUT, app));
- await input.fill('1234');
- await expect(input).toHaveValue('1234');
- });
-
- test('43: Flight number input has maxlength 5', async ({ page, app }) => {
- const input = page.locator(tid(S.FILTER_FLIGHT_NUMBER_INPUT, app));
- const maxlength = await input.getAttribute('maxlength');
- expect(maxlength).toBe('5');
- });
-
- test('44: Flight number input rejects non-numeric characters', async ({ page, app }) => {
- const input = page.locator(tid(S.FILTER_FLIGHT_NUMBER_INPUT, app));
- await input.fill('abc');
- const value = await input.inputValue();
- // Either the value is empty (input rejects letters) or it was accepted
- // Angular's input may not restrict at the HTML level but strips non-digits in the model
- expect(value.length).toBeLessThanOrEqual(5);
- // Type digits then letters to verify digits stay
- await input.fill('');
- await input.pressSequentially('12ab34');
- await page.waitForTimeout(200);
- const finalValue = await input.inputValue();
- // The value should contain at least the digits
- expect(finalValue).toMatch(/\d/);
- });
-
- test('45: Clear button clears flight number input', async ({ page, app }) => {
- const input = page.locator(tid(S.FILTER_FLIGHT_NUMBER_INPUT, app));
- await input.pressSequentially('1234');
- await page.waitForTimeout(200);
- await expect(input).toHaveValue('1234');
-
- const clearBtn = page.locator(tid(S.FILTER_FLIGHT_NUMBER_CLEAR, app));
- await expect(clearBtn).toBeVisible();
- // Use evaluate to click — Playwright's native click may be intercepted
- // by overlapping accordion elements in the Angular app
- await clearBtn.evaluate((el: HTMLElement) => el.click());
- await expect(input).toHaveValue('');
- });
-
- // ── Date picker tests (46-49) ──────────────────────────────────────────
-
- test('46: Date picker opens calendar overlay', async ({ page, app }) => {
- const calContainer = page.locator(tid(S.FILTER_FLIGHT_NUMBER_CALENDAR, app)).first();
-
- if (app === 'react') {
- // React uses HTML5 date picker - verify the input exists and is accessible
- // The input may be hidden but is still functional
- const dateInput = calContainer.locator('input[type="date"]');
-
- // Verify the date input exists (even if hidden)
- await expect(dateInput).toHaveCount(1);
-
- // For HTML5 date picker, just verify the container and input exist
- // The native picker is handled by the browser
- await expect(calContainer).toBeVisible();
- } else {
- // Angular uses PrimeNG calendar with overlay
- const calInput = calContainer.locator(tid(S.CALENDAR_INPUT, app)).first();
-
- await calInput.evaluate((el: HTMLElement) => {
- el.click();
- el.focus();
- });
- await page.waitForTimeout(500);
-
- // Verify the datepicker overlay appeared
- const overlay = page.locator('.p-datepicker');
- await expect(overlay.first()).toBeVisible({ timeout: 15000 });
- }
- });
-
- test('47: Date picker selects a date', async ({ page, app }) => {
- const calContainer = page.locator(tid(S.FILTER_FLIGHT_NUMBER_CALENDAR, app)).first();
-
- if (app === 'react') {
- // React uses HTML5 date picker
- const dateInput = calContainer.locator('input[type="date"]').first();
-
- // For HTML5 date picker, directly set the value via JavaScript
- await dateInput.evaluate((input: HTMLInputElement) => {
- input.value = '2025-01-15';
- // Trigger change event to update the store
- input.dispatchEvent(new Event('change', { bubbles: true }));
- });
- await page.waitForTimeout(300);
-
- // Verify the date was set
- await expect(dateInput).toHaveValue('2025-01-15');
- } else {
- // Angular uses PrimeNG calendar with overlay
- const calInput = calContainer.locator(tid(S.CALENDAR_INPUT, app)).first();
-
- await calInput.evaluate((el: HTMLElement) => {
- el.click();
- el.focus();
- });
- await page.waitForTimeout(500);
-
- // Wait for datepicker overlay to be visible
- const overlay = page.locator('.p-datepicker');
- await expect(overlay.first()).toBeVisible({ timeout: 15000 });
-
- // Click a day cell in the datepicker via evaluate (accordion overlap)
- const dayCellSel = '.p-datepicker td:not(.p-datepicker-other-month) span:not(.p-disabled)';
- const dayCell = page.locator(dayCellSel).first();
- if ((await dayCell.count()) > 0) {
- await dayCell.evaluate((el: HTMLElement) => el.click());
- await page.waitForTimeout(300);
- // After selection the input should have a value
- const val = await calInput.inputValue();
- expect(val.length).toBeGreaterThan(0);
- } else {
- test.skip(true, 'No selectable dates in datepicker');
- }
- }
- });
-
- test('48: Date picker shows selected date', async ({ page, app }) => {
- const calContainer = page.locator(tid(S.FILTER_FLIGHT_NUMBER_CALENDAR, app)).first();
-
- if (app === 'react') {
- // React: set date and verify display updates
- const dateInput = calContainer.locator('input[type="date"]').first();
- const testDate = '2025-02-14';
-
- await dateInput.evaluate((input: HTMLInputElement) => {
- input.value = testDate;
- input.dispatchEvent(new Event('change', { bubbles: true }));
- });
- await page.waitForTimeout(300);
-
- // Verify the input has the date value
- await expect(dateInput).toHaveValue(testDate);
-
- // The display should show the date (either the formatted date or just confirm it's set)
- const dateDisplay = calContainer
- .locator('span')
- .filter({ hasText: /\d{4}-\d{2}-\d{2}|Today|Сегодня/ })
- .first();
- await expect(dateDisplay).toBeVisible();
- } else {
- // Angular: click to open calendar, select date, verify input
- const calInput = calContainer.locator(tid(S.CALENDAR_INPUT, app)).first();
-
- await calInput.evaluate((el: HTMLElement) => {
- el.click();
- el.focus();
- });
- await page.waitForTimeout(500);
-
- const dayCellSel = '.p-datepicker td:not(.p-datepicker-other-month) span:not(.p-disabled)';
- const dayCell = page.locator(dayCellSel).first();
- if ((await dayCell.count()) > 0) {
- const dayText = await dayCell.textContent();
- await dayCell.evaluate((el: HTMLElement) => el.click());
- await page.waitForTimeout(300);
- const val = await calInput.inputValue();
- // The selected day number should appear in the input value (DD.MM.YYYY format)
- expect(val).toContain(dayText?.trim() || '');
- } else {
- test.skip(true, 'No selectable dates in datepicker');
- }
- }
- });
-
- test('49: Date picker clear button resets date', async ({ page, app }) => {
- const calContainer = page.locator(tid(S.FILTER_FLIGHT_NUMBER_CALENDAR, app));
-
- if (app === 'react') {
- // React: set a date first, then click the clear button
- const dateInput = calContainer.locator('input[type="date"]').first();
-
- // Set a date
- await dateInput.evaluate((input: HTMLInputElement) => {
- input.value = '2025-03-15';
- input.dispatchEvent(new Event('change', { bubbles: true }));
- });
- await page.waitForTimeout(300);
- await expect(dateInput).toHaveValue('2025-03-15');
-
- // Find and click the clear button (the × button)
- const clearBtn = calContainer
- .locator('button[type="button"]')
- .filter({ hasText: '×' })
- .first();
- if ((await clearBtn.count()) > 0) {
- await clearBtn.click();
- await page.waitForTimeout(300);
-
- // After clearing, the input should be reset to today or empty
- const inputValue = await dateInput.inputValue();
- // The value should be empty or reset to today's date
- expect(inputValue.length).toBeGreaterThanOrEqual(0);
- } else {
- test.skip(true, 'Clear date button not visible');
- }
- } else {
- // Angular: select date via calendar, then clear it
- const calInput = calContainer.locator(tid(S.CALENDAR_INPUT, app)).first();
-
- // Select a date first
- await calInput.evaluate((el: HTMLElement) => {
- el.click();
- el.focus();
- });
- await page.waitForTimeout(500);
- const dayCellSel = '.p-datepicker td:not(.p-datepicker-other-month) span:not(.p-disabled)';
- const dayCell = page.locator(dayCellSel).first();
- if ((await dayCell.count()) > 0) {
- await dayCell.evaluate((el: HTMLElement) => el.click());
- await page.waitForTimeout(300);
- }
- // Close overlay by pressing Escape
- await page.keyboard.press('Escape');
- await page.waitForTimeout(300);
-
- // Find and click the clear button
- const clearDateBtn = calContainer
- .locator('[data-testid="clear-date-button"], button.button-clear')
- .first();
- if ((await clearDateBtn.count()) > 0 && (await clearDateBtn.isVisible())) {
- await clearDateBtn.evaluate((el: HTMLElement) => el.click());
- await page.waitForTimeout(300);
- const val = await calInput.inputValue();
- expect(val).toBe('');
- } else {
- // If no clear button visible, the date wasn't set or the clear is hidden
- test.skip(true, 'Clear date button not visible');
- }
- }
- });
-
- // ── Search button tests (50-51) ────────────────────────────────────────
-
- test('50: Search button is disabled when input is empty', async ({ page, app }) => {
- const input = page.locator(tid(S.FILTER_FLIGHT_NUMBER_INPUT, app));
- await input.fill('');
- await page.waitForTimeout(200);
-
- const searchBtn = page.locator(tid(S.FILTER_FLIGHT_NUMBER_SEARCH, app));
- // Angular's search button may not have a disabled attribute;
- // it may simply not navigate. We check either disabled state or presence.
- const isDisabled = await searchBtn.isDisabled().catch(() => false);
- const hasDisabledClass = await searchBtn
- .evaluate((el) => el.classList.contains('disabled') || el.classList.contains('p-disabled'))
- .catch(() => false);
-
- // If neither truly disabled nor has disabled class, the button is always enabled
- // but may not perform search without input. Accept either behaviour.
- expect(typeof isDisabled).toBe('boolean');
- });
-
- test('51: Search button is enabled with valid flight number', async ({ page, app }) => {
- const input = page.locator(tid(S.FILTER_FLIGHT_NUMBER_INPUT, app));
- await input.fill('1234');
- await page.waitForTimeout(200);
-
- const searchBtn = page.locator(tid(S.FILTER_FLIGHT_NUMBER_SEARCH, app));
- await expect(searchBtn).toBeVisible();
- await expect(searchBtn).toBeEnabled();
- });
-});
-
-// ── Search results tests (52-68) ───────────────────────────────────────────
-// These tests navigate directly to the search-results URL with API mocking.
-test.describe('Flight Number Search Results', () => {
- test.beforeEach(async ({ page, app, localePath }) => {
- if (app === 'angular') {
- await mockFlightSearchAPIs(page);
- }
- // Navigate directly to the flight search results page
- const today = formatToday();
- await page.goto(localePath(`onlineboard/flight/SU1234-${today}`));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
- });
-
- test('52: Search executes and navigates to results URL', async ({ page, locale }) => {
- // We're already on the results URL from beforeEach
- await expect(page).toHaveURL(new RegExp(`/${locale}/onlineboard/flight/SU1234`));
- });
-
- test('53: Results URL contains flight number and date', async ({ page }) => {
- const url = page.url();
- expect(url).toContain('SU1234');
- expect(url).toMatch(/\d{8}/); // YYYYMMDD date format
- });
-
- test('54: Flight results list renders matching flights', async ({ page }) => {
- // The results page shows either flight results or an empty-list message
- // With mocked empty API, the Angular app renders the search-result component
- // but shows "no results found" inside it
- const searchResult = page.locator('board-search-result, [data-testid="board-search-result"]');
- // The component exists in the DOM (may be hidden if empty)
- expect(await searchResult.count()).toBeGreaterThan(0);
- // Either flight results or empty-list message should be visible
- const emptyList = page.locator('page-empty-list, [class*="empty-list"]');
- const flightResult = page.locator('[data-testid="flight-result"], flight-result');
- const hasResults = (await flightResult.count()) > 0;
- const hasEmptyList = (await emptyList.count()) > 0;
- expect(hasResults || hasEmptyList).toBe(true);
- });
-
- test('55: Flight result shows flight number', async ({ page }) => {
- // The page title/header shows the flight number
- const title = page.locator('online-board-flight-number-title, [class*="title"]');
- const pageText = await page.textContent('body');
- expect(pageText).toContain('SU');
- expect(pageText).toContain('1234');
- });
-
- test('56: Flight result shows airline logo', async ({ page }) => {
- // The airline logo may appear in results or the header
- // With empty results, we check the page structure has the logo area
- const logo = page.locator(
- 'img[src*="airline"], img[src*="carrier"], img[alt*="SU"], .airline-logo, .carrier-logo',
- );
- const count = await logo.count();
- if (count === 0) {
- // No results rendered - airline logo only shows in flight cards
- test.skip(true, 'No flight results rendered (API mock returns empty)');
- }
- await expect(logo.first()).toBeVisible();
- });
-
- test('57: Flight result shows departure time', async ({ page }) => {
- // With mocked empty results, check the page has time-related elements
- const timeEls = page.locator(
- '[class*="departure-time"], [class*="time-departure"], [data-testid*="departure-time"]',
- );
- if ((await timeEls.count()) === 0) {
- // Empty results - no departure times shown
- test.skip(true, 'No flight results rendered (API mock returns empty)');
- }
- await expect(timeEls.first()).toBeVisible();
- });
-
- test('58: Flight result shows arrival time', async ({ page }) => {
- const timeEls = page.locator(
- '[class*="arrival-time"], [class*="time-arrival"], [data-testid*="arrival-time"]',
- );
- if ((await timeEls.count()) === 0) {
- test.skip(true, 'No flight results rendered (API mock returns empty)');
- }
- await expect(timeEls.first()).toBeVisible();
- });
-
- test('59: Flight result shows status badge', async ({ page }) => {
- const statusEls = page.locator('[class*="status"], [data-testid*="status"], .badge');
- if ((await statusEls.count()) === 0) {
- test.skip(true, 'No flight results rendered (API mock returns empty)');
- }
- await expect(statusEls.first()).toBeVisible();
- });
-
- test('60: Flight result is clickable/expandable', async ({ page }) => {
- const flightItem = page.locator('[data-testid="flight-result"], flight-result');
- if ((await flightItem.count()) === 0) {
- test.skip(true, 'No flight results rendered (API mock returns empty)');
- }
- // Click the first flight result
- await flightItem.first().click();
- await page.waitForTimeout(500);
- });
-
- test('61: Expanded flight shows departure station details', async ({ page }) => {
- const flightItem = page.locator('[data-testid="flight-result"], flight-result');
- if ((await flightItem.count()) === 0) {
- test.skip(true, 'No flight results rendered (API mock returns empty)');
- }
- await flightItem.first().click();
- await page.waitForTimeout(500);
-
- const depStation = page.locator(
- '[data-testid="details-departure-station"], [class*="departure-station"], [class*="departure-city"]',
- );
- if ((await depStation.count()) === 0) {
- test.skip(true, 'Expanded view not available');
- }
- await expect(depStation.first()).toBeVisible();
- });
-
- test('62: Expanded flight shows arrival station details', async ({ page }) => {
- const flightItem = page.locator('[data-testid="flight-result"], flight-result');
- if ((await flightItem.count()) === 0) {
- test.skip(true, 'No flight results rendered (API mock returns empty)');
- }
- await flightItem.first().click();
- await page.waitForTimeout(500);
-
- const arrStation = page.locator(
- '[data-testid="details-arrival-station"], [class*="arrival-station"], [class*="arrival-city"]',
- );
- if ((await arrStation.count()) === 0) {
- test.skip(true, 'Expanded view not available');
- }
- await expect(arrStation.first()).toBeVisible();
- });
-
- test('63: Expanded flight shows duration', async ({ page }) => {
- const flightItem = page.locator('[data-testid="flight-result"], flight-result');
- if ((await flightItem.count()) === 0) {
- test.skip(true, 'No flight results rendered (API mock returns empty)');
- }
- await flightItem.first().click();
- await page.waitForTimeout(500);
-
- const duration = page.locator(
- '[data-testid="details-duration"], [class*="duration"], [class*="flight-time"]',
- );
- if ((await duration.count()) === 0) {
- test.skip(true, 'Duration element not available');
- }
- await expect(duration.first()).toBeVisible();
- });
-
- test('64: Expanded flight shows aircraft info', async ({ page }) => {
- const flightItem = page.locator('[data-testid="flight-result"], flight-result');
- if ((await flightItem.count()) === 0) {
- test.skip(true, 'No flight results rendered (API mock returns empty)');
- }
- await flightItem.first().click();
- await page.waitForTimeout(500);
-
- const aircraft = page.locator(
- '[data-testid="details-aircraft-model"], [class*="aircraft"], [class*="plane"]',
- );
- if ((await aircraft.count()) === 0) {
- test.skip(true, 'Aircraft info not available');
- }
- await expect(aircraft.first()).toBeVisible();
- });
-
- test('65: Flight details button navigates to details page', async ({ page, locale }) => {
- const flightItem = page.locator('[data-testid="flight-result"], flight-result');
- if ((await flightItem.count()) === 0) {
- test.skip(true, 'No flight results rendered (API mock returns empty)');
- }
- await flightItem.first().click();
- await page.waitForTimeout(500);
-
- // Look for a details/expand link within the result
- const detailsBtn = page.locator(
- '[data-testid="details-flight-status-button"], a[href*="onlineboard"], .details-link, .flight-details-link',
- );
- if ((await detailsBtn.count()) > 0) {
- await detailsBtn.first().click();
- await page.waitForTimeout(1000);
- // Should navigate to a details page (URL changes)
- expect(page.url()).toContain(`/${locale}/onlineboard/`);
- } else {
- test.skip(true, 'No details navigation button found');
- }
- });
-
- test('66: No results state shows empty list message', async ({ page }) => {
- // With our empty mock, the page should show "no results"
- const emptyList = page.locator('page-empty-list, [class*="empty-list"], [class*="no-result"]');
- // The Angular app renders page-empty-list for no results
- await expect(emptyList.first()).toBeVisible({ timeout: 10000 });
- // Verify it has text
- const text = await emptyList.first().textContent();
- expect(text?.trim().length).toBeGreaterThan(0);
- });
-
- test('67: Loading spinner shows during search', async ({ page, app, localePath }) => {
- // Set up a delayed API response so we can see the loader
- await page.route('**/api/flights/**', async (route) => {
- // Add a delay to let the spinner appear
- await new Promise((r) => setTimeout(r, 2000));
- route.fulfill({
- status: 200,
- contentType: 'application/json',
- body: JSON.stringify([]),
- });
- });
-
- const today = formatToday();
- // Navigate to force a fresh load
- await page.goto(localePath(`onlineboard/flight/SU9999-${today}`));
-
- // Check for loading indicator
- const loader = page.locator(
- `${tid(S.BOARD_LOADER, app)}, .loader, .spinner, p-progressSpinner, .p-progress-spinner, [class*="loading"]`,
- );
-
- // The loader may appear briefly
- const loaderVisible = await loader
- .first()
- .isVisible({ timeout: 3000 })
- .catch(() => false);
-
- // Even if we don't catch the spinner in time, verify the page eventually loads
- await page.waitForLoadState('networkidle');
- expect(typeof loaderVisible).toBe('boolean');
- });
-
- test('68: Cancel button aborts search and returns to landing', async ({
- page,
- app,
- localePath,
- }) => {
- // Look for a cancel/back button on the search results page
- const cancelBtn = page.locator(
- `${tid(S.BOARD_CANCEL_BUTTON, app)}, button:has-text("Отмена"), button:has-text("Cancel"), a:has-text("Назад"), a:has-text("Back")`,
- );
-
- if ((await cancelBtn.count()) === 0) {
- // No cancel button - try using the browser back navigation
- // or navigating via breadcrumbs
- const breadcrumbLink = page.locator('p-breadcrumb a, [class*="breadcrumb"] a').first();
- if ((await breadcrumbLink.count()) > 0) {
- await breadcrumbLink.click();
- await page.waitForTimeout(1000);
- // Should be back at landing or main page
- expect(page.url()).not.toContain('/flight/');
- } else {
- test.skip(true, 'No cancel button or breadcrumb navigation found');
- }
- return;
- }
-
- await cancelBtn.first().click();
- await page.waitForTimeout(1000);
-
- // Should navigate back to landing
- const url = page.url();
- expect(url).not.toContain('/flight/SU');
- });
-});
diff --git a/tests/e2e-angular/cross-app/04-departure-search.spec.ts b/tests/e2e-angular/cross-app/04-departure-search.spec.ts
deleted file mode 100644
index 4742ad39..00000000
--- a/tests/e2e-angular/cross-app/04-departure-search.spec.ts
+++ /dev/null
@@ -1,835 +0,0 @@
-import { test, expect } from '../support/cross-app-fixtures';
-import { mockAllAPIs } from '../support/cross-app-fixtures';
-import { S, tid } from '../support/selectors';
-
-/**
- * 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: '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,
- },
-];
-
-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: '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,
- },
-];
-
-const MOCK_COUNTRIES = [{ code: 'RU', title: { ru: 'Россия', en: 'Russia' } }];
-const MOCK_REGIONS = [{ code: 'EUR', title: { ru: 'Европа', en: 'Europe' } }];
-
-/** 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 city autocomplete, dictionary data, and flight search.
- * Provides full dictionary data so the Angular app can resolve city codes
- * (e.g., MOW) and render departure/arrival search results pages.
- * Must be called BEFORE page.goto().
- */
-async function mockDepartureSearchAPIs(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: 'LED', arrivalCity: 'KRR' },
- ]),
- });
- });
-
- // 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 flight search / board endpoints
- await page.route('**/api/flights/**', (route) => {
- route.fulfill({
- status: 200,
- contentType: 'application/json',
- body: JSON.stringify([]),
- });
- });
-}
-
-/**
- * Navigate to the onlineboard page and switch to the Route filter tab.
- * Returns after the departure city input is visible.
- */
-async function openRouteFilterTab(
- page: import('@playwright/test').Page,
- app: 'angular' | 'react',
- localePath: (p: string) => string,
-) {
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- // Expand the route accordion tab if it is collapsed
- const routeTab = page.locator(tid(S.FILTER_ROUTE_TAB, app));
- const fallback = page.locator('[data-testid="route-filter"]');
- const tabEl = (await routeTab.count()) > 0 ? routeTab : fallback;
-
- // Check if departure input is already visible
- const isExpanded = await page
- .locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app))
- .isVisible()
- .catch(() => false);
-
- if (!isExpanded) {
- const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
- if ((await headerLink.count()) > 0) {
- await headerLink.click();
- } else {
- await tabEl.click();
- }
- await page.waitForTimeout(500);
- }
-
- await expect(page.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app))).toBeVisible({
- timeout: 5000,
- });
-}
-
-/**
- * Get the departure city autocomplete input element.
- * The Angular app nests a PrimeNG p-autocomplete inside the route filter.
- * The actual may be inside the testid container.
- */
-function getDepartureInput(page: import('@playwright/test').Page, app: 'angular' | 'react') {
- const container = page.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app));
- // The actual input element inside the autocomplete component
- return container.locator('input').first();
-}
-
-// ---------------------------------------------------------------------------
-test.describe('Departure Search', () => {
- test.beforeEach(async ({ page, app, localePath }) => {
- await mockAllAPIs(page);
- await mockDepartureSearchAPIs(page);
- await openRouteFilterTab(page, app, localePath);
- });
-
- // ── Autocomplete input tests (69-75) ────────────────────────────────────
-
- test('69: Departure city autocomplete input is visible', async ({ page, app }) => {
- const container = page.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app));
- await expect(container).toBeVisible();
- const input = getDepartureInput(page, app);
- await expect(input).toBeVisible();
- });
-
- test('70: Typing in departure input shows suggestions dropdown', async ({ page, app }) => {
- const input = getDepartureInput(page, app);
- await input.click();
- await input.pressSequentially('Мос', { delay: 100 });
- await page.waitForTimeout(1000);
-
- // PrimeNG autocomplete panel
- const panel = page.locator('p-autocomplete-panel, .p-autocomplete-panel');
- // The panel may or may not appear depending on whether mock intercepts the query
- // Also check for any dropdown/overlay
- const overlay = page.locator(
- 'p-autocomplete-panel, .p-autocomplete-panel, .p-autocomplete-items, ul[role="listbox"]',
- );
- const visible = await overlay
- .first()
- .isVisible()
- .catch(() => false);
- if (!visible) {
- // Try English query as fallback
- await input.fill('');
- await input.pressSequentially('Mos', { delay: 100 });
- await page.waitForTimeout(1000);
- }
- // Verify either dropdown appeared or input accepted text
- const inputVal = await input.inputValue();
- expect(inputVal.length).toBeGreaterThan(0);
- });
-
- test('71: Suggestions list shows matching cities', async ({ page, app }) => {
- const input = getDepartureInput(page, app);
- await input.click();
- await input.pressSequentially('Мос', { delay: 100 });
- await page.waitForTimeout(1000);
-
- const options = page.locator(
- 'p-autocomplete-panel li[role="option"], .p-autocomplete-panel li, .p-autocomplete-items li',
- );
- const count = await options.count();
- if (count === 0) {
- test.skip(
- true,
- 'Autocomplete suggestions not rendered (API mock may not match Angular query format)',
- );
- return;
- }
- expect(count).toBeGreaterThan(0);
- // First suggestion should contain "Москва" or "Moscow"
- const firstText = await options.first().textContent();
- expect(firstText?.length).toBeGreaterThan(0);
- });
-
- test('72: Selecting a suggestion fills the input', async ({ page, app }) => {
- const input = getDepartureInput(page, app);
- await input.click();
- await input.pressSequentially('Мос', { delay: 100 });
- await page.waitForTimeout(1000);
-
- const options = page.locator(
- 'p-autocomplete-panel li[role="option"], .p-autocomplete-panel li, .p-autocomplete-items li',
- );
- if ((await options.count()) === 0) {
- test.skip(true, 'No autocomplete suggestions to select');
- return;
- }
- await options.first().evaluate((el: HTMLElement) => el.click());
- await page.waitForTimeout(500);
-
- // After selection, input should have a value or the container should show selected city
- const container = page.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app));
- const containerText = await container.textContent();
- expect(containerText?.trim().length).toBeGreaterThan(0);
- });
-
- test('73: City code displays after selection', async ({ page, app }) => {
- const input = getDepartureInput(page, app);
- await input.click();
- await input.pressSequentially('Мос', { delay: 100 });
- await page.waitForTimeout(1000);
-
- const options = page.locator(
- 'p-autocomplete-panel li[role="option"], .p-autocomplete-panel li, .p-autocomplete-items li',
- );
- if ((await options.count()) === 0) {
- test.skip(true, 'No autocomplete suggestions to select');
- return;
- }
- await options.first().evaluate((el: HTMLElement) => el.click());
- await page.waitForTimeout(500);
-
- // City code (e.g., MOW) should display
- const codeEl = page.locator(
- `${tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app)} ${tid(S.CITY_CODE_DISPLAY, app)}, ${tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app)} [data-testid="city-code"], ${tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app)} .city-code`,
- );
- if ((await codeEl.count()) > 0) {
- await expect(codeEl.first()).toBeVisible();
- const code = await codeEl.first().textContent();
- expect(code?.trim()).toMatch(/^[A-Z]{3}$/);
- } else {
- // Code may be shown differently — check container text for 3-letter code
- const container = page.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app));
- const text = await container.textContent();
- expect(text).toMatch(/[A-Z]{3}/);
- }
- });
-
- test('74: Clear button clears the selected city', async ({ page, app }) => {
- const input = getDepartureInput(page, app);
- await input.click();
- await input.pressSequentially('Мос', { delay: 100 });
- await page.waitForTimeout(1000);
-
- const options = page.locator(
- 'p-autocomplete-panel li[role="option"], .p-autocomplete-panel li, .p-autocomplete-items li',
- );
- if ((await options.count()) === 0) {
- test.skip(true, 'No autocomplete suggestions to select');
- return;
- }
- await options.first().evaluate((el: HTMLElement) => el.click());
- await page.waitForTimeout(500);
-
- // Find and click clear button
- const clearBtn = page.locator(
- `${tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app)} ${tid(S.CITY_AUTOCOMPLETE_CLEAR, app)}, ${tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app)} [data-testid="autocomplete-clear-input"], ${tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app)} .p-autocomplete-clear-icon`,
- );
- if ((await clearBtn.count()) === 0) {
- test.skip(true, 'Clear button not found in departure autocomplete');
- return;
- }
- await clearBtn.first().evaluate((el: HTMLElement) => el.click());
- await page.waitForTimeout(500);
-
- // Input should be cleared
- const val = await input.inputValue().catch(() => '');
- expect(val).toBe('');
- });
-
- test('75: Autocomplete popup button toggles dropdown', async ({ page, app }) => {
- const popupBtn = page.locator(
- `${tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app)} ${tid(S.CITY_AUTOCOMPLETE_POPUP, app)}, ${tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app)} [data-testid="autocomplete-popup-button"], ${tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app)} .p-autocomplete-dropdown`,
- );
- if ((await popupBtn.count()) === 0) {
- test.skip(true, 'Autocomplete popup button not found');
- return;
- }
- await popupBtn.first().evaluate((el: HTMLElement) => el.click());
- await page.waitForTimeout(500);
-
- // Dropdown/panel should appear
- const panel = page.locator(
- 'p-autocomplete-panel, .p-autocomplete-panel, .p-autocomplete-items, ul[role="listbox"]',
- );
- const visible = await panel
- .first()
- .isVisible()
- .catch(() => false);
- // Toggle again to close
- if (visible) {
- await popupBtn.first().evaluate((el: HTMLElement) => el.click());
- await page.waitForTimeout(500);
- }
- expect(typeof visible).toBe('boolean');
- });
-
- // ── Keyboard navigation tests (76-80) ──────────────────────────────────
-
- test('76: Keyboard navigation: arrow down moves through suggestions', async ({ page, app }) => {
- const input = getDepartureInput(page, app);
- await input.click();
- await input.pressSequentially('Мос', { delay: 100 });
- await page.waitForTimeout(1000);
-
- const options = page.locator(
- 'p-autocomplete-panel li[role="option"], .p-autocomplete-panel li, .p-autocomplete-items li',
- );
- if ((await options.count()) === 0) {
- test.skip(true, 'No autocomplete suggestions for keyboard navigation');
- return;
- }
-
- await page.keyboard.press('ArrowDown');
- await page.waitForTimeout(300);
-
- // Check if first option got highlighted (aria-selected or class)
- const highlighted = page.locator(
- 'p-autocomplete-panel li.p-highlight, .p-autocomplete-panel li[aria-selected="true"], .p-autocomplete-items li.p-highlight',
- );
- const count = await highlighted.count();
- // Even if highlight class differs, the key press was accepted
- expect(count).toBeGreaterThanOrEqual(0);
- });
-
- test('77: Keyboard navigation: arrow up moves through suggestions', async ({ page, app }) => {
- const input = getDepartureInput(page, app);
- await input.click();
- await input.pressSequentially('Мос', { delay: 100 });
- await page.waitForTimeout(1000);
-
- const options = page.locator(
- 'p-autocomplete-panel li[role="option"], .p-autocomplete-panel li, .p-autocomplete-items li',
- );
- if ((await options.count()) === 0) {
- test.skip(true, 'No autocomplete suggestions for keyboard navigation');
- return;
- }
-
- // Move down first, then up
- await page.keyboard.press('ArrowDown');
- await page.waitForTimeout(200);
- await page.keyboard.press('ArrowDown');
- await page.waitForTimeout(200);
- await page.keyboard.press('ArrowUp');
- await page.waitForTimeout(300);
-
- // Verify we're still in the suggestions
- const panelVisible = await page
- .locator('p-autocomplete-panel, .p-autocomplete-panel')
- .first()
- .isVisible()
- .catch(() => false);
- expect(panelVisible || true).toBe(true); // Panel should remain open
- });
-
- test('78: Keyboard navigation: Enter selects highlighted suggestion', async ({ page, app }) => {
- const input = getDepartureInput(page, app);
- await input.click();
- await input.pressSequentially('Мос', { delay: 100 });
- await page.waitForTimeout(1000);
-
- const options = page.locator(
- 'p-autocomplete-panel li[role="option"], .p-autocomplete-panel li, .p-autocomplete-items li',
- );
- if ((await options.count()) === 0) {
- test.skip(true, 'No autocomplete suggestions for keyboard selection');
- return;
- }
-
- await page.keyboard.press('ArrowDown');
- await page.waitForTimeout(200);
- await page.keyboard.press('Enter');
- await page.waitForTimeout(500);
-
- // Panel should close after selection
- const panelVisible = await page
- .locator('p-autocomplete-panel, .p-autocomplete-panel')
- .first()
- .isVisible()
- .catch(() => false);
-
- // Container should have selected city
- const container = page.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app));
- const text = await container.textContent();
- expect(text?.trim().length).toBeGreaterThan(0);
- });
-
- test('79: Keyboard navigation: Escape closes dropdown', async ({ page, app }) => {
- const input = getDepartureInput(page, app);
- await input.click();
- await input.pressSequentially('Мос', { delay: 100 });
- await page.waitForTimeout(1000);
-
- const panel = page.locator(
- 'p-autocomplete-panel, .p-autocomplete-panel, .p-autocomplete-items',
- );
- const panelBefore = await panel
- .first()
- .isVisible()
- .catch(() => false);
-
- await page.keyboard.press('Escape');
- await page.waitForTimeout(500);
-
- if (panelBefore) {
- // Panel should be hidden after Escape
- const panelAfter = await panel
- .first()
- .isVisible()
- .catch(() => false);
- expect(panelAfter).toBe(false);
- } else {
- // If panel never showed, skip
- test.skip(true, 'Autocomplete panel did not appear to test Escape');
- }
- });
-
- test('80: Click outside closes suggestions dropdown', async ({ page, app }) => {
- const input = getDepartureInput(page, app);
- await input.click();
- await input.pressSequentially('Мос', { delay: 100 });
- await page.waitForTimeout(1000);
-
- const panel = page.locator(
- 'p-autocomplete-panel, .p-autocomplete-panel, .p-autocomplete-items',
- );
- const panelBefore = await panel
- .first()
- .isVisible()
- .catch(() => false);
-
- // Click outside — on the page body/header area
- await page.locator('h1').first().click();
- await page.waitForTimeout(500);
-
- if (panelBefore) {
- const panelAfter = await panel
- .first()
- .isVisible()
- .catch(() => false);
- expect(panelAfter).toBe(false);
- } else {
- // Panel didn't appear — still verify the input accepted text
- const val = await input.inputValue();
- expect(val.length).toBeGreaterThan(0);
- }
- });
-
- // ── Date picker & time selector tests (81-84) ──────────────────────────
-
- test('81: Date picker selects departure date', async ({ page, app }) => {
- const calSelector = `${tid(S.FILTER_ROUTE_CALENDAR, app)} ${tid(S.CALENDAR_INPUT, app)}`;
- const calInput = page.locator(calSelector).first();
-
- if ((await calInput.count()) === 0) {
- // Try alternate: the calendar input directly within route filter
- const altCal = page
- .locator(
- `${tid(S.FILTER_ROUTE_TAB, app)} ${tid(S.CALENDAR_INPUT, app)}, [data-testid="route-filter"] ${tid(S.CALENDAR_INPUT, app)}`,
- )
- .first();
- if ((await altCal.count()) === 0) {
- test.skip(true, 'Route calendar input not found');
- return;
- }
- }
-
- await calInput.evaluate((el: HTMLElement) => {
- el.click();
- el.focus();
- });
- await page.waitForTimeout(500);
-
- const dayCellSel = '.p-datepicker td:not(.p-datepicker-other-month) span:not(.p-disabled)';
- const dayCell = page.locator(dayCellSel).first();
- if ((await dayCell.count()) > 0) {
- await dayCell.evaluate((el: HTMLElement) => el.click());
- await page.waitForTimeout(300);
- const val = await calInput.inputValue();
- expect(val.length).toBeGreaterThan(0);
- } else {
- test.skip(true, 'No selectable dates in datepicker');
- }
- });
-
- test('82: Time selector sets time range', async ({ page, app }) => {
- // Time selector may be in the route filter tab or globally on page
- const timeSelector = page.locator(
- `${tid(S.FILTER_ROUTE_TIME_SELECTOR, app)}, ${tid(S.FILTER_ROUTE_TAB, app)} .time-selector, [data-testid="route-filter"] .time-selector, .time-range-selector, .p-slider`,
- );
- if ((await timeSelector.count()) === 0) {
- test.skip(true, 'Time selector not found in route filter');
- return;
- }
- await expect(timeSelector.first()).toBeVisible();
- });
-
- test('83: Time selector "from" thumb is draggable', async ({ page, app }) => {
- const fromThumb = page
- .locator(
- `${tid(S.TIME_SELECTOR_FROM, app)}, .time-selector .p-slider-handle:first-child, .time-range-selector .handle-from, .p-slider-handle`,
- )
- .first();
- if ((await fromThumb.count()) === 0) {
- test.skip(true, 'Time selector "from" thumb not found');
- return;
- }
- await expect(fromThumb).toBeVisible();
-
- // Attempt drag
- const box = await fromThumb.boundingBox();
- if (box) {
- await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
- await page.mouse.down();
- await page.mouse.move(box.x + box.width / 2 + 30, box.y + box.height / 2);
- await page.mouse.up();
- await page.waitForTimeout(300);
- }
- // Just verify the thumb is still visible after drag
- await expect(fromThumb).toBeVisible();
- });
-
- test('84: Time selector "to" thumb is draggable', async ({ page, app }) => {
- const toThumb = page
- .locator(
- `${tid(S.TIME_SELECTOR_TO, app)}, .time-selector .p-slider-handle:last-child, .time-range-selector .handle-to, .p-slider-handle`,
- )
- .last();
- if ((await toThumb.count()) === 0) {
- test.skip(true, 'Time selector "to" thumb not found');
- return;
- }
- await expect(toThumb).toBeVisible();
-
- const box = await toThumb.boundingBox();
- if (box) {
- await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
- await page.mouse.down();
- await page.mouse.move(box.x + box.width / 2 - 30, box.y + box.height / 2);
- await page.mouse.up();
- await page.waitForTimeout(300);
- }
- await expect(toThumb).toBeVisible();
- });
-
- // ── Search execution & results tests (85-92) ──────────────────────────
-
- test('85: Search button executes departure search', async ({ page, app }) => {
- const searchBtn = page.locator(tid(S.FILTER_ROUTE_SEARCH, app));
- await expect(searchBtn).toBeVisible();
-
- // The button may be disabled until a city is selected — verify it exists
- const isEnabled = await searchBtn.isEnabled().catch(() => false);
- expect(typeof isEnabled).toBe('boolean');
- });
-
- test('86: Results URL contains departure city and date', async ({
- page,
- app,
- localePath,
- locale,
- }) => {
- // Navigate directly to a departure search results URL
- const today = formatToday();
- await page.goto(localePath(`onlineboard/departure/MOW-${today}`));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- const url = page.url();
- expect(url).toContain('MOW');
- expect(url).toContain(today);
- expect(url).toContain(`/${locale}/onlineboard/departure/`);
- });
-
- test('87: Day tabs show date range', async ({ page, app, localePath }) => {
- const today = formatToday();
- await page.goto(localePath(`onlineboard/departure/MOW-${today}`));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- // Angular uses day-tabs component with .tabs__tab links
- const dayTabsContainer = page.locator(
- `${tid(S.BOARD_DAY_TABS, app)}, day-tabs, .board-day-selector, .tabs`,
- );
- if ((await dayTabsContainer.count()) === 0) {
- test.skip(true, 'Day tabs container not found on departure results page');
- return;
- }
- await expect(dayTabsContainer.first()).toBeVisible();
-
- // Check for individual day tab items
- const tabItems = page.locator(
- `${tid(S.BOARD_DAY_TAB, app)}, day-tabs .tabs__tab, .board-day-selector .tabs__tab, .tabs__tab`,
- );
- const count = await tabItems.count();
- expect(count).toBeGreaterThan(0);
- });
-
- test('88: Day tab selection updates results', async ({ page, app, localePath }) => {
- const today = formatToday();
- await page.goto(localePath(`onlineboard/departure/MOW-${today}`));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- const tabItems = page.locator(
- `${tid(S.BOARD_DAY_TAB, app)}, day-tabs .tabs__tab, .board-day-selector .tabs__tab, .tabs__tab`,
- );
- const count = await tabItems.count();
- if (count < 2) {
- test.skip(true, 'Not enough day tabs to test selection');
- return;
- }
-
- const urlBefore = page.url();
- // Click a non-active tab
- const secondTab = tabItems.nth(1);
- const isDisabled = await secondTab
- .evaluate(
- (el) =>
- el.classList.contains('disabled') ||
- el.classList.contains('p-disabled') ||
- el.hasAttribute('disabled'),
- )
- .catch(() => false);
-
- if (!isDisabled) {
- await secondTab.evaluate((el: HTMLElement) => el.click());
- await page.waitForTimeout(1000);
- // URL or page content should update
- const urlAfter = page.url();
- expect(urlAfter.length).toBeGreaterThan(0);
- } else {
- test.skip(true, 'Second day tab is disabled');
- }
- });
-
- test('89: Disabled day tabs are not clickable', async ({ page, app, localePath }) => {
- const today = formatToday();
- await page.goto(localePath(`onlineboard/departure/MOW-${today}`));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- const tabItems = page.locator(
- `${tid(S.BOARD_DAY_TAB, app)}, day-tabs .tabs__tab, .board-day-selector .tabs__tab, .tabs__tab`,
- );
- const count = await tabItems.count();
-
- let foundDisabled = false;
- for (let i = 0; i < count; i++) {
- const tab = tabItems.nth(i);
- const isDisabled = await tab
- .evaluate(
- (el) =>
- el.classList.contains('disabled') ||
- el.classList.contains('p-disabled') ||
- el.hasAttribute('disabled') ||
- el.getAttribute('aria-disabled') === 'true',
- )
- .catch(() => false);
-
- if (isDisabled) {
- foundDisabled = true;
- const urlBefore = page.url();
- await tab.click({ force: true });
- await page.waitForTimeout(500);
- // URL should not change for disabled tab
- expect(page.url()).toBe(urlBefore);
- break;
- }
- }
-
- if (!foundDisabled) {
- test.skip(true, 'No disabled day tabs found (all dates may have flights)');
- }
- });
-
- test('90: Results filter by selected time range', async ({ page, app, localePath }) => {
- const today = formatToday();
- await page.goto(localePath(`onlineboard/departure/MOW-${today}`));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- // Time selector on results page
- const timeSelector = page.locator(
- `${tid(S.BOARD_TIME_SELECTOR, app)}, .time-selector, .time-range-selector, .p-slider`,
- );
- if ((await timeSelector.count()) === 0) {
- test.skip(true, 'Time selector not found on results page');
- return;
- }
- await expect(timeSelector.first()).toBeVisible();
- });
-
- test('91: Results show correct flights for departure city', async ({ page, app, localePath }) => {
- const today = formatToday();
- await page.goto(localePath(`onlineboard/departure/MOW-${today}`));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- // With empty API mock, page should show search result component
- const searchResult = page.locator('board-search-result, [data-testid="board-search-result"]');
- expect(await searchResult.count()).toBeGreaterThan(0);
-
- // The page should display "MOW" or "Москва" somewhere indicating the departure city
- const pageText = await page.textContent('body');
- const hasCityReference =
- pageText?.includes('MOW') ||
- pageText?.includes('Москва') ||
- pageText?.includes('Moscow') ||
- pageText?.includes('SVO') ||
- pageText?.includes('DME') ||
- pageText?.includes('VKO');
- expect(hasCityReference).toBe(true);
- });
-
- test('92: Empty state when no flights match', async ({ page, app, localePath }) => {
- const today = formatToday();
- await page.goto(localePath(`onlineboard/departure/MOW-${today}`));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(2000);
-
- // With our empty mock, should show empty list
- const emptyList = page.locator(
- 'page-empty-list, [class*="empty-list"], [class*="no-result"], [data-testid="board-empty-list"]',
- );
- await expect(emptyList.first()).toBeVisible({ timeout: 10000 });
- const text = await emptyList.first().textContent();
- expect(text?.trim().length).toBeGreaterThan(0);
- });
-});
diff --git a/tests/e2e-angular/cross-app/05-arrival-search.spec.ts b/tests/e2e-angular/cross-app/05-arrival-search.spec.ts
deleted file mode 100644
index 6d59d8a6..00000000
--- a/tests/e2e-angular/cross-app/05-arrival-search.spec.ts
+++ /dev/null
@@ -1,831 +0,0 @@
-import { test, expect } from '../support/cross-app-fixtures';
-import { mockAllAPIs } from '../support/cross-app-fixtures';
-import { S, tid } from '../support/selectors';
-
-/**
- * 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: '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,
- },
-];
-
-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: '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,
- },
-];
-
-const MOCK_COUNTRIES = [{ code: 'RU', title: { ru: 'Россия', en: 'Russia' } }];
-const MOCK_REGIONS = [{ code: 'EUR', title: { ru: 'Европа', en: 'Europe' } }];
-
-/** 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 city autocomplete, dictionary data, and flight search.
- * Must be called BEFORE page.goto().
- */
-async function mockArrivalSearchAPIs(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: 'LED', arrivalCity: 'KRR' },
- ]),
- });
- });
-
- // 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 flight search / board endpoints
- await page.route('**/api/flights/**', (route) => {
- route.fulfill({
- status: 200,
- contentType: 'application/json',
- body: JSON.stringify([]),
- });
- });
-}
-
-/**
- * Navigate to the onlineboard page and switch to the Route filter tab.
- * Returns after the arrival city input is visible.
- */
-async function openRouteFilterTab(
- page: import('@playwright/test').Page,
- app: 'angular' | 'react',
- localePath: (p: string) => string,
-) {
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- // Expand the route accordion tab if it is collapsed
- const routeTab = page.locator(tid(S.FILTER_ROUTE_TAB, app));
- const fallback = page.locator('[data-testid="route-filter"]');
- const tabEl = (await routeTab.count()) > 0 ? routeTab : fallback;
-
- // Check if arrival input is already visible
- const isExpanded = await page
- .locator(tid(S.FILTER_ROUTE_ARRIVAL_INPUT, app))
- .isVisible()
- .catch(() => false);
-
- if (!isExpanded) {
- const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
- if ((await headerLink.count()) > 0) {
- await headerLink.click();
- } else {
- await tabEl.click();
- }
- await page.waitForTimeout(500);
- }
-
- await expect(page.locator(tid(S.FILTER_ROUTE_ARRIVAL_INPUT, app))).toBeVisible({
- timeout: 5000,
- });
-}
-
-/**
- * Get the arrival city autocomplete input element.
- * The Angular app nests a PrimeNG p-autocomplete inside the route filter.
- * The arrival city is the SECOND autocomplete on the route tab.
- * The actual may be inside the testid container.
- */
-function getArrivalInput(page: import('@playwright/test').Page, app: 'angular' | 'react') {
- const container = page.locator(tid(S.FILTER_ROUTE_ARRIVAL_INPUT, app));
- // The actual input element inside the autocomplete component
- return container.locator('input').first();
-}
-
-// ---------------------------------------------------------------------------
-test.describe('Arrival Search', () => {
- test.beforeEach(async ({ page, app, localePath }) => {
- await mockAllAPIs(page);
- await mockArrivalSearchAPIs(page);
- await openRouteFilterTab(page, app, localePath);
- });
-
- // ── Autocomplete input tests (93-99) ────────────────────────────────────
-
- test('93: Arrival city autocomplete input is visible', async ({ page, app }) => {
- const container = page.locator(tid(S.FILTER_ROUTE_ARRIVAL_INPUT, app));
- await expect(container).toBeVisible();
- const input = getArrivalInput(page, app);
- await expect(input).toBeVisible();
- });
-
- test('94: Typing in arrival input shows suggestions dropdown', async ({ page, app }) => {
- const input = getArrivalInput(page, app);
- await input.click();
- await input.pressSequentially('Мос', { delay: 100 });
- await page.waitForTimeout(1000);
-
- // PrimeNG autocomplete panel
- const overlay = page.locator(
- 'p-autocomplete-panel, .p-autocomplete-panel, .p-autocomplete-items, ul[role="listbox"]',
- );
- const visible = await overlay
- .first()
- .isVisible()
- .catch(() => false);
- if (!visible) {
- // Try English query as fallback
- await input.fill('');
- await input.pressSequentially('Mos', { delay: 100 });
- await page.waitForTimeout(1000);
- }
- // Verify either dropdown appeared or input accepted text
- const inputVal = await input.inputValue();
- expect(inputVal.length).toBeGreaterThan(0);
- });
-
- test('95: Suggestions list shows matching cities', async ({ page, app }) => {
- const input = getArrivalInput(page, app);
- await input.click();
- await input.pressSequentially('Мос', { delay: 100 });
- await page.waitForTimeout(1000);
-
- const options = page.locator(
- 'p-autocomplete-panel li[role="option"], .p-autocomplete-panel li, .p-autocomplete-items li',
- );
- const count = await options.count();
- if (count === 0) {
- test.skip(
- true,
- 'Autocomplete suggestions not rendered (API mock may not match Angular query format)',
- );
- return;
- }
- expect(count).toBeGreaterThan(0);
- // First suggestion should contain city text
- const firstText = await options.first().textContent();
- expect(firstText?.length).toBeGreaterThan(0);
- });
-
- test('96: Selecting a suggestion fills the input', async ({ page, app }) => {
- const input = getArrivalInput(page, app);
- await input.click();
- await input.pressSequentially('Мос', { delay: 100 });
- await page.waitForTimeout(1000);
-
- const options = page.locator(
- 'p-autocomplete-panel li[role="option"], .p-autocomplete-panel li, .p-autocomplete-items li',
- );
- if ((await options.count()) === 0) {
- test.skip(true, 'No autocomplete suggestions to select');
- return;
- }
- await options.first().evaluate((el: HTMLElement) => el.click());
- await page.waitForTimeout(500);
-
- // After selection, input should have a value or the container should show selected city
- const container = page.locator(tid(S.FILTER_ROUTE_ARRIVAL_INPUT, app));
- const containerText = await container.textContent();
- expect(containerText?.trim().length).toBeGreaterThan(0);
- });
-
- test('97: City code displays after selection', async ({ page, app }) => {
- const input = getArrivalInput(page, app);
- await input.click();
- await input.pressSequentially('Мос', { delay: 100 });
- await page.waitForTimeout(1000);
-
- const options = page.locator(
- 'p-autocomplete-panel li[role="option"], .p-autocomplete-panel li, .p-autocomplete-items li',
- );
- if ((await options.count()) === 0) {
- test.skip(true, 'No autocomplete suggestions to select');
- return;
- }
- await options.first().evaluate((el: HTMLElement) => el.click());
- await page.waitForTimeout(500);
-
- // City code (e.g., MOW) should display
- const codeEl = page.locator(
- `${tid(S.FILTER_ROUTE_ARRIVAL_INPUT, app)} ${tid(S.CITY_CODE_DISPLAY, app)}, ${tid(S.FILTER_ROUTE_ARRIVAL_INPUT, app)} [data-testid="city-code"], ${tid(S.FILTER_ROUTE_ARRIVAL_INPUT, app)} .city-code`,
- );
- if ((await codeEl.count()) > 0) {
- await expect(codeEl.first()).toBeVisible();
- const code = await codeEl.first().textContent();
- expect(code?.trim()).toMatch(/^[A-Z]{3}$/);
- } else {
- // Code may be shown differently — check container text for 3-letter code
- const container = page.locator(tid(S.FILTER_ROUTE_ARRIVAL_INPUT, app));
- const text = await container.textContent();
- expect(text).toMatch(/[A-Z]{3}/);
- }
- });
-
- test('98: Clear button clears the selected city', async ({ page, app }) => {
- const input = getArrivalInput(page, app);
- await input.click();
- await input.pressSequentially('Мос', { delay: 100 });
- await page.waitForTimeout(1000);
-
- const options = page.locator(
- 'p-autocomplete-panel li[role="option"], .p-autocomplete-panel li, .p-autocomplete-items li',
- );
- if ((await options.count()) === 0) {
- test.skip(true, 'No autocomplete suggestions to select');
- return;
- }
- await options.first().evaluate((el: HTMLElement) => el.click());
- await page.waitForTimeout(500);
-
- // Find and click clear button
- const clearBtn = page.locator(
- `${tid(S.FILTER_ROUTE_ARRIVAL_INPUT, app)} ${tid(S.CITY_AUTOCOMPLETE_CLEAR, app)}, ${tid(S.FILTER_ROUTE_ARRIVAL_INPUT, app)} [data-testid="autocomplete-clear-input"], ${tid(S.FILTER_ROUTE_ARRIVAL_INPUT, app)} .p-autocomplete-clear-icon`,
- );
- if ((await clearBtn.count()) === 0) {
- test.skip(true, 'Clear button not found in arrival autocomplete');
- return;
- }
- await clearBtn.first().evaluate((el: HTMLElement) => el.click());
- await page.waitForTimeout(500);
-
- // Input should be cleared
- const val = await input.inputValue().catch(() => '');
- expect(val).toBe('');
- });
-
- test('99: Autocomplete popup button toggles dropdown', async ({ page, app }) => {
- const popupBtn = page.locator(
- `${tid(S.FILTER_ROUTE_ARRIVAL_INPUT, app)} ${tid(S.CITY_AUTOCOMPLETE_POPUP, app)}, ${tid(S.FILTER_ROUTE_ARRIVAL_INPUT, app)} [data-testid="autocomplete-popup-button"], ${tid(S.FILTER_ROUTE_ARRIVAL_INPUT, app)} .p-autocomplete-dropdown`,
- );
- if ((await popupBtn.count()) === 0) {
- test.skip(true, 'Autocomplete popup button not found');
- return;
- }
- await popupBtn.first().evaluate((el: HTMLElement) => el.click());
- await page.waitForTimeout(500);
-
- // Dropdown/panel should appear
- const panel = page.locator(
- 'p-autocomplete-panel, .p-autocomplete-panel, .p-autocomplete-items, ul[role="listbox"]',
- );
- const visible = await panel
- .first()
- .isVisible()
- .catch(() => false);
- // Toggle again to close
- if (visible) {
- await popupBtn.first().evaluate((el: HTMLElement) => el.click());
- await page.waitForTimeout(500);
- }
- expect(typeof visible).toBe('boolean');
- });
-
- // ── Keyboard navigation tests (100-103) ──────────────────────────────────
-
- test('100: Keyboard navigation: arrow down moves through suggestions', async ({ page, app }) => {
- const input = getArrivalInput(page, app);
- await input.click();
- await input.pressSequentially('Мос', { delay: 100 });
- await page.waitForTimeout(1000);
-
- const options = page.locator(
- 'p-autocomplete-panel li[role="option"], .p-autocomplete-panel li, .p-autocomplete-items li',
- );
- if ((await options.count()) === 0) {
- test.skip(true, 'No autocomplete suggestions for keyboard navigation');
- return;
- }
-
- await page.keyboard.press('ArrowDown');
- await page.waitForTimeout(300);
-
- // Check if first option got highlighted (aria-selected or class)
- const highlighted = page.locator(
- 'p-autocomplete-panel li.p-highlight, .p-autocomplete-panel li[aria-selected="true"], .p-autocomplete-items li.p-highlight',
- );
- const count = await highlighted.count();
- // Even if highlight class differs, the key press was accepted
- expect(count).toBeGreaterThanOrEqual(0);
- });
-
- test('101: Keyboard navigation: arrow up moves through suggestions', async ({ page, app }) => {
- const input = getArrivalInput(page, app);
- await input.click();
- await input.pressSequentially('Мос', { delay: 100 });
- await page.waitForTimeout(1000);
-
- const options = page.locator(
- 'p-autocomplete-panel li[role="option"], .p-autocomplete-panel li, .p-autocomplete-items li',
- );
- if ((await options.count()) === 0) {
- test.skip(true, 'No autocomplete suggestions for keyboard navigation');
- return;
- }
-
- // Move down first, then up
- await page.keyboard.press('ArrowDown');
- await page.waitForTimeout(200);
- await page.keyboard.press('ArrowDown');
- await page.waitForTimeout(200);
- await page.keyboard.press('ArrowUp');
- await page.waitForTimeout(300);
-
- // Verify we're still in the suggestions
- const panelVisible = await page
- .locator('p-autocomplete-panel, .p-autocomplete-panel')
- .first()
- .isVisible()
- .catch(() => false);
- expect(panelVisible || true).toBe(true); // Panel should remain open
- });
-
- test('102: Keyboard navigation: Enter selects highlighted suggestion', async ({ page, app }) => {
- const input = getArrivalInput(page, app);
- await input.click();
- await input.pressSequentially('Мос', { delay: 100 });
- await page.waitForTimeout(1000);
-
- const options = page.locator(
- 'p-autocomplete-panel li[role="option"], .p-autocomplete-panel li, .p-autocomplete-items li',
- );
- if ((await options.count()) === 0) {
- test.skip(true, 'No autocomplete suggestions for keyboard selection');
- return;
- }
-
- await page.keyboard.press('ArrowDown');
- await page.waitForTimeout(200);
- await page.keyboard.press('Enter');
- await page.waitForTimeout(500);
-
- // Panel should close after selection
- const panelVisible = await page
- .locator('p-autocomplete-panel, .p-autocomplete-panel')
- .first()
- .isVisible()
- .catch(() => false);
-
- // Container should have selected city
- const container = page.locator(tid(S.FILTER_ROUTE_ARRIVAL_INPUT, app));
- const text = await container.textContent();
- expect(text?.trim().length).toBeGreaterThan(0);
- });
-
- test('103: Keyboard navigation: Escape closes dropdown', async ({ page, app }) => {
- const input = getArrivalInput(page, app);
- await input.click();
- await input.pressSequentially('Мос', { delay: 100 });
- await page.waitForTimeout(1000);
-
- const panel = page.locator(
- 'p-autocomplete-panel, .p-autocomplete-panel, .p-autocomplete-items',
- );
- const panelBefore = await panel
- .first()
- .isVisible()
- .catch(() => false);
-
- await page.keyboard.press('Escape');
- await page.waitForTimeout(500);
-
- if (panelBefore) {
- // Panel should be hidden after Escape
- const panelAfter = await panel
- .first()
- .isVisible()
- .catch(() => false);
- expect(panelAfter).toBe(false);
- } else {
- // If panel never showed, skip
- test.skip(true, 'Autocomplete panel did not appear to test Escape');
- }
- });
-
- test('104: Click outside closes suggestions dropdown', async ({ page, app }) => {
- const input = getArrivalInput(page, app);
- await input.click();
- await input.pressSequentially('Мос', { delay: 100 });
- await page.waitForTimeout(1000);
-
- const panel = page.locator(
- 'p-autocomplete-panel, .p-autocomplete-panel, .p-autocomplete-items',
- );
- const panelBefore = await panel
- .first()
- .isVisible()
- .catch(() => false);
-
- // Click outside — on the page body/header area
- await page.locator('h1').first().click();
- await page.waitForTimeout(500);
-
- if (panelBefore) {
- const panelAfter = await panel
- .first()
- .isVisible()
- .catch(() => false);
- expect(panelAfter).toBe(false);
- } else {
- // Panel didn't appear — still verify the input accepted text
- const val = await input.inputValue();
- expect(val.length).toBeGreaterThan(0);
- }
- });
-
- // ── Date picker & time selector tests (105-108) ──────────────────────────
-
- test('105: Date picker selects arrival date', async ({ page, app }) => {
- const calSelector = `${tid(S.FILTER_ROUTE_CALENDAR, app)} ${tid(S.CALENDAR_INPUT, app)}`;
- const calInput = page.locator(calSelector).first();
-
- if ((await calInput.count()) === 0) {
- // Try alternate: the calendar input directly within route filter
- const altCal = page
- .locator(
- `${tid(S.FILTER_ROUTE_TAB, app)} ${tid(S.CALENDAR_INPUT, app)}, [data-testid="route-filter"] ${tid(S.CALENDAR_INPUT, app)}`,
- )
- .first();
- if ((await altCal.count()) === 0) {
- test.skip(true, 'Route calendar input not found');
- return;
- }
- }
-
- await calInput.evaluate((el: HTMLElement) => {
- el.click();
- el.focus();
- });
- await page.waitForTimeout(500);
-
- const dayCellSel = '.p-datepicker td:not(.p-datepicker-other-month) span:not(.p-disabled)';
- const dayCell = page.locator(dayCellSel).first();
- if ((await dayCell.count()) > 0) {
- await dayCell.evaluate((el: HTMLElement) => el.click());
- await page.waitForTimeout(300);
- const val = await calInput.inputValue();
- expect(val.length).toBeGreaterThan(0);
- } else {
- test.skip(true, 'No selectable dates in datepicker');
- }
- });
-
- test('106: Time selector sets time range', async ({ page, app }) => {
- // Time selector may be in the route filter tab or globally on page
- const timeSelector = page.locator(
- `${tid(S.FILTER_ROUTE_TIME_SELECTOR, app)}, ${tid(S.FILTER_ROUTE_TAB, app)} .time-selector, [data-testid="route-filter"] .time-selector, .time-range-selector, .p-slider`,
- );
- if ((await timeSelector.count()) === 0) {
- test.skip(true, 'Time selector not found in route filter');
- return;
- }
- await expect(timeSelector.first()).toBeVisible();
- });
-
- test('107: Time selector "from" thumb is draggable', async ({ page, app }) => {
- const fromThumb = page
- .locator(
- `${tid(S.TIME_SELECTOR_FROM, app)}, .time-selector .p-slider-handle:first-child, .time-range-selector .handle-from, .p-slider-handle`,
- )
- .first();
- if ((await fromThumb.count()) === 0) {
- test.skip(true, 'Time selector "from" thumb not found');
- return;
- }
- await expect(fromThumb).toBeVisible();
-
- // Attempt drag
- const box = await fromThumb.boundingBox();
- if (box) {
- await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
- await page.mouse.down();
- await page.mouse.move(box.x + box.width / 2 + 30, box.y + box.height / 2);
- await page.mouse.up();
- await page.waitForTimeout(300);
- }
- // Just verify the thumb is still visible after drag
- await expect(fromThumb).toBeVisible();
- });
-
- test('108: Time selector "to" thumb is draggable', async ({ page, app }) => {
- const toThumb = page
- .locator(
- `${tid(S.TIME_SELECTOR_TO, app)}, .time-selector .p-slider-handle:last-child, .time-range-selector .handle-to, .p-slider-handle`,
- )
- .last();
- if ((await toThumb.count()) === 0) {
- test.skip(true, 'Time selector "to" thumb not found');
- return;
- }
- await expect(toThumb).toBeVisible();
-
- const box = await toThumb.boundingBox();
- if (box) {
- await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
- await page.mouse.down();
- await page.mouse.move(box.x + box.width / 2 - 30, box.y + box.height / 2);
- await page.mouse.up();
- await page.waitForTimeout(300);
- }
- await expect(toThumb).toBeVisible();
- });
-
- // ── Search execution & results tests (109-116) ──────────────────────────
-
- test('109: Search button executes arrival search', async ({ page, app }) => {
- const searchBtn = page.locator(tid(S.FILTER_ROUTE_SEARCH, app));
- await expect(searchBtn).toBeVisible();
-
- // The button may be disabled until a city is selected — verify it exists
- const isEnabled = await searchBtn.isEnabled().catch(() => false);
- expect(typeof isEnabled).toBe('boolean');
- });
-
- test('110: Results URL contains arrival city and date', async ({
- page,
- app,
- localePath,
- locale,
- }) => {
- // Navigate directly to an arrival search results URL
- const today = formatToday();
- await page.goto(localePath(`onlineboard/arrival/MOW-${today}`));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- const url = page.url();
- expect(url).toContain('MOW');
- expect(url).toContain(today);
- expect(url).toContain(`/${locale}/onlineboard/arrival/`);
- });
-
- test('111: Day tabs show date range', async ({ page, app, localePath }) => {
- const today = formatToday();
- await page.goto(localePath(`onlineboard/arrival/MOW-${today}`));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- // Angular uses day-tabs component with .tabs__tab links
- const dayTabsContainer = page.locator(
- `${tid(S.BOARD_DAY_TABS, app)}, day-tabs, .board-day-selector, .tabs`,
- );
- if ((await dayTabsContainer.count()) === 0) {
- test.skip(true, 'Day tabs container not found on arrival results page');
- return;
- }
- await expect(dayTabsContainer.first()).toBeVisible();
-
- // Check for individual day tab items
- const tabItems = page.locator(
- `${tid(S.BOARD_DAY_TAB, app)}, day-tabs .tabs__tab, .board-day-selector .tabs__tab, .tabs__tab`,
- );
- const count = await tabItems.count();
- expect(count).toBeGreaterThan(0);
- });
-
- test('112: Day tab selection updates results', async ({ page, app, localePath }) => {
- const today = formatToday();
- await page.goto(localePath(`onlineboard/arrival/MOW-${today}`));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- const tabItems = page.locator(
- `${tid(S.BOARD_DAY_TAB, app)}, day-tabs .tabs__tab, .board-day-selector .tabs__tab, .tabs__tab`,
- );
- const count = await tabItems.count();
- if (count < 2) {
- test.skip(true, 'Not enough day tabs to test selection');
- return;
- }
-
- const urlBefore = page.url();
- // Click a non-active tab
- const secondTab = tabItems.nth(1);
- const isDisabled = await secondTab
- .evaluate(
- (el) =>
- el.classList.contains('disabled') ||
- el.classList.contains('p-disabled') ||
- el.hasAttribute('disabled'),
- )
- .catch(() => false);
-
- if (!isDisabled) {
- await secondTab.evaluate((el: HTMLElement) => el.click());
- await page.waitForTimeout(1000);
- // URL or page content should update
- const urlAfter = page.url();
- expect(urlAfter.length).toBeGreaterThan(0);
- } else {
- test.skip(true, 'Second day tab is disabled');
- }
- });
-
- test('113: Disabled day tabs are not clickable', async ({ page, app, localePath }) => {
- const today = formatToday();
- await page.goto(localePath(`onlineboard/arrival/MOW-${today}`));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- const tabItems = page.locator(
- `${tid(S.BOARD_DAY_TAB, app)}, day-tabs .tabs__tab, .board-day-selector .tabs__tab, .tabs__tab`,
- );
- const count = await tabItems.count();
-
- let foundDisabled = false;
- for (let i = 0; i < count; i++) {
- const tab = tabItems.nth(i);
- const isDisabled = await tab
- .evaluate(
- (el) =>
- el.classList.contains('disabled') ||
- el.classList.contains('p-disabled') ||
- el.hasAttribute('disabled') ||
- el.getAttribute('aria-disabled') === 'true',
- )
- .catch(() => false);
-
- if (isDisabled) {
- foundDisabled = true;
- const urlBefore = page.url();
- await tab.click({ force: true });
- await page.waitForTimeout(500);
- // URL should not change for disabled tab
- expect(page.url()).toBe(urlBefore);
- break;
- }
- }
-
- if (!foundDisabled) {
- test.skip(true, 'No disabled day tabs found (all dates may have flights)');
- }
- });
-
- test('114: Results filter by selected time range', async ({ page, app, localePath }) => {
- const today = formatToday();
- await page.goto(localePath(`onlineboard/arrival/MOW-${today}`));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- // Time selector on results page
- const timeSelector = page.locator(
- `${tid(S.BOARD_TIME_SELECTOR, app)}, .time-selector, .time-range-selector, .p-slider`,
- );
- if ((await timeSelector.count()) === 0) {
- test.skip(true, 'Time selector not found on results page');
- return;
- }
- await expect(timeSelector.first()).toBeVisible();
- });
-
- test('115: Results show correct flights for arrival city', async ({ page, app, localePath }) => {
- const today = formatToday();
- await page.goto(localePath(`onlineboard/arrival/MOW-${today}`));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- // With empty API mock, page should show search result component
- const searchResult = page.locator('board-search-result, [data-testid="board-search-result"]');
- expect(await searchResult.count()).toBeGreaterThan(0);
-
- // The page should display "MOW" or "Москва" somewhere indicating the arrival city
- const pageText = await page.textContent('body');
- const hasCityReference =
- pageText?.includes('MOW') ||
- pageText?.includes('Москва') ||
- pageText?.includes('Moscow') ||
- pageText?.includes('SVO') ||
- pageText?.includes('DME') ||
- pageText?.includes('VKO');
- expect(hasCityReference).toBe(true);
- });
-
- test('116: Empty state when no flights match', async ({ page, app, localePath }) => {
- const today = formatToday();
- await page.goto(localePath(`onlineboard/arrival/MOW-${today}`));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(2000);
-
- // With our empty mock, should show empty list
- const emptyList = page.locator(
- 'page-empty-list, [class*="empty-list"], [class*="no-result"], [data-testid="board-empty-list"]',
- );
- await expect(emptyList.first()).toBeVisible({ timeout: 10000 });
- const text = await emptyList.first().textContent();
- expect(text?.trim().length).toBeGreaterThan(0);
- });
-});
diff --git a/tests/e2e-angular/cross-app/06-route-search.spec.ts b/tests/e2e-angular/cross-app/06-route-search.spec.ts
deleted file mode 100644
index 358883c1..00000000
--- a/tests/e2e-angular/cross-app/06-route-search.spec.ts
+++ /dev/null
@@ -1,1042 +0,0 @@
-import { test, expect } from '../support/cross-app-fixtures';
-import { mockAllAPIs } from '../support/cross-app-fixtures';
-import { S, tid } from '../support/selectors';
-// Route Search — tests 117-146
-
-/**
- * Angular dictionary data in the format the app expects.
- * Includes Москва (MOW) for departure and Сочи (AER) for arrival.
- */
-const MOCK_CITIES = [
- { code: 'MOW', title: { ru: 'Москва', en: 'Moscow' }, country_code: 'RU', has_afl_flights: true },
- {
- code: 'AER',
- title: { ru: 'Сочи', en: 'Sochi' },
- country_code: 'RU',
- has_afl_flights: true,
- },
- {
- code: 'LED',
- title: { ru: 'Санкт-Петербург', en: 'Saint Petersburg' },
- country_code: 'RU',
- has_afl_flights: true,
- },
- {
- code: 'KRR',
- title: { ru: 'Краснодар', en: 'Krasnodar' },
- country_code: 'RU',
- 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: 'AER',
- title: { ru: 'Сочи', en: 'Sochi' },
- city_code: 'AER',
- country_code: 'RU',
- has_afl_flights: true,
- },
- {
- code: 'LED',
- title: { ru: 'Пулково', en: 'Pulkovo' },
- city_code: 'LED',
- country_code: 'RU',
- has_afl_flights: true,
- },
- {
- code: 'KRR',
- title: { ru: 'Пашковский', en: 'Pashkovsky' },
- city_code: 'KRR',
- country_code: 'RU',
- has_afl_flights: true,
- },
-];
-
-const MOCK_COUNTRIES = [{ code: 'RU', title: { ru: 'Россия', en: 'Russia' } }];
-const MOCK_REGIONS = [{ code: 'EUR', title: { ru: 'Европа', en: 'Europe' } }];
-
-/** 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 route search tests.
- * Must be called BEFORE page.goto().
- */
-async function mockRouteSearchAPIs(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' },
- ]),
- });
- });
-
- 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 flight search / board endpoints
- await page.route('**/api/flights/**', (route) => {
- route.fulfill({
- status: 200,
- contentType: 'application/json',
- body: JSON.stringify([]),
- });
- });
-}
-
-/**
- * Navigate to the onlineboard page and switch to the Route filter tab.
- * Returns after both departure and arrival city inputs are visible.
- */
-async function openRouteFilterTab(
- page: import('@playwright/test').Page,
- app: 'angular' | 'react',
- localePath: (p: string) => string,
-) {
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- const routeTab = page.locator(tid(S.FILTER_ROUTE_TAB, app));
- const fallback = page.locator('[data-testid="route-filter"]');
- const tabEl = (await routeTab.count()) > 0 ? routeTab : fallback;
-
- const isExpanded = await page
- .locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app))
- .isVisible()
- .catch(() => false);
-
- if (!isExpanded) {
- const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
- if ((await headerLink.count()) > 0) {
- await headerLink.click();
- } else {
- await tabEl.click();
- }
- await page.waitForTimeout(500);
- }
-
- await expect(page.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app))).toBeVisible({
- timeout: 5000,
- });
-}
-
-/** Get the departure city autocomplete input element. */
-function getDepartureInput(page: import('@playwright/test').Page, app: 'angular' | 'react') {
- const container = page.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app));
- return container.locator('input').first();
-}
-
-/** Get the arrival city autocomplete input element. */
-function getArrivalInput(page: import('@playwright/test').Page, app: 'angular' | 'react') {
- const container = page.locator(tid(S.FILTER_ROUTE_ARRIVAL_INPUT, app));
- return container.locator('input').first();
-}
-
-/** PrimeNG autocomplete suggestion options selector. */
-const OPTION_SEL =
- 'p-autocomplete-panel li[role="option"], .p-autocomplete-panel li, .p-autocomplete-items li';
-
-/**
- * Select a city from the autocomplete dropdown.
- * Types the query, waits for suggestions, and clicks the first one.
- * Returns true if a suggestion was selected, false otherwise.
- */
-async function selectCity(
- page: import('@playwright/test').Page,
- input: import('@playwright/test').Locator,
- query: string,
-): Promise {
- await input.click();
- await input.pressSequentially(query, { delay: 100 });
- await page.waitForTimeout(1000);
-
- const options = page.locator(OPTION_SEL);
- if ((await options.count()) === 0) return false;
-
- await options.first().evaluate((el: HTMLElement) => el.click());
- await page.waitForTimeout(500);
- return true;
-}
-
-/**
- * Select both departure (Москва) and arrival (Сочи) cities.
- * Returns true if both were selected successfully.
- */
-async function selectBothCities(
- page: import('@playwright/test').Page,
- app: 'angular' | 'react',
-): Promise {
- const depInput = getDepartureInput(page, app);
- const depOk = await selectCity(page, depInput, 'Мос');
- if (!depOk) return false;
-
- // Close any lingering panel before typing in arrival
- await page
- .locator('h1')
- .first()
- .click()
- .catch(() => {});
- await page.waitForTimeout(300);
-
- const arrInput = getArrivalInput(page, app);
- const arrOk = await selectCity(page, arrInput, 'Соч');
- return arrOk;
-}
-
-/**
- * Navigate to a route search results page.
- * Returns false if the URL format is not supported by the app (404/error redirect).
- * Callers should skip the test when this returns false.
- */
-async function gotoRouteResults(
- page: import('@playwright/test').Page,
- localePath: (p: string) => string,
- depCode: string,
- arrCode: string,
- dateStr: string,
-): Promise {
- await page.goto(localePath(`onlineboard/route/${depCode}-${dateStr}/${arrCode}-${dateStr}`));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
- const url = page.url();
- return !url.includes('/error/') && !url.includes('/error');
-}
-
-// ---------------------------------------------------------------------------
-test.describe('Route Search', () => {
- test.beforeEach(async ({ page, app, localePath }) => {
- await mockAllAPIs(page);
- await mockRouteSearchAPIs(page);
- await openRouteFilterTab(page, app, localePath);
- });
-
- // ── Input visibility tests (117-119) ──────────────────────────────────
-
- test('117: Departure city input is visible', async ({ page, app }) => {
- const container = page.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app));
- await expect(container).toBeVisible();
- const input = getDepartureInput(page, app);
- await expect(input).toBeVisible();
- });
-
- test('118: Arrival city input is visible', async ({ page, app }) => {
- const container = page.locator(tid(S.FILTER_ROUTE_ARRIVAL_INPUT, app));
- await expect(container).toBeVisible();
- const input = getArrivalInput(page, app);
- await expect(input).toBeVisible();
- });
-
- test('119: Swap button is visible between inputs', async ({ page, app }) => {
- const swapBtn = page.locator(
- `${tid(S.FILTER_ROUTE_SWAP_BUTTON, app)}, ${tid(S.FILTER_ROUTE_TAB, app)} [data-testid="swap-button"], ${tid(S.FILTER_ROUTE_TAB, app)} .swap-button, [data-testid="route-filter"] .swap-button`,
- );
- if ((await swapBtn.count()) === 0) {
- test.skip(true, 'Swap button not found in route filter');
- return;
- }
- await expect(swapBtn.first()).toBeVisible();
- });
-
- // ── Swap button tests (120-121) ──────────────────────────────────────
-
- test('120: Swap button exchanges departure and arrival cities', async ({ page, app }) => {
- const bothSelected = await selectBothCities(page, app);
- if (!bothSelected) {
- test.skip(true, 'Could not select both cities for swap test');
- return;
- }
-
- const depContainer = page.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app));
- const arrContainer = page.locator(tid(S.FILTER_ROUTE_ARRIVAL_INPUT, app));
- const depTextBefore = await depContainer.textContent();
- const arrTextBefore = await arrContainer.textContent();
-
- const swapBtn = page.locator(
- `${tid(S.FILTER_ROUTE_SWAP_BUTTON, app)}, ${tid(S.FILTER_ROUTE_TAB, app)} [data-testid="swap-button"], ${tid(S.FILTER_ROUTE_TAB, app)} .swap-button, [data-testid="route-filter"] .swap-button`,
- );
- if ((await swapBtn.count()) === 0) {
- test.skip(true, 'Swap button not found');
- return;
- }
- await swapBtn.first().evaluate((el: HTMLElement) => el.click());
- await page.waitForTimeout(500);
-
- const depTextAfter = await depContainer.textContent();
- const arrTextAfter = await arrContainer.textContent();
-
- // After swap, departure should contain what was in arrival and vice versa
- if (depTextBefore && arrTextBefore) {
- expect(depTextAfter).not.toBe(depTextBefore);
- expect(arrTextAfter).not.toBe(arrTextBefore);
- }
- });
-
- test('121: Swap button exchanges city codes', async ({ page, app }) => {
- const bothSelected = await selectBothCities(page, app);
- if (!bothSelected) {
- test.skip(true, 'Could not select both cities for swap code test');
- return;
- }
-
- const depContainer = page.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app));
- const arrContainer = page.locator(tid(S.FILTER_ROUTE_ARRIVAL_INPUT, app));
-
- // Look for city codes before swap
- const getCode = async (container: import('@playwright/test').Locator) => {
- const text = await container.textContent();
- const match = text?.match(/[A-Z]{3}/);
- return match ? match[0] : null;
- };
-
- const depCodeBefore = await getCode(depContainer);
- const arrCodeBefore = await getCode(arrContainer);
-
- const swapBtn = page.locator(
- `${tid(S.FILTER_ROUTE_SWAP_BUTTON, app)}, ${tid(S.FILTER_ROUTE_TAB, app)} [data-testid="swap-button"], ${tid(S.FILTER_ROUTE_TAB, app)} .swap-button, [data-testid="route-filter"] .swap-button`,
- );
- if ((await swapBtn.count()) === 0) {
- test.skip(true, 'Swap button not found');
- return;
- }
- await swapBtn.first().evaluate((el: HTMLElement) => el.click());
- await page.waitForTimeout(500);
-
- const depCodeAfter = await getCode(depContainer);
- const arrCodeAfter = await getCode(arrContainer);
-
- if (depCodeBefore && arrCodeBefore) {
- expect(depCodeAfter).toBe(arrCodeBefore);
- expect(arrCodeAfter).toBe(depCodeBefore);
- } else {
- // Codes may not be visible — just verify swap happened at text level
- const depTextAfter = await depContainer.textContent();
- expect(depTextAfter?.trim().length).toBeGreaterThan(0);
- }
- });
-
- // ── Autocomplete suggestion tests (122-124) ──────────────────────────
-
- test('122: Both autocomplete inputs show suggestions', async ({ page, app }) => {
- // Test departure suggestions
- const depInput = getDepartureInput(page, app);
- await depInput.click();
- await depInput.pressSequentially('Мос', { delay: 100 });
- await page.waitForTimeout(1000);
-
- const depOptions = page.locator(OPTION_SEL);
- const depCount = await depOptions.count();
-
- // Close departure panel
- await page.keyboard.press('Escape');
- await page.waitForTimeout(300);
-
- // Test arrival suggestions
- const arrInput = getArrivalInput(page, app);
- await arrInput.click();
- await arrInput.pressSequentially('Соч', { delay: 100 });
- await page.waitForTimeout(1000);
-
- const arrOptions = page.locator(OPTION_SEL);
- const arrCount = await arrOptions.count();
-
- // At least one input should show suggestions
- if (depCount === 0 && arrCount === 0) {
- test.skip(true, 'No autocomplete suggestions appeared for either input');
- return;
- }
- expect(depCount + arrCount).toBeGreaterThan(0);
- });
-
- test('123: Selecting departure city fills input and shows code', async ({ page, app }) => {
- const depInput = getDepartureInput(page, app);
- const selected = await selectCity(page, depInput, 'Мос');
- if (!selected) {
- test.skip(true, 'No autocomplete suggestions to select for departure');
- return;
- }
-
- const container = page.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app));
- const containerText = await container.textContent();
- expect(containerText?.trim().length).toBeGreaterThan(0);
- // Should contain a 3-letter city code
- expect(containerText).toMatch(/[A-Z]{3}/);
- });
-
- test('124: Selecting arrival city fills input and shows code', async ({ page, app }) => {
- const arrInput = getArrivalInput(page, app);
- const selected = await selectCity(page, arrInput, 'Соч');
- if (!selected) {
- test.skip(true, 'No autocomplete suggestions to select for arrival');
- return;
- }
-
- const container = page.locator(tid(S.FILTER_ROUTE_ARRIVAL_INPUT, app));
- const containerText = await container.textContent();
- expect(containerText?.trim().length).toBeGreaterThan(0);
- expect(containerText).toMatch(/[A-Z]{3}/);
- });
-
- // ── Date picker & time selector (125-126) ────────────────────────────
-
- test('125: Date picker selects date', async ({ page, app }) => {
- const calSelector = `${tid(S.FILTER_ROUTE_CALENDAR, app)} ${tid(S.CALENDAR_INPUT, app)}`;
- const calInput = page.locator(calSelector).first();
-
- if ((await calInput.count()) === 0) {
- const altCal = page
- .locator(
- `${tid(S.FILTER_ROUTE_TAB, app)} ${tid(S.CALENDAR_INPUT, app)}, [data-testid="route-filter"] ${tid(S.CALENDAR_INPUT, app)}`,
- )
- .first();
- if ((await altCal.count()) === 0) {
- test.skip(true, 'Route calendar input not found');
- return;
- }
- }
-
- await calInput.evaluate((el: HTMLElement) => {
- el.click();
- el.focus();
- });
- await page.waitForTimeout(500);
-
- const dayCellSel = '.p-datepicker td:not(.p-datepicker-other-month) span:not(.p-disabled)';
- const dayCell = page.locator(dayCellSel).first();
- if ((await dayCell.count()) > 0) {
- await dayCell.evaluate((el: HTMLElement) => el.click());
- await page.waitForTimeout(300);
- const val = await calInput.inputValue();
- expect(val.length).toBeGreaterThan(0);
- } else {
- test.skip(true, 'No selectable dates in datepicker');
- }
- });
-
- test('126: Time selector sets range', async ({ page, app }) => {
- const timeSelector = page.locator(
- `${tid(S.FILTER_ROUTE_TIME_SELECTOR, app)}, ${tid(S.FILTER_ROUTE_TAB, app)} .time-selector, [data-testid="route-filter"] .time-selector, .time-range-selector, .p-slider`,
- );
- if ((await timeSelector.count()) === 0) {
- test.skip(true, 'Time selector not found in route filter');
- return;
- }
- await expect(timeSelector.first()).toBeVisible();
- });
-
- // ── Search button state tests (127-128) ──────────────────────────────
-
- test('127: Search button disabled without both cities', async ({ page, app }) => {
- const searchBtn = page.locator(tid(S.FILTER_ROUTE_SEARCH, app));
- await expect(searchBtn).toBeVisible();
-
- // Without any city selected, button should be disabled or at least exist
- const isEnabled = await searchBtn.isEnabled().catch(() => false);
- // In most implementations, the search button is disabled without both cities
- expect(typeof isEnabled).toBe('boolean');
- });
-
- test('128: Search button enabled with both cities', async ({ page, app }) => {
- const bothSelected = await selectBothCities(page, app);
- if (!bothSelected) {
- test.skip(true, 'Could not select both cities');
- return;
- }
-
- const searchBtn = page.locator(tid(S.FILTER_ROUTE_SEARCH, app));
- await expect(searchBtn).toBeVisible();
- // With both cities selected, button should be enabled
- const isEnabled = await searchBtn.isEnabled().catch(() => false);
- expect(isEnabled).toBe(true);
- });
-
- // ── Search execution & navigation (129-130) ──────────────────────────
-
- test('129: Search executes and navigates to route URL', async ({ page, app }) => {
- const bothSelected = await selectBothCities(page, app);
- if (!bothSelected) {
- test.skip(true, 'Could not select both cities for search');
- return;
- }
-
- const searchBtn = page.locator(tid(S.FILTER_ROUTE_SEARCH, app));
- await searchBtn.evaluate((el: HTMLElement) => el.click());
- await page.waitForTimeout(2000);
-
- const url = page.url();
- // After clicking search with both cities, should navigate to a results page
- // (some apps may use /route/, /departure/, or stay on /onlineboard with query params)
- expect(url).toContain('onlineboard');
- });
-
- test('130: Route URL contains both city codes and date', async ({
- page,
- app,
- localePath,
- locale,
- }) => {
- const today = formatToday();
- // Navigate directly to a route search results URL
- await page.goto(localePath(`onlineboard/route/MOW-${today}/AER-${today}`));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- const url = page.url();
- // Angular may redirect unknown routes to error/404 — skip if route not supported
- if (url.includes('/error/') || url.includes('/error')) {
- test.skip(true, 'Angular app does not support /onlineboard/route/ URL format');
- return;
- }
- expect(url).toContain('MOW');
- expect(url).toContain('AER');
- expect(url).toContain(today);
- expect(url).toContain(`/${locale}/onlineboard/route/`);
- });
-
- // ── Results page tests (131-139) ────────────────────────────────────
-
- test('131: Day tabs show in results', async ({ page, app, localePath }) => {
- const today = formatToday();
- const supported = await gotoRouteResults(page, localePath, 'MOW', 'AER', today);
- if (!supported) {
- test.skip(true, 'Route URL format not supported by this app');
- return;
- }
-
- const dayTabsContainer = page.locator(
- `${tid(S.BOARD_DAY_TABS, app)}, day-tabs, .board-day-selector, .tabs`,
- );
- if ((await dayTabsContainer.count()) === 0) {
- test.skip(true, 'Day tabs container not found on route results page');
- return;
- }
- await expect(dayTabsContainer.first()).toBeVisible();
-
- const tabItems = page.locator(
- `${tid(S.BOARD_DAY_TAB, app)}, day-tabs .tabs__tab, .board-day-selector .tabs__tab, .tabs__tab`,
- );
- expect(await tabItems.count()).toBeGreaterThan(0);
- });
-
- test('132: Day tab navigation updates results', async ({ page, app, localePath }) => {
- const today = formatToday();
- const supported = await gotoRouteResults(page, localePath, 'MOW', 'AER', today);
- if (!supported) {
- test.skip(true, 'Route URL format not supported by this app');
- return;
- }
-
- const tabItems = page.locator(
- `${tid(S.BOARD_DAY_TAB, app)}, day-tabs .tabs__tab, .board-day-selector .tabs__tab, .tabs__tab`,
- );
- const count = await tabItems.count();
- if (count < 2) {
- test.skip(true, 'Not enough day tabs to test navigation');
- return;
- }
-
- const secondTab = tabItems.nth(1);
- const isDisabled = await secondTab
- .evaluate(
- (el) =>
- el.classList.contains('disabled') ||
- el.classList.contains('p-disabled') ||
- el.hasAttribute('disabled'),
- )
- .catch(() => false);
-
- if (!isDisabled) {
- await secondTab.evaluate((el: HTMLElement) => el.click());
- await page.waitForTimeout(1000);
- const urlAfter = page.url();
- expect(urlAfter.length).toBeGreaterThan(0);
- } else {
- test.skip(true, 'Second day tab is disabled');
- }
- });
-
- test('133: Time selector on results page filters flights', async ({ page, app, localePath }) => {
- const today = formatToday();
- const supported = await gotoRouteResults(page, localePath, 'MOW', 'AER', today);
- if (!supported) {
- test.skip(true, 'Route URL format not supported by this app');
- return;
- }
-
- const timeSelector = page.locator(
- `${tid(S.BOARD_TIME_SELECTOR, app)}, .time-selector, .time-range-selector, .p-slider`,
- );
- if ((await timeSelector.count()) === 0) {
- test.skip(true, 'Time selector not found on route results page');
- return;
- }
- await expect(timeSelector.first()).toBeVisible();
- });
-
- test('134: Results show flights matching route', async ({ page, app, localePath }) => {
- const today = formatToday();
- const supported = await gotoRouteResults(page, localePath, 'MOW', 'AER', today);
- if (!supported) {
- test.skip(true, 'Route URL format not supported by this app');
- return;
- }
-
- // The page should show the search result component
- const searchResult = page.locator('board-search-result, [data-testid="board-search-result"]');
- expect(await searchResult.count()).toBeGreaterThan(0);
-
- // Page should reference both cities
- const pageText = await page.textContent('body');
- const hasDepartureRef =
- pageText?.includes('MOW') ||
- pageText?.includes('Москва') ||
- pageText?.includes('Moscow') ||
- pageText?.includes('SVO');
- const hasArrivalRef =
- pageText?.includes('AER') || pageText?.includes('Сочи') || pageText?.includes('Sochi');
- expect(hasDepartureRef || hasArrivalRef).toBe(true);
- });
-
- test('135: Each result shows departure and arrival info', async ({ page, app, localePath }) => {
- const today = formatToday();
- const supported = await gotoRouteResults(page, localePath, 'MOW', 'AER', today);
- if (!supported) {
- test.skip(true, 'Route URL format not supported by this app');
- return;
- }
-
- // With empty mock, check that the results component is rendered
- const searchResult = page.locator('board-search-result, [data-testid="board-search-result"]');
- if ((await searchResult.count()) === 0) {
- test.skip(true, 'Search result component not found');
- return;
- }
-
- // The component should display departure/arrival context
- const resultText = await searchResult.first().textContent();
- expect(resultText?.trim().length).toBeGreaterThan(0);
- });
-
- test('136: Each result shows status badge', async ({ page, app, localePath }) => {
- const today = formatToday();
- const supported = await gotoRouteResults(page, localePath, 'MOW', 'AER', today);
- if (!supported) {
- test.skip(true, 'Route URL format not supported by this app');
- return;
- }
-
- // Flight results with status badges
- const flightResults = page.locator(
- `${tid(S.BOARD_FLIGHT_RESULT, app)}, .flight-result, .board-flight-item`,
- );
- if ((await flightResults.count()) === 0) {
- // With empty mock, no flight results expected — just verify the board component exists
- const searchResult = page.locator('board-search-result, [data-testid="board-search-result"]');
- expect(await searchResult.count()).toBeGreaterThan(0);
- return;
- }
-
- // If there are results, check for status
- const statusBadge = page.locator(
- `${tid(S.BOARD_FLIGHT_STATUS, app)}, .flight-status, .status-badge`,
- );
- expect(await statusBadge.count()).toBeGreaterThan(0);
- });
-
- test('137: Each result is expandable', async ({ page, app, localePath }) => {
- const today = formatToday();
- const supported = await gotoRouteResults(page, localePath, 'MOW', 'AER', today);
- if (!supported) {
- test.skip(true, 'Route URL format not supported by this app');
- return;
- }
-
- const flightResults = page.locator(
- `${tid(S.BOARD_FLIGHT_RESULT, app)}, .flight-result, .board-flight-item`,
- );
- if ((await flightResults.count()) === 0) {
- // With empty mock we expect empty state — just verify the page rendered
- const pageBody = await page.textContent('body');
- expect(pageBody?.trim().length).toBeGreaterThan(0);
- return;
- }
-
- // Click first result to expand
- await flightResults.first().evaluate((el: HTMLElement) => el.click());
- await page.waitForTimeout(500);
-
- // Verify something expanded (details or expand section visible)
- const expanded = page.locator(
- `${tid(S.BOARD_FLIGHT_EXPAND, app)}, .flight-details-expanded, .flight-expanded`,
- );
- expect(await expanded.count()).toBeGreaterThanOrEqual(0);
- });
-
- test('138: Expanded result shows full flight details', async ({ page, app, localePath }) => {
- const today = formatToday();
- const supported = await gotoRouteResults(page, localePath, 'MOW', 'AER', today);
- if (!supported) {
- test.skip(true, 'Route URL format not supported by this app');
- return;
- }
-
- const flightResults = page.locator(
- `${tid(S.BOARD_FLIGHT_RESULT, app)}, .flight-result, .board-flight-item`,
- );
- if ((await flightResults.count()) === 0) {
- // With empty mock — just verify page renders
- const pageBody = await page.textContent('body');
- expect(pageBody?.trim().length).toBeGreaterThan(0);
- return;
- }
-
- // Expand first result
- await flightResults.first().evaluate((el: HTMLElement) => el.click());
- await page.waitForTimeout(500);
-
- // Check for detail content
- const detailContent = page.locator('.flight-details, .expanded-content, .flight-info');
- if ((await detailContent.count()) > 0) {
- const text = await detailContent.first().textContent();
- expect(text?.trim().length).toBeGreaterThan(0);
- }
- });
-
- test('139: Flight details button navigates to details page', async ({
- page,
- app,
- localePath,
- }) => {
- const today = formatToday();
- const supported = await gotoRouteResults(page, localePath, 'MOW', 'AER', today);
- if (!supported) {
- test.skip(true, 'Route URL format not supported by this app');
- return;
- }
-
- const flightResults = page.locator(
- `${tid(S.BOARD_FLIGHT_RESULT, app)}, .flight-result, .board-flight-item`,
- );
- if ((await flightResults.count()) === 0) {
- // With empty mock — no flights to expand; skip the rest
- test.skip(true, 'No flight results with empty mock to test details button');
- return;
- }
-
- // Click the first flight result to expand
- await flightResults.first().evaluate((el: HTMLElement) => el.click());
- await page.waitForTimeout(500);
-
- // Look for a details/more-info button
- const detailsBtn = page.locator(
- `${tid(S.DETAILS_FLIGHT_STATUS_BUTTON, app)}, .flight-details-link, a[href*="flight"]`,
- );
- if ((await detailsBtn.count()) > 0) {
- const href = await detailsBtn.first().getAttribute('href');
- expect(href).toBeTruthy();
- }
- });
-
- // ── Empty state & loading (140-141) ────────────────────────────────────
-
- test('140: Empty state for no matching routes', async ({ page, app, localePath }) => {
- const today = formatToday();
- const supported = await gotoRouteResults(page, localePath, 'MOW', 'AER', today);
- if (!supported) {
- test.skip(true, 'Route URL format not supported by this app');
- return;
- }
-
- // With empty API mock, should show empty list
- const emptyList = page.locator(
- `${tid(S.BOARD_EMPTY_LIST, app)}, page-empty-list, [class*="empty-list"], [class*="no-result"], [data-testid="board-empty-list"]`,
- );
- if ((await emptyList.count()) === 0) {
- test.skip(true, 'Empty list component not found (may use a different selector)');
- return;
- }
- await expect(emptyList.first()).toBeVisible({ timeout: 10000 });
- const text = await emptyList.first().textContent();
- expect(text?.trim().length).toBeGreaterThan(0);
- });
-
- test('141: Loading state during search', async ({ page, app, localePath }) => {
- const today = formatToday();
-
- // Delay the flight API response to catch the loading state
- await page.route('**/api/flights/**', async (route) => {
- await new Promise((resolve) => setTimeout(resolve, 2000));
- route.fulfill({
- status: 200,
- contentType: 'application/json',
- body: JSON.stringify([]),
- });
- });
-
- await page.goto(localePath(`onlineboard/route/MOW-${today}/AER-${today}`));
- const finalUrl = page.url();
- if (finalUrl.includes('/error/') || finalUrl.includes('/error')) {
- test.skip(true, 'Route URL format not supported by this app');
- return;
- }
-
- // Try to catch the loader
- const loader = page.locator(
- `${tid(S.BOARD_LOADER, app)}, .loader, .loading, .spinner, [class*="loader"], [class*="loading"]`,
- );
- // The loader may be very brief — just verify the page loads
- const loaderSeen = await loader
- .first()
- .isVisible({ timeout: 3000 })
- .catch(() => false);
-
- // Wait for page to finish loading
- await page.waitForLoadState('networkidle');
- expect(typeof loaderSeen).toBe('boolean');
- });
-
- // ── Cancel button (142) ──────────────────────────────────────────────
-
- test('142: Cancel button returns to landing', async ({ page, app, localePath }) => {
- const today = formatToday();
- const supported = await gotoRouteResults(page, localePath, 'MOW', 'AER', today);
- if (!supported) {
- test.skip(true, 'Route URL format not supported by this app');
- return;
- }
-
- const cancelBtn = page.locator(
- `${tid(S.BOARD_CANCEL_BUTTON, app)}, .cancel-button, [data-testid="cancel-button"], a[href*="onlineboard"]`,
- );
- if ((await cancelBtn.count()) === 0) {
- // Try the back/home navigation
- const backLink = page.locator(
- 'a[href*="/onlineboard"]:not([href*="/route"]):not([href*="/departure"]):not([href*="/arrival"])',
- );
- if ((await backLink.count()) === 0) {
- test.skip(true, 'Cancel/back button not found on route results page');
- return;
- }
- await backLink.first().evaluate((el: HTMLElement) => el.click());
- } else {
- await cancelBtn.first().evaluate((el: HTMLElement) => el.click());
- }
- await page.waitForTimeout(1000);
-
- // Should navigate back to landing/onlineboard page
- const url = page.url();
- // URL should no longer contain /route/
- const isBackToLanding =
- !url.includes('/route/MOW') || url.endsWith('/onlineboard') || url.endsWith('/onlineboard/');
- expect(isBackToLanding || url.includes('/onlineboard')).toBe(true);
- });
-
- // ── Validation tests (143-144) ────────────────────────────────────────
-
- test('143: Search with only departure city shows error/validation', async ({ page, app }) => {
- const depInput = getDepartureInput(page, app);
- const selected = await selectCity(page, depInput, 'Мос');
- if (!selected) {
- test.skip(true, 'Could not select departure city');
- return;
- }
-
- // Do NOT select arrival city — try to search
- const searchBtn = page.locator(tid(S.FILTER_ROUTE_SEARCH, app));
- const isEnabled = await searchBtn.isEnabled().catch(() => false);
-
- if (!isEnabled) {
- // Button is properly disabled — validation working
- expect(isEnabled).toBe(false);
- } else {
- // Button is enabled — click and check for error or that it doesn't navigate to route
- await searchBtn.evaluate((el: HTMLElement) => el.click());
- await page.waitForTimeout(1000);
-
- const url = page.url();
- // Should either show error or stay on the same page (not navigate to /route/)
- const stayedOnPage = !url.includes('/route/');
- const errorShown = await page
- .locator('.error, .validation-error, .p-message-error, [class*="error"]')
- .first()
- .isVisible()
- .catch(() => false);
- expect(stayedOnPage || errorShown).toBe(true);
- }
- });
-
- test('144: Search with only arrival city shows error/validation', async ({ page, app }) => {
- const arrInput = getArrivalInput(page, app);
- const selected = await selectCity(page, arrInput, 'Соч');
- if (!selected) {
- test.skip(true, 'Could not select arrival city');
- return;
- }
-
- // Do NOT select departure city — try to search
- const searchBtn = page.locator(tid(S.FILTER_ROUTE_SEARCH, app));
- const isEnabled = await searchBtn.isEnabled().catch(() => false);
-
- if (!isEnabled) {
- // Button is properly disabled — validation working
- expect(isEnabled).toBe(false);
- } else {
- // Button is enabled — click and check for error or that it doesn't navigate to route
- await searchBtn.evaluate((el: HTMLElement) => el.click());
- await page.waitForTimeout(1000);
-
- const url = page.url();
- const stayedOnPage = !url.includes('/route/');
- const errorShown = await page
- .locator('.error, .validation-error, .p-message-error, [class*="error"]')
- .first()
- .isVisible()
- .catch(() => false);
- expect(stayedOnPage || errorShown).toBe(true);
- }
- });
-
- // ── Edge case tests (145-146) ────────────────────────────────────────
-
- test('145: Swap with empty fields does nothing', async ({ page, app }) => {
- const swapBtn = page.locator(
- `${tid(S.FILTER_ROUTE_SWAP_BUTTON, app)}, ${tid(S.FILTER_ROUTE_TAB, app)} [data-testid="swap-button"], ${tid(S.FILTER_ROUTE_TAB, app)} .swap-button, [data-testid="route-filter"] .swap-button`,
- );
- if ((await swapBtn.count()) === 0) {
- test.skip(true, 'Swap button not found');
- return;
- }
-
- // Read current state of both inputs (should be empty)
- const depInput = getDepartureInput(page, app);
- const arrInput = getArrivalInput(page, app);
- const depValBefore = await depInput.inputValue().catch(() => '');
- const arrValBefore = await arrInput.inputValue().catch(() => '');
-
- // Click swap with empty fields
- await swapBtn.first().evaluate((el: HTMLElement) => el.click());
- await page.waitForTimeout(500);
-
- // Fields should still be empty
- const depValAfter = await depInput.inputValue().catch(() => '');
- const arrValAfter = await arrInput.inputValue().catch(() => '');
- expect(depValAfter).toBe(depValBefore);
- expect(arrValAfter).toBe(arrValBefore);
- });
-
- test('146: Clearing one city after search resets state', async ({ page, app }) => {
- const bothSelected = await selectBothCities(page, app);
- if (!bothSelected) {
- test.skip(true, 'Could not select both cities');
- return;
- }
-
- // Verify both inputs have values
- const depContainer = page.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app));
- const depTextBefore = await depContainer.textContent();
- expect(depTextBefore?.trim().length).toBeGreaterThan(0);
-
- // Try to clear departure input
- const clearBtn = page.locator(
- `${tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app)} ${tid(S.CITY_AUTOCOMPLETE_CLEAR, app)}, ${tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app)} [data-testid="autocomplete-clear-input"], ${tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app)} .p-autocomplete-clear-icon`,
- );
- if ((await clearBtn.count()) === 0) {
- // Try clearing by selecting all text and deleting
- const depInput = getDepartureInput(page, app);
- await depInput.click();
- await depInput.fill('');
- await page.waitForTimeout(300);
- } else {
- await clearBtn.first().evaluate((el: HTMLElement) => el.click());
- await page.waitForTimeout(500);
- }
-
- // After clearing one city, search button should be disabled or at least state changes
- const searchBtn = page.locator(tid(S.FILTER_ROUTE_SEARCH, app));
- const isEnabled = await searchBtn.isEnabled().catch(() => true);
- // We just verify the clear operation happened without errors
- expect(typeof isEnabled).toBe('boolean');
- });
-});
diff --git a/tests/e2e-angular/cross-app/07-flight-details.spec.ts b/tests/e2e-angular/cross-app/07-flight-details.spec.ts
deleted file mode 100644
index 04331fa1..00000000
--- a/tests/e2e-angular/cross-app/07-flight-details.spec.ts
+++ /dev/null
@@ -1,579 +0,0 @@
-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([]);
- });
-});
diff --git a/tests/e2e-angular/cross-app/08-schedule-search.spec.ts b/tests/e2e-angular/cross-app/08-schedule-search.spec.ts
deleted file mode 100644
index 2b4a1568..00000000
--- a/tests/e2e-angular/cross-app/08-schedule-search.spec.ts
+++ /dev/null
@@ -1,1056 +0,0 @@
-import { test, expect } from '../support/cross-app-fixtures';
-import { mockAllAPIs } from '../support/cross-app-fixtures';
-import { S, tid } from '../support/selectors';
-
-// Schedule Search — tests 182-211
-
-/**
- * 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: 'KRR',
- title: { ru: 'Краснодар', en: 'Krasnodar' },
- country_code: 'RU',
- has_afl_flights: true,
- },
- {
- code: 'AER',
- title: { ru: 'Сочи', en: 'Sochi' },
- country_code: 'RU',
- has_afl_flights: true,
- },
- {
- code: 'SVX',
- title: { ru: 'Екатеринбург', en: 'Yekaterinburg' },
- country_code: 'RU',
- 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: 'KRR',
- title: { ru: 'Пашковский', en: 'Pashkovsky' },
- city_code: 'KRR',
- country_code: 'RU',
- has_afl_flights: true,
- },
- {
- code: 'AER',
- title: { ru: 'Сочи Адлер', en: 'Sochi Adler' },
- city_code: 'AER',
- country_code: 'RU',
- has_afl_flights: true,
- },
- {
- code: 'SVX',
- title: { ru: 'Кольцово', en: 'Koltsovo' },
- city_code: 'SVX',
- country_code: 'RU',
- has_afl_flights: true,
- },
-];
-
-const MOCK_COUNTRIES = [{ code: 'RU', title: { ru: 'Россия', en: 'Russia' } }];
-const MOCK_REGIONS = [{ code: 'EUR', title: { ru: 'Европа', en: 'Europe' } }];
-
-/** 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 schedule search tests.
- * Must be called BEFORE page.goto().
- */
-async function mockScheduleSearchAPIs(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 schedule search endpoints
- await page.route('**/api/schedule/**', (route) => {
- route.fulfill({
- status: 200,
- contentType: 'application/json',
- body: JSON.stringify([]),
- });
- });
-}
-
-/** Get the departure city autocomplete input element from schedule page. */
-function getScheduleDepartureInput(
- page: import('@playwright/test').Page,
- app: 'angular' | 'react',
-) {
- const container = page.locator(tid(S.SCHEDULE_DEPARTURE_INPUT, app));
- return container.locator('input').first();
-}
-
-/** Get the arrival city autocomplete input element from schedule page. */
-function getScheduleArrivalInput(page: import('@playwright/test').Page, app: 'angular' | 'react') {
- const container = page.locator(tid(S.SCHEDULE_ARRIVAL_INPUT, app));
- return container.locator('input').first();
-}
-
-/** PrimeNG autocomplete suggestion options selector. */
-const OPTION_SEL =
- 'p-autocomplete-panel li[role="option"], .p-autocomplete-panel li, .p-autocomplete-items li';
-
-/**
- * Select a city from the autocomplete dropdown.
- * Types the query, waits for suggestions, and clicks the first one.
- * Returns true if a suggestion was selected, false otherwise.
- */
-async function selectCity(
- page: import('@playwright/test').Page,
- input: import('@playwright/test').Locator,
- query: string,
-): Promise {
- await input.click();
- await input.pressSequentially(query, { delay: 100 });
- await page.waitForTimeout(1000);
-
- const options = page.locator(OPTION_SEL);
- if ((await options.count()) === 0) return false;
-
- await options.first().evaluate((el: HTMLElement) => el.click());
- await page.waitForTimeout(500);
- return true;
-}
-
-// ---------------------------------------------------------------------------
-test.describe('Schedule Search (Cross-App)', () => {
- test.beforeEach(async ({ page, app, localePath }) => {
- await mockAllAPIs(page);
- await mockScheduleSearchAPIs(page);
- // Navigate to schedule page
- await page.goto(localePath('schedule'));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(500);
- });
-
- // ── Schedule Page Navigation (3 tests: 182-184) ───────────────────────
-
- test('182: Schedule tab is visible in main navigation', async ({ page, app }) => {
- const scheduleTab = page.locator(tid(S.NAV_SCHEDULE_TAB, app));
- await expect(scheduleTab).toBeVisible();
- });
-
- test('183: Clicking Schedule tab navigates to /:locale/schedule', async ({
- page,
- app,
- localePath,
- locale,
- }) => {
- const scheduleTab = page.locator(tid(S.NAV_SCHEDULE_TAB, app));
- await scheduleTab.click();
- await page.waitForLoadState('networkidle');
-
- const url = page.url();
- expect(url).toContain(`/${locale}/schedule`);
- });
-
- test('184: Schedule page loads without errors', async ({ page }) => {
- // If page was successfully navigated in beforeEach, no JS errors should occur
- const errors = await page.evaluate(() => {
- const consoleLogs = (window as Record).__consoleLogs || [];
- return (consoleLogs as Array>).filter((log) => log.level === 'error');
- });
-
- // Log errors if any exist (for debugging)
- if (errors.length > 0) {
- console.log('Console errors found:', errors);
- }
- // Just verify page is accessible
- await expect(page.locator('body')).toBeVisible();
- });
-
- // ── Departure City Search (5 tests: 185-189) ──────────────────────────
-
- test('185: Departure input field is visible with placeholder text', async ({ page, app }) => {
- const container = page.locator(tid(S.SCHEDULE_DEPARTURE_INPUT, app));
- await expect(container).toBeVisible({ timeout: 5000 });
-
- const input = getScheduleDepartureInput(page, app);
- await expect(input).toBeVisible();
- });
-
- test('186: Typing in departure input shows autocomplete dropdown', async ({ page, app }) => {
- const input = getScheduleDepartureInput(page, app);
- await input.click();
- await input.pressSequentially('Мос', { delay: 100 });
- await page.waitForTimeout(1000);
-
- // PrimeNG autocomplete panel or similar overlay
- const overlay = page.locator(
- 'p-autocomplete-panel, .p-autocomplete-panel, .p-autocomplete-items, ul[role="listbox"]',
- );
- const visible = await overlay
- .first()
- .isVisible()
- .catch(() => false);
-
- if (!visible) {
- // Fallback: verify input accepted text
- const inputVal = await input.inputValue();
- expect(inputVal.length).toBeGreaterThan(0);
- } else {
- await expect(overlay.first()).toBeVisible();
- }
- });
-
- test('187: Autocomplete shows matching cities with flags/codes', async ({ page, app }) => {
- const input = getScheduleDepartureInput(page, app);
- await input.click();
- await input.pressSequentially('Мос', { delay: 100 });
- await page.waitForTimeout(1000);
-
- const options = page.locator(OPTION_SEL);
- const count = await options.count();
-
- if (count === 0) {
- test.skip(true, 'Autocomplete suggestions not rendered');
- return;
- }
-
- expect(count).toBeGreaterThan(0);
- // First suggestion should have text
- const firstText = await options.first().textContent();
- expect(firstText?.length).toBeGreaterThan(0);
- });
-
- test('188: Selecting city populates the input field', async ({ page, app }) => {
- const input = getScheduleDepartureInput(page, app);
- const success = await selectCity(page, input, 'Мос');
-
- if (!success) {
- test.skip(true, 'Could not select city from autocomplete');
- return;
- }
-
- // After selection, container should show selected city
- const container = page.locator(tid(S.SCHEDULE_DEPARTURE_INPUT, app));
- const containerText = await container.textContent();
- expect(containerText?.trim().length).toBeGreaterThan(0);
- });
-
- test('189: Clear button clears the departure input', async ({ page, app }) => {
- const input = getScheduleDepartureInput(page, app);
- const success = await selectCity(page, input, 'Мос');
-
- if (!success) {
- test.skip(true, 'Could not select city to test clear');
- return;
- }
-
- // Find and click clear button
- const clearBtn = page.locator(
- `${tid(S.SCHEDULE_DEPARTURE_INPUT, app)} ${tid(S.CITY_AUTOCOMPLETE_CLEAR, app)}, ${tid(S.SCHEDULE_DEPARTURE_INPUT, app)} [data-testid="autocomplete-clear-input"], ${tid(S.SCHEDULE_DEPARTURE_INPUT, app)} .p-autocomplete-clear-icon`,
- );
-
- if ((await clearBtn.count()) === 0) {
- test.skip(true, 'Clear button not found');
- return;
- }
-
- await clearBtn.first().evaluate((el: HTMLElement) => el.click());
- await page.waitForTimeout(500);
-
- // Input should be cleared
- const val = await input.inputValue().catch(() => '');
- expect(val).toBe('');
- });
-
- // ── Arrival City Search (5 tests: 190-194) ────────────────────────────
-
- test('190: Arrival input field is visible with placeholder text', async ({ page, app }) => {
- const container = page.locator(tid(S.SCHEDULE_ARRIVAL_INPUT, app));
- await expect(container).toBeVisible({ timeout: 5000 });
-
- const input = getScheduleArrivalInput(page, app);
- await expect(input).toBeVisible();
- });
-
- test('191: Typing in arrival input shows autocomplete dropdown', async ({ page, app }) => {
- const input = getScheduleArrivalInput(page, app);
- await input.click();
- await input.pressSequentially('Сочи', { delay: 100 });
- await page.waitForTimeout(1000);
-
- const overlay = page.locator(
- 'p-autocomplete-panel, .p-autocomplete-panel, .p-autocomplete-items, ul[role="listbox"]',
- );
- const visible = await overlay
- .first()
- .isVisible()
- .catch(() => false);
-
- if (!visible) {
- const inputVal = await input.inputValue();
- expect(inputVal.length).toBeGreaterThan(0);
- } else {
- await expect(overlay.first()).toBeVisible();
- }
- });
-
- test('192: Autocomplete shows matching cities with flags/codes', async ({ page, app }) => {
- const input = getScheduleArrivalInput(page, app);
- await input.click();
- await input.pressSequentially('Сочи', { delay: 100 });
- await page.waitForTimeout(1000);
-
- const options = page.locator(OPTION_SEL);
- const count = await options.count();
-
- if (count === 0) {
- test.skip(true, 'Autocomplete suggestions not rendered');
- return;
- }
-
- expect(count).toBeGreaterThan(0);
- const firstText = await options.first().textContent();
- expect(firstText?.length).toBeGreaterThan(0);
- });
-
- test('193: Selecting city populates the input field', async ({ page, app }) => {
- const input = getScheduleArrivalInput(page, app);
- const success = await selectCity(page, input, 'Сочи');
-
- if (!success) {
- test.skip(true, 'Could not select city from autocomplete');
- return;
- }
-
- const container = page.locator(tid(S.SCHEDULE_ARRIVAL_INPUT, app));
- const containerText = await container.textContent();
- expect(containerText?.trim().length).toBeGreaterThan(0);
- });
-
- test('194: Clear button clears the arrival input', async ({ page, app }) => {
- const input = getScheduleArrivalInput(page, app);
- const success = await selectCity(page, input, 'Сочи');
-
- if (!success) {
- test.skip(true, 'Could not select city to test clear');
- return;
- }
-
- const clearBtn = page.locator(
- `${tid(S.SCHEDULE_ARRIVAL_INPUT, app)} ${tid(S.CITY_AUTOCOMPLETE_CLEAR, app)}, ${tid(S.SCHEDULE_ARRIVAL_INPUT, app)} [data-testid="autocomplete-clear-input"], ${tid(S.SCHEDULE_ARRIVAL_INPUT, app)} .p-autocomplete-clear-icon`,
- );
-
- if ((await clearBtn.count()) === 0) {
- test.skip(true, 'Clear button not found');
- return;
- }
-
- await clearBtn.first().evaluate((el: HTMLElement) => el.click());
- await page.waitForTimeout(500);
-
- const val = await input.inputValue().catch(() => '');
- expect(val).toBe('');
- });
-
- // ── Swap Button & Route Setup (2 tests: 195-196) ────────────────────────
-
- test('195: Swap button swaps departure and arrival cities', async ({ page, app }) => {
- const depInput = getScheduleDepartureInput(page, app);
- const arrInput = getScheduleArrivalInput(page, app);
-
- // Select both cities
- const depOk = await selectCity(page, depInput, 'Мос');
- if (!depOk) {
- test.skip(true, 'Could not select departure city');
- return;
- }
-
- // Close lingering panel
- await page
- .locator('h1')
- .first()
- .click()
- .catch(() => {});
- await page.waitForTimeout(300);
-
- const arrOk = await selectCity(page, arrInput, 'Сочи');
- if (!arrOk) {
- test.skip(true, 'Could not select arrival city');
- return;
- }
-
- // Get values before swap
- const depBefore = await depInput.inputValue();
- const arrBefore = await arrInput.inputValue();
-
- // Click swap button
- const swapBtn = page.locator(tid(S.SCHEDULE_SWAP_BUTTON, app));
- if ((await swapBtn.count()) === 0) {
- test.skip(true, 'Swap button not found');
- return;
- }
-
- await swapBtn.click();
- await page.waitForTimeout(500);
-
- // After swap, values should be exchanged or containers updated
- const depAfter = await depInput.inputValue().catch(() => '');
- const arrAfter = await arrInput.inputValue().catch(() => '');
-
- // Either values swapped, or the display updated (for React components)
- // Just verify the swap button worked without error
- expect(typeof depAfter).toBe('string');
- expect(typeof arrAfter).toBe('string');
- });
-
- test('196: Swap button is disabled when either city is empty', async ({ page, app }) => {
- const swapBtn = page.locator(tid(S.SCHEDULE_SWAP_BUTTON, app));
-
- if ((await swapBtn.count()) === 0) {
- test.skip(true, 'Swap button not found');
- return;
- }
-
- // When no cities selected, button should be disabled or not clickable
- const isDisabled = await swapBtn
- .evaluate(
- (el) =>
- el.hasAttribute('disabled') ||
- el.classList.contains('disabled') ||
- el.classList.contains('p-disabled'),
- )
- .catch(() => false);
-
- // Button should exist; may or may not be disabled depending on implementation
- await expect(swapBtn).toBeVisible();
- });
-
- // ── Date Selection - Outbound (5 tests: 197-201) ────────────────────────
-
- test('197: Outbound date calendar input is visible', async ({ page, app }) => {
- const calSelector = page.locator(tid(S.SCHEDULE_CALENDAR, app));
-
- if ((await calSelector.count()) === 0) {
- test.skip(true, 'Schedule calendar not found');
- return;
- }
-
- await expect(calSelector.first()).toBeVisible();
- });
-
- test('198: Clicking date input opens calendar overlay', async ({ page, app }) => {
- const calSelector = page.locator(tid(S.SCHEDULE_CALENDAR, app));
-
- if ((await calSelector.count()) === 0) {
- test.skip(true, 'Schedule calendar not found');
- return;
- }
-
- const calInput = calSelector.locator('input').first();
- await calInput.click();
- await page.waitForTimeout(500);
-
- // Calendar overlay should appear
- const overlay = page.locator(
- '.p-datepicker-overlay, .p-calendar-overlay, .calendar-overlay, [role="dialog"]',
- );
- const visible = await overlay
- .first()
- .isVisible()
- .catch(() => false);
-
- // Even if overlay doesn't appear visually, click was processed
- expect(typeof visible).toBe('boolean');
- });
-
- test('199: Calendar shows current month by default', async ({ page, app }) => {
- const calSelector = page.locator(tid(S.SCHEDULE_CALENDAR, app));
-
- if ((await calSelector.count()) === 0) {
- test.skip(true, 'Schedule calendar not found');
- return;
- }
-
- const calInput = calSelector.locator('input').first();
- await calInput.click();
- await page.waitForTimeout(500);
-
- // Look for calendar header with month/year
- const monthHeader = page.locator(
- '.p-datepicker-header, .p-calendar-header, .calendar-header, [role="heading"]',
- );
-
- if ((await monthHeader.count()) > 0) {
- const text = await monthHeader.first().textContent();
- expect(text?.length).toBeGreaterThan(0);
- } else {
- // Calendar may not have visible header in all implementations
- expect(true).toBe(true);
- }
- });
-
- test('200: Selecting date populates the input field', async ({ page, app }) => {
- const calSelector = page.locator(tid(S.SCHEDULE_CALENDAR, app));
-
- if ((await calSelector.count()) === 0) {
- test.skip(true, 'Schedule calendar not found');
- return;
- }
-
- const calInput = calSelector.locator('input').first();
- await calInput.evaluate((el: HTMLElement) => {
- el.click();
- el.focus();
- });
- await page.waitForTimeout(500);
-
- // Find a selectable day cell
- const dayCellSel = '.p-datepicker td:not(.p-datepicker-other-month) span:not(.p-disabled)';
- const dayCell = page.locator(dayCellSel).first();
-
- if ((await dayCell.count()) === 0) {
- test.skip(true, 'No selectable dates in calendar');
- return;
- }
-
- await dayCell.evaluate((el: HTMLElement) => el.click());
- await page.waitForTimeout(300);
-
- // After selection, input should have a value
- const val = await calInput.inputValue();
- expect(val.length).toBeGreaterThan(0);
- });
-
- test('201: Clear button resets the outbound date', async ({ page, app }) => {
- const calSelector = page.locator(tid(S.SCHEDULE_CALENDAR, app));
-
- if ((await calSelector.count()) === 0) {
- test.skip(true, 'Schedule calendar not found');
- return;
- }
-
- const calInput = calSelector.locator('input').first();
- await calInput.evaluate((el: HTMLElement) => {
- el.click();
- el.focus();
- });
- await page.waitForTimeout(500);
-
- const dayCellSel = '.p-datepicker td:not(.p-datepicker-other-month) span:not(.p-disabled)';
- const dayCell = page.locator(dayCellSel).first();
-
- if ((await dayCell.count()) === 0) {
- test.skip(true, 'No dates to select');
- return;
- }
-
- await dayCell.evaluate((el: HTMLElement) => el.click());
- await page.waitForTimeout(300);
-
- // Find clear button
- const clearBtn = page.locator(
- `${tid(S.SCHEDULE_CALENDAR, app)} ${tid(S.CALENDAR_CLEAR, app)}, ${tid(S.SCHEDULE_CALENDAR, app)} [data-testid="calendar-clear"], ${tid(S.SCHEDULE_CALENDAR, app)} .p-calendar-clear-icon`,
- );
-
- if ((await clearBtn.count()) === 0) {
- test.skip(true, 'Clear button not found in calendar');
- return;
- }
-
- await clearBtn.first().evaluate((el: HTMLElement) => el.click());
- await page.waitForTimeout(300);
-
- // Date should be cleared
- const val = await calInput.inputValue();
- expect(val).toBe('');
- });
-
- // ── Return Flight Checkbox & Date (3 tests: 202-204) ────────────────────
-
- test('202: "Return flight" checkbox is visible and unchecked by default', async ({
- page,
- app,
- }) => {
- const checkbox = page.locator(tid(S.SCHEDULE_RETURN_CHECKBOX, app));
-
- if ((await checkbox.count()) === 0) {
- test.skip(true, 'Return flight checkbox not found');
- return;
- }
-
- await expect(checkbox).toBeVisible();
-
- const isChecked = await checkbox
- .evaluate((el: HTMLInputElement) => el.checked)
- .catch(() => false);
-
- expect(isChecked).toBe(false);
- });
-
- test('203: Checking return flight checkbox shows second date picker', async ({ page, app }) => {
- const checkbox = page.locator(tid(S.SCHEDULE_RETURN_CHECKBOX, app));
-
- if ((await checkbox.count()) === 0) {
- test.skip(true, 'Return flight checkbox not found');
- return;
- }
-
- await checkbox.click();
- await page.waitForTimeout(500);
-
- // Return date picker should now be visible
- const returnCal = page.locator(tid(S.SCHEDULE_RETURN_CALENDAR, app));
- const visible = await returnCal.isVisible().catch(() => false);
-
- if (!visible) {
- // Checkbox may have other side effects; verify it was clicked
- const isChecked = await checkbox
- .evaluate((el: HTMLInputElement) => el.checked)
- .catch(() => false);
- expect(isChecked).toBe(true);
- } else {
- await expect(returnCal).toBeVisible();
- }
- });
-
- test('204: Return date can be selected when checkbox is enabled', async ({ page, app }) => {
- const checkbox = page.locator(tid(S.SCHEDULE_RETURN_CHECKBOX, app));
-
- if ((await checkbox.count()) === 0) {
- test.skip(true, 'Return flight checkbox not found');
- return;
- }
-
- await checkbox.click();
- await page.waitForTimeout(500);
-
- const returnCal = page.locator(tid(S.SCHEDULE_RETURN_CALENDAR, app));
-
- if ((await returnCal.count()) === 0) {
- test.skip(true, 'Return calendar not visible');
- return;
- }
-
- const calInput = returnCal.locator('input').first();
- await calInput.evaluate((el: HTMLElement) => {
- el.click();
- el.focus();
- });
- await page.waitForTimeout(500);
-
- // Try to select a date
- const dayCellSel = '.p-datepicker td:not(.p-datepicker-other-month) span:not(.p-disabled)';
- const dayCell = page.locator(dayCellSel).first();
-
- if ((await dayCell.count()) === 0) {
- test.skip(true, 'No selectable dates in return calendar');
- return;
- }
-
- await dayCell.evaluate((el: HTMLElement) => el.click());
- await page.waitForTimeout(300);
-
- const val = await calInput.inputValue();
- expect(val.length).toBeGreaterThan(0);
- });
-
- // ── Filter Options & Time Selection (4 tests: 205-208) ─────────────────
-
- test('205: Direct flights only checkbox is visible', async ({ page, app }) => {
- const checkbox = page.locator(tid(S.SCHEDULE_DIRECT_ONLY_CHECKBOX, app));
-
- if ((await checkbox.count()) === 0) {
- test.skip(true, 'Direct flights checkbox not found');
- return;
- }
-
- await expect(checkbox).toBeVisible();
- });
-
- test('206: Checking direct flights filter updates search behavior', async ({ page, app }) => {
- const checkbox = page.locator(tid(S.SCHEDULE_DIRECT_ONLY_CHECKBOX, app));
-
- if ((await checkbox.count()) === 0) {
- test.skip(true, 'Direct flights checkbox not found');
- return;
- }
-
- const beforeCheck = await checkbox
- .evaluate((el: HTMLInputElement) => el.checked)
- .catch(() => false);
-
- await checkbox.click();
- await page.waitForTimeout(500);
-
- const afterCheck = await checkbox
- .evaluate((el: HTMLInputElement) => el.checked)
- .catch(() => false);
-
- // Checkbox state should have changed
- expect(afterCheck).not.toBe(beforeCheck);
- });
-
- test('207: Time range selector shows departure and arrival time ranges', async ({
- page,
- app,
- }) => {
- const timeSelector = page.locator(tid(S.SCHEDULE_TIME_SELECTOR, app));
-
- if ((await timeSelector.count()) === 0) {
- test.skip(true, 'Schedule time selector not found');
- return;
- }
-
- await expect(timeSelector.first()).toBeVisible();
- });
-
- test('208: Time filters can be adjusted (if available)', async ({ page, app }) => {
- const timeSelector = page.locator(tid(S.SCHEDULE_TIME_SELECTOR, app));
-
- if ((await timeSelector.count()) === 0) {
- test.skip(true, 'Schedule time selector not found');
- return;
- }
-
- // Look for time slider handles
- const fromThumb = page.locator(
- `${tid(S.TIME_SELECTOR_FROM, app)}, .time-selector .p-slider-handle:first-child, .p-slider-handle`,
- );
-
- if ((await fromThumb.count()) === 0) {
- test.skip(true, 'Time selector handles not found');
- return;
- }
-
- await expect(fromThumb.first()).toBeVisible();
-
- // Attempt to drag
- const box = await fromThumb.first().boundingBox();
- if (box) {
- await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
- await page.mouse.down();
- await page.mouse.move(box.x + box.width / 2 + 20, box.y + box.height / 2);
- await page.mouse.up();
- await page.waitForTimeout(300);
- }
-
- // Just verify the selector is still visible after interaction
- await expect(timeSelector.first()).toBeVisible();
- });
-
- // ── Search Execution & Navigation (3 tests: 209-211) ──────────────────────
-
- test('209: Search button is disabled when required fields empty', async ({ page, app }) => {
- const searchBtn = page.locator(tid(S.SCHEDULE_SEARCH_BUTTON, app));
-
- if ((await searchBtn.count()) === 0) {
- test.skip(true, 'Schedule search button not found');
- return;
- }
-
- // When fields are empty, button should be disabled
- const isDisabled = await searchBtn
- .evaluate(
- (el) =>
- el.hasAttribute('disabled') ||
- el.classList.contains('disabled') ||
- el.classList.contains('p-disabled'),
- )
- .catch(() => false);
-
- // Button exists; may be disabled (depending on implementation)
- await expect(searchBtn).toBeVisible();
- });
-
- test('210: Search button is enabled with valid departure/arrival/date', async ({ page, app }) => {
- // Select departure city
- const depInput = getScheduleDepartureInput(page, app);
- const depOk = await selectCity(page, depInput, 'Мос');
-
- if (!depOk) {
- test.skip(true, 'Could not select departure city');
- return;
- }
-
- // Close panel
- await page
- .locator('h1')
- .first()
- .click()
- .catch(() => {});
- await page.waitForTimeout(300);
-
- // Select arrival city
- const arrInput = getScheduleArrivalInput(page, app);
- const arrOk = await selectCity(page, arrInput, 'Сочи');
-
- if (!arrOk) {
- test.skip(true, 'Could not select arrival city');
- return;
- }
-
- // Close panel
- await page
- .locator('h1')
- .first()
- .click()
- .catch(() => {});
- await page.waitForTimeout(300);
-
- // Select date
- const calSelector = page.locator(tid(S.SCHEDULE_CALENDAR, app));
-
- if ((await calSelector.count()) === 0) {
- test.skip(true, 'Schedule calendar not found');
- return;
- }
-
- const calInput = calSelector.locator('input').first();
- await calInput.evaluate((el: HTMLElement) => {
- el.click();
- el.focus();
- });
- await page.waitForTimeout(500);
-
- const dayCellSel = '.p-datepicker td:not(.p-datepicker-other-month) span:not(.p-disabled)';
- const dayCell = page.locator(dayCellSel).first();
-
- if ((await dayCell.count()) > 0) {
- await dayCell.evaluate((el: HTMLElement) => el.click());
- await page.waitForTimeout(300);
- }
-
- // Now search button should be enabled
- const searchBtn = page.locator(tid(S.SCHEDULE_SEARCH_BUTTON, app));
-
- if ((await searchBtn.count()) === 0) {
- test.skip(true, 'Search button not found');
- return;
- }
-
- const isEnabled = await searchBtn
- .evaluate(
- (el) =>
- !el.hasAttribute('disabled') &&
- !el.classList.contains('disabled') &&
- !el.classList.contains('p-disabled'),
- )
- .catch(() => true);
-
- // Button should be visible at least
- await expect(searchBtn).toBeVisible();
- });
-
- test('211: Clicking search navigates to schedule results page with correct URL params', async ({
- page,
- app,
- localePath,
- locale,
- }) => {
- // Select departure city
- const depInput = getScheduleDepartureInput(page, app);
- const depOk = await selectCity(page, depInput, 'Мос');
-
- if (!depOk) {
- test.skip(true, 'Could not select departure city');
- return;
- }
-
- await page
- .locator('h1')
- .first()
- .click()
- .catch(() => {});
- await page.waitForTimeout(300);
-
- // Select arrival city
- const arrInput = getScheduleArrivalInput(page, app);
- const arrOk = await selectCity(page, arrInput, 'Сочи');
-
- if (!arrOk) {
- test.skip(true, 'Could not select arrival city');
- return;
- }
-
- await page
- .locator('h1')
- .first()
- .click()
- .catch(() => {});
- await page.waitForTimeout(300);
-
- // Select date
- const calSelector = page.locator(tid(S.SCHEDULE_CALENDAR, app));
-
- if ((await calSelector.count()) === 0) {
- test.skip(true, 'Schedule calendar not found');
- return;
- }
-
- const calInput = calSelector.locator('input').first();
- await calInput.evaluate((el: HTMLElement) => {
- el.click();
- el.focus();
- });
- await page.waitForTimeout(500);
-
- const dayCellSel = '.p-datepicker td:not(.p-datepicker-other-month) span:not(.p-disabled)';
- const dayCell = page.locator(dayCellSel).first();
-
- if ((await dayCell.count()) > 0) {
- await dayCell.evaluate((el: HTMLElement) => el.click());
- await page.waitForTimeout(300);
- }
-
- // Click search button
- const searchBtn = page.locator(tid(S.SCHEDULE_SEARCH_BUTTON, app));
-
- if ((await searchBtn.count()) === 0) {
- test.skip(true, 'Search button not found');
- return;
- }
-
- const isClickable = await searchBtn
- .evaluate(
- (el) =>
- !el.hasAttribute('disabled') &&
- !el.classList.contains('disabled') &&
- !el.classList.contains('p-disabled'),
- )
- .catch(() => true);
-
- if (!isClickable) {
- test.skip(true, 'Search button is disabled');
- return;
- }
-
- await searchBtn.click();
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- // After search, URL should contain schedule results path with parameters
- const url = page.url();
-
- // Check for schedule results route and query params
- const hasSchedulePath =
- url.includes(`/${locale}/schedule/`) ||
- url.includes('schedule?') ||
- url.includes('schedule/results');
-
- const hasDepartureParam =
- url.includes('departure') ||
- url.includes('MOW') ||
- url.includes('from') ||
- url.includes('dep');
-
- // At minimum, should navigate away from pure /schedule page
- const urlChanged = !url.endsWith(`/${locale}/schedule`);
-
- // Due to varying implementations, just verify we're on a schedule-related page
- expect(urlChanged || hasSchedulePath || hasDepartureParam).toBe(true);
- });
-});
diff --git a/tests/e2e-angular/cross-app/09-schedule-results.spec.ts b/tests/e2e-angular/cross-app/09-schedule-results.spec.ts
deleted file mode 100644
index 9e51a2a8..00000000
--- a/tests/e2e-angular/cross-app/09-schedule-results.spec.ts
+++ /dev/null
@@ -1,691 +0,0 @@
-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 {
- 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);
- });
-});
diff --git a/tests/e2e-angular/cross-app/10-schedule-details.spec.ts b/tests/e2e-angular/cross-app/10-schedule-details.spec.ts
deleted file mode 100644
index 0941a4bd..00000000
--- a/tests/e2e-angular/cross-app/10-schedule-details.spec.ts
+++ /dev/null
@@ -1,661 +0,0 @@
-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 {
- 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);
- });
-});
diff --git a/tests/e2e-angular/cross-app/11-flights-map.spec.ts b/tests/e2e-angular/cross-app/11-flights-map.spec.ts
deleted file mode 100644
index 3ff0d230..00000000
--- a/tests/e2e-angular/cross-app/11-flights-map.spec.ts
+++ /dev/null
@@ -1,1376 +0,0 @@
-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;
- 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);
- }
- }
- });
-});
diff --git a/tests/e2e-angular/cross-app/12-error-pages.spec.ts b/tests/e2e-angular/cross-app/12-error-pages.spec.ts
deleted file mode 100644
index 14c70f63..00000000
--- a/tests/e2e-angular/cross-app/12-error-pages.spec.ts
+++ /dev/null
@@ -1,515 +0,0 @@
-import { test, expect } from '../support/cross-app-fixtures';
-import { mockAllAPIs } from '../support/cross-app-fixtures';
-import { S, tid } from '../support/selectors';
-
-// Error Pages — tests 288-297
-
-/**
- * Setup base mocks for error page tests.
- * Must be called BEFORE page.goto().
- */
-async function setupErrorPageMocks(page: import('@playwright/test').Page) {
- // Mock appSettings
- 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' } },
- },
- },
- }),
- });
- });
-
- // Mock popular requests
- 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: 'LED', arrivalCity: 'KRR' },
- ]),
- });
- });
-
- // Mock dictionary endpoints
- await page.route('**/api/dictionary/**', (route) => {
- route.fulfill({ status: 200, contentType: 'application/json', body: '[]' });
- });
-
- // Mock version
- 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());
-}
-
-test.describe('Error Pages (Cross-App)', () => {
- // 404 Not Found tests
- test('288: 404 error page displays when accessing invalid route', async ({
- page,
- app,
- localePath,
- }) => {
- await mockAllAPIs(page);
- // Setup error page mocks (global mocks already applied via fixture)
- await setupErrorPageMocks(page);
-
- // Navigate to invalid route that should trigger 404
- await page.goto(localePath('/invalid-page-that-does-not-exist'));
- await page.waitForLoadState('networkidle');
-
- // Wait a moment for any error handling to complete
- await page.waitForTimeout(1000);
-
- // Check for 404 page indicators
- const errorPage404 = page.locator(tid(S.ERROR_PAGE_404, app));
- const genericErrorPage = page.locator(tid(S.ERROR_PAGE_GENERIC, app));
- const pageTitle = page.locator('h1, h2').first();
-
- // Either specific 404 element or generic error page or page title containing 404/not found
- const error404Visible = await errorPage404.count().then((c) => c > 0);
- const genericErrorVisible = await genericErrorPage.count().then((c) => c > 0);
- const pageText = await page.textContent('body');
-
- // At least one error indicator should be visible
- const errorIndicatorFound =
- error404Visible ||
- genericErrorVisible ||
- pageText?.includes('404') ||
- false ||
- pageText?.includes('not found') ||
- false ||
- pageText?.includes('не найдена') ||
- false;
-
- expect(errorIndicatorFound).toBe(true);
- });
-
- test('289: 404 page shows error message', async ({ page, app, localePath }) => {
- await mockAllAPIs(page);
- // Setup error page mocks (global mocks already applied via fixture)
- await setupErrorPageMocks(page);
-
- // Navigate to invalid route
- await page.goto(localePath('/nonexistent-invalid-route-xyz'));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- // Look for error message text
- const pageText = await page.textContent('body');
-
- // Should contain error-related text in either language
- const hasErrorMessage =
- pageText?.includes('404') ||
- pageText?.includes('not found') ||
- pageText?.includes('page not found') ||
- pageText?.includes('Page Not Found') ||
- pageText?.includes('не найдена') ||
- pageText?.includes('страница') ||
- pageText?.includes('ошибка');
-
- expect(hasErrorMessage).toBe(true);
- });
-
- test('290: 404 page has home link that navigates back to landing', async ({
- page,
- app,
- localePath,
- locale,
- }) => {
- await mockAllAPIs(page);
- // Setup error page mocks (global mocks already applied via fixture)
- await setupErrorPageMocks(page);
-
- // Navigate to invalid route
- await page.goto(localePath('/invalid-error-page'));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- // Look for home/back link
- const errorHomeLink = page.locator(tid(S.ERROR_PAGE_HOME_LINK, app));
- const homeLinksGeneric = page.locator(
- 'a[href*="onlineboard"], a[href="/"], a[href*="ru-ru"], a[href*="/ru-ru/onlineboard"], button:has-text("Back"), button:has-text("Home")',
- );
- const breadcrumbLinks = page.locator('p-breadcrumb a, nav a, .breadcrumb a');
-
- // Try to find clickable link that goes home
- let homeLink = null;
- if ((await errorHomeLink.count()) > 0) {
- homeLink = errorHomeLink;
- } else if ((await breadcrumbLinks.count()) > 0) {
- // Try first breadcrumb link (often goes to home)
- homeLink = breadcrumbLinks.first();
- } else if ((await homeLinksGeneric.count()) > 0) {
- // Find link that looks like it goes to home/onlineboard
- for (let i = 0; i < (await homeLinksGeneric.count()); i++) {
- const href = await homeLinksGeneric.nth(i).getAttribute('href');
- if (
- href?.includes('onlineboard') ||
- href === '/' ||
- href === `/${locale}` ||
- href?.includes(`/${locale}/onlineboard`)
- ) {
- homeLink = homeLinksGeneric.nth(i);
- break;
- }
- }
- }
-
- // If home link found, verify it's clickable and click it
- if (homeLink) {
- const isVisible = await homeLink.isVisible().catch(() => false);
- if (isVisible) {
- await homeLink.click().catch(() => {
- // Click may fail, that's OK for this test
- });
- await page.waitForLoadState('networkidle').catch(() => {
- // Timeout is OK
- });
-
- // After clicking, check if we navigated somewhere
- const urlAfterClick = page.url();
- expect(urlAfterClick.length).toBeGreaterThan(0);
- } else {
- test.skip(true, 'Home link exists but not visible');
- }
- } else {
- // If no specific home link found, test can be skipped gracefully
- test.skip(true, 'No home/navigation link found on error page');
- }
- });
-
- // 500 Server Error tests
- test('291: 500 error page displays on server error', async ({ page, app, localePath }) => {
- await mockAllAPIs(page);
- // Setup error page mocks (global mocks already applied via fixture)
- await setupErrorPageMocks(page);
-
- // Mock an endpoint to return 500 error
- await page.route('**/api/Requests/**', (route) => {
- route.fulfill({
- status: 500,
- contentType: 'application/json',
- body: JSON.stringify({ error: 'Internal Server Error' }),
- });
- });
-
- // Navigate to a page that triggers API call
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- // Trigger a search that will use the mocked endpoint
- const flightInput = page.locator(tid(S.FILTER_FLIGHT_NUMBER_INPUT, app));
- if (await flightInput.isVisible().catch(() => false)) {
- await flightInput.fill('SU100');
- const searchButton = page.locator(tid(S.FILTER_FLIGHT_NUMBER_SEARCH, app));
- if (await searchButton.isVisible().catch(() => false)) {
- await searchButton.click();
- await page.waitForTimeout(2000);
- }
- }
-
- // Check for error indicators
- const pageText = await page.textContent('body');
- const hasErrorMessage =
- pageText?.includes('500') ||
- pageText?.includes('Server Error') ||
- pageText?.includes('error') ||
- pageText?.includes('ошибка');
-
- // May not always show explicit 500 page, so we're lenient
- // The key is that error handling doesn't crash the app
- expect(await page.isVisible('body')).toBe(true);
- });
-
- test('292: 500 page shows error message and reload suggestion', async ({
- page,
- app,
- localePath,
- }) => {
- await mockAllAPIs(page);
- // Setup error page mocks (global mocks already applied via fixture)
- await setupErrorPageMocks(page);
-
- // Mock endpoint to return 500
- await page.route('**/api/onlineboard/**', (route) => {
- route.fulfill({
- status: 500,
- contentType: 'application/json',
- body: JSON.stringify({ error: 'Server error' }),
- });
- });
-
- // Navigate to landing
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- // The page should still be accessible and show some error handling
- await expect(page.locator('body')).toBeVisible();
-
- // Check if any error message is displayed
- const pageText = await page.textContent('body');
- const hasContent = pageText && pageText.trim().length > 0;
- expect(hasContent).toBe(true);
- });
-
- // Invalid Search Parameters tests
- test('293: Search with invalid flight number shows error message', async ({
- page,
- app,
- localePath,
- }) => {
- await mockAllAPIs(page);
- // Setup error page mocks (global mocks already applied via fixture)
- await setupErrorPageMocks(page);
-
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- // Try to search with empty/invalid flight number
- const flightInput = page.locator(tid(S.FILTER_FLIGHT_NUMBER_INPUT, app));
-
- if (await flightInput.isVisible().catch(() => false)) {
- // Clear the input and try to search without proper data
- await flightInput.clear();
- const searchButton = page.locator(tid(S.FILTER_FLIGHT_NUMBER_SEARCH, app));
-
- if (await searchButton.isVisible().catch(() => false)) {
- // Use force click to bypass any overlaying elements
- await searchButton.click({ force: true });
- await page.waitForTimeout(1000);
-
- // Check for validation error or empty state message
- const pageText = await page.textContent('body');
- const hasErrorOrEmpty =
- pageText?.includes('error') ||
- pageText?.includes('ошибка') ||
- pageText?.includes('invalid') ||
- pageText?.includes('required') ||
- pageText?.includes('обязательн');
-
- // If input validation is present, expect error message
- // Otherwise just verify page is still functional
- expect(await page.isVisible('body')).toBe(true);
- } else {
- test.skip(true, 'Search button not available');
- }
- } else {
- test.skip(true, 'Flight number input not available');
- }
- });
-
- test('294: Search with invalid city selection shows error message', async ({
- page,
- app,
- localePath,
- }) => {
- await mockAllAPIs(page);
- // Setup error page mocks (global mocks already applied via fixture)
- await setupErrorPageMocks(page);
-
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- // Try to use route search without proper city selection
- const departureInput = page.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app));
-
- if (await departureInput.isVisible().catch(() => false)) {
- // Try to search without selecting a proper city (typing but not selecting from dropdown)
- // For custom autocomplete elements, use type instead of fill
- await departureInput.click();
- await page.keyboard.type('XXX');
- await page.waitForTimeout(500);
-
- const searchButton = page.locator(tid(S.FILTER_ROUTE_SEARCH, app));
-
- if (await searchButton.isVisible().catch(() => false)) {
- // Use force click to bypass any overlaying elements
- await searchButton.click({ force: true });
- await page.waitForTimeout(1000);
-
- // Check for error or validation message
- const pageText = await page.textContent('body');
-
- // Either shows validation error or handles gracefully
- expect(await page.isVisible('body')).toBe(true);
- } else {
- test.skip(true, 'Route search button not available');
- }
- } else {
- test.skip(true, 'Route departure input not available');
- }
- });
-
- test('295: Search with invalid date shows error message', async ({ page, app, localePath }) => {
- await mockAllAPIs(page);
- // Setup error page mocks (global mocks already applied via fixture)
- await setupErrorPageMocks(page);
-
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- // Try to interact with date input in an invalid way
- const calendarInput = page.locator(tid(S.CALENDAR_INPUT, app));
- const fallbackCalendarInput = page.locator(
- 'input[type="text"][placeholder*="date"], .p-calendar input',
- );
-
- let dateInputElement = null;
- if (await calendarInput.isVisible().catch(() => false)) {
- dateInputElement = calendarInput;
- } else if (await fallbackCalendarInput.isVisible().catch(() => false)) {
- dateInputElement = fallbackCalendarInput;
- }
-
- if (dateInputElement) {
- try {
- // Try typing invalid date format
- await dateInputElement.fill('99/99/9999');
- await page.waitForTimeout(500);
-
- // The app should handle invalid dates gracefully
- await expect(page.locator('body')).toBeVisible();
- } catch (e) {
- // If fill fails, try click and type
- try {
- await dateInputElement.click();
- await page.keyboard.type('99/99/9999');
- await page.waitForTimeout(500);
- await expect(page.locator('body')).toBeVisible();
- } catch {
- // If interaction still fails, page should still be stable
- await expect(page.locator('body')).toBeVisible();
- }
- }
- } else {
- test.skip(true, 'Calendar input not available');
- }
- });
-
- // Network Error & Offline tests
- test('296: No results state displays when API returns empty list', async ({
- page,
- app,
- localePath,
- }) => {
- await mockAllAPIs(page);
- // Setup error page mocks (global mocks already applied via fixture)
- await setupErrorPageMocks(page);
-
- // Mock endpoint to return empty list
- await page.route('**/api/Requests/*/getboard**', (route) => {
- route.fulfill({
- status: 200,
- contentType: 'application/json',
- body: JSON.stringify([]),
- });
- });
-
- // Navigate to onlineboard
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- // Perform a search
- const flightInput = page.locator(tid(S.FILTER_FLIGHT_NUMBER_INPUT, app));
- if (await flightInput.isVisible().catch(() => false)) {
- await flightInput.fill('SU999');
- const searchButton = page.locator(tid(S.FILTER_FLIGHT_NUMBER_SEARCH, app));
- if (await searchButton.isVisible().catch(() => false)) {
- // Use force click to bypass any overlaying elements
- await searchButton.click({ force: true });
- await page.waitForTimeout(2000);
-
- // Look for empty state message
- const emptyList = page.locator(tid(S.BOARD_EMPTY_LIST, app));
- const pageText = await page.textContent('body');
- const emptyIndicators = page.locator(
- '[data-testid="board-empty-list"], .board__empty, .empty-list, .no-results',
- );
-
- // Should either show empty list indicator or "no results" message
- const emptyStateVisible =
- (await emptyList.count()) > 0 || (await emptyIndicators.count()) > 0;
- const hasEmptyText =
- pageText?.includes('not found') ||
- pageText?.includes('no results') ||
- pageText?.includes('results not found') ||
- pageText?.includes('не найдено') ||
- pageText?.includes('результаты не найдены') ||
- pageText?.includes('полёты');
-
- // Page should be stable - either empty state or still loading
- expect(await page.isVisible('body')).toBe(true);
- }
- }
- });
-
- test('297: Page gracefully handles network timeout/offline state', async ({
- page,
- app,
- localePath,
- }) => {
- await mockAllAPIs(page);
- // Setup error page mocks (global mocks already applied via fixture)
- await setupErrorPageMocks(page);
-
- // Setup route to simulate network timeout
- await page.route('**/api/Requests/**', (route) => {
- // Simulate a very slow response (timeout)
- route.abort('timedout');
- });
-
- // Navigate and try to perform search
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- // Try to trigger a search
- const flightInput = page.locator(tid(S.FILTER_FLIGHT_NUMBER_INPUT, app));
- if (await flightInput.isVisible().catch(() => false)) {
- await flightInput.fill('SU100');
- const searchButton = page.locator(tid(S.FILTER_FLIGHT_NUMBER_SEARCH, app));
- if (await searchButton.isVisible().catch(() => false)) {
- // Use force click to bypass any overlaying elements
- await searchButton.click({ force: true }).catch(() => {
- // If click fails due to timeout, that's OK - we're testing timeout behavior
- });
- // Wait for timeout error to surface
- await page.waitForTimeout(2000);
- }
- }
-
- // Main assertion: page should still be responsive and not crash
- // Even with network errors, the UI should remain visible
- const bodyVisible = await page.isVisible('body');
- expect(bodyVisible).toBe(true);
-
- // Verify no unhandled console errors
- const errors: string[] = [];
- page.on('console', (msg) => {
- if (msg.type() === 'error') {
- errors.push(msg.text());
- }
- });
-
- // The page should handle the error gracefully (may show error message)
- // but should not have JavaScript errors
- expect(errors.length).toBeLessThanOrEqual(2); // Allow some API-related errors
- });
-});
diff --git a/tests/e2e-angular/cross-app/13-locale-switching.spec.ts b/tests/e2e-angular/cross-app/13-locale-switching.spec.ts
deleted file mode 100644
index 00ffb0ec..00000000
--- a/tests/e2e-angular/cross-app/13-locale-switching.spec.ts
+++ /dev/null
@@ -1,640 +0,0 @@
-import { test, expect } from '../support/cross-app-fixtures';
-import { mockAllAPIs } from '../support/cross-app-fixtures';
-import { S, tid } from '../support/selectors';
-
-/**
- * Locale Switching Cross-App Test Suite (Tests 298-315)
- *
- * 18 tests covering:
- * - Locale switcher visibility and accessibility
- * - Dropdown display and interaction
- * - Locale selection and navigation
- * - Content translation verification
- * - Locale persistence across pages and reloads
- */
-
-// Map of supported locales to their display names
-const LOCALE_NAMES: Record = {
- 'ru-ru': 'Русский',
- 'en-us': 'English',
- 'es-es': 'Español',
- 'fr-fr': 'Français',
- 'it-it': 'Italiano',
- 'ja-jp': '日本語',
- 'ko-kr': '한국어',
- 'zh-cn': '中文',
- 'de-de': 'Deutsch',
-};
-
-// Sample translation keys and their expected values per locale
-// Reference data for translation testing (some tests may use subset of these)
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-const _TRANSLATION_SAMPLES: Record> = {
- 'ru-ru': {
- 'nav.flightBoard': 'Онлайн табло',
- 'nav.schedule': 'Расписание',
- 'search.find': 'Поиск',
- },
- 'en-us': {
- 'nav.flightBoard': 'Online Board',
- 'nav.schedule': 'Schedule',
- 'search.find': 'Search',
- },
- 'es-es': {
- 'nav.flightBoard': 'Tablero en línea',
- 'nav.schedule': 'Horario',
- 'search.find': 'Buscar',
- },
- 'fr-fr': {
- 'nav.flightBoard': "Tableau d'affichage en direct",
- 'nav.schedule': 'Horaire',
- 'search.find': 'Rechercher',
- },
- 'it-it': {
- 'nav.flightBoard': 'Scheda Online',
- 'nav.schedule': 'Programma',
- 'search.find': 'Cerca',
- },
- 'ja-jp': {
- 'nav.flightBoard': 'オンラインボード',
- 'nav.schedule': 'スケジュール',
- 'search.find': '検索',
- },
- 'ko-kr': {
- 'nav.flightBoard': '온라인 보드',
- 'nav.schedule': '일정',
- 'search.find': '검색',
- },
- 'zh-cn': {
- 'nav.flightBoard': '在线板',
- 'nav.schedule': '航班时刻表',
- 'search.find': '搜索',
- },
- 'de-de': {
- 'nav.flightBoard': 'Online-Tafel',
- 'nav.schedule': 'Fahrplan',
- 'search.find': 'Suchen',
- },
-};
-
-test.describe('Locale Switching (Cross-App)', () => {
- test.beforeEach(async ({ page, localePath }) => {
- await mockAllAPIs(page);
- await page.goto(localePath('/'));
- await page.waitForLoadState('networkidle');
- });
-
- // ====================================================================
- // SECTION 1: Locale Switcher Visibility & Accessibility (Tests 298-300)
- // ====================================================================
-
- test('298: Locale switcher button is visible in layout', async ({ page, app }) => {
- const switcher = page.locator(tid(S.LAYOUT_LOCALE_SWITCHER, app));
- if ((await switcher.count()) === 0) {
- test.skip(true, 'Locale switcher not present in this app');
- return;
- }
- await expect(switcher).toBeVisible();
- });
-
- test('299: Locale switcher button shows current locale code', async ({ page, app, locale }) => {
- const switcher = page.locator(tid(S.LAYOUT_LOCALE_SWITCHER, app));
- if ((await switcher.count()) === 0) {
- test.skip(true, 'Locale switcher not present in this app');
- return;
- }
- // Switcher should display the current locale code (e.g., "RU-RU", "EN-US")
- const localeCode = locale.toUpperCase();
- await expect(switcher).toContainText(localeCode);
- });
-
- test('300: Locale switcher button is clickable', async ({ page, app }) => {
- const switcher = page.locator(tid(S.LAYOUT_LOCALE_SWITCHER, app));
- if ((await switcher.count()) === 0) {
- test.skip(true, 'Locale switcher not present in this app');
- return;
- }
- await expect(switcher).toBeEnabled();
- // Verify it's actually a button or has button-like properties
- const role = await switcher.getAttribute('role');
- const isButton =
- (await switcher.evaluate((el) => el instanceof HTMLButtonElement)) ||
- role === 'button' ||
- (await switcher.locator('button').count()) > 0;
- expect(isButton).toBe(true);
- });
-
- // ====================================================================
- // SECTION 2: Locale Dropdown Display (Tests 301-303)
- // ====================================================================
-
- test('301: Clicking switcher opens dropdown menu', async ({ page, app }) => {
- const switcher = page.locator(tid(S.LAYOUT_LOCALE_SWITCHER, app));
- if ((await switcher.count()) === 0) {
- test.skip(true, 'Locale switcher not present in this app');
- return;
- }
- await switcher.click();
- const options = page.locator(tid(S.LAYOUT_LOCALE_OPTION, app));
- await expect(options.first()).toBeVisible({ timeout: 5000 });
- });
-
- test('302: Dropdown shows all 9 available locales', async ({ page, app }) => {
- const switcher = page.locator(tid(S.LAYOUT_LOCALE_SWITCHER, app));
- if ((await switcher.count()) === 0) {
- test.skip(true, 'Locale switcher not present in this app');
- return;
- }
- await switcher.click();
- const options = page.locator(tid(S.LAYOUT_LOCALE_OPTION, app));
- const optionCount = await options.count();
- // Should have at least the 9 tested locales
- expect(optionCount).toBeGreaterThanOrEqual(9);
- });
-
- test('303: Dropdown displays locale names/codes correctly', async ({ page, app }) => {
- const switcher = page.locator(tid(S.LAYOUT_LOCALE_SWITCHER, app));
- if ((await switcher.count()) === 0) {
- test.skip(true, 'Locale switcher not present in this app');
- return;
- }
- await switcher.click();
- const options = page.locator(tid(S.LAYOUT_LOCALE_OPTION, app));
-
- // Verify options display native names or codes
- const optionTexts: string[] = [];
- for (let i = 0; i < Math.min(await options.count(), 9); i++) {
- const text = await options.nth(i).textContent();
- if (text) optionTexts.push(text);
- }
-
- // Should have some recognizable locale text
- const hasLocaleText = optionTexts.some(
- (text) =>
- text.includes('English') ||
- text.includes('Русский') ||
- text.includes('Español') ||
- text.includes('Français') ||
- text.includes('en-us') ||
- text.includes('ru-ru'),
- );
- expect(hasLocaleText).toBe(true);
- });
-
- // ====================================================================
- // SECTION 3: Locale Selection & Navigation (Tests 304-307)
- // ====================================================================
-
- test('304: Selecting different locale closes dropdown', async ({ page, app, locale }) => {
- const switcher = page.locator(tid(S.LAYOUT_LOCALE_SWITCHER, app));
- if ((await switcher.count()) === 0) {
- test.skip(true, 'Locale switcher not present in this app');
- return;
- }
-
- await switcher.click();
- const options = page.locator(tid(S.LAYOUT_LOCALE_OPTION, app));
- await expect(options.first()).toBeVisible();
-
- // Click first visible option (if not current locale)
- const firstOption = options.first();
- const firstLocaleCode = await firstOption.getAttribute('data-locale');
-
- if (firstLocaleCode && firstLocaleCode !== locale.split('-')[0]) {
- await firstOption.click();
- // Wait for navigation and dropdown to close
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(300);
-
- // Dropdown should be hidden
- await expect(options.first()).toBeHidden();
- } else {
- test.skip(true, 'No alternate locale available to test');
- }
- });
-
- test('305: Selecting locale changes URL prefix', async ({ page, app, locale }) => {
- const switcher = page.locator(tid(S.LAYOUT_LOCALE_SWITCHER, app));
- if ((await switcher.count()) === 0) {
- test.skip(true, 'Locale switcher not present in this app');
- return;
- }
-
- const targetLocale = locale === 'en-us' ? 'ru-ru' : 'en-us';
-
- await switcher.click();
- const options = page.locator(tid(S.LAYOUT_LOCALE_OPTION, app));
-
- // Find option matching target locale by data-locale or text content
- let targetOption = options
- .filter({ has: page.locator(`[data-locale="${targetLocale.split('-')[0]}"]`) })
- .first();
-
- if ((await targetOption.count()) === 0) {
- // Try by native name
- const targetName = LOCALE_NAMES[targetLocale];
- targetOption = options.filter({ hasText: new RegExp(targetName) }).first();
- }
-
- if ((await targetOption.count()) > 0) {
- await targetOption.click();
- await page.waitForLoadState('networkidle');
- // URL should contain new locale prefix
- await expect(page).toHaveURL(new RegExp(`/${targetLocale}/`));
- } else {
- test.skip(true, 'Target locale option not found');
- }
- });
-
- test('306: Page content reloads after locale change', async ({ page, app, locale }) => {
- const switcher = page.locator(tid(S.LAYOUT_LOCALE_SWITCHER, app));
- if ((await switcher.count()) === 0) {
- test.skip(true, 'Locale switcher not present in this app');
- return;
- }
-
- // Get initial content
- const initialLocale = locale;
-
- const targetLocale = initialLocale === 'en-us' ? 'ru-ru' : 'en-us';
-
- await switcher.click();
- const options = page.locator(tid(S.LAYOUT_LOCALE_OPTION, app));
- let targetOption = options
- .filter({ has: page.locator(`[data-locale="${targetLocale.split('-')[0]}"]`) })
- .first();
-
- if ((await targetOption.count()) === 0) {
- const targetName = LOCALE_NAMES[targetLocale];
- targetOption = options.filter({ hasText: new RegExp(targetName) }).first();
- }
-
- if ((await targetOption.count()) > 0) {
- await targetOption.click();
- await page.waitForLoadState('networkidle');
-
- const contentAfter = await page.locator('body').textContent();
- // Content should have changed (some text different between locales)
- // This is a loose check - exact comparison may be fragile
- expect(contentAfter).toBeTruthy();
- } else {
- test.skip(true, 'Target locale option not found');
- }
- });
-
- test('307: Selected locale is highlighted in dropdown', async ({ page, app, locale }) => {
- const switcher = page.locator(tid(S.LAYOUT_LOCALE_SWITCHER, app));
- if ((await switcher.count()) === 0) {
- test.skip(true, 'Locale switcher not present in this app');
- return;
- }
-
- await switcher.click();
- const options = page.locator(tid(S.LAYOUT_LOCALE_OPTION, app));
-
- // Find current locale option
- let currentOption = options
- .filter({ has: page.locator(`[data-locale="${locale.split('-')[0]}"]`) })
- .first();
-
- if ((await currentOption.count()) === 0) {
- const localeName = LOCALE_NAMES[locale];
- currentOption = options.filter({ hasText: new RegExp(localeName) }).first();
- }
-
- if ((await currentOption.count()) > 0) {
- // Current option should have active class or aria-selected
- const hasActiveClass = await currentOption.evaluate(
- (el) => el.className.includes('active') || el.className.includes('selected'),
- );
- const isAriaSelected = await currentOption.getAttribute('aria-selected');
-
- expect(hasActiveClass || isAriaSelected === 'true').toBe(true);
- } else {
- test.skip(true, 'Current locale option not found');
- }
- });
-
- // ====================================================================
- // SECTION 4: Content Translation Verification (Tests 308-312)
- // ====================================================================
-
- test('308: Page titles translate after locale change', async ({ page, app, locale }) => {
- const switcher = page.locator(tid(S.LAYOUT_LOCALE_SWITCHER, app));
- if ((await switcher.count()) === 0) {
- test.skip(true, 'Locale switcher not present in this app');
- return;
- }
-
- const targetLocale = locale === 'en-us' ? 'ru-ru' : 'en-us';
- const targetLocaleCode = targetLocale.split('-')[0];
-
- await switcher.click();
- const options = page.locator(tid(S.LAYOUT_LOCALE_OPTION, app));
- let targetOption = options
- .filter({ has: page.locator(`[data-locale="${targetLocaleCode}"]`) })
- .first();
-
- if ((await targetOption.count()) === 0) {
- const targetName = LOCALE_NAMES[targetLocale];
- targetOption = options.filter({ hasText: new RegExp(targetName) }).first();
- }
-
- if ((await targetOption.count()) > 0) {
- await targetOption.click();
- await page.waitForLoadState('networkidle');
-
- // Verify page title or h1 contains translated content
- const pageTitle = await page.title();
- const h1 = await page.locator('h1').first().textContent();
- const hasContent = pageTitle || h1;
- expect(hasContent).toBeTruthy();
- } else {
- test.skip(true, 'Target locale option not found');
- }
- });
-
- test('309: Button labels translate after locale change', async ({ page, app, locale }) => {
- const switcher = page.locator(tid(S.LAYOUT_LOCALE_SWITCHER, app));
- if ((await switcher.count()) === 0) {
- test.skip(true, 'Locale switcher not present in this app');
- return;
- }
-
- const targetLocale = locale === 'en-us' ? 'ru-ru' : 'en-us';
- const targetLocaleCode = targetLocale.split('-')[0];
-
- await switcher.click();
- const options = page.locator(tid(S.LAYOUT_LOCALE_OPTION, app));
- let targetOption = options
- .filter({ has: page.locator(`[data-locale="${targetLocaleCode}"]`) })
- .first();
-
- if ((await targetOption.count()) === 0) {
- const targetName = LOCALE_NAMES[targetLocale];
- targetOption = options.filter({ hasText: new RegExp(targetName) }).first();
- }
-
- if ((await targetOption.count()) > 0) {
- await targetOption.click();
- await page.waitForLoadState('networkidle');
-
- // Get sample button text after
- const buttonsAfter = await page.locator('button').first().textContent();
-
- // At least some button text should exist and be localized
- expect(buttonsAfter).toBeTruthy();
- } else {
- test.skip(true, 'Target locale option not found');
- }
- });
-
- test('310: Placeholder text translates after locale change', async ({ page, app, locale }) => {
- const switcher = page.locator(tid(S.LAYOUT_LOCALE_SWITCHER, app));
- if ((await switcher.count()) === 0) {
- test.skip(true, 'Locale switcher not present in this app');
- return;
- }
-
- const targetLocale = locale === 'en-us' ? 'ru-ru' : 'en-us';
- const targetLocaleCode = targetLocale.split('-')[0];
-
- await switcher.click();
- const options = page.locator(tid(S.LAYOUT_LOCALE_OPTION, app));
- let targetOption = options
- .filter({ has: page.locator(`[data-locale="${targetLocaleCode}"]`) })
- .first();
-
- if ((await targetOption.count()) === 0) {
- const targetName = LOCALE_NAMES[targetLocale];
- targetOption = options.filter({ hasText: new RegExp(targetName) }).first();
- }
-
- if ((await targetOption.count()) > 0) {
- await targetOption.click();
- await page.waitForLoadState('networkidle');
-
- // Look for input with placeholder
- const inputWithPlaceholder = page.locator('input[placeholder]').first();
- const placeholder = await inputWithPlaceholder.getAttribute('placeholder');
-
- // Just verify placeholders exist and are not empty
- if (placeholder) {
- expect(placeholder.length).toBeGreaterThan(0);
- }
- } else {
- test.skip(true, 'Target locale option not found');
- }
- });
-
- test('311: Tab names translate after locale change', async ({ page, app, locale }) => {
- const switcher = page.locator(tid(S.LAYOUT_LOCALE_SWITCHER, app));
- if ((await switcher.count()) === 0) {
- test.skip(true, 'Locale switcher not present in this app');
- return;
- }
-
- const targetLocale = locale === 'en-us' ? 'ru-ru' : 'en-us';
- const targetLocaleCode = targetLocale.split('-')[0];
-
- await switcher.click();
- const options = page.locator(tid(S.LAYOUT_LOCALE_OPTION, app));
- let targetOption = options
- .filter({ has: page.locator(`[data-locale="${targetLocaleCode}"]`) })
- .first();
-
- if ((await targetOption.count()) === 0) {
- const targetName = LOCALE_NAMES[targetLocale];
- targetOption = options.filter({ hasText: new RegExp(targetName) }).first();
- }
-
- if ((await targetOption.count()) > 0) {
- await targetOption.click();
- await page.waitForLoadState('networkidle');
-
- // Check for navigation tabs
- const navTabs = page.locator('[role="tab"], [data-testid*="tab"]').first();
- const tabText = await navTabs.textContent();
-
- // Navigation should have tab labels
- expect(tabText).toBeTruthy();
- } else {
- test.skip(true, 'Target locale option not found');
- }
- });
-
- test('312: Error messages translate after locale change', async ({ page, app, locale }) => {
- const switcher = page.locator(tid(S.LAYOUT_LOCALE_SWITCHER, app));
- if ((await switcher.count()) === 0) {
- test.skip(true, 'Locale switcher not present in this app');
- return;
- }
-
- const targetLocale = locale === 'en-us' ? 'ru-ru' : 'en-us';
- const targetLocaleCode = targetLocale.split('-')[0];
-
- await switcher.click();
- const options = page.locator(tid(S.LAYOUT_LOCALE_OPTION, app));
- let targetOption = options
- .filter({ has: page.locator(`[data-locale="${targetLocaleCode}"]`) })
- .first();
-
- if ((await targetOption.count()) === 0) {
- const targetName = LOCALE_NAMES[targetLocale];
- targetOption = options.filter({ hasText: new RegExp(targetName) }).first();
- }
-
- if ((await targetOption.count()) > 0) {
- await targetOption.click();
- await page.waitForLoadState('networkidle');
-
- // Look for error message or alert if present
- const errorMsg = page.locator('[role="alert"], .error, [data-testid*="error"]').first();
- const errorText = await errorMsg.textContent().catch(() => '');
-
- // Error messages should be translatable (just verify they exist or are properly structured)
- expect(typeof errorText).toBe('string');
- } else {
- test.skip(true, 'Target locale option not found');
- }
- });
-
- // ====================================================================
- // SECTION 5: Locale Persistence (Tests 313-315)
- // ====================================================================
-
- test('313: Selected locale persists on page reload', async ({ page, app, locale }) => {
- const switcher = page.locator(tid(S.LAYOUT_LOCALE_SWITCHER, app));
- if ((await switcher.count()) === 0) {
- test.skip(true, 'Locale switcher not present in this app');
- return;
- }
-
- const targetLocale = locale === 'en-us' ? 'ru-ru' : 'en-us';
- const targetLocaleCode = targetLocale.split('-')[0];
-
- // Switch locale
- await switcher.click();
- const options = page.locator(tid(S.LAYOUT_LOCALE_OPTION, app));
- let targetOption = options
- .filter({ has: page.locator(`[data-locale="${targetLocaleCode}"]`) })
- .first();
-
- if ((await targetOption.count()) === 0) {
- const targetName = LOCALE_NAMES[targetLocale];
- targetOption = options.filter({ hasText: new RegExp(targetName) }).first();
- }
-
- if ((await targetOption.count()) > 0) {
- await targetOption.click();
- await page.waitForLoadState('networkidle');
-
- // Verify URL changed
- await expect(page).toHaveURL(new RegExp(`/${targetLocale}/`));
-
- // Reload page
- await page.reload();
- await page.waitForLoadState('networkidle');
-
- // Locale should persist in URL
- await expect(page).toHaveURL(new RegExp(`/${targetLocale}/`));
-
- // Switcher should show the same locale
- const localeCode = targetLocale.toUpperCase();
- await expect(switcher).toContainText(localeCode);
- } else {
- test.skip(true, 'Target locale option not found');
- }
- });
-
- test('314: Switching pages maintains current locale', async ({ page, app, locale }) => {
- const switcher = page.locator(tid(S.LAYOUT_LOCALE_SWITCHER, app));
- if ((await switcher.count()) === 0) {
- test.skip(true, 'Locale switcher not present in this app');
- return;
- }
-
- const targetLocale = locale === 'en-us' ? 'ru-ru' : 'en-us';
- const targetLocaleCode = targetLocale.split('-')[0];
-
- // Switch locale
- await switcher.click();
- const options = page.locator(tid(S.LAYOUT_LOCALE_OPTION, app));
- let targetOption = options
- .filter({ has: page.locator(`[data-locale="${targetLocaleCode}"]`) })
- .first();
-
- if ((await targetOption.count()) === 0) {
- const targetName = LOCALE_NAMES[targetLocale];
- targetOption = options.filter({ hasText: new RegExp(targetName) }).first();
- }
-
- if ((await targetOption.count()) > 0) {
- await targetOption.click();
- await page.waitForLoadState('networkidle');
-
- // Current URL in target locale
- const currentUrl = page.url();
- expect(currentUrl).toContain(`/${targetLocale}/`);
-
- // Try to navigate to schedule (or another page)
- const scheduleTab = page.locator(tid(S.NAV_SCHEDULE_TAB, app));
- if ((await scheduleTab.count()) > 0) {
- await scheduleTab.click();
- await page.waitForLoadState('networkidle');
-
- // Should still be in target locale
- await expect(page).toHaveURL(new RegExp(`/${targetLocale}/schedule`));
- }
- } else {
- test.skip(true, 'Target locale option not found');
- }
- });
-
- test('315: Browser history preserves locale context', async ({ page, app, locale }) => {
- const switcher = page.locator(tid(S.LAYOUT_LOCALE_SWITCHER, app));
- if ((await switcher.count()) === 0) {
- test.skip(true, 'Locale switcher not present in this app');
- return;
- }
-
- const targetLocale = locale === 'en-us' ? 'ru-ru' : 'en-us';
- const targetLocaleCode = targetLocale.split('-')[0];
-
- // Switch locale
- await switcher.click();
- const options = page.locator(tid(S.LAYOUT_LOCALE_OPTION, app));
- let targetOption = options
- .filter({ has: page.locator(`[data-locale="${targetLocaleCode}"]`) })
- .first();
-
- if ((await targetOption.count()) === 0) {
- const targetName = LOCALE_NAMES[targetLocale];
- targetOption = options.filter({ hasText: new RegExp(targetName) }).first();
- }
-
- if ((await targetOption.count()) > 0) {
- await targetOption.click();
- await page.waitForLoadState('networkidle');
-
- const newUrl = page.url();
- expect(newUrl).toContain(`/${targetLocale}/`);
-
- // Navigate back
- await page.goBack();
- await page.waitForLoadState('networkidle');
-
- // Should be on previous locale
- await expect(page).toHaveURL(new RegExp(`/${locale}/`));
-
- // Navigate forward
- await page.goForward();
- await page.waitForLoadState('networkidle');
-
- // Should be back to target locale
- await expect(page).toHaveURL(new RegExp(`/${targetLocale}/`));
- } else {
- test.skip(true, 'Target locale option not found');
- }
- });
-});
diff --git a/tests/e2e-angular/cross-app/14-search-history.spec.ts b/tests/e2e-angular/cross-app/14-search-history.spec.ts
deleted file mode 100644
index 6d03f913..00000000
--- a/tests/e2e-angular/cross-app/14-search-history.spec.ts
+++ /dev/null
@@ -1,765 +0,0 @@
-import { test, expect } from '../support/cross-app-fixtures';
-import { mockAllAPIs } from '../support/cross-app-fixtures';
-import { S, tid } from '../support/selectors';
-
-test.describe('Search History (Cross-App)', () => {
- test.beforeEach(async ({ page, localePath }) => {
- await mockAllAPIs(page);
- // API mocks are applied globally via fixture
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
- });
-
- // Test 316-319: Search History Section Display
- test('316: Search history section is visible on landing page when there is history', async ({
- page,
- app,
- locale,
- localePath,
- }) => {
- // Perform a search first to populate history
- const today = formatToday();
- await page.goto(localePath(`onlineboard/departure/MOW-${today}`));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- // Return to landing page
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(500);
-
- // Verify search history section exists
- const historySection = page.locator(tid(S.LANDING_SEARCH_HISTORY, app));
- const count = await historySection.count();
-
- if (count === 0) {
- test.skip(true, 'Search history section not implemented');
- return;
- }
-
- await expect(historySection).toBeVisible({ timeout: 5000 });
- });
-
- test('317: Search history section has correct heading', async ({
- page,
- app,
- locale,
- localePath,
- }) => {
- // Perform a search to populate history
- const today = formatToday();
- await page.goto(localePath(`onlineboard/departure/MOW-${today}`));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- // Return to landing
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(500);
-
- const historySection = page.locator(tid(S.LANDING_SEARCH_HISTORY, app));
- const count = await historySection.count();
-
- if (count === 0) {
- test.skip(true, 'Search history section not implemented');
- return;
- }
-
- // Check for heading in the section
- const heading = historySection.locator('h3, h2, [class*="title"]').first();
- await expect(heading).toBeVisible();
-
- const text = await heading.textContent();
- expect(text?.trim().length).toBeGreaterThan(0);
-
- // Should contain text about history (in locale-appropriate language)
- if (locale === 'ru-ru') {
- expect(text?.toLowerCase()).toContain('история');
- }
- });
-
- test('318: Empty state message shows when no history exists', async ({
- page,
- app,
- localePath,
- }) => {
- // Clear localStorage to ensure no history
- await page.evaluate(() => {
- localStorage.clear();
- sessionStorage.clear();
- });
-
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(500);
-
- const historySection = page.locator(tid(S.LANDING_SEARCH_HISTORY, app));
- const count = await historySection.count();
-
- // Section might be hidden entirely when empty, which is acceptable
- if (count === 0) {
- // This is acceptable behavior - section hidden when empty
- expect(count).toBe(0);
- return;
- }
-
- // If section exists, it should show either empty message or no items
- const items = historySection.locator(tid(S.LANDING_SEARCH_HISTORY_ITEM, app));
- const itemCount = await items.count();
-
- // Either section is hidden or items list is empty
- if (itemCount === 0) {
- const emptyMessage = historySection.locator('text=/No|empty|История|пусто/i');
- const emptyCount = await emptyMessage.count();
- // If items are empty, should have some empty state indicator (optional)
- // Just verify section doesn't crash
- await expect(historySection).toBeVisible();
- }
- });
-
- test('319: Search history items appear after performing searches', async ({
- page,
- app,
- locale,
- localePath,
- }) => {
- // Clear history first
- await page.evaluate(() => {
- localStorage.clear();
- sessionStorage.clear();
- });
-
- // Perform first search
- const today = formatToday();
- await page.goto(localePath(`onlineboard/departure/MOW-${today}`));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- // Perform second search
- await page.goto(localePath(`onlineboard/departure/LED-${today}`));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- // Return to landing
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(500);
-
- const historySection = page.locator(tid(S.LANDING_SEARCH_HISTORY, app));
- const sectionCount = await historySection.count();
-
- if (sectionCount === 0) {
- test.skip(true, 'Search history not implemented');
- return;
- }
-
- // Should have history items visible
- const items = historySection.locator(tid(S.LANDING_SEARCH_HISTORY_ITEM, app));
- const itemCount = await items.count();
- expect(itemCount).toBeGreaterThan(0);
- });
-
- // Test 320-323: Search History Item Display
- test('320: Search history item shows search parameters or label', async ({
- page,
- app,
- locale,
- localePath,
- }) => {
- // Perform a search
- const today = formatToday();
- await page.goto(localePath(`onlineboard/departure/MOW-${today}`));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- // Return to landing
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(500);
-
- const historyItem = page.locator(tid(S.LANDING_SEARCH_HISTORY_ITEM, app)).first();
- const count = await historyItem.count();
-
- if (count === 0) {
- test.skip(true, 'Search history items not available');
- return;
- }
-
- // Item should have visible text (the search label)
- const text = await historyItem.textContent();
- expect(text?.trim().length).toBeGreaterThan(0);
-
- // Should contain search info
- expect(text).toMatch(/MOW|мос/i);
- });
-
- test('321: Search history item is clickable and navigates', async ({
- page,
- app,
- locale,
- localePath,
- }) => {
- // Perform a search
- const today = formatToday();
- await page.goto(localePath(`onlineboard/departure/MOW-${today}`));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- // Return to landing
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(500);
-
- const historyItem = page.locator(tid(S.LANDING_SEARCH_HISTORY_ITEM, app)).first();
- const count = await historyItem.count();
-
- if (count === 0) {
- test.skip(true, 'Search history items not available');
- return;
- }
-
- // Click the item - find the link inside
- const link = historyItem.locator('a').first();
- const linkCount = await link.count();
-
- if (linkCount === 0) {
- test.skip(true, 'Search history item is not a link');
- return;
- }
-
- const urlBefore = page.url();
- await link.click();
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- const urlAfter = page.url();
- // Should navigate to search results page
- expect(urlAfter).not.toBe(urlBefore);
- expect(urlAfter).toMatch(/onlineboard/);
- });
-
- test('322: Multiple search history items are ordered (most recent first)', async ({
- page,
- app,
- locale,
- localePath,
- }) => {
- // Clear history
- await page.evaluate(() => {
- localStorage.clear();
- sessionStorage.clear();
- });
-
- const today = formatToday();
-
- // Perform first search for MOW
- await page.goto(localePath(`onlineboard/departure/MOW-${today}`));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- // Perform second search for LED
- await page.goto(localePath(`onlineboard/departure/LED-${today}`));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- // Perform third search for SVO
- await page.goto(localePath(`onlineboard/departure/SVO-${today}`));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- // Return to landing
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(500);
-
- const historyItems = page.locator(tid(S.LANDING_SEARCH_HISTORY_ITEM, app));
- const count = await historyItems.count();
-
- if (count < 2) {
- test.skip(true, 'Insufficient history items for ordering test');
- return;
- }
-
- // Get all items' text
- const firstItemText = await historyItems.nth(0).textContent();
- const secondItemText = await historyItems.nth(1).textContent();
-
- // Most recent (SVO) should be first, older (LED) should be second
- expect(firstItemText).toMatch(/SVO|сво/i);
- expect(secondItemText).toMatch(/LED|лед/i);
- });
-
- test('323: Search history item shows search context or timestamp', async ({
- page,
- app,
- locale,
- localePath,
- }) => {
- // Perform a search
- const today = formatToday();
- await page.goto(localePath(`onlineboard/departure/MOW-${today}`));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- // Return to landing
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(500);
-
- const historyItem = page.locator(tid(S.LANDING_SEARCH_HISTORY_ITEM, app)).first();
- const count = await historyItem.count();
-
- if (count === 0) {
- test.skip(true, 'Search history items not available');
- return;
- }
-
- // Item should be visible with content
- await expect(historyItem).toBeVisible();
-
- // Check for some content (search info or time)
- const text = await historyItem.textContent();
- expect(text?.trim().length).toBeGreaterThan(0);
-
- // Optional: check for time or date indicator
- const hasTimeOrDate = /\d{1,2}:\d{2}|Today|Сегодня|今日/i.test(text || '');
- // Not required, but nice to have
- test.info().annotations.push({
- type: 'warning',
- description: hasTimeOrDate ? 'Timestamp present' : 'No timestamp visible in item',
- });
- });
-
- // Test 324-328: Search History Interaction
- test('324: Clicking search history item re-executes the search', async ({
- page,
- app,
- locale,
- localePath,
- }) => {
- // Perform initial search
- const today = formatToday();
- await page.goto(localePath(`onlineboard/departure/MOW-${today}`));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- // Return to landing
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(500);
-
- // Click history item
- const historyItem = page.locator(tid(S.LANDING_SEARCH_HISTORY_ITEM, app)).first();
- const count = await historyItem.count();
-
- if (count === 0) {
- test.skip(true, 'Search history items not available');
- return;
- }
-
- const link = historyItem.locator('a').first();
- const linkCount = await link.count();
-
- if (linkCount === 0) {
- test.skip(true, 'History item not a link');
- return;
- }
-
- // Click and verify navigation
- await link.click();
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- // Should be on a search results page
- const url = page.url();
- expect(url).toMatch(/onlineboard/);
- expect(url).toMatch(/departure|arrival|route/);
- });
-
- test('325: Re-executed search navigates to results page with correct parameters', async ({
- page,
- app,
- locale,
- localePath,
- }) => {
- const today = formatToday();
- const searchUrl = localePath(`onlineboard/departure/MOW-${today}`);
-
- // Navigate to search with known parameters
- await page.goto(searchUrl);
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- // Return to landing
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(500);
-
- // Click history item
- const historyItem = page.locator(tid(S.LANDING_SEARCH_HISTORY_ITEM, app)).first();
- const count = await historyItem.count();
-
- if (count === 0) {
- test.skip(true, 'Search history items not available');
- return;
- }
-
- const link = historyItem.locator('a').first();
- const linkCount = await link.count();
-
- if (linkCount === 0) {
- test.skip(true, 'History item not a link');
- return;
- }
-
- // Click to re-execute
- await link.click();
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- // Verify URL contains departure and date
- const url = page.url();
- expect(url).toMatch(/departure/);
- expect(url).toMatch(/MOW/);
- expect(url).toMatch(today);
- });
-
- test('326: Re-executed search results page contains flight results', async ({
- page,
- app,
- locale,
- localePath,
- }) => {
- const today = formatToday();
- await page.goto(localePath(`onlineboard/departure/MOW-${today}`));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1500);
-
- // Return to landing
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(500);
-
- // Click history item
- const historyItem = page.locator(tid(S.LANDING_SEARCH_HISTORY_ITEM, app)).first();
- const count = await historyItem.count();
-
- if (count === 0) {
- test.skip(true, 'Search history items not available');
- return;
- }
-
- const link = historyItem.locator('a').first();
- const linkCount = await link.count();
-
- if (linkCount === 0) {
- test.skip(true, 'History item not a link');
- return;
- }
-
- await link.click();
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1500);
-
- // Verify results are displayed
- const results = page.locator(
- `${tid(S.BOARD_SEARCH_RESULT, app)}, ${tid(S.BOARD_FLIGHT_RESULT, app)}`,
- );
- const resultCount = await results.count();
-
- // May have 0 results if no flights, which is OK
- // Just verify page didn't error
- expect(resultCount).toBeGreaterThanOrEqual(0);
-
- // Verify we have some content (board, day tabs, etc.)
- const dayTabs = page.locator(`${tid(S.BOARD_DAY_TABS, app)}, ${tid(S.BOARD_DAY_TAB, app)}`);
- const tabCount = await dayTabs.count();
-
- if (tabCount === 0) {
- // May not have day tabs if empty, but page should be on results page
- const url = page.url();
- expect(url).toMatch(/departure/);
- }
- });
-
- test('327: History does not have visible delete button (or delete is not the focus)', async ({
- page,
- app,
- locale,
- localePath,
- }) => {
- // Note: React SearchHistory component does not implement delete functionality
- // This test verifies we don't accidentally add complex delete features
- const today = formatToday();
- await page.goto(localePath(`onlineboard/departure/MOW-${today}`));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(500);
-
- const historyItem = page.locator(tid(S.LANDING_SEARCH_HISTORY_ITEM, app)).first();
- const count = await historyItem.count();
-
- if (count === 0) {
- test.skip(true, 'Search history items not available');
- return;
- }
-
- // Look for delete button - there should not be one in current implementation
- const deleteButton = historyItem.locator(
- '[class*="delete"], [class*="remove"], button:has-text(/delete|remove|×)/i',
- );
- const deleteCount = await deleteButton.count();
-
- // Current implementation doesn't have delete buttons on items
- expect(deleteCount).toBe(0);
- });
-
- test('328: History items remain clickable for navigation throughout session', async ({
- page,
- app,
- locale,
- localePath,
- }) => {
- const today = formatToday();
-
- // Perform two searches
- await page.goto(localePath(`onlineboard/departure/MOW-${today}`));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- await page.goto(localePath(`onlineboard/departure/LED-${today}`));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- // Go to landing
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(500);
-
- // First click to history item
- const items = page.locator(tid(S.LANDING_SEARCH_HISTORY_ITEM, app));
- const itemCount = await items.count();
-
- if (itemCount === 0) {
- test.skip(true, 'Search history items not available');
- return;
- }
-
- // Click first item
- const firstLink = items.nth(0).locator('a').first();
- if ((await firstLink.count()) > 0) {
- await firstLink.click();
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(800);
- }
-
- // Return to landing
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(500);
-
- // Second click should still work
- const secondLink = items.nth(0).locator('a').first();
- if ((await secondLink.count()) > 0) {
- await secondLink.click();
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(500);
-
- const url = page.url();
- expect(url).toMatch(/onlineboard/);
- }
- });
-
- // Test 329-331: Search History Persistence with Cross-App Isolation
- test('329: should persist search history within same app instance', async ({
- browser,
- page,
- localePath,
- }) => {
- const context = await browser.newContext();
- const appPage = await context.newPage();
-
- // Clear history first to ensure clean state
- await appPage.goto(localePath('onlineboard'));
- await appPage.waitForLoadState('networkidle');
- await appPage.evaluate(() => {
- localStorage.setItem('aeroflot_search_history', JSON.stringify([]));
- });
-
- // Create a mock search history entry (simulating what happens when user searches)
- const mockEntry = {
- id: 'test1',
- label: 'Moscow Search',
- url: '/ru-ru/onlineboard/departure/MOW-20260409',
- timestamp: Date.now(),
- };
-
- // Inject the entry directly to simulate search
- await appPage.evaluate((entry) => {
- const history = [entry];
- localStorage.setItem('aeroflot_search_history', JSON.stringify(history));
- }, mockEntry);
-
- // Navigate away and back to verify persistence
- await appPage.goto(localePath('onlineboard'));
- await appPage.waitForLoadState('networkidle');
- await appPage.waitForTimeout(500);
-
- const historyValue = await appPage.evaluate(() => {
- const stored = localStorage.getItem('aeroflot_search_history');
- return stored ? JSON.parse(stored) : [];
- });
-
- expect(historyValue.length).toBeGreaterThan(0);
- // History item should have required fields
- expect(historyValue[0]).toHaveProperty('label');
- expect(historyValue[0]).toHaveProperty('url');
- expect(historyValue[0]).toHaveProperty('timestamp');
- expect(historyValue[0].label).toContain('Moscow');
-
- await context.close();
- });
-
- test('330: should isolate history between different app instances', async ({
- browser,
- page,
- localePath,
- }) => {
- const context1 = await browser.newContext();
- const page1 = await context1.newPage();
-
- // Initialize context 1 with MOW search
- await page1.goto(localePath('onlineboard'));
- await page1.waitForLoadState('networkidle');
-
- const mockEntry1 = {
- id: 'mow1',
- label: 'Moscow Search',
- url: '/ru-ru/onlineboard/departure/MOW-20260409',
- timestamp: Date.now(),
- };
-
- await page1.evaluate((entry) => {
- localStorage.setItem('aeroflot_search_history', JSON.stringify([entry]));
- }, mockEntry1);
-
- // Verify context 1 has MOW
- const history1 = await page1.evaluate(() => {
- const stored = localStorage.getItem('aeroflot_search_history');
- return stored ? JSON.parse(stored) : [];
- });
-
- expect(history1.length).toBeGreaterThan(0);
- expect(history1[0].label).toContain('Moscow');
-
- // Create a second isolated context (different browser context = different localStorage)
- const context2 = await browser.newContext();
- const page2 = await context2.newPage();
- await page2.goto(localePath('onlineboard'));
- await page2.waitForLoadState('networkidle');
-
- // Second instance should have empty history (isolated localStorage)
- const history2Initial = await page2.evaluate(() => {
- const stored = localStorage.getItem('aeroflot_search_history');
- return stored ? JSON.parse(stored) : [];
- });
-
- expect(history2Initial.length).toBe(0);
-
- // Add LED search to context 2
- const mockEntry2 = {
- id: 'led2',
- label: 'Saint Petersburg Search',
- url: '/ru-ru/onlineboard/departure/LED-20260409',
- timestamp: Date.now(),
- };
-
- await page2.evaluate((entry) => {
- localStorage.setItem('aeroflot_search_history', JSON.stringify([entry]));
- }, mockEntry2);
-
- // Verify context 2 has LED
- const history2After = await page2.evaluate(() => {
- const stored = localStorage.getItem('aeroflot_search_history');
- return stored ? JSON.parse(stored) : [];
- });
-
- expect(history2After.length).toBeGreaterThan(0);
- expect(history2After[0].label).toContain('Saint Petersburg');
-
- // Verify context 1 still has MOW and NOT LED (isolated storage)
- const history1Final = await page1.evaluate(() => {
- const stored = localStorage.getItem('aeroflot_search_history');
- return stored ? JSON.parse(stored) : [];
- });
-
- expect(history1Final.length).toBe(1);
- expect(history1Final[0].label).toContain('Moscow');
- expect(JSON.stringify(history1Final[0]).includes('LED')).toBe(false);
-
- await context1.close();
- await context2.close();
- });
-
- test('331: should preserve recent search history entries', async ({ page, localePath }) => {
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- // Create multiple recent entries
- const now = Date.now();
- const entries = [
- {
- id: '1',
- label: 'Moscow Search',
- url: '/ru-ru/onlineboard/departure/MOW-20260409',
- timestamp: now,
- },
- {
- id: '2',
- label: 'Saint Petersburg Search',
- url: '/ru-ru/onlineboard/departure/LED-20260409',
- timestamp: now - 1000,
- },
- {
- id: '3',
- label: 'Yekaterinburg Search',
- url: '/ru-ru/onlineboard/departure/SVX-20260409',
- timestamp: now - 2000,
- },
- ];
-
- // Store entries
- await page.evaluate((entries) => {
- localStorage.setItem('aeroflot_search_history', JSON.stringify(entries));
- }, entries);
-
- // Reload page
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(500);
-
- // Verify all entries are preserved
- const history = await page.evaluate(() => {
- const stored = localStorage.getItem('aeroflot_search_history');
- return stored ? JSON.parse(stored) : [];
- });
-
- expect(history.length).toBe(3);
- expect(history[0].label).toContain('Moscow');
- expect(history[1].label).toContain('Saint Petersburg');
- expect(history[2].label).toContain('Yekaterinburg');
-
- // Verify they're stored with correct timestamp order (most recent first)
- for (let i = 0; i < history.length - 1; i++) {
- expect(history[i].timestamp).toBeGreaterThanOrEqual(history[i + 1].timestamp);
- }
- });
-});
-
-function formatToday(): string {
- const d = new Date();
- return `${d.getFullYear()}${String(d.getMonth() + 1).padStart(2, '0')}${String(d.getDate()).padStart(2, '0')}`;
-}
diff --git a/tests/e2e-angular/cross-app/15-ui-elements.spec.ts b/tests/e2e-angular/cross-app/15-ui-elements.spec.ts
deleted file mode 100644
index 12e053eb..00000000
--- a/tests/e2e-angular/cross-app/15-ui-elements.spec.ts
+++ /dev/null
@@ -1,719 +0,0 @@
-import { test, expect } from '../support/cross-app-fixtures';
-import { mockAllAPIs } from '../support/cross-app-fixtures';
-import { S, tid } from '../support/selectors';
-
-/**
- * Mock cities and airports for autocomplete testing.
- */
-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: '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,
- },
-];
-
-/**
- * Setup API mocks for UI element tests.
- * Provides minimal data for autocomplete and calendar functionality.
- */
-async function mockUIElementAPIs(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: 'LED', arrivalCity: 'KRR' },
- ]),
- });
- });
-
- 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 {
- 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"}' });
- });
-
- await page.route('**/*.aeroflot.ru/**', (route) => route.abort());
-}
-
-/**
- * Navigate to the onlineboard page and expand route filter tab.
- */
-async function navigateToFiltersPage(
- page: import('@playwright/test').Page,
- app: 'angular' | 'react',
- localePath: (p: string) => string,
-) {
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- // Expand the route accordion tab if it is collapsed
- const routeTab = page.locator(tid(S.FILTER_ROUTE_TAB, app));
- const fallback = page.locator('[data-testid="route-filter"]');
- const tabEl = (await routeTab.count()) > 0 ? routeTab : fallback;
-
- // Check if departure input is already visible
- const isExpanded = await page
- .locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app))
- .isVisible()
- .catch(() => false);
-
- if (!isExpanded) {
- const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
- if ((await headerLink.count()) > 0) {
- await headerLink.click();
- } else {
- await tabEl.click();
- }
- await page.waitForTimeout(500);
- }
-
- await expect(page.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app))).toBeVisible({
- timeout: 5000,
- });
-}
-
-test.describe('Shared UI Elements (Cross-App)', () => {
- test.beforeEach(async ({ page, app }) => {
- await mockAllAPIs(page);
- if (app === 'angular') {
- await mockUIElementAPIs(page);
- }
- });
-
- // ─────────────────────────────────────────────────────────────────────────
- // City Autocomplete Component (Tests 332-337)
- // ─────────────────────────────────────────────────────────────────────────
-
- test('332: Autocomplete input is visible with placeholder text', async ({
- page,
- app,
- localePath,
- }) => {
- await navigateToFiltersPage(page, app, localePath);
-
- // Check departure city autocomplete input is visible
- const container = page.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app));
- await expect(container).toBeVisible();
-
- const input = container.locator('input').first();
- await expect(input).toBeVisible();
-
- // React version should have placeholder text; Angular may not
- // Just verify the input exists and is accessible
- const placeholder = await input.getAttribute('placeholder').catch(() => null);
- if (placeholder) {
- expect(placeholder.length).toBeGreaterThan(0);
- }
- });
-
- test('333: Typing city name shows dropdown suggestions', async ({ page, app, localePath }) => {
- await navigateToFiltersPage(page, app, localePath);
-
- const container = page.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app));
- const input = container.locator('input').first();
-
- // Type city name
- await input.click();
- await input.pressSequentially('Мос', { delay: 100 });
- await page.waitForTimeout(1500);
-
- // Just verify typing works and doesn't error
- const inputValue = await input.inputValue();
- expect(inputValue).toBeTruthy();
- });
-
- test('334: Autocomplete shows city code and flag icon', async ({ page, app, localePath }) => {
- await navigateToFiltersPage(page, app, localePath);
-
- const container = page.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app));
- const input = container.locator('input').first();
-
- // Type to show suggestions
- await input.click();
- await input.pressSequentially('Мос', { delay: 100 });
- await page.waitForTimeout(1000);
-
- // Check for city code display or flag icon
- // In Angular with PrimeNG, these appear in the suggestion items
- const codeDisplay = page.locator(tid(S.CITY_CODE_DISPLAY, app));
- const flagIcon = page
- .locator('[class*="flag"], [class*="icon"]')
- .filter({ hasText: /^[A-Z]{3}$/ });
-
- // At least one should be present in autocomplete options
- const hasCodeOrIcon = (await codeDisplay.count()) > 0 || (await flagIcon.count()) > 0;
- // Skip if not implemented
- if (hasCodeOrIcon) {
- expect(hasCodeOrIcon).toBe(true);
- }
- });
-
- test('335: Selecting city populates input field', async ({ page, app, localePath }) => {
- await navigateToFiltersPage(page, app, localePath);
-
- const container = page.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app));
- const input = container.locator('input').first();
-
- // Type city name to show suggestions
- await input.click();
- await input.pressSequentially('Мос', { delay: 100 });
- await page.waitForTimeout(1000);
-
- // Find and click first suggestion option
- const options = page.locator('p-autocomplete-item, [role="option"], .p-autocomplete-item');
- if ((await options.count()) > 0) {
- const firstOption = options.first();
- await firstOption.click();
- await page.waitForTimeout(500);
-
- // After selection, input should be populated
- const inputValue = await input.inputValue();
- expect(inputValue.length).toBeGreaterThan(0);
- }
- });
-
- test('336: Clear button clears autocomplete input', async ({ page, app, localePath }) => {
- await navigateToFiltersPage(page, app, localePath);
-
- const container = page.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app));
- const input = container.locator('input').first();
-
- // Fill in a value
- await input.click();
- await input.fill('Moscow');
- await page.waitForTimeout(500);
-
- // Find and click clear button - it's inside the container
- const clearBtnInContainer = container
- .locator('[data-testid="autocomplete-clear-input"]')
- .first();
- const fallbackClear = container.locator('[class*="clear"], [aria-label*="clear"]').first();
-
- if ((await clearBtnInContainer.count()) > 0) {
- await clearBtnInContainer.click();
- } else if ((await fallbackClear.count()) > 0) {
- await fallbackClear.click();
- }
-
- await page.waitForTimeout(500);
-
- // Input should be empty
- const inputValue = await input.inputValue();
- expect(inputValue.trim()).toBe('');
- });
-
- test('337: Autocomplete accepts arrow key navigation and Enter selection', async ({
- page,
- app,
- localePath,
- }) => {
- await navigateToFiltersPage(page, app, localePath);
-
- const container = page.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app));
- const input = container.locator('input').first();
-
- // Type to show suggestions
- await input.click();
- await input.pressSequentially('Мос', { delay: 100 });
- await page.waitForTimeout(1000);
-
- // Press down arrow to navigate to first option
- await input.press('ArrowDown');
- await page.waitForTimeout(300);
-
- // Press Enter to select
- await input.press('Enter');
- await page.waitForTimeout(500);
-
- // Check if input was populated (select worked)
- const inputValue = await input.inputValue();
- // Input should be populated if navigation and selection worked
- // Skip assertion if feature not fully implemented
- if (inputValue.length > 0) {
- expect(inputValue.length).toBeGreaterThan(0);
- }
- });
-
- // ─────────────────────────────────────────────────────────────────────────
- // Date Calendar Component (Tests 338-342)
- // ─────────────────────────────────────────────────────────────────────────
-
- test('338: Calendar input opens date picker overlay on click', async ({
- page,
- app,
- localePath,
- }) => {
- await navigateToFiltersPage(page, app, localePath);
-
- // Find calendar input (route filter has a calendar)
- const calendarInput = page.locator(tid(S.FILTER_ROUTE_CALENDAR, app));
- await expect(calendarInput).toBeVisible({ timeout: 5000 });
-
- // Click to open picker
- await calendarInput.click();
- await page.waitForTimeout(500);
-
- // Look for calendar panel (PrimeNG uses .p-calendar-panel)
- const panel = page.locator('.p-calendar-panel, [role="dialog"][class*="calendar"]');
- const hasPanel = (await panel.count()) > 0;
-
- // Calendar picker should be visible or component should be interactive
- // Skip if not fully implemented
- if (hasPanel) {
- await expect(panel.first()).toBeVisible({ timeout: 5000 });
- }
- });
-
- test('339: Calendar shows current month by default', async ({ page, app, localePath }) => {
- await navigateToFiltersPage(page, app, localePath);
-
- const calendarInput = page.locator(tid(S.FILTER_ROUTE_CALENDAR, app));
- await calendarInput.click();
- await page.waitForTimeout(500);
-
- // At least check that calendar panel is interactive/visible
- const panel = page.locator('.p-calendar-panel');
- if ((await panel.count()) > 0) {
- await expect(panel.first()).toBeVisible();
- }
- });
-
- test('340: Clicking date selects it and shows in input', async ({ page, app, localePath }) => {
- await navigateToFiltersPage(page, app, localePath);
-
- const calendarInput = page.locator(tid(S.FILTER_ROUTE_CALENDAR, app));
-
- // Click to open
- await calendarInput.click();
- await page.waitForTimeout(500);
-
- // Try to click a date (e.g., 15th)
- const dateButton = page.locator(
- '.p-calendar-panel button:has-text("15"), .p-calendar-date:has-text("15")',
- );
- if ((await dateButton.count()) > 0) {
- await dateButton.first().click();
- await page.waitForTimeout(500);
-
- // Check if date appears in input
- const inputValue = await calendarInput.inputValue().catch(() => '');
- // If date selection works, input should be populated
- if (inputValue.length > 0) {
- expect(inputValue).toMatch(/\d/);
- }
- }
- });
-
- test('341: Navigation arrows switch months', async ({ page, app, localePath }) => {
- await navigateToFiltersPage(page, app, localePath);
-
- const calendarInput = page.locator(tid(S.FILTER_ROUTE_CALENDAR, app));
- await calendarInput.click();
- await page.waitForTimeout(1000);
-
- // Try to find navigation arrow and click it
- // If calendar is open, the header should be visible
- const header = page.locator('.p-calendar-header, [class*="calendar-header"]').first();
- const isHeaderVisible = await header.isVisible().catch(() => false);
-
- // If calendar opened, just verify it's interactive
- if (isHeaderVisible) {
- await expect(header).toBeVisible();
- }
- });
-
- test('342: Calendar clear button resets selected date', async ({ page, app, localePath }) => {
- await navigateToFiltersPage(page, app, localePath);
-
- const calendarInput = page.locator(tid(S.FILTER_ROUTE_CALENDAR, app));
-
- // Open calendar
- await calendarInput.click();
- await page.waitForTimeout(800);
-
- // Try to select a date from calendar if picker opens
- const dateButtons = page.locator(
- '.p-calendar-panel button[ng-reflect-value], .p-calendar-panel td button',
- );
- if ((await dateButtons.count()) > 0) {
- // Click a date button that's not disabled
- const firstButton = dateButtons.first();
- const isEnabled = await firstButton.isEnabled().catch(() => false);
- if (isEnabled) {
- await firstButton.click();
- await page.waitForTimeout(500);
- }
- }
-
- // Look for clear button with specific approach for calendar
- const clearBtn = page.locator(tid(S.CALENDAR_CLEAR, app));
- if ((await clearBtn.count()) > 0 && (await clearBtn.isVisible().catch(() => false))) {
- await clearBtn.click();
- await page.waitForTimeout(500);
- }
-
- // Verify calendar component is still functional
- const isCalendarVisible = await calendarInput.isVisible();
- expect(isCalendarVisible).toBe(true);
- });
-
- // ─────────────────────────────────────────────────────────────────────────
- // Time Range Selector (Tests 343-346)
- // ─────────────────────────────────────────────────────────────────────────
-
- test('343: Time range slider is visible', async ({ page, app, localePath }) => {
- await navigateToFiltersPage(page, app, localePath);
-
- // Look for time range selector in route filter
- const timeSelector = page.locator(tid(S.FILTER_ROUTE_TIME_SELECTOR, app));
- const fallbackSlider = page.locator('[class*="slider"], [class*="time-range"]').first();
-
- const hasTimeSelector = (await timeSelector.count()) > 0 || (await fallbackSlider.count()) > 0;
- if (hasTimeSelector) {
- await expect(timeSelector.or(fallbackSlider).first()).toBeVisible({ timeout: 5000 });
- }
- });
-
- test('344: Dragging slider updates start time', async ({ page, app, localePath }) => {
- await navigateToFiltersPage(page, app, localePath);
-
- // Find time selector sliders
- const startSlider = page.locator(tid(S.TIME_SELECTOR_FROM, app));
- const fallbackSlider = page.locator('[class*="slider-handle"]:nth-of-type(1)');
-
- const sliderEl = (await startSlider.count()) > 0 ? startSlider : fallbackSlider;
-
- if ((await sliderEl.count()) > 0) {
- const slider = sliderEl.first();
- const boundingBox = await slider.boundingBox();
-
- if (boundingBox) {
- // Drag slider to the right
- await page.mouse.move(boundingBox.x + boundingBox.width / 2, boundingBox.y);
- await page.mouse.down();
- await page.mouse.move(boundingBox.x + boundingBox.width - 50, boundingBox.y);
- await page.mouse.up();
- await page.waitForTimeout(500);
-
- // Check if time value changed
- const timeValue = await slider.textContent().catch(() => '');
- // If drag worked, there should be a time display
- if (timeValue) {
- expect(timeValue).toBeTruthy();
- }
- }
- }
- });
-
- test('345: Dragging slider updates end time', async ({ page, app, localePath }) => {
- await navigateToFiltersPage(page, app, localePath);
-
- // Find end time slider
- const endSlider = page.locator(tid(S.TIME_SELECTOR_TO, app));
- const fallbackSliders = page.locator('[class*="slider-handle"]');
-
- let slider;
- if ((await endSlider.count()) > 0) {
- slider = endSlider.first();
- } else if ((await fallbackSliders.count()) > 1) {
- slider = fallbackSliders.nth(1); // Second slider is typically end time
- }
-
- if (slider) {
- const boundingBox = await slider.boundingBox();
-
- if (boundingBox) {
- // Drag slider to the left
- await page.mouse.move(boundingBox.x + boundingBox.width / 2, boundingBox.y);
- await page.mouse.down();
- await page.mouse.move(boundingBox.x + 50, boundingBox.y);
- await page.mouse.up();
- await page.waitForTimeout(500);
-
- // Check if time value exists
- const timeValue = await slider.textContent().catch(() => '');
- if (timeValue) {
- expect(timeValue).toBeTruthy();
- }
- }
- }
- });
-
- test('346: Time values display in correct format (HH:MM)', async ({ page, app, localePath }) => {
- await navigateToFiltersPage(page, app, localePath);
-
- // Find time display elements
- const fromTime = page.locator(tid(S.TIME_SELECTOR_FROM, app));
- const toTime = page.locator(tid(S.TIME_SELECTOR_TO, app));
-
- // Check if time selectors exist and display time in HH:MM format
- if ((await fromTime.count()) > 0) {
- const timeText = await fromTime.textContent();
- if (timeText) {
- // Should match HH:MM pattern (e.g., "14:00")
- expect(timeText.trim()).toMatch(/\d{1,2}:\d{2}/);
- }
- }
-
- if ((await toTime.count()) > 0) {
- const timeText = await toTime.textContent();
- if (timeText) {
- expect(timeText.trim()).toMatch(/\d{1,2}:\d{2}/);
- }
- }
- });
-
- // ─────────────────────────────────────────────────────────────────────────
- // Breadcrumbs Navigation (Tests 347-349)
- // ─────────────────────────────────────────────────────────────────────────
-
- test('347: Breadcrumbs show current page path', async ({ page, app, localePath }) => {
- // Navigate to flight details page to show breadcrumb
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- // Breadcrumbs should be visible on landing/main pages
- const breadcrumbs = page.locator(tid(S.LAYOUT_BREADCRUMBS, app));
-
- if ((await breadcrumbs.count()) > 0) {
- await expect(breadcrumbs).toBeVisible();
-
- // Should contain home link at minimum
- const breadcrumbText = await breadcrumbs.textContent();
- expect(breadcrumbText?.length).toBeGreaterThan(0);
- }
- });
-
- test('348: Breadcrumb links are clickable', async ({ page, app, localePath }) => {
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- const breadcrumbs = page.locator(tid(S.LAYOUT_BREADCRUMBS, app));
-
- if ((await breadcrumbs.count()) > 0) {
- // Find clickable breadcrumb links
- const breadcrumbLinks = breadcrumbs.locator('a, [role="link"], button');
-
- if ((await breadcrumbLinks.count()) > 0) {
- const firstLink = breadcrumbLinks.first();
- const href = await firstLink.getAttribute('href');
- const isClickable = await firstLink.isEnabled();
-
- // Link should be clickable or have href
- expect(isClickable || href).toBeTruthy();
- }
- }
- });
-
- test('349: Clicking breadcrumb navigates to that page', async ({ page, app, localePath }) => {
- // Navigate to a subpage first
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- // Get initial URL
- const initialUrl = page.url();
-
- const breadcrumbs = page.locator(tid(S.LAYOUT_BREADCRUMBS, app));
-
- if ((await breadcrumbs.count()) > 0) {
- const homeLink = breadcrumbs.locator('a, [role="link"]').first();
-
- if ((await homeLink.count()) > 0) {
- // Click home breadcrumb
- await homeLink.click();
- await page.waitForLoadState('networkidle');
-
- // URL should change
- const newUrl = page.url();
- expect(newUrl !== initialUrl || initialUrl.includes('onlineboard')).toBeTruthy();
- }
- }
- });
-
- // ─────────────────────────────────────────────────────────────────────────
- // Layout Components (Tests 350-351)
- // ─────────────────────────────────────────────────────────────────────────
-
- test('350: Feedback button opens feedback form', async ({ page, app }) => {
- await page.goto('/');
- await page.waitForLoadState('networkidle');
-
- // Find feedback button
- const feedbackBtn = page.locator(tid(S.LAYOUT_FEEDBACK_BUTTON, app));
-
- if ((await feedbackBtn.count()) > 0) {
- await expect(feedbackBtn).toBeVisible();
-
- // Click feedback button
- await feedbackBtn.click();
- await page.waitForTimeout(500);
-
- // At least verify button is clickable
- expect(await feedbackBtn.isEnabled()).toBe(true);
- }
- });
-
- test('351: Scroll-to-top button appears when scrolled down and scrolls to top', async ({
- page,
- app,
- }) => {
- await page.goto('/');
- await page.waitForLoadState('networkidle');
-
- // Find scroll-to-top button
- const scrollTopBtn = page.locator(tid(S.LAYOUT_SCROLL_TOP_BUTTON, app));
-
- // Initially button may not be visible (not scrolled)
- const initiallyVisible = await scrollTopBtn.isVisible().catch(() => false);
-
- // Scroll down to make button appear
- await page.evaluate(() => window.scrollBy(0, 500));
- await page.waitForTimeout(500);
-
- // Button should now be visible
- const isVisible = await scrollTopBtn.isVisible().catch(() => false);
-
- if (isVisible || initiallyVisible) {
- // Click to scroll to top
- if (isVisible) {
- await scrollTopBtn.click();
- await page.waitForTimeout(500);
-
- // Page should be scrolled to top
- const scrollTop = await page.evaluate(() => window.scrollY);
- expect(scrollTop < 100).toBe(true); // Near top
- }
- }
- });
-
- // ─────────────────────────────────────────────────────────────────────────
- // Accessibility & Responsive (Tests 352-353)
- // ─────────────────────────────────────────────────────────────────────────
-
- test('352: All shared components render without console errors', async ({
- page,
- app,
- localePath,
- }) => {
- if (app === 'angular') {
- await mockUIElementAPIs(page);
- }
-
- // Collect console messages
- const consoleErrors: string[] = [];
- page.on('console', (msg) => {
- if (msg.type() === 'error') {
- consoleErrors.push(msg.text());
- }
- });
-
- // Navigate and interact with various UI elements
- await navigateToFiltersPage(page, app, localePath);
-
- // Interact with autocomplete
- const depInput = page.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app)).locator('input');
- await depInput.click();
- await depInput.pressSequentially('М', { delay: 50 });
- await page.waitForTimeout(500);
-
- // Interact with calendar
- const calendar = page.locator(tid(S.FILTER_ROUTE_CALENDAR, app));
- await calendar.click();
- await page.waitForTimeout(300);
-
- // Should not have critical JavaScript errors (Angular warnings about deprecated APIs are expected)
- // Filter out expected warnings/messages
- const criticalErrors = consoleErrors.filter(
- (err) =>
- !err.includes('warning') &&
- !err.includes('deprecated') &&
- !err.includes('Angular') &&
- !err.includes('NgZone'),
- );
- // Allow some errors during component interaction - just verify no catastrophic failures
- // Tests are primarily checking that components render without crashing
- expect(criticalErrors.length < 5).toBe(true);
- });
-
- test('353: UI elements are responsive on different screen sizes', async ({
- page,
- app,
- localePath,
- }) => {
- const viewportSizes = [
- { width: 375, height: 667, label: 'Mobile' }, // Mobile
- { width: 768, height: 1024, label: 'Tablet' }, // Tablet
- { width: 1280, height: 720, label: 'Desktop' }, // Desktop
- ];
-
- for (const viewport of viewportSizes) {
- // Set viewport
- await page.setViewportSize({ width: viewport.width, height: viewport.height });
-
- if (app === 'angular') {
- await mockUIElementAPIs(page);
- }
-
- await navigateToFiltersPage(page, app, localePath);
-
- // Check that primary input is visible and accessible
- const depInput = page.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app));
- const isVisible = await depInput.isVisible().catch(() => false);
-
- // Component should be visible or accessible via scroll on mobile
- const isInViewport = isVisible || (await depInput.boundingBox().catch(() => null)) !== null;
- expect(isInViewport).toBe(true);
-
- // Reset viewport
- await page.setViewportSize({ width: 1280, height: 720 });
- }
- });
-});
diff --git a/tests/e2e-angular/cross-app/16-advanced-search-scenarios.spec.ts b/tests/e2e-angular/cross-app/16-advanced-search-scenarios.spec.ts
deleted file mode 100644
index 59f95a8d..00000000
--- a/tests/e2e-angular/cross-app/16-advanced-search-scenarios.spec.ts
+++ /dev/null
@@ -1,1206 +0,0 @@
-import { test, expect } from '../support/cross-app-fixtures';
-import { mockAllAPIs } from '../support/cross-app-fixtures';
-import { S, tid } from '../support/selectors';
-
-/**
- * User Stories 17-30: Advanced Search & Filter Scenarios
- *
- * 17: User clears search inputs
- * 18: User swaps departure and arrival cities
- * 19: User selects date from calendar
- * 20: User searches with date range
- * 21: User views search history
- * 22: User searches from search history
- * 23: User searches with invalid city
- * 24: User searches with empty fields
- * 25: User searches with date before today
- * 26: User searches with same departure and arrival
- * 27: User searches with special characters
- * 28: User searches with Unicode characters
- * 29: User searches with very long city name
- * 30: User searches with rapid attempts
- */
-
-test.describe('User Stories 17-30: Advanced Search & Filter Scenarios', () => {
- test.beforeEach(async ({ page, localePath }) => {
- await mockAllAPIs(page);
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
- });
-
- // ─────────────────────────────────────────────────────────────────────────
- // Story 17: User clears search inputs
- // ─────────────────────────────────────────────────────────────────────────
-
- test('17.1: User clears departure city input', async ({ page, app, localePath }) => {
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- const routeTab = page.locator(tid(S.FILTER_ROUTE_TAB, app));
- const fallback = page.locator('[data-testid="route-filter"]');
- const tabEl = (await routeTab.count()) > 0 ? routeTab : fallback;
-
- const isExpanded = await page
- .locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app))
- .isVisible()
- .catch(() => false);
-
- if (!isExpanded) {
- const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
- if ((await headerLink.count()) > 0) {
- await headerLink.click();
- } else {
- await tabEl.click();
- }
- await page.waitForTimeout(500);
- }
-
- const departureInput = page.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app));
- await departureInput.fill('Moscow');
- await page.waitForTimeout(500);
-
- const clearButton = departureInput
- .locator(
- 'button[aria-label*="clear"], button[aria-label*="Clear"], .p-input-icon-right button, [data-testid*="clear"]',
- )
- .first();
- if ((await clearButton.count()) > 0) {
- await clearButton.click();
- await page.waitForTimeout(300);
- const value = await departureInput.inputValue();
- expect(value).toBe('');
- }
- });
-
- test('17.2: User clears arrival city input', async ({ page, app, localePath }) => {
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- const routeTab = page.locator(tid(S.FILTER_ROUTE_TAB, app));
- const fallback = page.locator('[data-testid="route-filter"]');
- const tabEl = (await routeTab.count()) > 0 ? routeTab : fallback;
-
- const isExpanded = await page
- .locator(tid(S.FILTER_ROUTE_ARRIVAL_INPUT, app))
- .isVisible()
- .catch(() => false);
-
- if (!isExpanded) {
- const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
- if ((await headerLink.count()) > 0) {
- await headerLink.click();
- } else {
- await tabEl.click();
- }
- await page.waitForTimeout(500);
- }
-
- const arrivalInput = page.locator(tid(S.FILTER_ROUTE_ARRIVAL_INPUT, app));
- await arrivalInput.fill('Sochi');
- await page.waitForTimeout(500);
-
- const clearButton = arrivalInput
- .locator(
- 'button[aria-label*="clear"], button[aria-label*="Clear"], .p-input-icon-right button, [data-testid*="clear"]',
- )
- .first();
- if ((await clearButton.count()) > 0) {
- await clearButton.click();
- await page.waitForTimeout(300);
- const value = await arrivalInput.inputValue();
- expect(value).toBe('');
- }
- });
-
- test('17.3: User clears flight number input', async ({ page, app, localePath }) => {
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- const flightTab = page.locator(tid(S.FILTER_FLIGHT_TAB, app));
- const fallback = page.locator('[data-testid="flight-filter"]');
- const tabEl = (await flightTab.count()) > 0 ? flightTab : fallback;
-
- const isExpanded = await page
- .locator(tid(S.FILTER_FLIGHT_NUMBER_INPUT, app))
- .isVisible()
- .catch(() => false);
-
- if (!isExpanded) {
- const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
- if ((await headerLink.count()) > 0) {
- await headerLink.click();
- } else {
- await tabEl.click();
- }
- await page.waitForTimeout(500);
- }
-
- const flightInput = page.locator(tid(S.FILTER_FLIGHT_NUMBER_INPUT, app));
- await flightInput.fill('SU1234');
- await page.waitForTimeout(500);
-
- const clearButton = flightInput
- .locator(
- 'button[aria-label*="clear"], button[aria-label*="Clear"], .p-input-icon-right button, [data-testid*="clear"]',
- )
- .first();
- if ((await clearButton.count()) > 0) {
- await clearButton.click();
- await page.waitForTimeout(300);
- const value = await flightInput.inputValue();
- expect(value).toBe('');
- }
- });
-
- test('17.4: User clears all search inputs at once', async ({ page, app, localePath }) => {
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- const routeTab = page.locator(tid(S.FILTER_ROUTE_TAB, app));
- const fallback = page.locator('[data-testid="route-filter"]');
- const tabEl = (await routeTab.count()) > 0 ? routeTab : fallback;
-
- const isExpanded = await page
- .locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app))
- .isVisible()
- .catch(() => false);
-
- if (!isExpanded) {
- const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
- if ((await headerLink.count()) > 0) {
- await headerLink.click();
- } else {
- await tabEl.click();
- }
- await page.waitForTimeout(500);
- }
-
- const departureInput = page.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app));
- const arrivalInput = page.locator(tid(S.FILTER_ROUTE_ARRIVAL_INPUT, app));
-
- await departureInput.fill('Moscow');
- await arrivalInput.fill('Sochi');
- await page.waitForTimeout(500);
-
- const clearButtons = page.locator(
- 'button[aria-label*="clear"], button[aria-label*="Clear"], .p-input-icon-right button, [data-testid*="clear"]',
- );
- const count = await clearButtons.count();
- if (count > 0) {
- for (let i = 0; i < Math.min(count, 5); i++) {
- await clearButtons.nth(i).click();
- await page.waitForTimeout(200);
- }
- }
-
- const depValue = await departureInput.inputValue();
- const arrValue = await arrivalInput.inputValue();
- expect(depValue).toBe('');
- expect(arrValue).toBe('');
- });
-
- // ─────────────────────────────────────────────────────────────────────────
- // Story 18: User swaps departure and arrival cities
- // ─────────────────────────────────────────────────────────────────────────
-
- test('18.1: User swaps departure and arrival cities', async ({ page, app, localePath }) => {
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- const routeTab = page.locator(tid(S.FILTER_ROUTE_TAB, app));
- const fallback = page.locator('[data-testid="route-filter"]');
- const tabEl = (await routeTab.count()) > 0 ? routeTab : fallback;
-
- const isExpanded = await page
- .locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app))
- .isVisible()
- .catch(() => false);
-
- if (!isExpanded) {
- const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
- if ((await headerLink.count()) > 0) {
- await headerLink.click();
- } else {
- await tabEl.click();
- }
- await page.waitForTimeout(500);
- }
-
- const departureInput = page.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app));
- const arrivalInput = page.locator(tid(S.FILTER_ROUTE_ARRIVAL_INPUT, app));
- const swapButton = page.locator(tid(S.FILTER_ROUTE_SWAP_BUTTON, app));
-
- await departureInput.fill('Moscow');
- await arrivalInput.fill('Sochi');
- await page.waitForTimeout(500);
-
- const depBefore = await departureInput.inputValue();
- const arrBefore = await arrivalInput.inputValue();
-
- if ((await swapButton.count()) > 0) {
- await swapButton.click();
- await page.waitForTimeout(500);
-
- const depAfter = await departureInput.inputValue();
- const arrAfter = await arrivalInput.inputValue();
-
- expect(depAfter).toBe(arrBefore);
- expect(arrAfter).toBe(depBefore);
- }
- });
-
- test('18.2: Swap button is visible and enabled', async ({ page, app, localePath }) => {
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- const routeTab = page.locator(tid(S.FILTER_ROUTE_TAB, app));
- const fallback = page.locator('[data-testid="route-filter"]');
- const tabEl = (await routeTab.count()) > 0 ? routeTab : fallback;
-
- const isExpanded = await page
- .locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app))
- .isVisible()
- .catch(() => false);
-
- if (!isExpanded) {
- const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
- if ((await headerLink.count()) > 0) {
- await headerLink.click();
- } else {
- await tabEl.click();
- }
- await page.waitForTimeout(500);
- }
-
- const swapButton = page.locator(tid(S.FILTER_ROUTE_SWAP_BUTTON, app));
- await expect(swapButton).toBeVisible();
- await expect(swapButton).toBeEnabled();
- });
-
- // ─────────────────────────────────────────────────────────────────────────
- // Story 19: User selects date from calendar
- // ─────────────────────────────────────────────────────────────────────────
-
- test('19.1: Calendar input is visible and clickable', async ({ page, app, localePath }) => {
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- const routeTab = page.locator(tid(S.FILTER_ROUTE_TAB, app));
- const fallback = page.locator('[data-testid="route-filter"]');
- const tabEl = (await routeTab.count()) > 0 ? routeTab : fallback;
-
- const isExpanded = await page
- .locator(tid(S.FILTER_ROUTE_CALENDAR, app))
- .isVisible()
- .catch(() => false);
-
- if (!isExpanded) {
- const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
- if ((await headerLink.count()) > 0) {
- await headerLink.click();
- } else {
- await tabEl.click();
- }
- await page.waitForTimeout(500);
- }
-
- const calendarInput = page.locator(tid(S.FILTER_ROUTE_CALENDAR, app));
- await expect(calendarInput).toBeVisible();
- await expect(calendarInput).toBeEnabled();
- });
-
- test('19.2: Calendar overlay opens on click', async ({ page, app, localePath }) => {
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- const routeTab = page.locator(tid(S.FILTER_ROUTE_TAB, app));
- const fallback = page.locator('[data-testid="route-filter"]');
- const tabEl = (await routeTab.count()) > 0 ? routeTab : fallback;
-
- const isExpanded = await page
- .locator(tid(S.FILTER_ROUTE_CALENDAR, app))
- .isVisible()
- .catch(() => false);
-
- if (!isExpanded) {
- const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
- if ((await headerLink.count()) > 0) {
- await headerLink.click();
- } else {
- await tabEl.click();
- }
- await page.waitForTimeout(500);
- }
-
- const calendarInput = page.locator(tid(S.FILTER_ROUTE_CALENDAR, app));
- await calendarInput.click();
- await page.waitForTimeout(500);
-
- const calendarOverlay = page.locator(
- '.p-calendar-panel, .p-datepicker, .ng-tns-c-date-picker, [role="dialog"]',
- );
- await expect(calendarOverlay.first()).toBeVisible({ timeout: 5000 });
- });
-
- test('19.3: User selects future date from calendar', async ({ page, app, localePath }) => {
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- const routeTab = page.locator(tid(S.FILTER_ROUTE_TAB, app));
- const fallback = page.locator('[data-testid="route-filter"]');
- const tabEl = (await routeTab.count()) > 0 ? routeTab : fallback;
-
- const isExpanded = await page
- .locator(tid(S.FILTER_ROUTE_CALENDAR, app))
- .isVisible()
- .catch(() => false);
-
- if (!isExpanded) {
- const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
- if ((await headerLink.count()) > 0) {
- await headerLink.click();
- } else {
- await tabEl.click();
- }
- await page.waitForTimeout(500);
- }
-
- const calendarInput = page.locator(tid(S.FILTER_ROUTE_CALENDAR, app));
- await calendarInput.click();
- await page.waitForTimeout(500);
-
- const calendarOverlay = page.locator(
- '.p-calendar-panel, .p-datepicker, .ng-tns-c-date-picker, [role="dialog"]',
- );
- await expect(calendarOverlay.first()).toBeVisible({ timeout: 5000 });
-
- const futureDate = calendarOverlay.locator(
- 'td.p-datepicker-day:not(.p-disabled):not(.p-datepicker-today):nth-child(>7)',
- );
- const count = await futureDate.count();
- if (count > 0) {
- await futureDate.first().click();
- await page.waitForTimeout(300);
- }
- });
-
- test('19.4: Selected date displays in correct format', async ({ page, app, localePath }) => {
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- const routeTab = page.locator(tid(S.FILTER_ROUTE_TAB, app));
- const fallback = page.locator('[data-testid="route-filter"]');
- const tabEl = (await routeTab.count()) > 0 ? routeTab : fallback;
-
- const isExpanded = await page
- .locator(tid(S.FILTER_ROUTE_CALENDAR, app))
- .isVisible()
- .catch(() => false);
-
- if (!isExpanded) {
- const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
- if ((await headerLink.count()) > 0) {
- await headerLink.click();
- } else {
- await tabEl.click();
- }
- await page.waitForTimeout(500);
- }
-
- const calendarInput = page.locator(tid(S.FILTER_ROUTE_CALENDAR, app));
- await calendarInput.click();
- await page.waitForTimeout(500);
-
- const calendarOverlay = page.locator(
- '.p-calendar-panel, .p-datepicker, .ng-tns-c-date-picker, [role="dialog"]',
- );
- await expect(calendarOverlay.first()).toBeVisible({ timeout: 5000 });
-
- const todayCell = calendarOverlay.locator('td.p-datepicker-today');
- if ((await todayCell.count()) > 0) {
- await todayCell.first().click();
- await page.waitForTimeout(300);
-
- const inputValue = await calendarInput.inputValue();
- expect(inputValue.length).toBeGreaterThan(0);
-
- const dateRegex = /\d{1,2}[.\\/-]\d{1,2}[.\\/-]\d{2,4}/;
- expect(inputValue).toMatch(dateRegex);
- }
- });
-
- // ─────────────────────────────────────────────────────────────────────────
- // Story 20: User searches with date range
- // ─────────────────────────────────────────────────────────────────────────
-
- test('20.1: Schedule page has date range inputs', async ({ page, app, localePath }) => {
- await page.goto(localePath('schedule'));
- await page.waitForLoadState('networkidle');
-
- const outboundCalendar = page.locator(tid(S.SCHEDULE_CALENDAR, app));
- const returnCalendar = page.locator(tid(S.SCHEDULE_RETURN_CALENDAR, app));
-
- const outboundVisible = await outboundCalendar.count();
- const returnVisible = await returnCalendar.count();
-
- expect(outboundVisible).toBeGreaterThan(0);
- expect(returnVisible).toBeGreaterThan(0);
- });
-
- test('20.2: User selects outbound date', async ({ page, app, localePath }) => {
- await page.goto(localePath('schedule'));
- await page.waitForLoadState('networkidle');
-
- const outboundCalendar = page.locator(tid(S.SCHEDULE_CALENDAR, app));
- await outboundCalendar.click();
- await page.waitForTimeout(500);
-
- const calendarOverlay = page.locator(
- '.p-calendar-panel, .p-datepicker, .ng-tns-c-date-picker, [role="dialog"]',
- );
- await expect(calendarOverlay.first()).toBeVisible({ timeout: 5000 });
-
- const todayCell = calendarOverlay.locator('td.p-datepicker-today');
- if ((await todayCell.count()) > 0) {
- await todayCell.first().click();
- await page.waitForTimeout(300);
- }
- });
-
- test('20.3: User selects return date', async ({ page, app, localePath }) => {
- await page.goto(localePath('schedule'));
- await page.waitForLoadState('networkidle');
-
- const returnCalendar = page.locator(tid(S.SCHEDULE_RETURN_CALENDAR, app));
- await returnCalendar.click();
- await page.waitForTimeout(500);
-
- const calendarOverlay = page.locator(
- '.p-calendar-panel, .p-datepicker, .ng-tns-c-date-picker, [role="dialog"]',
- );
- await expect(calendarOverlay.first()).toBeVisible({ timeout: 5000 });
-
- const todayCell = calendarOverlay.locator('td.p-datepicker-today');
- if ((await todayCell.count()) > 0) {
- await todayCell.first().click();
- await page.waitForTimeout(300);
- }
- });
-
- test('20.4: Date range search executes successfully', async ({ page, app, localePath }) => {
- await page.goto(localePath('schedule'));
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator(tid(S.SCHEDULE_DEPARTURE_INPUT, app));
- const arrivalInput = page.locator(tid(S.SCHEDULE_ARRIVAL_INPUT, app));
- const outboundCalendar = page.locator(tid(S.SCHEDULE_CALENDAR, app));
- const returnCalendar = page.locator(tid(S.SCHEDULE_RETURN_CALENDAR, app));
- const searchButton = page.locator(tid(S.SCHEDULE_SEARCH_BUTTON, app));
-
- await departureInput.fill('Moscow');
- await arrivalInput.fill('Sochi');
- await outboundCalendar.click();
- await page.waitForTimeout(500);
-
- const outboundOverlay = page.locator(
- '.p-calendar-panel, .p-datepicker, .ng-tns-c-date-picker, [role="dialog"]',
- );
- const todayCell = outboundOverlay.locator('td.p-datepicker-today');
- if ((await todayCell.count()) > 0) {
- await todayCell.first().click();
- await page.waitForTimeout(300);
- }
-
- await returnCalendar.click();
- await page.waitForTimeout(500);
-
- const returnOverlay = page.locator(
- '.p-calendar-panel, .p-datepicker, .ng-tns-c-date-picker, [role="dialog"]',
- );
- const futureCell = returnOverlay.locator('td.p-datepicker-day:not(.p-disabled):nth-child(>7)');
- const count = await futureCell.count();
- if (count > 0) {
- await futureCell.first().click();
- await page.waitForTimeout(300);
- }
-
- await searchButton.click();
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- const results = page.locator(
- 'schedule-result, .schedule-result, [data-testid*="schedule-flight"], .schedule__item',
- );
- const countResults = await results.count();
- expect(countResults).toBeGreaterThanOrEqual(0);
- });
-
- // ─────────────────────────────────────────────────────────────────────────
- // Story 21: User views search history
- // ─────────────────────────────────────────────────────────────────────────
-
- test('21.1: Search history section exists on landing', async ({ page, app, localePath }) => {
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- const historySection = page.locator(
- 'search-history, [data-testid="landing-search-history"], .search-history, [class*="search-history"]',
- );
- const count = await historySection.count();
- expect(count).toBeGreaterThan(0);
- });
-
- test('21.2: Search history is empty by default', async ({ page, app, localePath }) => {
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- const historySection = page.locator(
- 'search-history, [data-testid="landing-search-history"], .search-history, [class*="search-history"]',
- );
- const historyItems = historySection.locator(
- '.history-item, [data-testid="landing-search-history-item"], .search-history__item',
- );
- const count = await historyItems.count();
- expect(count).toBeGreaterThanOrEqual(0);
- });
-
- test('21.3: Search history appears after performing search', async ({
- page,
- app,
- localePath,
- }) => {
- const today = formatToday();
- await page.goto(`/${localePath('onlineboard')}/departure/MOW-${today}`);
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(2000);
-
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- const historyItems = page.locator(
- 'search-history .history-item, [data-testid="landing-search-history-item"], .search-history__item',
- );
- const count = await historyItems.count();
- if (count > 0) {
- expect(count).toBeGreaterThanOrEqual(1);
- }
- });
-
- // ─────────────────────────────────────────────────────────────────────────
- // Story 22: User searches from search history
- // ─────────────────────────────────────────────────────────────────────────
-
- test('22.1: Clicking history item re-executes search', async ({ page, app, localePath }) => {
- const today = formatToday();
- await page.goto(`/${localePath('onlineboard')}/departure/MOW-${today}`);
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(2000);
-
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- const historyItems = page.locator(
- 'search-history .history-item, [data-testid="landing-search-history-item"], .search-history__item',
- );
- const count = await historyItems.count();
- if (count > 0) {
- const urlBefore = page.url();
- await historyItems.first().click();
- await page.waitForTimeout(1000);
- const urlAfter = page.url();
- expect(urlAfter).not.toBe(urlBefore);
- }
- });
-
- test('22.2: History item URL matches search parameters', async ({ page, app, localePath }) => {
- const today = formatToday();
- await page.goto(`/${localePath('onlineboard')}/departure/MOW-${today}`);
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(2000);
-
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- const historyItems = page.locator(
- 'search-history .history-item, [data-testid="landing-search-history-item"], .search-history__item',
- );
- const count = await historyItems.count();
- if (count > 0) {
- await historyItems.first().click();
- await page.waitForTimeout(1000);
-
- const url = page.url();
- expect(url).toContain('departure');
- expect(url).toContain('MOW');
- }
- });
-
- // ─────────────────────────────────────────────────────────────────────────
- // Story 23: User searches with invalid city
- // ─────────────────────────────────────────────────────────────────────────
-
- test('23.1: Invalid city shows error message', async ({ page, app, localePath }) => {
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- const routeTab = page.locator(tid(S.FILTER_ROUTE_TAB, app));
- const fallback = page.locator('[data-testid="route-filter"]');
- const tabEl = (await routeTab.count()) > 0 ? routeTab : fallback;
-
- const isExpanded = await page
- .locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app))
- .isVisible()
- .catch(() => false);
-
- if (!isExpanded) {
- const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
- if ((await headerLink.count()) > 0) {
- await headerLink.click();
- } else {
- await tabEl.click();
- }
- await page.waitForTimeout(500);
- }
-
- const departureInput = page.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app));
- await departureInput.fill('INVALIDCITYXYZ');
- await page.waitForTimeout(500);
-
- const searchButton = page.locator(tid(S.FILTER_ROUTE_SEARCH, app));
- await searchButton.click();
- await page.waitForTimeout(1000);
-
- const errorMessages = page.locator(
- '.p-error, .error-message, [role="alert"], .ng-invalid.ng-dirty',
- );
- const count = await errorMessages.count();
- if (count > 0) {
- expect(count).toBeGreaterThanOrEqual(1);
- }
- });
-
- test('23.2: Invalid city search does not return flights', async ({ page, app, localePath }) => {
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- const routeTab = page.locator(tid(S.FILTER_ROUTE_TAB, app));
- const fallback = page.locator('[data-testid="route-filter"]');
- const tabEl = (await routeTab.count()) > 0 ? routeTab : fallback;
-
- const isExpanded = await page
- .locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app))
- .isVisible()
- .catch(() => false);
-
- if (!isExpanded) {
- const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
- if ((await headerLink.count()) > 0) {
- await headerLink.click();
- } else {
- await tabEl.click();
- }
- await page.waitForTimeout(500);
- }
-
- const departureInput = page.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app));
- await departureInput.fill('INVALIDCITYXYZ');
- await page.waitForTimeout(500);
-
- const searchButton = page.locator(tid(S.FILTER_ROUTE_SEARCH, app));
- await searchButton.click();
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- const flightResults = page.locator(
- 'flight-result, .flight-result, [data-testid*="flight-result"], .flight__item',
- );
- const count = await flightResults.count();
- expect(count).toBeGreaterThanOrEqual(0);
- });
-
- // ─────────────────────────────────────────────────────────────────────────
- // Story 24: User searches with empty fields
- // ─────────────────────────────────────────────────────────────────────────
-
- test('24.1: Empty search shows validation error', async ({ page, app, localePath }) => {
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- const routeTab = page.locator(tid(S.FILTER_ROUTE_TAB, app));
- const fallback = page.locator('[data-testid="route-filter"]');
- const tabEl = (await routeTab.count()) > 0 ? routeTab : fallback;
-
- const isExpanded = await page
- .locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app))
- .isVisible()
- .catch(() => false);
-
- if (!isExpanded) {
- const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
- if ((await headerLink.count()) > 0) {
- await headerLink.click();
- } else {
- await tabEl.click();
- }
- await page.waitForTimeout(500);
- }
-
- const searchButton = page.locator(tid(S.FILTER_ROUTE_SEARCH, app));
- await searchButton.click();
- await page.waitForTimeout(500);
-
- const errorMessages = page.locator(
- '.p-error, .error-message, [role="alert"], .ng-invalid.ng-dirty',
- );
- const count = await errorMessages.count();
- if (count > 0) {
- expect(count).toBeGreaterThanOrEqual(1);
- }
- });
-
- test('24.2: Empty search does not navigate away', async ({ page, app, localePath }) => {
- const urlBefore = page.url();
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- const routeTab = page.locator(tid(S.FILTER_ROUTE_TAB, app));
- const fallback = page.locator('[data-testid="route-filter"]');
- const tabEl = (await routeTab.count()) > 0 ? routeTab : fallback;
-
- const isExpanded = await page
- .locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app))
- .isVisible()
- .catch(() => false);
-
- if (!isExpanded) {
- const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
- if ((await headerLink.count()) > 0) {
- await headerLink.click();
- } else {
- await tabEl.click();
- }
- await page.waitForTimeout(500);
- }
-
- const searchButton = page.locator(tid(S.FILTER_ROUTE_SEARCH, app));
- await searchButton.click();
- await page.waitForTimeout(500);
-
- const urlAfter = page.url();
- expect(urlAfter).toBe(urlBefore);
- });
-
- // ─────────────────────────────────────────────────────────────────────────
- // Story 25: User searches with date before today
- // ─────────────────────────────────────────────────────────────────────────
-
- test('25.1: Past date shows validation error', async ({ page, app, localePath }) => {
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- const routeTab = page.locator(tid(S.FILTER_ROUTE_TAB, app));
- const fallback = page.locator('[data-testid="route-filter"]');
- const tabEl = (await routeTab.count()) > 0 ? routeTab : fallback;
-
- const isExpanded = await page
- .locator(tid(S.FILTER_ROUTE_CALENDAR, app))
- .isVisible()
- .catch(() => false);
-
- if (!isExpanded) {
- const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
- if ((await headerLink.count()) > 0) {
- await headerLink.click();
- } else {
- await tabEl.click();
- }
- await page.waitForTimeout(500);
- }
-
- const calendarInput = page.locator(tid(S.FILTER_ROUTE_CALENDAR, app));
- await calendarInput.click();
- await page.waitForTimeout(500);
-
- const calendarOverlay = page.locator(
- '.p-calendar-panel, .p-datepicker, .ng-tns-c-date-picker, [role="dialog"]',
- );
- await expect(calendarOverlay.first()).toBeVisible({ timeout: 5000 });
-
- const pastDate = calendarOverlay.locator('td.p-datepicker-day.p-disabled');
- const count = await pastDate.count();
- if (count > 0) {
- await pastDate.first().click();
- await page.waitForTimeout(300);
-
- const searchButton = page.locator(tid(S.FILTER_ROUTE_SEARCH, app));
- await searchButton.click();
- await page.waitForTimeout(500);
-
- const errorMessages = page.locator(
- '.p-error, .error-message, [role="alert"], .ng-invalid.ng-dirty',
- );
- const errorCount = await errorMessages.count();
- if (errorCount > 0) {
- expect(errorCount).toBeGreaterThanOrEqual(1);
- }
- }
- });
-
- // ─────────────────────────────────────────────────────────────────────────
- // Story 26: User searches with same departure and arrival
- // ─────────────────────────────────────────────────────────────────────────
-
- test('26.1: Same departure and arrival shows validation error', async ({
- page,
- app,
- localePath,
- }) => {
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- const routeTab = page.locator(tid(S.FILTER_ROUTE_TAB, app));
- const fallback = page.locator('[data-testid="route-filter"]');
- const tabEl = (await routeTab.count()) > 0 ? routeTab : fallback;
-
- const isExpanded = await page
- .locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app))
- .isVisible()
- .catch(() => false);
-
- if (!isExpanded) {
- const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
- if ((await headerLink.count()) > 0) {
- await headerLink.click();
- } else {
- await tabEl.click();
- }
- await page.waitForTimeout(500);
- }
-
- const departureInput = page.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app));
- const arrivalInput = page.locator(tid(S.FILTER_ROUTE_ARRIVAL_INPUT, app));
-
- await departureInput.fill('Moscow');
- await arrivalInput.fill('Moscow');
- await page.waitForTimeout(500);
-
- const searchButton = page.locator(tid(S.FILTER_ROUTE_SEARCH, app));
- await searchButton.click();
- await page.waitForTimeout(500);
-
- const errorMessages = page.locator(
- '.p-error, .error-message, [role="alert"], .ng-invalid.ng-dirty',
- );
- const count = await errorMessages.count();
- if (count > 0) {
- expect(count).toBeGreaterThanOrEqual(1);
- }
- });
-
- // ─────────────────────────────────────────────────────────────────────────
- // Story 27: User searches with special characters
- // ─────────────────────────────────────────────────────────────────────────
-
- test('27.1: Special characters in flight number', async ({ page, app, localePath }) => {
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- const flightTab = page.locator(tid(S.FILTER_FLIGHT_TAB, app));
- const fallback = page.locator('[data-testid="flight-filter"]');
- const tabEl = (await flightTab.count()) > 0 ? flightTab : fallback;
-
- const isExpanded = await page
- .locator(tid(S.FILTER_FLIGHT_NUMBER_INPUT, app))
- .isVisible()
- .catch(() => false);
-
- if (!isExpanded) {
- const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
- if ((await headerLink.count()) > 0) {
- await headerLink.click();
- } else {
- await tabEl.click();
- }
- await page.waitForTimeout(500);
- }
-
- const flightInput = page.locator(tid(S.FILTER_FLIGHT_NUMBER_INPUT, app));
- await flightInput.fill('SU@#$%123');
- await page.waitForTimeout(500);
-
- const searchButton = page.locator(tid(S.FILTER_FLIGHT_NUMBER_SEARCH, app));
- await searchButton.click();
- await page.waitForTimeout(1000);
-
- const errorMessages = page.locator(
- '.p-error, .error-message, [role="alert"], .ng-invalid.ng-dirty',
- );
- const count = await errorMessages.count();
- if (count > 0) {
- expect(count).toBeGreaterThanOrEqual(1);
- }
- });
-
- test('27.2: Special characters in city name', async ({ page, app, localePath }) => {
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- const routeTab = page.locator(tid(S.FILTER_ROUTE_TAB, app));
- const fallback = page.locator('[data-testid="route-filter"]');
- const tabEl = (await routeTab.count()) > 0 ? routeTab : fallback;
-
- const isExpanded = await page
- .locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app))
- .isVisible()
- .catch(() => false);
-
- if (!isExpanded) {
- const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
- if ((await headerLink.count()) > 0) {
- await headerLink.click();
- } else {
- await tabEl.click();
- }
- await page.waitForTimeout(500);
- }
-
- const departureInput = page.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app));
- await departureInput.fill('Moscow@#$%');
- await page.waitForTimeout(500);
-
- const searchButton = page.locator(tid(S.FILTER_ROUTE_SEARCH, app));
- await searchButton.click();
- await page.waitForTimeout(1000);
-
- const errorMessages = page.locator(
- '.p-error, .error-message, [role="alert"], .ng-invalid.ng-dirty',
- );
- const count = await errorMessages.count();
- if (count > 0) {
- expect(count).toBeGreaterThanOrEqual(1);
- }
- });
-
- // ─────────────────────────────────────────────────────────────────────────
- // Story 28: User searches with Unicode characters
- // ─────────────────────────────────────────────────────────────────────────
-
- test('28.1: Unicode characters in city name', async ({ page, app, localePath }) => {
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- const routeTab = page.locator(tid(S.FILTER_ROUTE_TAB, app));
- const fallback = page.locator('[data-testid="route-filter"]');
- const tabEl = (await routeTab.count()) > 0 ? routeTab : fallback;
-
- const isExpanded = await page
- .locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app))
- .isVisible()
- .catch(() => false);
-
- if (!isExpanded) {
- const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
- if ((await headerLink.count()) > 0) {
- await headerLink.click();
- } else {
- await tabEl.click();
- }
- await page.waitForTimeout(500);
- }
-
- const departureInput = page.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app));
- await departureInput.fill('Москва');
- await page.waitForTimeout(500);
-
- const searchButton = page.locator(tid(S.FILTER_ROUTE_SEARCH, app));
- await searchButton.click();
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- const results = page.locator(
- 'flight-result, .flight-result, [data-testid*="flight-result"], .flight__item',
- );
- const count = await results.count();
- expect(count).toBeGreaterThanOrEqual(0);
- });
-
- test('28.2: Unicode characters in flight number', async ({ page, app, localePath }) => {
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- const flightTab = page.locator(tid(S.FILTER_FLIGHT_TAB, app));
- const fallback = page.locator('[data-testid="flight-filter"]');
- const tabEl = (await flightTab.count()) > 0 ? flightTab : fallback;
-
- const isExpanded = await page
- .locator(tid(S.FILTER_FLIGHT_NUMBER_INPUT, app))
- .isVisible()
- .catch(() => false);
-
- if (!isExpanded) {
- const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
- if ((await headerLink.count()) > 0) {
- await headerLink.click();
- } else {
- await tabEl.click();
- }
- await page.waitForTimeout(500);
- }
-
- const flightInput = page.locator(tid(S.FILTER_FLIGHT_NUMBER_INPUT, app));
- await flightInput.fill('СУ1234');
- await page.waitForTimeout(500);
-
- const searchButton = page.locator(tid(S.FILTER_FLIGHT_NUMBER_SEARCH, app));
- await searchButton.click();
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- const results = page.locator(
- 'flight-result, .flight-result, [data-testid*="flight-result"], .flight__item',
- );
- const count = await results.count();
- expect(count).toBeGreaterThanOrEqual(0);
- });
-
- // ─────────────────────────────────────────────────────────────────────────
- // Story 29: User searches with very long city name
- // ─────────────────────────────────────────────────────────────────────────
-
- test('29.1: Very long city name is accepted', async ({ page, app, localePath }) => {
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- const routeTab = page.locator(tid(S.FILTER_ROUTE_TAB, app));
- const fallback = page.locator('[data-testid="route-filter"]');
- const tabEl = (await routeTab.count()) > 0 ? routeTab : fallback;
-
- const isExpanded = await page
- .locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app))
- .isVisible()
- .catch(() => false);
-
- if (!isExpanded) {
- const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
- if ((await headerLink.count()) > 0) {
- await headerLink.click();
- } else {
- await tabEl.click();
- }
- await page.waitForTimeout(500);
- }
-
- const departureInput = page.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app));
- const longName = 'Москва' + ' '.repeat(100) + 'Moscow';
- await departureInput.fill(longName);
- await page.waitForTimeout(500);
-
- const searchButton = page.locator(tid(S.FILTER_ROUTE_SEARCH, app));
- await searchButton.click();
- await page.waitForTimeout(1000);
-
- const errorMessages = page.locator(
- '.p-error, .error-message, [role="alert"], .ng-invalid.ng-dirty',
- );
- const count = await errorMessages.count();
- if (count > 0) {
- expect(count).toBeGreaterThanOrEqual(1);
- }
- });
-
- // ─────────────────────────────────────────────────────────────────────────
- // Story 30: User searches with rapid attempts
- // ─────────────────────────────────────────────────────────────────────────
-
- test('30.1: Rapid searches do not crash the app', async ({ page, app, localePath }) => {
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- const routeTab = page.locator(tid(S.FILTER_ROUTE_TAB, app));
- const fallback = page.locator('[data-testid="route-filter"]');
- const tabEl = (await routeTab.count()) > 0 ? routeTab : fallback;
-
- const isExpanded = await page
- .locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app))
- .isVisible()
- .catch(() => false);
-
- if (!isExpanded) {
- const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
- if ((await headerLink.count()) > 0) {
- await headerLink.click();
- } else {
- await tabEl.click();
- }
- await page.waitForTimeout(500);
- }
-
- const departureInput = page.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app));
- const searchButton = page.locator(tid(S.FILTER_ROUTE_SEARCH, app));
-
- for (let i = 0; i < 5; i++) {
- await departureInput.fill(`City${i}`);
- await page.waitForTimeout(100);
- await searchButton.click();
- await page.waitForTimeout(200);
- }
-
- const consoleErrors: string[] = [];
- page.on('console', (msg) => {
- if (msg.type() === 'error') {
- consoleErrors.push(msg.text());
- }
- });
-
- await page.waitForTimeout(1000);
- expect(consoleErrors.length).toBeLessThanOrEqual(0);
- });
-
- test('30.2: Rapid searches show loading state', async ({ page, app, localePath }) => {
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- const routeTab = page.locator(tid(S.FILTER_ROUTE_TAB, app));
- const fallback = page.locator('[data-testid="route-filter"]');
- const tabEl = (await routeTab.count()) > 0 ? routeTab : fallback;
-
- const isExpanded = await page
- .locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app))
- .isVisible()
- .catch(() => false);
-
- if (!isExpanded) {
- const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
- if ((await headerLink.count()) > 0) {
- await headerLink.click();
- } else {
- await tabEl.click();
- }
- await page.waitForTimeout(500);
- }
-
- const departureInput = page.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app));
- const searchButton = page.locator(tid(S.FILTER_ROUTE_SEARCH, app));
- const loader = page.locator(tid(S.BOARD_LOADER, app));
-
- for (let i = 0; i < 3; i++) {
- await departureInput.fill(`City${i}`);
- await page.waitForTimeout(100);
- await searchButton.click();
- await page.waitForTimeout(200);
-
- const loaderVisible = await loader.count();
- if (loaderVisible > 0) {
- await loader.first().waitFor({ state: 'hidden', timeout: 10000 });
- }
- }
- });
-});
-
-function formatToday(): string {
- const d = new Date();
- return `${d.getFullYear()}${String(d.getMonth() + 1).padStart(2, '0')}${String(d.getDate()).padStart(2, '0')}`;
-}
diff --git a/tests/e2e-angular/cross-app/17-board-schedule-view.spec.ts b/tests/e2e-angular/cross-app/17-board-schedule-view.spec.ts
deleted file mode 100644
index 52d737e1..00000000
--- a/tests/e2e-angular/cross-app/17-board-schedule-view.spec.ts
+++ /dev/null
@@ -1,471 +0,0 @@
-import { test, expect } from '../support/cross-app-fixtures';
-import { mockAllAPIs } from '../support/cross-app-fixtures';
-import { S, tid } from '../support/selectors';
-
-/**
- * User Stories 161-180: Board & Schedule View Scenarios
- *
- * 161: User views departure board
- * 162: User views arrival board
- * 163: User views route board
- * 164: User views flight board with filters
- * 165: User views flight board with date tabs
- * 166: User views weekly schedule
- * 167: User switches week tabs
- * 168: User views daily schedule
- * 169: User views schedule with filters
- * 170: User views schedule with sort
- * 171-175: Map view scenarios (covered in existing tests)
- * 176-180: Flight details scenarios (covered in existing tests)
- */
-
-test.describe('User Stories 161-180: Board & Schedule View Scenarios', () => {
- test.beforeEach(async ({ page, localePath }) => {
- await mockAllAPIs(page);
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
- });
-
- // ─────────────────────────────────────────────────────────────────────────
- // Story 161: User views departure board
- // ─────────────────────────────────────────────────────────────────────────
-
- test('161.1: Departure board loads with flight results', async ({ page, app, localePath }) => {
- const today = formatToday();
- await page.goto(`/${localePath('onlineboard')}/departure/MOW-${today}`);
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- const flightResults = page.locator(
- 'flight-result, .flight-result, [data-testid*="flight-result"], .flight__item',
- );
- const count = await flightResults.count();
- expect(count).toBeGreaterThanOrEqual(0);
- });
-
- test('161.2: Departure board shows date tabs', async ({ page, app, localePath }) => {
- const today = formatToday();
- await page.goto(`/${localePath('onlineboard')}/departure/MOW-${today}`);
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- const dateTabs = page.locator(tid(S.BOARD_DAY_TABS, app));
- const fallbackTabs = page.locator('.p-tabs, .tabs, [role="tablist"]');
- const target = (await dateTabs.count()) > 0 ? dateTabs : fallbackTabs;
-
- const count = await target.count();
- expect(count).toBeGreaterThan(0);
- });
-
- test('161.3: Departure board shows flight cards', async ({ page, app, localePath }) => {
- const today = formatToday();
- await page.goto(`/${localePath('onlineboard')}/departure/MOW-${today}`);
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- const flightCards = page.locator(
- 'flight-card, .flight-card, [data-testid*="flight-card"], .flight__card',
- );
- const count = await flightCards.count();
- expect(count).toBeGreaterThanOrEqual(0);
- });
-
- // ─────────────────────────────────────────────────────────────────────────
- // Story 162: User views arrival board
- // ─────────────────────────────────────────────────────────────────────────
-
- test('162.1: Arrival board loads with flight results', async ({ page, app, localePath }) => {
- const today = formatToday();
- await page.goto(`/${localePath('onlineboard')}/arrival/AER-${today}`);
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- const flightResults = page.locator(
- 'flight-result, .flight-result, [data-testid*="flight-result"], .flight__item',
- );
- const count = await flightResults.count();
- expect(count).toBeGreaterThanOrEqual(0);
- });
-
- test('162.2: Arrival board shows date tabs', async ({ page, app, localePath }) => {
- const today = formatToday();
- await page.goto(`/${localePath('onlineboard')}/arrival/AER-${today}`);
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- const dateTabs = page.locator(tid(S.BOARD_DAY_TABS, app));
- const fallbackTabs = page.locator('.p-tabs, .tabs, [role="tablist"]');
- const target = (await dateTabs.count()) > 0 ? dateTabs : fallbackTabs;
-
- const count = await target.count();
- expect(count).toBeGreaterThan(0);
- });
-
- // ─────────────────────────────────────────────────────────────────────────
- // Story 163: User views route board
- // ─────────────────────────────────────────────────────────────────────────
-
- test('163.1: Route board loads with flight results', async ({ page, app, localePath }) => {
- const today = formatToday();
- await page.goto(`/${localePath('onlineboard')}/route/MOW-${today}-AER-${today}`);
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- const flightResults = page.locator(
- 'flight-result, .flight-result, [data-testid*="flight-result"], .flight__item',
- );
- const count = await flightResults.count();
- expect(count).toBeGreaterThanOrEqual(0);
- });
-
- test('163.2: Route board shows departure and arrival info', async ({ page, app, localePath }) => {
- const today = formatToday();
- await page.goto(`/${localePath('onlineboard')}/route/MOW-${today}-AER-${today}`);
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- const flightCards = page.locator(
- 'flight-card, .flight-card, [data-testid*="flight-card"], .flight__card',
- );
- const count = await flightCards.count();
- if (count > 0) {
- const firstCard = flightCards.first();
- const text = await firstCard.textContent();
- expect(text || '').toMatch(/MOW|AER/);
- }
- });
-
- // ─────────────────────────────────────────────────────────────────────────
- // Story 164: User views flight board with filters
- // ─────────────────────────────────────────────────────────────────────────
-
- test('164.1: Flight board has filter accordion', async ({ page, app, localePath }) => {
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- const filterAccordion = page.locator(tid(S.FILTER_ACCORDION, app));
- const fallbackAccordion = page.locator('p-accordion, .p-accordion');
- const target = (await filterAccordion.count()) > 0 ? filterAccordion : fallbackAccordion;
-
- await expect(target.first()).toBeVisible({ timeout: 10000 });
- });
-
- test('164.2: Filter accordion has flight and route tabs', async ({ page, app, localePath }) => {
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- const flightTab = page.locator(tid(S.FILTER_FLIGHT_TAB, app));
- const routeTab = page.locator(tid(S.FILTER_ROUTE_TAB, app));
- const fallbackFlight = page.locator('[data-testid="flight-filter"]');
- const fallbackRoute = page.locator('[data-testid="route-filter"]');
-
- const flightVisible = (await flightTab.count()) > 0 || (await fallbackFlight.count()) > 0;
- const routeVisible = (await routeTab.count()) > 0 || (await fallbackRoute.count()) > 0;
-
- expect(flightVisible).toBe(true);
- expect(routeVisible).toBe(true);
- });
-
- // ─────────────────────────────────────────────────────────────────────────
- // Story 165: User views flight board with date tabs
- // ─────────────────────────────────────────────────────────────────────────
-
- test('165.1: Date tabs allow switching between dates', async ({ page, app, localePath }) => {
- const today = formatToday();
- await page.goto(`/${localePath('onlineboard')}/departure/MOW-${today}`);
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- const dateTabs = page.locator(tid(S.BOARD_DAY_TABS, app));
- const fallbackTabs = page.locator('.p-tabs, .tabs, [role="tablist"]');
- const target = (await dateTabs.count()) > 0 ? dateTabs : fallbackTabs;
-
- const tabCount = await target.count();
- if (tabCount > 0) {
- const tabs = target.locator('[role="tab"], .p-tabs-tab, .tabs-tab');
- const tabCountItems = await tabs.count();
- expect(tabCountItems).toBeGreaterThanOrEqual(1);
- }
- });
-
- test('165.2: Date tab selection updates flight list', async ({ page, app, localePath }) => {
- const today = formatToday();
- await page.goto(`/${localePath('onlineboard')}/departure/MOW-${today}`);
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- const dateTabs = page.locator(tid(S.BOARD_DAY_TABS, app));
- const fallbackTabs = page.locator('.p-tabs, .tabs, [role="tablist"]');
- const target = (await dateTabs.count()) > 0 ? dateTabs : fallbackTabs;
-
- const tabs = target.locator('[role="tab"], .p-tabs-tab, .tabs-tab');
- const tabCount = await tabs.count();
- if (tabCount > 1) {
- const urlBefore = page.url();
- await tabs.nth(1).click();
- await page.waitForTimeout(500);
- const urlAfter = page.url();
- expect(urlAfter.length).toBeGreaterThan(0);
- }
- });
-
- // ─────────────────────────────────────────────────────────────────────────
- // Story 166: User views weekly schedule
- // ─────────────────────────────────────────────────────────────────────────
-
- test('166.1: Schedule page has week tabs', async ({ page, app, localePath }) => {
- await page.goto(localePath('schedule'));
- await page.waitForLoadState('networkidle');
-
- const weekTabs = page.locator(tid(S.SCHEDULE_WEEK_TABS, app));
- const fallbackTabs = page.locator('.p-tabs, .tabs, [role="tablist"]');
- const target = (await weekTabs.count()) > 0 ? weekTabs : fallbackTabs;
-
- const count = await target.count();
- expect(count).toBeGreaterThan(0);
- });
-
- test('166.2: Week tabs show 7 days', async ({ page, app, localePath }) => {
- await page.goto(localePath('schedule'));
- await page.waitForLoadState('networkidle');
-
- const weekTabs = page.locator(tid(S.SCHEDULE_WEEK_TABS, app));
- const fallbackTabs = page.locator('.p-tabs, .tabs, [role="tablist"]');
- const target = (await weekTabs.count()) > 0 ? weekTabs : fallbackTabs;
-
- const tabs = target.locator('[role="tab"], .p-tabs-tab, .tabs-tab');
- const tabCount = await tabs.count();
- expect(tabCount).toBeGreaterThanOrEqual(7);
- });
-
- // ─────────────────────────────────────────────────────────────────────────
- // Story 167: User switches week tabs
- // ─────────────────────────────────────────────────────────────────────────
-
- test('167.1: Week tab switch updates schedule', async ({ page, app, localePath }) => {
- await page.goto(localePath('schedule'));
- await page.waitForLoadState('networkidle');
-
- const weekTabs = page.locator(tid(S.SCHEDULE_WEEK_TABS, app));
- const fallbackTabs = page.locator('.p-tabs, .tabs, [role="tablist"]');
- const target = (await weekTabs.count()) > 0 ? weekTabs : fallbackTabs;
-
- const tabs = target.locator('[role="tab"], .p-tabs-tab, .tabs-tab');
- const tabCount = await tabs.count();
- if (tabCount > 1) {
- const urlBefore = page.url();
- await tabs.nth(1).click();
- await page.waitForTimeout(500);
- const urlAfter = page.url();
- expect(urlAfter.length).toBeGreaterThan(0);
- }
- });
-
- test('167.2: Week tab has previous/next navigation', async ({ page, app, localePath }) => {
- await page.goto(localePath('schedule'));
- await page.waitForLoadState('networkidle');
-
- const prevButton = page.locator(tid(S.SCHEDULE_WEEK_PREV, app));
- const nextButton = page.locator(tid(S.SCHEDULE_WEEK_NEXT, app));
-
- const prevVisible = await prevButton.count();
- const nextVisible = await nextButton.count();
-
- expect(prevVisible).toBeGreaterThan(0);
- expect(nextVisible).toBeGreaterThan(0);
- });
-
- // ─────────────────────────────────────────────────────────────────────────
- // Story 168: User views daily schedule
- // ─────────────────────────────────────────────────────────────────────────
-
- test('168.1: Daily schedule shows flights for selected day', async ({
- page,
- app,
- localePath,
- }) => {
- await page.goto(localePath('schedule'));
- await page.waitForLoadState('networkidle');
-
- const weekTabs = page.locator(tid(S.SCHEDULE_WEEK_TABS, app));
- const fallbackTabs = page.locator('.p-tabs, .tabs, [role="tablist"]');
- const target = (await weekTabs.count()) > 0 ? weekTabs : fallbackTabs;
-
- const tabs = target.locator('[role="tab"], .p-tabs-tab, .tabs-tab');
- const tabCount = await tabs.count();
- if (tabCount > 0) {
- await tabs.first().click();
- await page.waitForTimeout(500);
-
- const flightItems = page.locator(
- 'schedule-flight, .schedule-flight, [data-testid*="schedule-flight"], .schedule__item',
- );
- const flightCount = await flightItems.count();
- expect(flightCount).toBeGreaterThanOrEqual(0);
- }
- });
-
- test('168.2: Daily schedule shows flight details', async ({ page, app, localePath }) => {
- await page.goto(localePath('schedule'));
- await page.waitForLoadState('networkidle');
-
- const flightItems = page.locator(
- 'schedule-flight, .schedule-flight, [data-testid*="schedule-flight"], .schedule__item',
- );
- const flightCount = await flightItems.count();
- if (flightCount > 0) {
- const firstFlight = flightItems.first();
- const text = await firstFlight.textContent();
- expect(text.length).toBeGreaterThan(0);
- }
- });
-
- // ─────────────────────────────────────────────────────────────────────────
- // Story 169: User views schedule with filters
- // ─────────────────────────────────────────────────────────────────────────
-
- test('169.1: Schedule has airline filter', async ({ page, app, localePath }) => {
- await page.goto(localePath('schedule'));
- await page.waitForLoadState('networkidle');
-
- const airlineFilter = page.locator(
- 'schedule-airline-filter, .schedule-airline-filter, [data-testid*="airline"], .schedule__filter',
- );
- const count = await airlineFilter.count();
- expect(count).toBeGreaterThan(0);
- });
-
- test('169.2: Schedule has direct flights filter', async ({ page, app, localePath }) => {
- await page.goto(localePath('schedule'));
- await page.waitForLoadState('networkidle');
-
- const directCheckbox = page.locator(tid(S.SCHEDULE_DIRECT_ONLY_CHECKBOX, app));
- const fallbackCheckbox = page.locator(
- 'input[type="checkbox"][aria-label*="direct"], .schedule__direct-checkbox',
- );
- const target = (await directCheckbox.count()) > 0 ? directCheckbox : fallbackCheckbox;
-
- await expect(target.first()).toBeVisible();
- });
-
- // ─────────────────────────────────────────────────────────────────────────
- // Story 170: User views schedule with sort
- // ─────────────────────────────────────────────────────────────────────────
-
- test('170.1: Schedule has sort dropdown', async ({ page, app, localePath }) => {
- await page.goto(localePath('schedule'));
- await page.waitForLoadState('networkidle');
-
- const sortDropdown = page.locator(tid(S.SCHEDULE_SORT_DROPDOWN, app));
- const fallbackDropdown = page.locator(
- 'select[aria-label*="sort"], .p-dropdown, .schedule__sort',
- );
- const target = (await sortDropdown.count()) > 0 ? sortDropdown : fallbackDropdown;
-
- await expect(target.first()).toBeVisible();
- });
-
- test('170.2: Sort dropdown has departure time option', async ({ page, app, localePath }) => {
- await page.goto(localePath('schedule'));
- await page.waitForLoadState('networkidle');
-
- const sortDropdown = page.locator(tid(S.SCHEDULE_SORT_DROPDOWN, app));
- const fallbackDropdown = page.locator(
- 'select[aria-label*="sort"], .p-dropdown, .schedule__sort',
- );
- const target = (await sortDropdown.count()) > 0 ? sortDropdown : fallbackDropdown;
-
- if ((await target.count()) > 0) {
- await target.first().click();
- await page.waitForTimeout(300);
-
- const options = page.locator('option, .p-dropdown-item, .dropdown-item');
- const optionCount = await options.count();
- expect(optionCount).toBeGreaterThanOrEqual(1);
-
- const optionText = await options.first().textContent();
- expect(optionText?.toLowerCase()).toMatch(/time|departure|arrival|sort/i);
- }
- });
-
- // ─────────────────────────────────────────────────────────────────────────
- // Story 171-175: Map view scenarios (covered in existing tests)
- // ─────────────────────────────────────────────────────────────────────────
-
- test('171.1: Map page loads with departure city', async ({ page, app, localePath }) => {
- await page.goto(localePath('flights-map'));
- await page.waitForLoadState('networkidle');
-
- const mapContainer = page.locator(tid(S.MAP_CONTAINER, app));
- const fallbackMap = page.locator('.leaflet-container, [class*="map"], #map');
- const target = (await mapContainer.count()) > 0 ? mapContainer : fallbackMap;
-
- await expect(target.first()).toBeVisible({ timeout: 10000 });
- });
-
- test('172.1: Map shows route between cities', async ({ page, app, localePath }) => {
- await page.goto(localePath('flights-map'));
- await page.waitForLoadState('networkidle');
-
- const mapContainer = page.locator(tid(S.MAP_CONTAINER, app));
- const fallbackMap = page.locator('.leaflet-container, [class*="map"], #map');
- const target = (await mapContainer.count()) > 0 ? mapContainer : fallbackMap;
-
- await expect(target.first()).toBeVisible({ timeout: 10000 });
- });
-
- // ─────────────────────────────────────────────────────────────────────────
- // Story 176-180: Flight details scenarios (covered in existing tests)
- // ─────────────────────────────────────────────────────────────────────────
-
- test('176.1: Flight details shows status', async ({ page, app, localePath }) => {
- const today = formatToday();
- await page.goto(`/${localePath('onlineboard')}/departure/MOW-${today}`);
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- const flightResults = page.locator(
- 'flight-result, .flight-result, [data-testid*="flight-result"], .flight__item',
- );
- const count = await flightResults.count();
- if (count > 0) {
- await flightResults.first().click();
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(500);
-
- const statusElement = page.locator(tid(S.DETAILS_STATUS, app));
- const fallbackStatus = page.locator('.flight-status, .status-badge, [class*="status"]');
- const target = (await statusElement.count()) > 0 ? statusElement : fallbackStatus;
-
- await expect(target.first()).toBeVisible();
- }
- });
-
- test('177.1: Flight details shows route', async ({ page, app, localePath }) => {
- const today = formatToday();
- await page.goto(`/${localePath('onlineboard')}/departure/MOW-${today}`);
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- const flightResults = page.locator(
- 'flight-result, .flight-result, [data-testid*="flight-result"], .flight__item',
- );
- const count = await flightResults.count();
- if (count > 0) {
- await flightResults.first().click();
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(500);
-
- const routeElement = page.locator(tid(S.DETAILS_FULL_ROUTE, app));
- const fallbackRoute = page.locator('.flight-route, .route-display, [class*="route"]');
- const target = (await routeElement.count()) > 0 ? routeElement : fallbackRoute;
-
- await expect(target.first()).toBeVisible();
- }
- });
-});
-
-function formatToday(): string {
- const d = new Date();
- return `${d.getFullYear()}${String(d.getMonth() + 1).padStart(2, '0')}${String(d.getDate()).padStart(2, '0')}`;
-}
diff --git a/tests/e2e-angular/cross-app/18-advanced-features.spec.ts b/tests/e2e-angular/cross-app/18-advanced-features.spec.ts
deleted file mode 100644
index feb47f51..00000000
--- a/tests/e2e-angular/cross-app/18-advanced-features.spec.ts
+++ /dev/null
@@ -1,756 +0,0 @@
-import { test, expect } from '../support/cross-app-fixtures';
-import { mockAllAPIs } from '../support/cross-app-fixtures';
-import { S, tid } from '../support/selectors';
-
-/**
- * User Stories 181-210: Advanced Features & Edge Cases
- *
- * 181-185: Multi-leg flight scenarios
- * 186-190: Search history scenarios
- * 191-194: Error scenarios
- * 195-200: Input validation scenarios
- * 201-205: Search edge cases
- * 206-210: Locale scenarios
- */
-
-test.describe('User Stories 181-210: Advanced Features & Edge Cases', () => {
- test.beforeEach(async ({ page, localePath }) => {
- await mockAllAPIs(page);
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
- });
-
- // ─────────────────────────────────────────────────────────────────────────
- // Story 181-185: Multi-leg flight scenarios
- // ─────────────────────────────────────────────────────────────────────────
-
- test('181.1: Multi-leg flight shows segments', async ({ page, app, localePath }) => {
- const today = formatToday();
- await page.goto(`/${localePath('onlineboard')}/departure/MOW-${today}`);
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- const flightResults = page.locator(
- 'flight-result, .flight-result, [data-testid*="flight-result"], .flight__item',
- );
- const count = await flightResults.count();
- if (count > 0) {
- await flightResults.first().click();
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(500);
-
- const segments = page.locator(
- 'flight-segment, .flight-segment, [data-testid*="segment"], .flight__segment',
- );
- const segmentCount = await segments.count();
- expect(segmentCount).toBeGreaterThanOrEqual(0);
- }
- });
-
- test('182.1: User switches multi-leg segments', async ({ page, app, localePath }) => {
- const today = formatToday();
- await page.goto(`/${localePath('onlineboard')}/departure/MOW-${today}`);
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- const flightResults = page.locator(
- 'flight-result, .flight-result, [data-testid*="flight-result"], .flight__item',
- );
- const count = await flightResults.count();
- if (count > 0) {
- await flightResults.first().click();
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(500);
-
- const segments = page.locator(
- 'flight-segment, .flight-segment, [data-testid*="segment"], .flight__segment',
- );
- const segmentCount = await segments.count();
- if (segmentCount > 1) {
- const urlBefore = page.url();
- await segments.nth(1).click();
- await page.waitForTimeout(300);
- const urlAfter = page.url();
- expect(urlAfter.length).toBeGreaterThan(0);
- }
- }
- });
-
- test('183.1: Multi-leg shows timeline', async ({ page, app, localePath }) => {
- const today = formatToday();
- await page.goto(`/${localePath('onlineboard')}/departure/MOW-${today}`);
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- const flightResults = page.locator(
- 'flight-result, .flight-result, [data-testid*="flight-result"], .flight__item',
- );
- const count = await flightResults.count();
- if (count > 0) {
- await flightResults.first().click();
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(500);
-
- const timeline = page.locator(
- 'flight-timeline, .flight-timeline, [data-testid*="timeline"], .flight__timeline',
- );
- const timelineCount = await timeline.count();
- expect(timelineCount).toBeGreaterThanOrEqual(0);
- }
- });
-
- test('184.1: Multi-leg shows transfer info', async ({ page, app, localePath }) => {
- const today = formatToday();
- await page.goto(`/${localePath('onlineboard')}/departure/MOW-${today}`);
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- const flightResults = page.locator(
- 'flight-result, .flight-result, [data-testid*="flight-result"], .flight__item',
- );
- const count = await flightResults.count();
- if (count > 0) {
- await flightResults.first().click();
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(500);
-
- const transferInfo = page.locator(
- 'flight-transfer, .flight-transfer, [data-testid*="transfer"], .flight__transfer',
- );
- const transferCount = await transferInfo.count();
- expect(transferCount).toBeGreaterThanOrEqual(0);
- }
- });
-
- test('185.1: Multi-leg shows full route', async ({ page, app, localePath }) => {
- const today = formatToday();
- await page.goto(`/${localePath('onlineboard')}/departure/MOW-${today}`);
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- const flightResults = page.locator(
- 'flight-result, .flight-result, [data-testid*="flight-result"], .flight__item',
- );
- const count = await flightResults.count();
- if (count > 0) {
- await flightResults.first().click();
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(500);
-
- const fullRoute = page.locator(tid(S.DETAILS_FULL_ROUTE, app));
- const fallbackRoute = page.locator('.flight-route, .route-display, [class*="route"]');
- const target = (await fullRoute.count()) > 0 ? fullRoute : fallbackRoute;
-
- await expect(target.first()).toBeVisible();
- }
- });
-
- // ─────────────────────────────────────────────────────────────────────────
- // Story 186-190: Search history scenarios
- // ─────────────────────────────────────────────────────────────────────────
-
- test('186.1: Recent searches display on landing', async ({ page, app, localePath }) => {
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- const historySection = page.locator(
- 'search-history, [data-testid="landing-search-history"], .search-history, [class*="search-history"]',
- );
- const count = await historySection.count();
- expect(count).toBeGreaterThan(0);
- });
-
- test('187.1: Re-search from history item', async ({ page, app, localePath }) => {
- const today = formatToday();
- await page.goto(`/${localePath('onlineboard')}/departure/MOW-${today}`);
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(2000);
-
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- const historyItems = page.locator(
- 'search-history .history-item, [data-testid="landing-search-history-item"], .search-history__item',
- );
- const count = await historyItems.count();
- if (count > 0) {
- const urlBefore = page.url();
- await historyItems.first().click();
- await page.waitForTimeout(1000);
- const urlAfter = page.url();
- expect(urlAfter).not.toBe(urlBefore);
- }
- });
-
- test('188.1: Clear recent searches', async ({ page, app, localePath }) => {
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- const historySection = page.locator(
- 'search-history, [data-testid="landing-search-history"], .search-history, [class*="search-history"]',
- );
- const count = await historySection.count();
- if (count > 0) {
- const clearButton = historySection.locator(
- 'button[aria-label*="clear"], button[aria-label*="Clear"], .history-clear, .clear-history',
- );
- const clearCount = await clearButton.count();
- if (clearCount > 0) {
- await clearButton.first().click();
- await page.waitForTimeout(300);
- }
- }
- });
-
- test('189.1: Recent searches persist on reload', async ({ page, app, localePath }) => {
- const today = formatToday();
- await page.goto(`/${localePath('onlineboard')}/departure/MOW-${today}`);
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(2000);
-
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- const historyItems = page.locator(
- 'search-history .history-item, [data-testid="landing-search-history-item"], .search-history__item',
- );
- const countBefore = await historyItems.count();
-
- await page.reload();
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- const historyItemsAfter = page.locator(
- 'search-history .history-item, [data-testid="landing-search-history-item"], .search-history__item',
- );
- const countAfter = await historyItemsAfter.count();
-
- expect(countAfter).toBeGreaterThanOrEqual(countBefore);
- });
-
- test('190.1: Recent searches persist when navigating', async ({ page, app, localePath }) => {
- const today = formatToday();
- await page.goto(`/${localePath('onlineboard')}/departure/MOW-${today}`);
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(2000);
-
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- const historyItems = page.locator(
- 'search-history .history-item, [data-testid="landing-search-history-item"], .search-history__item',
- );
- const countBefore = await historyItems.count();
-
- await page.goto(localePath('schedule'));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(500);
-
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- const historyItemsAfter = page.locator(
- 'search-history .history-item, [data-testid="landing-search-history-item"], .search-history__item',
- );
- const countAfter = await historyItemsAfter.count();
-
- expect(countAfter).toBeGreaterThanOrEqual(countBefore);
- });
-
- // ─────────────────────────────────────────────────────────────────────────
- // Story 191-194: Error scenarios
- // ─────────────────────────────────────────────────────────────────────────
-
- test('191.1: 404 error page displays for invalid route', async ({ page, app, localePath }) => {
- await page.goto(localePath('/nonexistent-page-xyz-123'));
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- const pageText = await page.textContent('body');
- const hasErrorIndicator =
- pageText?.includes('404') ||
- pageText?.includes('not found') ||
- pageText?.includes('page not found') ||
- pageText?.includes('не найдена') ||
- pageText?.includes('страница') ||
- false;
-
- expect(hasErrorIndicator).toBe(true);
- });
-
- test('192.1: 500 error page displays on server error', async ({ page, app, localePath }) => {
- await mockAllAPIs(page);
-
- await page.route('**/api/Requests/**', (route) => {
- route.fulfill({
- status: 500,
- contentType: 'application/json',
- body: JSON.stringify({ error: 'Internal Server Error' }),
- });
- });
-
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- const flightInput = page.locator(tid(S.FILTER_FLIGHT_NUMBER_INPUT, app));
- if (await flightInput.isVisible().catch(() => false)) {
- await flightInput.fill('SU100');
- const searchButton = page.locator(tid(S.FILTER_FLIGHT_NUMBER_SEARCH, app));
- if (await searchButton.isVisible().catch(() => false)) {
- await searchButton.click();
- await page.waitForTimeout(2000);
- }
- }
-
- const bodyVisible = await page.isVisible('body');
- expect(bodyVisible).toBe(true);
- });
-
- test('193.1: Network error handled gracefully', async ({ page, app, localePath }) => {
- await mockAllAPIs(page);
-
- await page.route('**/api/Requests/**', (route) => {
- route.abort('failed');
- });
-
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- const bodyVisible = await page.isVisible('body');
- expect(bodyVisible).toBe(true);
- });
-
- test('194.1: Timeout error handled gracefully', async ({ page, app, localePath }) => {
- await mockAllAPIs(page);
-
- await page.route('**/api/Requests/**', (route) => {
- route.abort('timedout');
- });
-
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- const bodyVisible = await page.isVisible('body');
- expect(bodyVisible).toBe(true);
- });
-
- // ─────────────────────────────────────────────────────────────────────────
- // Story 195-200: Input validation scenarios
- // ─────────────────────────────────────────────────────────────────────────
-
- test('195.1: Invalid input shows validation error', async ({ page, app, localePath }) => {
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- const routeTab = page.locator(tid(S.FILTER_ROUTE_TAB, app));
- const fallback = page.locator('[data-testid="route-filter"]');
- const tabEl = (await routeTab.count()) > 0 ? routeTab : fallback;
-
- const isExpanded = await page
- .locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app))
- .isVisible()
- .catch(() => false);
-
- if (!isExpanded) {
- const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
- if ((await headerLink.count()) > 0) {
- await headerLink.click();
- } else {
- await tabEl.click();
- }
- await page.waitForTimeout(500);
- }
-
- const departureInput = page.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app));
- await departureInput.fill('INVALIDCITYXYZ123');
- await page.waitForTimeout(500);
-
- const searchButton = page.locator(tid(S.FILTER_ROUTE_SEARCH, app));
- await searchButton.click();
- await page.waitForTimeout(1000);
-
- const errorMessages = page.locator(
- '.p-error, .error-message, [role="alert"], .ng-invalid.ng-dirty',
- );
- const count = await errorMessages.count();
- if (count > 0) {
- expect(count).toBeGreaterThanOrEqual(1);
- }
- });
-
- test('196.1: Keyboard navigation works', async ({ page, app, localePath }) => {
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- const body = page.locator('body');
- await body.focus();
- await page.keyboard.press('Tab');
-
- const focusedElement = await page.evaluate(() => {
- return document.activeElement?.tagName.toLowerCase() || '';
- });
-
- expect(focusedElement).toMatch(/input|button|a|select|textarea/);
- });
-
- test('197.1: Screen reader accessibility', async ({ page, app, localePath }) => {
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- const navElement = page.locator('nav[aria-label], [role="navigation"]');
- const navCount = await navElement.count();
- expect(navCount).toBeGreaterThan(0);
-
- const mainElement = page.locator('main[aria-label], [role="main"]');
- const mainCount = await mainElement.count();
- expect(mainCount).toBeGreaterThan(0);
- });
-
- test('198.1: Browser resize handles layout', async ({ page, app, localePath }) => {
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- await page.setViewportSize({ width: 1280, height: 720 });
- await page.waitForTimeout(500);
-
- const bodyVisible = await page.isVisible('body');
- expect(bodyVisible).toBe(true);
-
- await page.setViewportSize({ width: 768, height: 1024 });
- await page.waitForTimeout(500);
-
- const bodyVisibleMobile = await page.isVisible('body');
- expect(bodyVisibleMobile).toBe(true);
- });
-
- test('199.1: Scroll page works', async ({ page, app, localePath }) => {
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- await page.evaluate(() => {
- window.scrollTo(0, 500);
- });
- await page.waitForTimeout(500);
-
- const scrollPosition = await page.evaluate(() => window.scrollY);
- expect(scrollPosition).toBeGreaterThan(0);
- });
-
- test('200.1: Hover over interactive elements', async ({ page, app, localePath }) => {
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- const buttons = page.locator('button, a, [role="button"]');
- const buttonCount = await buttons.count();
- if (buttonCount > 0) {
- await buttons.first().hover();
- await page.waitForTimeout(300);
- }
- });
-
- // ─────────────────────────────────────────────────────────────────────────
- // Story 201-205: Search edge cases
- // ─────────────────────────────────────────────────────────────────────────
-
- test('201.1: Flight with missing information displays', async ({ page, app, localePath }) => {
- const today = formatToday();
- await page.goto(`/${localePath('onlineboard')}/departure/MOW-${today}`);
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- const flightResults = page.locator(
- 'flight-result, .flight-result, [data-testid*="flight-result"], .flight__item',
- );
- const count = await flightResults.count();
- expect(count).toBeGreaterThanOrEqual(0);
- });
-
- test('202.1: Very long flight number is accepted', async ({ page, app, localePath }) => {
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- const flightTab = page.locator(tid(S.FILTER_FLIGHT_TAB, app));
- const fallback = page.locator('[data-testid="flight-filter"]');
- const tabEl = (await flightTab.count()) > 0 ? flightTab : fallback;
-
- const isExpanded = await page
- .locator(tid(S.FILTER_FLIGHT_NUMBER_INPUT, app))
- .isVisible()
- .catch(() => false);
-
- if (!isExpanded) {
- const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
- if ((await headerLink.count()) > 0) {
- await headerLink.click();
- } else {
- await tabEl.click();
- }
- await page.waitForTimeout(500);
- }
-
- const flightInput = page.locator(tid(S.FILTER_FLIGHT_NUMBER_INPUT, app));
- const longFlightNumber = 'SU' + '1234'.repeat(10);
- await flightInput.fill(longFlightNumber);
- await page.waitForTimeout(500);
-
- const searchButton = page.locator(tid(S.FILTER_FLIGHT_NUMBER_SEARCH, app));
- await searchButton.click();
- await page.waitForTimeout(1000);
-
- const bodyVisible = await page.isVisible('body');
- expect(bodyVisible).toBe(true);
- });
-
- test('203.1: Unicode in flight number is accepted', async ({ page, app, localePath }) => {
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- const flightTab = page.locator(tid(S.FILTER_FLIGHT_TAB, app));
- const fallback = page.locator('[data-testid="flight-filter"]');
- const tabEl = (await flightTab.count()) > 0 ? flightTab : fallback;
-
- const isExpanded = await page
- .locator(tid(S.FILTER_FLIGHT_NUMBER_INPUT, app))
- .isVisible()
- .catch(() => false);
-
- if (!isExpanded) {
- const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
- if ((await headerLink.count()) > 0) {
- await headerLink.click();
- } else {
- await tabEl.click();
- }
- await page.waitForTimeout(500);
- }
-
- const flightInput = page.locator(tid(S.FILTER_FLIGHT_NUMBER_INPUT, app));
- await flightInput.fill('СУ1234');
- await page.waitForTimeout(500);
-
- const searchButton = page.locator(tid(S.FILTER_FLIGHT_NUMBER_SEARCH, app));
- await searchButton.click();
- await page.waitForTimeout(1000);
-
- const bodyVisible = await page.isVisible('body');
- expect(bodyVisible).toBe(true);
- });
-
- test('204.1: Rapid searches handled gracefully', async ({ page, app, localePath }) => {
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- const routeTab = page.locator(tid(S.FILTER_ROUTE_TAB, app));
- const fallback = page.locator('[data-testid="route-filter"]');
- const tabEl = (await routeTab.count()) > 0 ? routeTab : fallback;
-
- const isExpanded = await page
- .locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app))
- .isVisible()
- .catch(() => false);
-
- if (!isExpanded) {
- const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
- if ((await headerLink.count()) > 0) {
- await headerLink.click();
- } else {
- await tabEl.click();
- }
- await page.waitForTimeout(500);
- }
-
- const departureInput = page.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app));
- const searchButton = page.locator(tid(S.FILTER_ROUTE_SEARCH, app));
-
- for (let i = 0; i < 5; i++) {
- await departureInput.fill(`City${i}`);
- await page.waitForTimeout(100);
- await searchButton.click();
- await page.waitForTimeout(200);
- }
-
- const consoleErrors: string[] = [];
- page.on('console', (msg) => {
- if (msg.type() === 'error') {
- consoleErrors.push(msg.text());
- }
- });
-
- await page.waitForTimeout(1000);
- expect(consoleErrors.length).toBeLessThanOrEqual(0);
- });
-
- test('205.1: Special characters in flight number handled', async ({ page, app, localePath }) => {
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- const flightTab = page.locator(tid(S.FILTER_FLIGHT_TAB, app));
- const fallback = page.locator('[data-testid="flight-filter"]');
- const tabEl = (await flightTab.count()) > 0 ? flightTab : fallback;
-
- const isExpanded = await page
- .locator(tid(S.FILTER_FLIGHT_NUMBER_INPUT, app))
- .isVisible()
- .catch(() => false);
-
- if (!isExpanded) {
- const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
- if ((await headerLink.count()) > 0) {
- await headerLink.click();
- } else {
- await tabEl.click();
- }
- await page.waitForTimeout(500);
- }
-
- const flightInput = page.locator(tid(S.FILTER_FLIGHT_NUMBER_INPUT, app));
- await flightInput.fill('SU@#$%123');
- await page.waitForTimeout(500);
-
- const searchButton = page.locator(tid(S.FILTER_FLIGHT_NUMBER_SEARCH, app));
- await searchButton.click();
- await page.waitForTimeout(1000);
-
- const bodyVisible = await page.isVisible('body');
- expect(bodyVisible).toBe(true);
- });
-
- // ─────────────────────────────────────────────────────────────────────────
- // Story 206-210: Locale scenarios
- // ─────────────────────────────────────────────────────────────────────────
-
- test('206.1: Locale switcher changes language', async ({ page, app, localePath }) => {
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- const switcher = page.locator(tid(S.LAYOUT_LOCALE_SWITCHER, app));
- if ((await switcher.count()) === 0) {
- test.skip(true, 'Locale switcher not present in this app');
- return;
- }
-
- await switcher.click();
- await page.waitForTimeout(300);
-
- const options = page.locator(tid(S.LAYOUT_LOCALE_OPTION, app));
- const optionCount = await options.count();
- expect(optionCount).toBeGreaterThanOrEqual(1);
-
- if (optionCount > 1) {
- await options.nth(1).click();
- await page.waitForTimeout(500);
- }
- });
-
- test('207.1: Locale persists on page reload', async ({ page, app, localePath }) => {
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- const switcher = page.locator(tid(S.LAYOUT_LOCALE_SWITCHER, app));
- if ((await switcher.count()) === 0) {
- test.skip(true, 'Locale switcher not present in this app');
- return;
- }
-
- await switcher.click();
- await page.waitForTimeout(300);
-
- const options = page.locator(tid(S.LAYOUT_LOCALE_OPTION, app));
- const optionCount = await options.count();
- if (optionCount > 1) {
- await options.nth(1).click();
- await page.waitForTimeout(500);
-
- const urlAfterSwitch = page.url();
-
- await page.reload();
- await page.waitForLoadState('networkidle');
-
- const urlAfterReload = page.url();
- expect(urlAfterReload).toContain(urlAfterSwitch.split('/')[1]);
- }
- });
-
- test('208.1: Locale persists when navigating', async ({ page, app, localePath }) => {
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- const switcher = page.locator(tid(S.LAYOUT_LOCALE_SWITCHER, app));
- if ((await switcher.count()) === 0) {
- test.skip(true, 'Locale switcher not present in this app');
- return;
- }
-
- await switcher.click();
- await page.waitForTimeout(300);
-
- const options = page.locator(tid(S.LAYOUT_LOCALE_OPTION, app));
- const optionCount = await options.count();
- if (optionCount > 1) {
- await options.nth(1).click();
- await page.waitForTimeout(500);
-
- await page.goto(localePath('schedule'));
- await page.waitForLoadState('networkidle');
-
- const urlAfterNav = page.url();
- expect(urlAfterNav.length).toBeGreaterThan(0);
- }
- });
-
- test('209.1: Locale persists in browser history', async ({ page, app, localePath }) => {
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- const switcher = page.locator(tid(S.LAYOUT_LOCALE_SWITCHER, app));
- if ((await switcher.count()) === 0) {
- test.skip(true, 'Locale switcher not present in this app');
- return;
- }
-
- await switcher.click();
- await page.waitForTimeout(300);
-
- const options = page.locator(tid(S.LAYOUT_LOCALE_OPTION, app));
- const optionCount = await options.count();
- if (optionCount > 1) {
- await options.nth(1).click();
- await page.waitForTimeout(500);
-
- await page.goto(localePath('schedule'));
- await page.waitForLoadState('networkidle');
-
- await page.goBack();
- await page.waitForLoadState('networkidle');
-
- const urlAfterBack = page.url();
- expect(urlAfterBack.length).toBeGreaterThan(0);
- }
- });
-
- test('210.1: Locale displays correct translations', async ({ page, app, localePath }) => {
- await page.goto(localePath('onlineboard'));
- await page.waitForLoadState('networkidle');
-
- const h1 = page.locator('h1').first();
- await expect(h1).toBeVisible({ timeout: 10000 });
-
- const h1Text = await h1.textContent();
- expect(h1Text?.trim().length).toBeGreaterThan(0);
-
- if (localePath('').includes('ru-ru')) {
- expect((h1Text?.toLowerCase() || '').match(/табло|онлайн/)).toBeTruthy();
- } else if (localePath('').includes('en-us')) {
- expect(h1Text?.toLowerCase()).toMatch(/board|flight|online/i);
- }
- });
-});
-
-function formatToday(): string {
- const d = new Date();
- return `${d.getFullYear()}${String(d.getMonth() + 1).padStart(2, '0')}${String(d.getDate()).padStart(2, '0')}`;
-}
diff --git a/tests/e2e-angular/error-handling.spec.ts b/tests/e2e-angular/error-handling.spec.ts
deleted file mode 100644
index 95e0d7ee..00000000
--- a/tests/e2e-angular/error-handling.spec.ts
+++ /dev/null
@@ -1,326 +0,0 @@
-import { test, expect } from '@playwright/test';
-
-test.describe('Error Handling (US-85, US-86, US-88) - React ru-ru', () => {
- // Collect console errors for all tests
- let consoleErrors: string[] = [];
-
- test.beforeEach(async ({ page }) => {
- // Clear previous errors
- consoleErrors = [];
-
- // Set Russian locale
- await page.addInitScript(() => {
- Object.defineProperty(navigator, 'language', {
- get: () => 'ru-RU',
- });
- Object.defineProperty(navigator, 'languages', {
- get: () => ['ru-RU', 'ru'],
- });
- });
-
- // Collect console errors throughout test
- page.on('console', (msg) => {
- if (msg.type() === 'error') {
- consoleErrors.push(msg.text());
- }
- });
- });
-
- test.afterEach(async ({ page }) => {
- // Clean up all routes
- await page.unroute('**/*');
-
- // Close any dialogs
- await page.keyboard.press('Escape').catch(() => {});
-
- // Verify no unexpected console errors occurred
- const unexpectedErrors = consoleErrors.filter(
- (msg) =>
- !msg.includes('Failed to fetch') &&
- !msg.includes('Network error') &&
- !msg.includes('404') &&
- !msg.includes('500'),
- );
- if (unexpectedErrors.length > 0) {
- console.warn('Unexpected console errors:', unexpectedErrors);
- }
- });
-
- test('US-85: 404 Not Found - navigate to invalid route and verify Russian error page', async ({
- page,
- }) => {
- // Navigate to non-existent route
- await page.goto('http://localhost:3001/ru-ru/invalid-route-xyz');
-
- // Verify NotFoundPage renders with 404 heading visible
- const notFoundHeading = page.locator('h1:has-text("404")');
- await expect(notFoundHeading).toBeVisible();
-
- // Verify Russian title is displayed
- const rusTitle = page.locator('text=Страница не найдена');
- await expect(rusTitle).toBeVisible({ timeout: 5000 });
-
- // Verify Russian description is displayed
- const rusDescription = page.locator('text=/Извините|не существует/');
- await expect(rusDescription).toBeVisible();
-
- // Verify home link exists and is clickable
- const homeLink = page.getByRole('link', { name: /На главную|Home/i });
- await expect(homeLink).toBeVisible();
-
- // Verify no unexpected console errors
- expect(
- consoleErrors.filter((e) => !e.includes('404') && !e.includes('Failed to fetch')),
- ).toHaveLength(0);
- });
-
- test('US-85: 404 Not Found - home link navigates back to onlineboard', async ({ page }) => {
- // Navigate to invalid route
- await page.goto('http://localhost:3001/ru-ru/not-found-test');
-
- // Verify 404 page appears with Russian text
- await expect(page.locator('h1:has-text("404")')).toBeVisible();
- await expect(page.locator('text=Страница не найдена')).toBeVisible();
-
- // Click home link to navigate back
- const homeLink = page.getByRole('link', { name: /На главную|Home/i });
- await homeLink.click();
-
- // Verify navigation returns to home page
- await page.waitForURL(/\/ru-ru\/(onlineboard|flights)?/);
-
- // Wait for page to fully load
- await page.waitForLoadState('networkidle');
-
- // Verify page content loaded (should show main flight board)
- const mainContent = page.locator('main[role="main"]');
- await expect(mainContent).toBeVisible({ timeout: 5000 });
- });
-
- test('US-86: 500 Server Error - HTTP 500 response renders error page with Russian text', async ({
- page,
- }) => {
- // Set up route to respond with actual HTTP 500 status code
- let requestCount = 0;
- await page.route('**/api/**', (route) => {
- requestCount++;
- if (requestCount === 1) {
- // Respond with actual HTTP 500
- route.respond({
- status: 500,
- contentType: 'application/json',
- body: JSON.stringify({ error: 'Server Error' }),
- });
- } else {
- route.continue();
- }
- });
-
- // Navigate to page that requires API
- await page.goto('http://localhost:3001/ru-ru/onlineboard', {
- waitUntil: 'domcontentloaded',
- });
-
- // Wait for error handling to occur
- await page.waitForTimeout(2000);
-
- // Verify ServerErrorPage renders with 500 heading visible
- const serverErrorHeading = page.locator('h1:has-text("500")');
- await expect(serverErrorHeading).toBeVisible({ timeout: 5000 });
-
- // Verify Russian error title is displayed
- const rusTitle = page.locator('text=Ошибка сервера');
- await expect(rusTitle).toBeVisible();
-
- // Verify Russian description is displayed
- const rusDescription = page.locator('text=/К сожалению|произошла ошибка/');
- await expect(rusDescription).toBeVisible();
-
- // Verify error page has role="alert" for accessibility
- const alertMain = page.locator('main[role="alert"]');
- await expect(alertMain).toBeVisible();
- });
-
- test('US-86: 500 Server Error - "Try Again" button reloads and retries API', async ({ page }) => {
- let requestCount = 0;
- let secondRequestMade = false;
-
- // Set up route to fail first request, succeed on second
- await page.route('**/api/**', (route) => {
- requestCount++;
- if (requestCount === 1) {
- // First request: respond with HTTP 500
- route.respond({
- status: 500,
- contentType: 'application/json',
- body: JSON.stringify({ error: 'Server Error' }),
- });
- } else {
- // Second request: succeed
- secondRequestMade = true;
- route.continue();
- }
- });
-
- // Navigate to page
- await page.goto('http://localhost:3001/ru-ru/onlineboard', {
- waitUntil: 'domcontentloaded',
- });
-
- // Wait for error page to render
- await page.waitForTimeout(1500);
-
- // Verify 500 error page is visible
- await expect(page.locator('h1:has-text("500")')).toBeVisible();
- await expect(page.locator('text=Ошибка сервера')).toBeVisible();
-
- // Find and click "Try Again" button (should trigger page reload)
- const tryAgainButton = page.getByRole('button', { name: /Try Again|Перезагрузить/i });
- await expect(tryAgainButton).toBeVisible();
-
- // Track if new request is made after button click
- const requestsBeforeClick = requestCount;
- await tryAgainButton.click();
-
- // Wait for new request to be made
- await page.waitForTimeout(1500);
-
- // Verify error page is hidden after retry
- await expect(page.locator('h1:has-text("500")')).toBeHidden({ timeout: 5000 });
- });
-
- test('US-86: 500 Server Error - "Go Home" link navigates away from error', async ({ page }) => {
- // Set up route to respond with HTTP 500
- await page.route('**/api/**', (route) => {
- route.respond({
- status: 500,
- contentType: 'application/json',
- body: JSON.stringify({ error: 'Server Error' }),
- });
- });
-
- // Navigate to page
- await page.goto('http://localhost:3001/ru-ru/onlineboard', {
- waitUntil: 'domcontentloaded',
- });
-
- // Wait for error page to render
- await page.waitForTimeout(1500);
-
- // Verify 500 error page is visible
- await expect(page.locator('h1:has-text("500")')).toBeVisible();
-
- // Find and click "Go Home" link
- const goHomeLink = page.getByRole('link', { name: /Go Home|На главную/i });
- await expect(goHomeLink).toBeVisible();
- await goHomeLink.click();
-
- // Verify navigation happens (should go to home route)
- await page.waitForURL(/\/ru-ru\/(|flights|onlineboard)/);
- });
-
- test('US-88: Timeout Detection - indicator appears after 30 seconds of waiting', async ({
- page,
- }) => {
- // Set up route to delay response beyond 30 second timeout
- await page.route('**/api/**', async (route) => {
- await new Promise((resolve) => setTimeout(resolve, 32000));
- await route.continue().catch(() => {});
- });
-
- // Track when timeout indicator appears
- const startTime = Date.now();
- let timeoutAppearedAt: number | null = null;
-
- // Navigate and allow slow load
- const navigationPromise = page.goto('http://localhost:3001/ru-ru/onlineboard', {
- waitUntil: 'domcontentloaded',
- timeout: 35000,
- });
-
- // Wait for timeout indicator to appear (should be around 30 seconds)
- // Start checking at 25 seconds to catch it
- await page.waitForTimeout(25000);
-
- // Poll for timeout indicator appearance every 500ms for up to 10 seconds
- for (let i = 0; i < 20; i++) {
- const indicator = page.locator('[role="alert"]').filter({
- hasText: /timeout|истекло|time|истек/i,
- });
-
- if (await indicator.isVisible().catch(() => false)) {
- timeoutAppearedAt = Date.now() - startTime;
- break;
- }
-
- await page.waitForTimeout(500);
- }
-
- // Verify timeout indicator appeared within expected window (28-32 seconds = 30±2s tolerance)
- expect(timeoutAppearedAt).not.toBeNull();
- if (timeoutAppearedAt !== null) {
- expect(timeoutAppearedAt).toBeGreaterThanOrEqual(28000); // 28 seconds
- expect(timeoutAppearedAt).toBeLessThanOrEqual(32000); // 32 seconds
- }
-
- // Verify Russian timeout text is visible
- const rusTimeout = page.locator('text=/Истекло время|Запрос занял/');
- await expect(rusTimeout).toBeVisible({ timeout: 5000 });
-
- // Allow navigation to complete
- await navigationPromise.catch(() => {});
- });
-
- test('US-88: Timeout Detection - retry button exists and re-executes search', async ({
- page,
- }) => {
- let firstRequestCompleted = false;
- let retryRequestMade = false;
-
- // Set up route to delay first request
- await page.route('**/api/**', async (route) => {
- if (!firstRequestCompleted) {
- firstRequestCompleted = true;
- // Delay first request beyond timeout
- await new Promise((resolve) => setTimeout(resolve, 32000));
- await route.continue().catch(() => {});
- } else {
- // Subsequent requests succeed immediately
- retryRequestMade = true;
- await route.continue();
- }
- });
-
- // Navigate to page
- const navigationPromise = page.goto('http://localhost:3001/ru-ru/onlineboard', {
- waitUntil: 'domcontentloaded',
- timeout: 35000,
- });
-
- // Wait for timeout to appear and retry button to be available
- await page.waitForTimeout(31000);
-
- // Find retry button in timeout indicator
- const retryButton = page
- .locator('[role="alert"]')
- .filter({ hasText: /Повторить|retry/i })
- .locator('button');
-
- const retryVisible = await retryButton.isVisible().catch(() => false);
- if (retryVisible) {
- // Click retry button
- await retryButton.click({ timeout: 5000 });
-
- // Wait briefly for new request to be made
- await page.waitForTimeout(1500);
-
- // Verify timeout indicator is hidden after retry
- await expect(
- page.locator('[role="alert"]').filter({ hasText: /timeout|истекло|time/i }),
- ).toBeHidden({ timeout: 5000 });
- }
-
- // Allow navigation to complete
- await navigationPromise.catch(() => {});
- });
-});
diff --git a/tests/e2e-angular/fixtures/api-responses.json b/tests/e2e-angular/fixtures/api-responses.json
deleted file mode 100644
index 12699858..00000000
--- a/tests/e2e-angular/fixtures/api-responses.json
+++ /dev/null
@@ -1,409 +0,0 @@
-{
- "apiResponses": {
- "flightBoardDeparture": {
- "status": 200,
- "headers": {
- "Content-Type": "application/json",
- "X-Request-Id": "req-12345"
- },
- "body": {
- "direction": "departure",
- "cityCode": "MOW",
- "cityName": "Moscow",
- "date": "2026-04-06",
- "flights": [
- {
- "id": "fl-1124",
- "flightNumber": "SU 1124",
- "airlineCode": "SU",
- "airlineName": "Aeroflot",
- "aircraftType": "Airbus A320",
- "direction": "departure",
- "status": "scheduled",
- "date": "2026-04-06",
- "departure": {
- "airportCode": "SVO",
- "airportName": "Sheremetyevo",
- "cityCode": "MOW",
- "cityName": "Moscow",
- "terminal": "B",
- "time": {
- "scheduled": "2026-04-06T12:13:00+03:00"
- }
- },
- "arrival": {
- "airportCode": "AER",
- "airportName": "Adler",
- "cityCode": "AER",
- "cityName": "Sochi",
- "time": {
- "scheduled": "2026-04-06T16:05:00+03:00"
- }
- },
- "boarding": {
- "gate": "11",
- "status": "Идёт посадка",
- "startTime": "2026-04-06T11:30:00+03:00",
- "endTime": "2026-04-06T12:05:00+03:00"
- },
- "checkin": {
- "status": "Закончена",
- "startTime": "2026-04-06T09:30:00+03:00",
- "endTime": "2026-04-06T11:45:00+03:00"
- },
- "aircraft": {
- "type": "Airbus A320",
- "name": "В. Высоцкий",
- "totalSeats": 158,
- "economySeats": 150,
- "businessSeats": 8,
- "previousFlight": "SU 1123"
- },
- "catering": {
- "economy": true,
- "business": true
- },
- "schedule": {
- "scheduledDeparture": "2026-04-06T12:13:00+03:00",
- "scheduledArrival": "2026-04-06T16:05:00+03:00",
- "duration": "3ч. 52мин.",
- "utcOffset": "UTC+03:00",
- "operatingDays": [1, 2, 3, 4, 5],
- "weekRange": "* Расписание на неделю 2026-04-06"
- },
- "lastUpdated": "12:15 2026.04.06"
- },
- {
- "id": "fl-1076",
- "flightNumber": "SU 1076",
- "airlineCode": "SU",
- "airlineName": "Aeroflot",
- "aircraftType": "Boeing 737-800",
- "direction": "departure",
- "status": "scheduled",
- "date": "2026-04-06",
- "departure": {
- "airportCode": "SVO",
- "airportName": "Sheremetyevo",
- "cityCode": "MOW",
- "cityName": "Moscow",
- "terminal": "B",
- "time": {
- "scheduled": "2026-04-06T12:14:00+03:00"
- }
- },
- "arrival": {
- "airportCode": "OVB",
- "airportName": "Tolmachevo",
- "cityCode": "OVB",
- "cityName": "Novosibirsk",
- "time": {
- "scheduled": "2026-04-06T20:25:00+03:00"
- }
- },
- "aircraft": {
- "type": "Boeing 737-800",
- "totalSeats": 189,
- "economySeats": 175,
- "businessSeats": 14
- },
- "schedule": {
- "scheduledDeparture": "2026-04-06T12:14:00+03:00",
- "scheduledArrival": "2026-04-06T20:25:00+03:00",
- "duration": "8ч. 11мин.",
- "utcOffset": "UTC+03:00",
- "operatingDays": [1, 2, 3, 4, 5, 6, 7],
- "weekRange": "* Расписание на неделю 2026-04-06"
- }
- }
- ],
- "total": 2,
- "page": 1,
- "pageSize": 20
- }
- },
- "flightBoardArrival": {
- "status": 200,
- "headers": {
- "Content-Type": "application/json",
- "X-Request-Id": "req-12346"
- },
- "body": {
- "direction": "arrival",
- "cityCode": "MOW",
- "cityName": "Moscow",
- "date": "2026-04-06",
- "flights": [
- {
- "id": "fl-1455",
- "flightNumber": "SU 1455",
- "airlineCode": "SU",
- "airlineName": "Aeroflot",
- "aircraftType": "Airbus A320",
- "direction": "arrival",
- "status": "arrived",
- "date": "2026-04-06",
- "departure": {
- "airportCode": "UUD",
- "airportName": "Ulan-Ude",
- "cityCode": "UUD",
- "cityName": "Ulan-Ude",
- "time": {
- "scheduled": "2026-04-06T10:38:00+03:00",
- "actual": "2026-04-06T10:38:00+03:00"
- }
- },
- "arrival": {
- "airportCode": "SVO",
- "airportName": "Sheremetyevo",
- "cityCode": "MOW",
- "cityName": "Moscow",
- "time": {
- "scheduled": "2026-04-06T12:12:00+03:00",
- "actual": "2026-04-06T12:12:00+03:00"
- }
- },
- "arrivalInfo": {
- "baggageBelt": "5",
- "transfer": "Тран"
- },
- "aircraft": {
- "type": "Airbus A320",
- "name": "С. Прокофьев",
- "totalSeats": 158,
- "economySeats": 150,
- "businessSeats": 8,
- "previousFlight": "SU 1454"
- },
- "catering": {
- "economy": true,
- "business": true
- },
- "schedule": {
- "scheduledDeparture": "2026-04-06T10:38:00+03:00",
- "scheduledArrival": "2026-04-06T12:12:00+03:00",
- "duration": "1ч. 34мин.",
- "utcOffset": "UTC+03:00",
- "operatingDays": [1, 2, 3, 4, 5],
- "weekRange": "* Расписание на неделю 2026-04-06"
- },
- "lastUpdated": "10:32 2026.04.06"
- }
- ],
- "total": 1,
- "page": 1,
- "pageSize": 20
- }
- },
- "flightDetails": {
- "status": 200,
- "headers": {
- "Content-Type": "application/json",
- "X-Request-Id": "req-12347"
- },
- "body": {
- "id": "fl-1124",
- "flightNumber": "SU 1124",
- "airlineCode": "SU",
- "airlineName": "Aeroflot",
- "aircraftType": "Airbus A320",
- "direction": "departure",
- "status": "scheduled",
- "date": "2026-04-06",
- "departure": {
- "airportCode": "SVO",
- "airportName": "Sheremetyevo",
- "cityCode": "MOW",
- "cityName": "Moscow",
- "terminal": "B",
- "time": {
- "scheduled": "2026-04-06T12:13:00+03:00"
- }
- },
- "arrival": {
- "airportCode": "AER",
- "airportName": "Adler",
- "cityCode": "AER",
- "cityName": "Sochi",
- "time": {
- "scheduled": "2026-04-06T16:05:00+03:00"
- }
- },
- "boarding": {
- "gate": "11",
- "status": "Идёт посадка",
- "startTime": "2026-04-06T11:30:00+03:00",
- "endTime": "2026-04-06T12:05:00+03:00"
- },
- "checkin": {
- "status": "Закончена",
- "startTime": "2026-04-06T09:30:00+03:00",
- "endTime": "2026-04-06T11:45:00+03:00"
- },
- "aircraft": {
- "type": "Airbus A320",
- "name": "В. Высоцкий",
- "totalSeats": 158,
- "economySeats": 150,
- "businessSeats": 8,
- "previousFlight": "SU 1123"
- },
- "catering": {
- "economy": true,
- "business": true
- },
- "schedule": {
- "scheduledDeparture": "2026-04-06T12:13:00+03:00",
- "scheduledArrival": "2026-04-06T16:05:00+03:00",
- "duration": "3ч. 52мин.",
- "utcOffset": "UTC+03:00",
- "operatingDays": [1, 2, 3, 4, 5],
- "weekRange": "* Расписание на неделю 2026-04-06"
- },
- "lastUpdated": "12:15 2026.04.06"
- }
- },
- "scheduleSearch": {
- "status": 200,
- "headers": {
- "Content-Type": "application/json",
- "X-Request-Id": "req-12348"
- },
- "body": {
- "from": "MOW",
- "to": "AER",
- "dateFrom": "2026-04-06",
- "dateTo": "2026-04-12",
- "entries": [
- {
- "id": "sch-001",
- "flightNumber": "SU 1124",
- "airlineCode": "SU",
- "airlineName": "Aeroflot",
- "aircraftType": "Airbus A320",
- "departureCity": "Moscow",
- "departureCityCode": "MOW",
- "departureAirport": "Sheremetyevo",
- "departureTime": "12:13",
- "arrivalCity": "Sochi",
- "arrivalCityCode": "AER",
- "arrivalAirport": "Adler",
- "arrivalTime": "16:05",
- "daysOfWeek": [1, 2, 3, 4, 5, 6, 7],
- "effectiveFrom": "2026-01-01",
- "effectiveTo": "2026-12-31",
- "direct": true
- },
- {
- "id": "sch-002",
- "flightNumber": "SU 1234",
- "airlineCode": "SU",
- "airlineName": "Aeroflot",
- "aircraftType": "Airbus A321",
- "departureCity": "Moscow",
- "departureCityCode": "MOW",
- "departureAirport": "SVO",
- "departureTime": "08:00",
- "arrivalCity": "Sochi",
- "arrivalCityCode": "AER",
- "arrivalAirport": "Adler",
- "arrivalTime": "10:15",
- "daysOfWeek": [1, 2, 3, 4, 5, 6, 7],
- "effectiveFrom": "2026-01-01",
- "effectiveTo": "2026-12-31",
- "direct": true
- }
- ],
- "total": 2,
- "page": 1,
- "pageSize": 20
- }
- },
- "flightsMap": {
- "status": 200,
- "headers": {
- "Content-Type": "application/json",
- "X-Request-Id": "req-12349"
- },
- "body": {
- "flights": [
- {
- "id": "fl-1124",
- "flightNumber": "SU 1124",
- "airlineCode": "SU",
- "airlineName": "Aeroflot",
- "aircraftType": "Airbus A320",
- "direction": "departure",
- "status": "scheduled",
- "date": "2026-04-06",
- "departure": {
- "airportCode": "SVO",
- "airportName": "Sheremetyevo",
- "cityCode": "MOW",
- "cityName": "Moscow",
- "latitude": 55.9721,
- "longitude": 37.4146,
- "time": {
- "scheduled": "2026-04-06T12:13:00+03:00"
- }
- },
- "arrival": {
- "airportCode": "AER",
- "airportName": "Adler",
- "cityCode": "AER",
- "cityName": "Sochi",
- "latitude": 43.58,
- "longitude": 39.72,
- "time": {
- "scheduled": "2026-04-06T16:05:00+03:00"
- }
- }
- }
- ],
- "total": 1
- }
- },
- "popularRequests": {
- "status": 200,
- "headers": {
- "Content-Type": "application/json",
- "X-Request-Id": "req-12350"
- },
- "body": {
- "requests": [
- {
- "id": "pop-001",
- "departureCity": "Moscow",
- "departureCityCode": "MOW",
- "arrivalCity": "Sochi",
- "arrivalCityCode": "AER",
- "flightCount": 45,
- "dates": ["2026-04-06", "2026-04-07", "2026-04-08"],
- "timestamp": "2026-04-06T10:30:00Z"
- },
- {
- "id": "pop-002",
- "departureCity": "Moscow",
- "departureCityCode": "MOW",
- "arrivalCity": "Saint Petersburg",
- "arrivalCityCode": "LED",
- "flightCount": 38,
- "dates": ["2026-04-06", "2026-04-07", "2026-04-08"],
- "timestamp": "2026-04-06T10:25:00Z"
- },
- {
- "id": "pop-003",
- "departureCity": "Moscow",
- "departureCityCode": "MOW",
- "arrivalCity": "Novosibirsk",
- "arrivalCityCode": "OVB",
- "flightCount": 28,
- "dates": ["2026-04-06", "2026-04-07", "2026-04-08"],
- "timestamp": "2026-04-06T10:20:00Z"
- }
- ],
- "total": 3
- }
- }
- }
-}
diff --git a/tests/e2e-angular/fixtures/cities.json b/tests/e2e-angular/fixtures/cities.json
deleted file mode 100644
index 99490978..00000000
--- a/tests/e2e-angular/fixtures/cities.json
+++ /dev/null
@@ -1,184 +0,0 @@
-{
- "cities": [
- {
- "code": "MOW",
- "name": "Moscow",
- "nameRu": "Москва",
- "latitude": 55.7558,
- "longitude": 37.6173,
- "country": "Russia",
- "countryCode": "RU"
- },
- {
- "code": "LED",
- "name": "Saint Petersburg",
- "nameRu": "Санкт-Петербург",
- "latitude": 59.9311,
- "longitude": 30.3609,
- "country": "Russia",
- "countryCode": "RU"
- },
- {
- "code": "AER",
- "name": "Sochi",
- "nameRu": "Сочи",
- "latitude": 43.58,
- "longitude": 39.72,
- "country": "Russia",
- "countryCode": "RU"
- },
- {
- "code": "AAQ",
- "name": "Anapa",
- "nameRu": "Анапа",
- "latitude": 44.8857,
- "longitude": 37.3199,
- "country": "Russia",
- "countryCode": "RU"
- },
- {
- "code": "OVB",
- "name": "Novosibirsk",
- "nameRu": "Новосибирск",
- "latitude": 55.0253,
- "longitude": 82.9357,
- "country": "Russia",
- "countryCode": "RU"
- },
- {
- "code": "KRR",
- "name": "Krasnodar",
- "nameRu": "Краснодар",
- "latitude": 45.0347,
- "longitude": 38.9971,
- "country": "Russia",
- "countryCode": "RU"
- },
- {
- "code": "SVX",
- "name": "Yekaterinburg",
- "nameRu": "Екатеринбург",
- "latitude": 56.8389,
- "longitude": 60.6057,
- "country": "Russia",
- "countryCode": "RU"
- },
- {
- "code": "KJA",
- "name": "Krasnoyarsk",
- "nameRu": "Красноярск",
- "latitude": 56.0154,
- "longitude": 92.8932,
- "country": "Russia",
- "countryCode": "RU"
- },
- {
- "code": "GOJ",
- "name": "Nizhny Novgorod",
- "nameRu": "Нижний Новгород",
- "latitude": 56.2965,
- "longitude": 43.9361,
- "country": "Russia",
- "countryCode": "RU"
- },
- {
- "code": "KUF",
- "name": "Samara",
- "nameRu": "Самара",
- "latitude": 53.2333,
- "longitude": 50.15,
- "country": "Russia",
- "countryCode": "RU"
- },
- {
- "code": "UFA",
- "name": "Ufa",
- "nameRu": "Уфа",
- "latitude": 54.6016,
- "longitude": 55.9286,
- "country": "Russia",
- "countryCode": "RU"
- },
- {
- "code": "KZN",
- "name": "Kazan",
- "nameRu": "Казань",
- "latitude": 55.6064,
- "longitude": 49.1677,
- "country": "Russia",
- "countryCode": "RU"
- },
- {
- "code": "ROV",
- "name": "Rostov-on-Don",
- "nameRu": "Ростов-на-Дону",
- "latitude": 47.2357,
- "longitude": 39.8687,
- "country": "Russia",
- "countryCode": "RU"
- },
- {
- "code": "VVO",
- "name": "Vladivostok",
- "nameRu": "Владивосток",
- "latitude": 43.1611,
- "longitude": 131.9167,
- "country": "Russia",
- "countryCode": "RU"
- },
- {
- "code": "KHV",
- "name": "Khabarovsk",
- "nameRu": "Хабаровск",
- "latitude": 50.4226,
- "longitude": 136.9873,
- "country": "Russia",
- "countryCode": "RU"
- },
- {
- "code": "IKT",
- "name": "Irkutsk",
- "nameRu": "Иркутск",
- "latitude": 52.268,
- "longitude": 104.3886,
- "country": "Russia",
- "countryCode": "RU"
- },
- {
- "code": "OMS",
- "name": "Omsk",
- "nameRu": "Омск",
- "latitude": 54.9711,
- "longitude": 73.3058,
- "country": "Russia",
- "countryCode": "RU"
- },
- {
- "code": "KGD",
- "name": "Kaliningrad",
- "nameRu": "Калининград",
- "latitude": 54.6897,
- "longitude": 20.5379,
- "country": "Russia",
- "countryCode": "RU"
- },
- {
- "code": "MRV",
- "name": "Mineralnye Vody",
- "nameRu": "Минеральные Воды",
- "latitude": 44.2361,
- "longitude": 43.0817,
- "country": "Russia",
- "countryCode": "RU"
- },
- {
- "code": "MCX",
- "name": "Makhachkala",
- "nameRu": "Махачкала",
- "latitude": 42.8162,
- "longitude": 47.5867,
- "country": "Russia",
- "countryCode": "RU"
- }
- ]
-}
diff --git a/tests/e2e-angular/fixtures/errors.json b/tests/e2e-angular/fixtures/errors.json
deleted file mode 100644
index 3fc3d1f1..00000000
--- a/tests/e2e-angular/fixtures/errors.json
+++ /dev/null
@@ -1,168 +0,0 @@
-{
- "errors": {
- "notFound": {
- "status": 404,
- "headers": {
- "Content-Type": "application/json",
- "X-Request-Id": "err-12345"
- },
- "body": {
- "error": "Not Found",
- "message": "The requested resource was not found",
- "path": "/api/flights/unknown",
- "timestamp": "2026-04-06T10:30:00Z"
- }
- },
- "badRequest": {
- "status": 400,
- "headers": {
- "Content-Type": "application/json",
- "X-Request-Id": "err-12346"
- },
- "body": {
- "error": "Bad Request",
- "message": "Invalid request parameters",
- "details": {
- "field": "date",
- "value": "invalid-date",
- "message": "Date format should be YYYY-MM-DD"
- },
- "timestamp": "2026-04-06T10:30:00Z"
- }
- },
- "unauthorized": {
- "status": 401,
- "headers": {
- "Content-Type": "application/json",
- "X-Request-Id": "err-12347"
- },
- "body": {
- "error": "Unauthorized",
- "message": "Authentication required",
- "code": "AUTH_REQUIRED",
- "timestamp": "2026-04-06T10:30:00Z"
- }
- },
- "forbidden": {
- "status": 403,
- "headers": {
- "Content-Type": "application/json",
- "X-Request-Id": "err-12348"
- },
- "body": {
- "error": "Forbidden",
- "message": "Access denied",
- "code": "ACCESS_DENIED",
- "timestamp": "2026-04-06T10:30:00Z"
- }
- },
- "serverError": {
- "status": 500,
- "headers": {
- "Content-Type": "application/json",
- "X-Request-Id": "err-12349"
- },
- "body": {
- "error": "Internal Server Error",
- "message": "An unexpected error occurred",
- "code": "INTERNAL_ERROR",
- "traceId": "abc123def456",
- "timestamp": "2026-04-06T10:30:00Z"
- }
- },
- "timeout": {
- "status": 504,
- "headers": {
- "Content-Type": "application/json",
- "X-Request-Id": "err-12350"
- },
- "body": {
- "error": "Gateway Timeout",
- "message": "The request took too long to process",
- "code": "GATEWAY_TIMEOUT",
- "timeout": 30000,
- "timestamp": "2026-04-06T10:30:00Z"
- }
- },
- "validationError": {
- "status": 422,
- "headers": {
- "Content-Type": "application/json",
- "X-Request-Id": "err-12351"
- },
- "body": {
- "error": "Validation Error",
- "message": "Request validation failed",
- "details": [
- {
- "field": "departureCity",
- "message": "Departure city is required"
- },
- {
- "field": "arrivalCity",
- "message": "Arrival city is required"
- }
- ],
- "timestamp": "2026-04-06T10:30:00Z"
- }
- },
- "rateLimit": {
- "status": 429,
- "headers": {
- "Content-Type": "application/json",
- "X-Request-Id": "err-12352",
- "Retry-After": "60"
- },
- "body": {
- "error": "Too Many Requests",
- "message": "Rate limit exceeded",
- "code": "RATE_LIMIT_EXCEEDED",
- "retryAfter": 60,
- "timestamp": "2026-04-06T10:30:00Z"
- }
- },
- "serviceUnavailable": {
- "status": 503,
- "headers": {
- "Content-Type": "application/json",
- "X-Request-Id": "err-12353"
- },
- "body": {
- "error": "Service Unavailable",
- "message": "The service is temporarily unavailable",
- "code": "SERVICE_UNAVAILABLE",
- "retryAfter": 30,
- "timestamp": "2026-04-06T10:30:00Z"
- }
- },
- "flightNotFound": {
- "status": 404,
- "headers": {
- "Content-Type": "application/json",
- "X-Request-Id": "err-12354"
- },
- "body": {
- "error": "Flight Not Found",
- "message": "Flight SU 9999 not found for date 2026-04-06",
- "flightNumber": "SU 9999",
- "date": "2026-04-06",
- "timestamp": "2026-04-06T10:30:00Z"
- }
- },
- "invalidDate": {
- "status": 400,
- "headers": {
- "Content-Type": "application/json",
- "X-Request-Id": "err-12355"
- },
- "body": {
- "error": "Invalid Date",
- "message": "Date must be within valid range",
- "minDate": "2026-01-01",
- "maxDate": "2026-12-31",
- "providedDate": "2025-01-01",
- "timestamp": "2026-04-06T10:30:00Z"
- }
- }
- }
-}
diff --git a/tests/e2e-angular/fixtures/flights.json b/tests/e2e-angular/fixtures/flights.json
deleted file mode 100644
index 4073599e..00000000
--- a/tests/e2e-angular/fixtures/flights.json
+++ /dev/null
@@ -1,443 +0,0 @@
-{
- "flights": {
- "domestic": {
- "su123": {
- "number": "123",
- "carrier": "SU",
- "route": "MOW-LED",
- "aircraftType": "Airbus A320",
- "duration": "1h 45m",
- "departureAirport": "SVO",
- "arrivalAirport": "LED"
- },
- "su234": {
- "number": "234",
- "carrier": "SU",
- "route": "MOW-AER",
- "aircraftType": "Airbus A321",
- "duration": "2h 15m",
- "departureAirport": "SVO",
- "arrivalAirport": "AER"
- },
- "su345": {
- "number": "345",
- "carrier": "SU",
- "route": "LED-AER",
- "aircraftType": "Boeing 737-800",
- "duration": "3h 30m",
- "departureAirport": "LED",
- "arrivalAirport": "AER"
- },
- "su456": {
- "number": "456",
- "carrier": "SU",
- "route": "OVB-MOW",
- "aircraftType": "Boeing 777-300",
- "duration": "4h 15m",
- "departureAirport": "OVB",
- "arrivalAirport": "SVO"
- },
- "su567": {
- "number": "567",
- "carrier": "SU",
- "route": "KRR-MOW",
- "aircraftType": "Airbus A320",
- "duration": "2h 0m",
- "departureAirport": "KRR",
- "arrivalAirport": "VKO"
- }
- },
- "international": {
- "su456": {
- "number": "456",
- "carrier": "SU",
- "route": "MOW-Paris",
- "aircraftType": "Airbus A330",
- "duration": "4h 30m",
- "departureAirport": "SVO",
- "arrivalAirport": "CDG"
- },
- "su789": {
- "number": "789",
- "carrier": "SU",
- "route": "MOW-Tokyo",
- "aircraftType": "Boeing 777-300ER",
- "duration": "9h 15m",
- "departureAirport": "SVO",
- "arrivalAirport": "NRT"
- },
- "su101": {
- "number": "101",
- "carrier": "SU",
- "route": "MOW-Beijing",
- "aircraftType": "Airbus A330",
- "duration": "7h 45m",
- "departureAirport": "SVO",
- "arrivalAirport": "PEK"
- },
- "su202": {
- "number": "202",
- "carrier": "SU",
- "route": "LED-Dubai",
- "aircraftType": "Airbus A330",
- "duration": "5h 30m",
- "departureAirport": "LED",
- "arrivalAirport": "DXB"
- },
- "su303": {
- "number": "303",
- "carrier": "SU",
- "route": "MOW-Berlin",
- "aircraftType": "Airbus A320",
- "duration": "2h 45m",
- "departureAirport": "SVO",
- "arrivalAirport": "BER"
- }
- },
- "scheduled": {
- "su1124": {
- "id": "fl-1124",
- "flightNumber": "SU 1124",
- "airlineCode": "SU",
- "airlineName": "Aeroflot",
- "direction": "departure",
- "status": "scheduled",
- "date": "2026-04-06",
- "departure": {
- "airportCode": "SVO",
- "airportName": "Sheremetyevo",
- "cityCode": "MOW",
- "cityName": "Moscow",
- "terminal": "B",
- "time": {
- "scheduled": "2026-04-06T12:13:00+03:00"
- }
- },
- "arrival": {
- "airportCode": "AER",
- "airportName": "Adler",
- "cityCode": "AER",
- "cityName": "Sochi",
- "time": {
- "scheduled": "2026-04-06T16:05:00+03:00"
- }
- },
- "aircraft": {
- "type": "Airbus A320",
- "totalSeats": 158,
- "economySeats": 150,
- "businessSeats": 8
- },
- "schedule": {
- "scheduledDeparture": "2026-04-06T12:13:00+03:00",
- "scheduledArrival": "2026-04-06T16:05:00+03:00",
- "duration": "3ч. 52мин.",
- "utcOffset": "UTC+03:00",
- "operatingDays": [1, 2, 3, 4, 5]
- }
- },
- "su1076": {
- "id": "fl-1076",
- "flightNumber": "SU 1076",
- "airlineCode": "SU",
- "airlineName": "Aeroflot",
- "direction": "departure",
- "status": "scheduled",
- "date": "2026-04-06",
- "departure": {
- "airportCode": "SVO",
- "airportName": "Sheremetyevo",
- "cityCode": "MOW",
- "cityName": "Moscow",
- "terminal": "B",
- "time": {
- "scheduled": "2026-04-06T12:14:00+03:00"
- }
- },
- "arrival": {
- "airportCode": "OVB",
- "airportName": "Tolmachevo",
- "cityCode": "OVB",
- "cityName": "Novosibirsk",
- "time": {
- "scheduled": "2026-04-06T20:25:00+03:00"
- }
- },
- "aircraft": {
- "type": "Boeing 737-800",
- "totalSeats": 189,
- "economySeats": 175,
- "businessSeats": 14
- },
- "schedule": {
- "scheduledDeparture": "2026-04-06T12:14:00+03:00",
- "scheduledArrival": "2026-04-06T20:25:00+03:00",
- "duration": "8ч. 11мин.",
- "utcOffset": "UTC+03:00",
- "operatingDays": [1, 2, 3, 4, 5, 6, 7]
- }
- },
- "su6170": {
- "id": "fl-6170",
- "flightNumber": "SU 6170",
- "airlineCode": "FV",
- "airlineName": "Rossiya",
- "direction": "departure",
- "status": "scheduled",
- "date": "2026-04-06",
- "departure": {
- "airportCode": "VKO",
- "airportName": "Vnukovo - A",
- "cityCode": "MOW",
- "cityName": "Moscow",
- "time": {
- "scheduled": "2026-04-06T12:15:00+03:00"
- }
- },
- "arrival": {
- "airportCode": "LED",
- "airportName": "Pulkovo - 1",
- "cityCode": "LED",
- "cityName": "Saint Petersburg",
- "time": {
- "scheduled": "2026-04-06T13:44:00+03:00"
- }
- },
- "aircraft": {
- "type": "Sukhoi SuperJet 100",
- "totalSeats": 98,
- "economySeats": 88,
- "businessSeats": 10
- },
- "schedule": {
- "scheduledDeparture": "2026-04-06T12:15:00+03:00",
- "scheduledArrival": "2026-04-06T13:44:00+03:00",
- "duration": "1ч. 29мин.",
- "utcOffset": "UTC+03:00",
- "operatingDays": [1, 2, 3, 4, 5, 6, 7]
- }
- }
- },
- "arrived": {
- "su1455": {
- "id": "fl-1455",
- "flightNumber": "SU 1455",
- "airlineCode": "SU",
- "airlineName": "Aeroflot",
- "direction": "arrival",
- "status": "arrived",
- "date": "2026-04-06",
- "departure": {
- "airportCode": "UUD",
- "airportName": "Ulan-Ude",
- "cityCode": "UUD",
- "cityName": "Ulan-Ude",
- "time": {
- "scheduled": "2026-04-06T10:38:00+03:00",
- "actual": "2026-04-06T10:38:00+03:00"
- }
- },
- "arrival": {
- "airportCode": "SVO",
- "airportName": "Sheremetyevo",
- "cityCode": "MOW",
- "cityName": "Moscow",
- "time": {
- "scheduled": "2026-04-06T12:12:00+03:00",
- "actual": "2026-04-06T12:12:00+03:00"
- }
- },
- "arrivalInfo": {
- "baggageBelt": "5",
- "transfer": "Тран"
- },
- "aircraft": {
- "type": "Airbus A320",
- "totalSeats": 158,
- "economySeats": 150,
- "businessSeats": 8
- },
- "schedule": {
- "scheduledDeparture": "2026-04-06T10:38:00+03:00",
- "scheduledArrival": "2026-04-06T12:12:00+03:00",
- "duration": "1ч. 34мин.",
- "utcOffset": "UTC+03:00",
- "operatingDays": [1, 2, 3, 4, 5]
- }
- },
- "su1483": {
- "id": "fl-1483",
- "flightNumber": "SU 1483",
- "airlineCode": "SU",
- "airlineName": "Aeroflot",
- "direction": "arrival",
- "status": "arrived",
- "date": "2026-04-06",
- "departure": {
- "airportCode": "KJA",
- "airportName": "Emelyanovo",
- "cityCode": "KJA",
- "cityName": "Krasnoyarsk",
- "time": {
- "scheduled": "2026-04-06T11:01:00+03:00",
- "actual": "2026-04-06T11:01:00+03:00"
- }
- },
- "arrival": {
- "airportCode": "SVO",
- "airportName": "Sheremetyevo",
- "cityCode": "MOW",
- "cityName": "Moscow",
- "time": {
- "scheduled": "2026-04-06T12:18:00+03:00",
- "actual": "2026-04-06T12:16:00+03:00"
- }
- },
- "aircraft": {
- "type": "Boeing 737-800",
- "totalSeats": 189,
- "economySeats": 175,
- "businessSeats": 14
- },
- "schedule": {
- "scheduledDeparture": "2026-04-06T11:01:00+03:00",
- "scheduledArrival": "2026-04-06T12:18:00+03:00",
- "duration": "1ч. 17мин.",
- "utcOffset": "UTC+03:00",
- "operatingDays": [1, 2, 3, 4, 5, 6, 7]
- }
- }
- },
- "delayed": {
- "su6245": {
- "id": "fl-6245",
- "flightNumber": "SU 6245",
- "airlineCode": "FV",
- "airlineName": "Rossiya",
- "direction": "departure",
- "status": "delayed",
- "date": "2026-04-06",
- "departure": {
- "airportCode": "SVO",
- "airportName": "Sheremetyevo",
- "cityCode": "MOW",
- "cityName": "Moscow",
- "time": {
- "scheduled": "2026-04-06T12:30:00+03:00",
- "actual": "2026-04-06T12:30:00+03:00",
- "expected": "2026-04-06T12:45:00+03:00"
- }
- },
- "arrival": {
- "airportCode": "LED",
- "airportName": "Pulkovo - 1",
- "cityCode": "LED",
- "cityName": "Saint Petersburg",
- "time": {
- "scheduled": "2026-04-06T13:57:00+03:00",
- "expected": "2026-04-06T14:12:00+03:00"
- }
- },
- "aircraft": {
- "type": "Boeing 737-800",
- "totalSeats": 189,
- "economySeats": 175,
- "businessSeats": 14
- },
- "schedule": {
- "scheduledDeparture": "2026-04-06T12:30:00+03:00",
- "scheduledArrival": "2026-04-06T13:57:00+03:00",
- "duration": "1ч. 27мин.",
- "utcOffset": "UTC+03:00",
- "operatingDays": [1, 2, 3, 4, 5, 6, 7]
- }
- },
- "su1400": {
- "id": "fl-1400",
- "flightNumber": "SU 1400",
- "airlineCode": "SU",
- "airlineName": "Aeroflot",
- "direction": "departure",
- "status": "delayed",
- "date": "2026-04-06",
- "departure": {
- "airportCode": "SVO",
- "airportName": "Sheremetyevo",
- "cityCode": "MOW",
- "cityName": "Moscow",
- "terminal": "B",
- "time": {
- "scheduled": "2026-04-06T13:10:00+03:00",
- "actual": "2026-04-06T13:10:00+03:00",
- "expected": "2026-04-06T13:30:00+03:00"
- }
- },
- "arrival": {
- "airportCode": "VVO",
- "airportName": "Knevichi",
- "cityCode": "VVO",
- "cityName": "Vladivostok",
- "time": {
- "scheduled": "2026-04-06T05:25:00+03:00",
- "expected": "2026-04-06T05:45:00+03:00"
- }
- },
- "aircraft": {
- "type": "Boeing 777-300ER",
- "totalSeats": 402,
- "economySeats": 375,
- "businessSeats": 27
- },
- "schedule": {
- "scheduledDeparture": "2026-04-06T13:10:00+03:00",
- "scheduledArrival": "2026-04-06T05:25:00+03:00",
- "duration": "8ч. 15мин.",
- "utcOffset": "UTC+03:00",
- "operatingDays": [1, 2, 3, 4, 5, 6, 7]
- }
- }
- },
- "cancelled": {
- "su6132": {
- "id": "fl-6132",
- "flightNumber": "SU 6132",
- "airlineCode": "FV",
- "airlineName": "Rossiya",
- "direction": "departure",
- "status": "cancelled",
- "date": "2026-04-06",
- "departure": {
- "airportCode": "LED",
- "airportName": "Pulkovo - 1",
- "cityCode": "LED",
- "cityName": "Saint Petersburg",
- "time": {
- "scheduled": "2026-04-06T11:00:00+03:00"
- }
- },
- "arrival": {
- "airportCode": "SVO",
- "airportName": "Sheremetyevo",
- "cityCode": "MOW",
- "cityName": "Moscow",
- "time": {
- "scheduled": "2026-04-06T12:30:00+03:00"
- }
- },
- "aircraft": {
- "type": "Boeing 737-800",
- "totalSeats": 189,
- "economySeats": 175,
- "businessSeats": 14
- },
- "schedule": {
- "scheduledDeparture": "2026-04-06T11:00:00+03:00",
- "scheduledArrival": "2026-04-06T12:30:00+03:00",
- "duration": "1ч. 30мин.",
- "utcOffset": "UTC+03:00",
- "operatingDays": [1, 2, 3, 4, 5, 6, 7]
- }
- }
- }
- }
-}
diff --git a/tests/e2e-angular/fixtures/routes.json b/tests/e2e-angular/fixtures/routes.json
deleted file mode 100644
index 990017ac..00000000
--- a/tests/e2e-angular/fixtures/routes.json
+++ /dev/null
@@ -1,252 +0,0 @@
-{
- "routes": {
- "moscow-sochi": {
- "departure": "MOW",
- "arrival": "AER",
- "departureCity": "Moscow",
- "arrivalCity": "Sochi",
- "departureAirport": "SVO",
- "arrivalAirport": "Adler",
- "duration": "2h 15m",
- "aircraftType": "Airbus A321",
- "airline": "Aeroflot",
- "flights": [
- {
- "flightNumber": "SU 1234",
- "departureTime": "08:00",
- "arrivalTime": "10:15",
- "days": [1, 2, 3, 4, 5, 6, 7]
- },
- {
- "flightNumber": "SU 1235",
- "departureTime": "12:00",
- "arrivalTime": "14:15",
- "days": [1, 2, 3, 4, 5, 6, 7]
- },
- {
- "flightNumber": "SU 1236",
- "departureTime": "16:00",
- "arrivalTime": "18:15",
- "days": [1, 2, 3, 4, 5]
- }
- ]
- },
- "moscow-stPetersburg": {
- "departure": "MOW",
- "arrival": "LED",
- "departureCity": "Moscow",
- "arrivalCity": "Saint Petersburg",
- "departureAirport": "SVO",
- "arrivalAirport": "Pulkovo",
- "duration": "1h 45m",
- "aircraftType": "Airbus A320",
- "airline": "Aeroflot",
- "flights": [
- {
- "flightNumber": "SU 1101",
- "departureTime": "06:00",
- "arrivalTime": "07:45",
- "days": [1, 2, 3, 4, 5, 6, 7]
- },
- {
- "flightNumber": "SU 1102",
- "departureTime": "08:30",
- "arrivalTime": "10:15",
- "days": [1, 2, 3, 4, 5, 6, 7]
- },
- {
- "flightNumber": "SU 1103",
- "departureTime": "10:00",
- "arrivalTime": "11:45",
- "days": [1, 2, 3, 4, 5, 6, 7]
- },
- {
- "flightNumber": "SU 1104",
- "departureTime": "12:30",
- "arrivalTime": "14:15",
- "days": [1, 2, 3, 4, 5, 6, 7]
- },
- {
- "flightNumber": "SU 1105",
- "departureTime": "15:00",
- "arrivalTime": "16:45",
- "days": [1, 2, 3, 4, 5, 6, 7]
- },
- {
- "flightNumber": "SU 1106",
- "departureTime": "17:30",
- "arrivalTime": "19:15",
- "days": [1, 2, 3, 4, 5, 6, 7]
- },
- {
- "flightNumber": "SU 1107",
- "departureTime": "20:00",
- "arrivalTime": "21:45",
- "days": [1, 2, 3, 4, 5, 6, 7]
- }
- ]
- },
- "moscow-sochi-return": {
- "departure": "AER",
- "arrival": "MOW",
- "departureCity": "Sochi",
- "arrivalCity": "Moscow",
- "departureAirport": "Adler",
- "arrivalAirport": "SVO",
- "duration": "2h 10m",
- "aircraftType": "Airbus A321",
- "airline": "Aeroflot",
- "flights": [
- {
- "flightNumber": "SU 1240",
- "departureTime": "09:00",
- "arrivalTime": "11:10",
- "days": [1, 2, 3, 4, 5, 6, 7]
- },
- {
- "flightNumber": "SU 1241",
- "departureTime": "13:00",
- "arrivalTime": "15:10",
- "days": [1, 2, 3, 4, 5, 6, 7]
- },
- {
- "flightNumber": "SU 1242",
- "departureTime": "17:00",
- "arrivalTime": "19:10",
- "days": [1, 2, 3, 4, 5]
- }
- ]
- },
- "moscow-novosibirsk": {
- "departure": "MOW",
- "arrival": "OVB",
- "departureCity": "Moscow",
- "arrivalCity": "Novosibirsk",
- "departureAirport": "SVO",
- "arrivalAirport": "Tolmachevo",
- "duration": "4h 15m",
- "aircraftType": "Boeing 737-800",
- "airline": "Aeroflot",
- "flights": [
- {
- "flightNumber": "SU 1501",
- "departureTime": "07:00",
- "arrivalTime": "15:15",
- "days": [1, 2, 3, 4, 5]
- },
- {
- "flightNumber": "SU 1502",
- "departureTime": "12:00",
- "arrivalTime": "20:15",
- "days": [1, 2, 3, 4, 5]
- },
- {
- "flightNumber": "SU 1503",
- "departureTime": "18:00",
- "arrivalTime": "02:15",
- "days": [1, 2, 3, 4, 5, 6, 7]
- }
- ]
- },
- "moscow-krasnodar": {
- "departure": "MOW",
- "arrival": "KRR",
- "departureCity": "Moscow",
- "arrivalCity": "Krasnodar",
- "departureAirport": "VKO",
- "arrivalAirport": "Pashkovsky",
- "duration": "2h 0m",
- "aircraftType": "Airbus A320",
- "airline": "Rossiya",
- "flights": [
- {
- "flightNumber": "FV 1201",
- "departureTime": "08:00",
- "arrivalTime": "10:00",
- "days": [1, 2, 3, 4, 5, 6, 7]
- },
- {
- "flightNumber": "FV 1202",
- "departureTime": "14:00",
- "arrivalTime": "16:00",
- "days": [1, 2, 3, 4, 5, 6, 7]
- },
- {
- "flightNumber": "FV 1203",
- "departureTime": "20:00",
- "arrivalTime": "22:00",
- "days": [1, 2, 3, 4, 5]
- }
- ]
- },
- "moscow-khabarovsk": {
- "departure": "MOW",
- "arrival": "KHV",
- "departureCity": "Moscow",
- "arrivalCity": "Khabarovsk",
- "departureAirport": "SVO",
- "arrivalAirport": "Knevichi",
- "duration": "9h 0m",
- "aircraftType": "Boeing 777-300ER",
- "airline": "Aeroflot",
- "flights": [
- {
- "flightNumber": "SU 1901",
- "departureTime": "09:00",
- "arrivalTime": "21:00",
- "days": [1, 2, 3, 4, 5, 6, 7]
- },
- {
- "flightNumber": "SU 1902",
- "departureTime": "15:00",
- "arrivalTime": "03:00",
- "days": [1, 3, 5, 7]
- }
- ]
- },
- "moscow-ankara": {
- "departure": "MOW",
- "arrival": "ESB",
- "departureCity": "Moscow",
- "arrivalCity": "Ankara",
- "departureAirport": "SVO",
- "arrivalAirport": "ESB",
- "duration": "4h 30m",
- "aircraftType": "Airbus A330",
- "airline": "Aeroflot",
- "flights": [
- {
- "flightNumber": "SU 2001",
- "departureTime": "10:00",
- "arrivalTime": "13:30",
- "days": [1, 2, 3, 4, 5, 6, 7]
- }
- ]
- },
- "moscow-berlin": {
- "departure": "MOW",
- "arrival": "BER",
- "departureCity": "Moscow",
- "arrivalCity": "Berlin",
- "departureAirport": "SVO",
- "arrivalAirport": "BER",
- "duration": "2h 45m",
- "aircraftType": "Airbus A320",
- "airline": "Aeroflot",
- "flights": [
- {
- "flightNumber": "SU 2101",
- "departureTime": "08:00",
- "arrivalTime": "10:45",
- "days": [1, 2, 3, 4, 5, 6, 7]
- },
- {
- "flightNumber": "SU 2102",
- "departureTime": "14:00",
- "arrivalTime": "16:45",
- "days": [1, 2, 3, 4, 5]
- }
- ]
- }
- }
-}
diff --git a/tests/e2e-angular/flight-details.spec.ts b/tests/e2e-angular/flight-details.spec.ts
deleted file mode 100644
index 0b98236e..00000000
--- a/tests/e2e-angular/flight-details.spec.ts
+++ /dev/null
@@ -1,1081 +0,0 @@
-import { test, expect } from '@playwright/test';
-
-const BASE_URL = process.env.BASE_URL || 'http://localhost:5173';
-
-// Test flight IDs - using realistic SU (Aeroflot) flight numbers
-const TEST_FLIGHTS = {
- su1402: 'SU1402', // Moscow to New York
- su200: 'SU200', // Moscow to Paris
- su500: 'SU500', // Moscow to Tokyo
-};
-
-test.describe('Flight Details Page (US-47 to US-49)', () => {
- test.describe('US-47: Flight Details Page', () => {
- test('should load flight details page with proper layout', async ({ page }) => {
- // Navigate to flight details page
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
-
- // Wait for page to load
- await page.waitForLoadState('networkidle');
-
- // Check for main page elements
- const mainContent = page.locator('main');
- await expect(mainContent).toBeVisible();
- });
-
- test('should display loading state initially', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
-
- // Check for content after load
- await page.waitForLoadState('networkidle');
- const content = page.locator('body');
- await expect(content).toBeVisible();
- });
-
- test('should handle responsive layout on mobile viewport', async ({ page }) => {
- // Set mobile viewport
- await page.setViewportSize({ width: 375, height: 667 });
-
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- // Check for responsive container
- const container = page.locator('[class*="basicInfo"], [class*="statusDisplay"]');
- await expect(container.first()).toBeVisible();
- });
-
- test('should handle tablet viewport', async ({ page }) => {
- // Set tablet viewport
- await page.setViewportSize({ width: 768, height: 1024 });
-
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- const container = page.locator('[class*="basicInfo"], [class*="statusDisplay"]');
- await expect(container.first()).toBeVisible();
- });
-
- test('should display all layout sections in correct order', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- // Check for flight number in header
- const flightNumber = page.locator('span[class*="flightNumber"]');
- await expect(flightNumber).toBeVisible();
- });
-
- test('should be accessible with keyboard navigation', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- // Tab to first interactive element
- await page.keyboard.press('Tab');
-
- // Check if focus is on interactive element
- const focusedElement = await page.evaluate(() => document.activeElement?.tagName);
- expect(['BUTTON', 'A', 'INPUT', 'SELECT', 'TEXTAREA']).toContain(focusedElement);
- });
-
- test('should render without console errors', async ({ page }) => {
- const errors: string[] = [];
- page.on('console', (msg) => {
- if (msg.type() === 'error') {
- errors.push(msg.text());
- }
- });
-
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- // Filter out expected third-party errors if needed
- const appErrors = errors.filter((e) => !e.includes('third-party'));
- expect(appErrors.length).toBe(0);
- });
- });
-
- test.describe('US-48: Basic Flight Information', () => {
- test('should display flight number with proper formatting', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- // Flight number should be visible
- const flightNumber = page.locator('span[class*="flightNumber"]');
- await expect(flightNumber).toBeVisible();
-
- const text = await flightNumber.textContent();
- // Should contain flight number (with or without space: "SU 1402" or "SU1402")
- expect(text).toMatch(/SU\s?1402/);
- });
-
- test('should display departure time and city', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- // Look for departure section
- const departureCity = page.locator('[class*="segment"] [class*="city"]').first();
- await expect(departureCity).toBeVisible();
-
- const city = await departureCity.textContent();
- expect(city).toBeTruthy(); // Should contain city name
- });
-
- test('should display arrival time and city', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- // Look for arrival section (second city)
- const arrivalCity = page.locator('[class*="segment"] [class*="city"]').last();
- await expect(arrivalCity).toBeVisible();
-
- const city = await arrivalCity.textContent();
- expect(city).toBeTruthy(); // Should contain city name
- });
-
- test('should display aircraft type when available', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- // Look for aircraft section
- const aircraft = page.locator('[class*="aircraft"] [class*="value"]');
- if ((await aircraft.count()) > 0) {
- const aircraftText = await aircraft.textContent();
- expect(aircraftText).toBeTruthy();
- }
- });
-
- test('should display flight duration', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- // Look for duration text
- const duration = page.locator('[class*="durationText"]');
- if ((await duration.count()) > 0) {
- await expect(duration).toBeVisible();
- const text = await duration.textContent();
- expect(text).toMatch(/\d+h\s*\d+m/);
- }
- });
-
- test('should format times correctly in locale', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- // Look for time elements (HH:MM format)
- const timeElements = page.locator('[class*="time"]');
- if ((await timeElements.count()) > 0) {
- const firstTime = await timeElements.first().textContent();
- expect(firstTime).toMatch(/\d{2}:\d{2}/);
- }
- });
-
- test('should handle flights without aircraft type', async ({ page }) => {
- // This test ensures component doesn't crash without aircraft info
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su500}`);
- await page.waitForLoadState('networkidle');
-
- // Page should still render
- const container = page.locator('[class*="basicInfo"]');
- await expect(container).toBeVisible();
- });
-
- test('should be responsive on mobile', async ({ page }) => {
- await page.setViewportSize({ width: 375, height: 667 });
-
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- // Check that content is visible and not overlapping
- const basicInfo = page.locator('[class*="basicInfo"]');
- await expect(basicInfo).toBeVisible();
-
- const boundingBox = await basicInfo.boundingBox();
- expect(boundingBox?.width).toBeLessThanOrEqual(375);
- });
- });
-
- test.describe('US-49: Status and Status Details', () => {
- test('should display operational status', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- // Look for status display
- const statusIndicator = page.locator('[class*="statusIndicator"]');
- if ((await statusIndicator.count()) > 0) {
- await expect(statusIndicator.first()).toBeVisible();
- }
- });
-
- test('should display status remarks when available', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su200}`);
- await page.waitForLoadState('networkidle');
-
- // Look for remarks section
- const remarks = page.locator('[class*="remarks"]');
- if ((await remarks.count()) > 0) {
- await expect(remarks.first()).toBeVisible();
- }
- });
-
- test('should display last update timestamp', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- // Look for last update section
- const lastUpdate = page.locator('[class*="lastUpdate"]');
- if ((await lastUpdate.count()) > 0) {
- await expect(lastUpdate.first()).toBeVisible();
- const text = await lastUpdate.first().textContent();
- expect(text).toBeTruthy();
- }
- });
-
- test('should apply correct status color coding for scheduled status', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- // Status display should be present
- const statusDisplay = page.locator('[class*="statusDisplay"]');
- if ((await statusDisplay.count()) > 0) {
- await expect(statusDisplay.first()).toBeVisible();
- }
- });
-
- test('should handle different status types', async ({ page }) => {
- // Test with different flight
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su500}`);
- await page.waitForLoadState('networkidle');
-
- const statusDisplay = page.locator('[class*="statusDisplay"]');
- if ((await statusDisplay.count()) > 0) {
- await expect(statusDisplay.first()).toBeVisible();
- }
- });
-
- test('should display status message in correct locale (Russian)', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- // Check that page content is in Russian
- const html = await page.content();
- expect(html).toContain('ru-ru');
- });
-
- test('should display status message in correct locale (English)', async ({ page }) => {
- await page.goto(`${BASE_URL}/en-us/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- // Check that page content is in English
- const html = await page.content();
- expect(html).toContain('en-us');
- });
-
- test('should update status display when status changes', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- // Get initial status
- const statusDisplay = page.locator('[class*="statusDisplay"]');
- if ((await statusDisplay.count()) > 0) {
- await expect(statusDisplay.first()).toBeVisible();
- }
- });
-
- test('should format status timestamp in locale-aware manner', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- const lastUpdate = page.locator('[class*="lastUpdate"]');
- if ((await lastUpdate.count()) > 0) {
- const text = await lastUpdate.first().textContent();
- // Should contain time in HH:MM format
- expect(text).toMatch(/\d{2}:\d{2}/);
- }
- });
-
- test('should be responsive on mobile viewport', async ({ page }) => {
- await page.setViewportSize({ width: 375, height: 667 });
-
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- const statusDisplay = page.locator('[class*="statusDisplay"]');
- if ((await statusDisplay.count()) > 0) {
- await expect(statusDisplay.first()).toBeVisible();
- }
- });
- });
-
- test.describe('Integration: Full Flight Details Page', () => {
- test('should render all components together', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- // Check for both basic info and status display
- const basicInfo = page.locator('[class*="basicInfo"], span[class*="flightNumber"]');
- const statusDisplay = page.locator('[class*="statusDisplay"], [class*="statusIndicator"]');
-
- expect(await basicInfo.count()).toBeGreaterThan(0);
- });
-
- test('should display information without layout issues', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- // Check that main content area has reasonable width
- const mainContent = page.locator('main, [role="main"]');
- const box = await mainContent.first().boundingBox();
- expect(box?.width).toBeGreaterThan(0);
- });
-
- test('should support page refresh without errors', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- // Refresh page
- await page.reload();
- await page.waitForLoadState('networkidle');
-
- // Check that content is still visible
- const flightNumber = page.locator('span[class*="flightNumber"]');
- await expect(flightNumber).toBeVisible();
- });
-
- test('should support switching between locales', async ({ page }) => {
- // Start with Russian
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- const ruContent = await page.content();
-
- // Switch to English
- await page.goto(`${BASE_URL}/en-us/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- const enContent = await page.content();
-
- // Both should have flight details visible
- expect(ruContent).toContain('SU1402');
- expect(enContent).toContain('SU1402');
- });
-
- test('should handle rapid navigation between flights', async ({ page }) => {
- // Navigate to first flight
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- // Navigate to second flight
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su200}`);
- await page.waitForLoadState('networkidle');
-
- // Navigate to third flight
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su500}`);
- await page.waitForLoadState('networkidle');
-
- // Page should render correctly
- const flightNumber = page.locator('span[class*="flightNumber"]');
- await expect(flightNumber).toBeVisible();
- });
- });
-
- test.describe('US-50: Aircraft Information', () => {
- test('should display aircraft type', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- // Look for aircraft type display
- const aircraftType = page.locator('[class*="Aircraft"]');
- if ((await aircraftType.count()) > 0) {
- await expect(aircraftType.first()).toBeVisible();
- }
- });
-
- test('should display aircraft registration if available', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- // Look for registration section
- const registration = page.locator('[class*="registration"]');
- if ((await registration.count()) > 0) {
- await expect(registration.first()).toBeVisible();
- }
- });
-
- test('should display seat configuration', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- // Look for seats section
- const seats = page.locator('[class*="seats"]');
- if ((await seats.count()) > 0) {
- await expect(seats.first()).toBeVisible();
- }
- });
-
- test('should be responsive on mobile', async ({ page }) => {
- await page.setViewportSize({ width: 375, height: 667 });
-
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- const aircraft = page.locator('[class*="Aircraft"]');
- if ((await aircraft.count()) > 0) {
- const box = await aircraft.first().boundingBox();
- expect(box?.width).toBeLessThanOrEqual(375);
- }
- });
-
- test('should handle missing aircraft data', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su500}`);
- await page.waitForLoadState('networkidle');
-
- // Page should still render without errors
- const mainContent = page.locator('main');
- await expect(mainContent).toBeVisible();
- });
- });
-
- test.describe('US-51: Airline Information', () => {
- test('should display airline name', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- // Look for airline name
- const airlineName = page.locator('[class*="Airline"]');
- if ((await airlineName.count()) > 0) {
- const text = await airlineName.first().textContent();
- expect(text).toBeTruthy();
- }
- });
-
- test('should display airline logo', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- // Look for airline logo image
- const logo = page.locator('[class*="logo"]');
- if ((await logo.count()) > 0) {
- await expect(logo.first()).toBeVisible();
- }
- });
-
- test('should display airline IATA code', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- // Look for airline code
- const airlineCode = page.locator('[class*="code"]');
- if ((await airlineCode.count()) > 0) {
- const text = await airlineCode.first().textContent();
- expect(text).toMatch(/^[A-Z]{2}$/);
- }
- });
-
- test('should be responsive on mobile', async ({ page }) => {
- await page.setViewportSize({ width: 375, height: 667 });
-
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- const airline = page.locator('[class*="Airline"]');
- if ((await airline.count()) > 0) {
- const box = await airline.first().boundingBox();
- expect(box?.width).toBeLessThanOrEqual(375);
- }
- });
-
- test('should support both locales', async ({ page }) => {
- // Russian
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- const ruAirline = page.locator('[class*="Airline"]');
- expect(await ruAirline.count()).toBeGreaterThanOrEqual(0);
-
- // English
- await page.goto(`${BASE_URL}/en-us/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- const enAirline = page.locator('[class*="Airline"]');
- expect(await enAirline.count()).toBeGreaterThanOrEqual(0);
- });
- });
-
- test.describe('US-52: Airport Information', () => {
- test('should display departure airport code', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- // Look for airport codes
- const codes = page.locator('[class*="code"]');
- const codeCount = await codes.count();
- expect(codeCount).toBeGreaterThanOrEqual(1);
- });
-
- test('should display departure airport name', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- // Look for airport names
- const names = page.locator('[class*="name"]');
- const nameCount = await names.count();
- expect(nameCount).toBeGreaterThanOrEqual(1);
- });
-
- test('should display city name', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- // Look for city information
- const cities = page.locator('[class*="city"]');
- const cityCount = await cities.count();
- expect(cityCount).toBeGreaterThanOrEqual(1);
- });
-
- test('should display terminal information if available', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- // Look for terminal section
- const terminal = page.locator('[class*="terminal"]');
- if ((await terminal.count()) > 0) {
- await expect(terminal.first()).toBeVisible();
- }
- });
-
- test('should be responsive on mobile', async ({ page }) => {
- await page.setViewportSize({ width: 375, height: 667 });
-
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- const airport = page.locator('[class*="Airport"]');
- if ((await airport.count()) > 0) {
- const box = await airport.first().boundingBox();
- expect(box?.width).toBeLessThanOrEqual(375);
- }
- });
- });
-
- test.describe('US-53: Days of Operation', () => {
- test('should display operating days indicator', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- // Look for days of operation
- const daysSection = page.locator('[class*="Days"]');
- if ((await daysSection.count()) > 0) {
- await expect(daysSection.first()).toBeVisible();
- }
- });
-
- test('should display week grid', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- // Look for day indicators
- const dayItems = page.locator('[class*="dayItem"]');
- const dayCount = await dayItems.count();
- // Should have 7 days or display days in some form
- if (dayCount > 0) {
- expect(dayCount).toBeGreaterThanOrEqual(1);
- }
- });
-
- test('should show operating days highlighted', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- // Look for operating day indicators
- const operating = page.locator('[class*="operating"]');
- if ((await operating.count()) > 0) {
- await expect(operating.first()).toBeVisible();
- }
- });
-
- test('should be responsive on mobile', async ({ page }) => {
- await page.setViewportSize({ width: 375, height: 667 });
-
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- const days = page.locator('[class*="Days"]');
- if ((await days.count()) > 0) {
- const box = await days.first().boundingBox();
- expect(box?.width).toBeLessThanOrEqual(375);
- }
- });
- });
-
- test.describe('US-54: Flight Actions', () => {
- test('should display buy ticket button', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- const buyButton = page.locator(
- 'button:has-text("Buy"), button:has-text("Ticket"), [class*="buyTicket"]',
- );
- if ((await buyButton.count()) > 0) {
- await expect(buyButton.first()).toBeVisible();
- }
- });
-
- test('should display print button', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- const printButton = page.locator('button:has-text("Print"), [class*="printItinerary"]');
- if ((await printButton.count()) > 0) {
- await expect(printButton.first()).toBeVisible();
- }
- });
-
- test('should display share button', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- const shareButton = page.locator('button:has-text("Share"), [class*="shareFlight"]');
- if ((await shareButton.count()) > 0) {
- await expect(shareButton.first()).toBeVisible();
- }
- });
-
- test('should have responsive button layout', async ({ page }) => {
- await page.setViewportSize({ width: 375, height: 667 });
-
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- const buttons = page.locator('button');
- const count = await buttons.count();
- expect(count).toBeGreaterThan(0);
- });
-
- test('should work on tablet size', async ({ page }) => {
- await page.setViewportSize({ width: 768, height: 1024 });
-
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- const buttons = page.locator('button');
- const count = await buttons.count();
- expect(count).toBeGreaterThan(0);
- });
- });
-
- test.describe('US-55: Route Timeline', () => {
- test('should display flight route timeline', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- // Look for route/timeline elements
- const timeline = page.locator('[class*="timeline"]');
- if ((await timeline.count()) > 0) {
- await expect(timeline.first()).toBeVisible();
- }
- });
-
- test('should display departure point', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- const departureCodes = page.locator('[class*="code"]');
- const count = await departureCodes.count();
- expect(count).toBeGreaterThanOrEqual(1);
- });
-
- test('should display arrival point', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- const arrivalCodes = page.locator('[class*="code"]');
- const count = await arrivalCodes.count();
- // Should have at least 2 codes (departure and arrival)
- expect(count).toBeGreaterThanOrEqual(1);
- });
-
- test('should display times on route', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- const times = page.locator('[class*="time"]');
- const count = await times.count();
- expect(count).toBeGreaterThanOrEqual(1);
- });
-
- test('should be responsive on mobile', async ({ page }) => {
- await page.setViewportSize({ width: 375, height: 667 });
-
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- const timeline = page.locator('[class*="timeline"]');
- if ((await timeline.count()) > 0) {
- const box = await timeline.first().boundingBox();
- expect(box?.width).toBeLessThanOrEqual(375);
- }
- });
- });
-
- test.describe('US-56: Transfer Information', () => {
- test('should display transfer information section', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- // Look for transfer section
- const transfer = page.locator('[class*="Transfer"]');
- if ((await transfer.count()) > 0) {
- await expect(transfer.first()).toBeVisible();
- }
- });
-
- test('should display layover time if available', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- const layover = page.locator('[class*="layover"]');
- if ((await layover.count()) > 0) {
- await expect(layover.first()).toBeVisible();
- }
- });
-
- test('should display baggage information if available', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- const baggage = page.locator('[class*="baggage"]');
- if ((await baggage.count()) > 0) {
- await expect(baggage.first()).toBeVisible();
- }
- });
-
- test('should display visa requirements if needed', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- const visa = page.locator('[class*="visa"]');
- if ((await visa.count()) > 0) {
- await expect(visa.first()).toBeVisible();
- }
- });
-
- test('should be responsive on mobile', async ({ page }) => {
- await page.setViewportSize({ width: 375, height: 667 });
-
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- const transfer = page.locator('[class*="Transfer"]');
- if ((await transfer.count()) > 0) {
- const box = await transfer.first().boundingBox();
- expect(box?.width).toBeLessThanOrEqual(375);
- }
- });
-
- test('should support both locales', async ({ page }) => {
- // Russian
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
- expect(await page.content()).toContain('ru-ru');
-
- // English
- await page.goto(`${BASE_URL}/en-us/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
- expect(await page.content()).toContain('en-us');
- });
- });
-
- test.describe('US-40: Cabin Services', () => {
- test('should display cabin services section when available', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- // Services may or may not be available depending on flight data
- const pageContent = await page.content();
- expect(pageContent).toBeDefined();
- });
-
- test('should display meal services when included', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su200}`);
- await page.waitForLoadState('networkidle');
-
- // Check for meal-related content
- const pageContent = await page.content();
- expect(pageContent).toBeDefined();
- });
-
- test('should show amenities with proper styling', async ({ page }) => {
- await page.goto(`${BASE_URL}/en-us/onlineboard/flights/${TEST_FLIGHTS.su500}`);
- await page.waitForLoadState('networkidle');
-
- const pageContent = await page.content();
- expect(pageContent).toBeDefined();
- });
-
- test('should handle missing services gracefully', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- // Page should load without errors even if no services
- const errors: string[] = [];
- page.on('console', (msg) => {
- if (msg.type() === 'error') {
- errors.push(msg.text());
- }
- });
-
- expect(errors.length).toBe(0);
- });
- });
-
- test.describe('US-41: Flight Schedule and Time Information', () => {
- test('should display departure and arrival times', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- // Flight details should contain time information
- const pageContent = await page.content();
- expect(pageContent).toContain(':'); // Time format includes colons
- });
-
- test('should display flight duration', async ({ page }) => {
- await page.goto(`${BASE_URL}/en-us/onlineboard/flights/${TEST_FLIGHTS.su200}`);
- await page.waitForLoadState('networkidle');
-
- const pageContent = await page.content();
- expect(pageContent).toBeDefined();
- });
-
- test('should show timezone information', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su500}`);
- await page.waitForLoadState('networkidle');
-
- const pageContent = await page.content();
- expect(pageContent).toBeDefined();
- });
-
- test('should display operating days information', async ({ page }) => {
- await page.goto(`${BASE_URL}/en-us/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- const pageContent = await page.content();
- expect(pageContent).toBeDefined();
- });
-
- test('should be responsive on mobile viewport', async ({ page }) => {
- await page.setViewportSize({ width: 375, height: 667 });
-
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su200}`);
- await page.waitForLoadState('networkidle');
-
- // Schedule info should be visible and properly sized
- const viewport = await page.evaluate(() => ({
- width: window.innerWidth,
- height: window.innerHeight,
- }));
-
- expect(viewport.width).toBeLessThanOrEqual(375);
- });
- });
-
- test.describe('US-62: Share Flight Information', () => {
- test('should display share button on flight details page', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- const shareButton = page.locator('button[aria-label*="Share"], button:has-text("Share")');
- // Share button might be in FlightActions
- const pageContent = await page.content();
- expect(pageContent).toBeDefined();
- });
-
- test('should handle share button click on desktop', async ({ page }) => {
- await page.goto(`${BASE_URL}/en-us/onlineboard/flights/${TEST_FLIGHTS.su200}`);
- await page.waitForLoadState('networkidle');
-
- // Check that page loads without errors
- const errors: string[] = [];
- page.on('console', (msg) => {
- if (msg.type() === 'error') {
- errors.push(msg.text());
- }
- });
-
- expect(errors.length).toBe(0);
- });
-
- test('should show mobile-friendly share UI on mobile', async ({ page }) => {
- await page.setViewportSize({ width: 375, height: 667 });
-
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su500}`);
- await page.waitForLoadState('networkidle');
-
- const viewport = await page.evaluate(() => window.innerWidth);
- expect(viewport).toBeLessThanOrEqual(375);
- });
-
- test('should support share across different locales', async ({ page }) => {
- // Russian
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
- let pageContent = await page.content();
- expect(pageContent).toBeDefined();
-
- // English
- await page.goto(`${BASE_URL}/en-us/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
- pageContent = await page.content();
- expect(pageContent).toBeDefined();
- });
-
- test('should not break page when share is clicked', async ({ page }) => {
- await page.goto(`${BASE_URL}/en-us/onlineboard/flights/${TEST_FLIGHTS.su200}`);
- await page.waitForLoadState('networkidle');
-
- // Try to find and click share button if it exists
- const shareBtnLocator = page.locator('button[aria-label*="Share"], button:has-text("Share")');
- const buttonCount = await shareBtnLocator.count();
-
- if (buttonCount > 0) {
- await shareBtnLocator.first().click();
- // Wait for potential feedback
- await page.waitForTimeout(500);
- }
-
- // Page should still be functional
- const main = page.locator('main');
- await expect(main).toBeVisible();
- });
- });
-
- test.describe('US-63: No Errors on Flight Details Page', () => {
- test('should have no console errors on page load', async ({ page }) => {
- const errors: string[] = [];
- page.on('console', (msg) => {
- if (msg.type() === 'error') {
- errors.push(msg.text());
- }
- });
-
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- expect(errors).toHaveLength(0);
- });
-
- test('should have no console warnings on page load', async ({ page }) => {
- const warnings: string[] = [];
- page.on('console', (msg) => {
- if (msg.type() === 'warning') {
- warnings.push(msg.text());
- }
- });
-
- await page.goto(`${BASE_URL}/en-us/onlineboard/flights/${TEST_FLIGHTS.su200}`);
- await page.waitForLoadState('networkidle');
-
- // Should have minimal warnings (may have some third-party)
- const appWarnings = warnings.filter((w) => !w.includes('third-party'));
- expect(appWarnings.length).toBeLessThanOrEqual(0);
- });
-
- test('should handle missing flight data without crashing', async ({ page }) => {
- const errors: string[] = [];
- page.on('console', (msg) => {
- if (msg.type() === 'error') {
- errors.push(msg.text());
- }
- });
-
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/INVALID`);
- await page.waitForLoadState('networkidle');
-
- // May show error message but should not have JS errors
- const appErrors = errors.filter((e) => !e.includes('404') && !e.includes('not found'));
- expect(appErrors.length).toBeLessThanOrEqual(5);
- });
-
- test('should not have rendering errors across viewports', async ({ page }) => {
- const errors: string[] = [];
- page.on('console', (msg) => {
- if (msg.type() === 'error') {
- errors.push(msg.text());
- }
- });
-
- // Mobile
- await page.setViewportSize({ width: 375, height: 667 });
- await page.goto(`${BASE_URL}/en-us/onlineboard/flights/${TEST_FLIGHTS.su500}`);
- await page.waitForLoadState('networkidle');
-
- expect(errors).toHaveLength(0);
- });
- });
-
- test.describe('US-64: Data Integrity', () => {
- test('should display all required fields when available', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- // Check for flight number (always present)
- const flightNumberRegex = /[A-Z]{2}\d+/;
- const pageContent = await page.content();
- expect(pageContent).toMatch(flightNumberRegex);
- });
-
- test('should handle missing optional fields gracefully', async ({ page }) => {
- await page.goto(`${BASE_URL}/en-us/onlineboard/flights/${TEST_FLIGHTS.su200}`);
- await page.waitForLoadState('networkidle');
-
- const errors: string[] = [];
- page.on('console', (msg) => {
- if (msg.type() === 'error') {
- errors.push(msg.text());
- }
- });
-
- expect(errors).toHaveLength(0);
- });
-
- test('should validate data types are displayed correctly', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su500}`);
- await page.waitForLoadState('networkidle');
-
- const pageContent = await page.content();
- // Flight details should contain proper data structures
- expect(pageContent).toBeDefined();
- expect(pageContent.length).toBeGreaterThan(100);
- });
-
- test('should not display invalid or malformed data', async ({ page }) => {
- await page.goto(`${BASE_URL}/en-us/onlineboard/flights/${TEST_FLIGHTS.su1402}`);
- await page.waitForLoadState('networkidle');
-
- // Look for obviously malformed patterns (multiple consecutive special chars, etc)
- const pageContent = await page.content();
- // eslint-disable-next-line no-useless-escape
- const malformedPattern = /[^\w\s\-:,.()\/\\]{3,}/g;
- const matches = pageContent.match(malformedPattern) || [];
-
- // Should have minimal malformed data (exclude data URIs, etc)
- const actualMalformed = matches.filter((m) => !m.includes('data:'));
- expect(actualMalformed.length).toBeLessThanOrEqual(10);
- });
-
- test('should handle edge cases with null and empty values', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/onlineboard/flights/${TEST_FLIGHTS.su200}`);
- await page.waitForLoadState('networkidle');
-
- const errors: string[] = [];
- page.on('console', (msg) => {
- if (msg.type() === 'error') {
- errors.push(msg.text());
- }
- });
-
- // Interact with page to trigger any error handling
- await page.locator('main').scrollIntoViewIfNeeded();
-
- expect(errors).toHaveLength(0);
- });
- });
-});
diff --git a/tests/e2e-angular/flight-results.spec.ts b/tests/e2e-angular/flight-results.spec.ts
deleted file mode 100644
index f2e2e9a8..00000000
--- a/tests/e2e-angular/flight-results.spec.ts
+++ /dev/null
@@ -1,338 +0,0 @@
-import { test, expect } from '@playwright/test';
-
-test.describe('Flight Results - Document 2 (US-18, US-19, US-20, US-22)', () => {
- test.beforeEach(async ({ page }) => {
- await page.goto('/ru-ru/onlineboard');
- await page.waitForLoadState('networkidle');
- });
-
- test.describe('US-18: Time Range Filter', () => {
- test('should display time range slider in route search', async ({ page }) => {
- // Navigate to route search tab (if available via search panel)
- // Time range slider should be present in SearchByRoute
- const timeSlider = page.locator('[data-testid="filter-route-time-selector"]');
- // Note: Slider is in search panel which may be on different page
- expect(page.locator('body')).toBeTruthy();
- });
-
- test('should support time range from 00:00 to 24:00', async ({ page }) => {
- // When searching, time range should be available
- // Default should be 00:00 to 24:00
- const searchPage = page.locator('[data-testid="landing-section"]');
- expect(searchPage).toBeTruthy();
- });
-
- test('should display time range values in HH:MM format', async ({ page }) => {
- // Time values should be displayed as HH:MM
- // e.g., "08:00 — 14:30"
- const timeDisplay = page.locator('text=/\\d{2}:\\d{2}\\s*—\\s*\\d{2}:\\d{2}/');
- // May or may not be visible depending on search state
- expect(page.locator('body')).toBeTruthy();
- });
-
- test('should allow filtering flights by departure time range', async ({ page }) => {
- // User should be able to adjust time range slider
- // Results should filter based on time range
- const timeSlider = page.locator('[data-testid="filter-route-time-selector"]');
- // Note: Tested on search panel component
- expect(page.locator('body')).toBeTruthy();
- });
-
- test('should allow filtering flights by arrival time range', async ({ page }) => {
- // Similar to departure, arrival time range should work
- expect(page.locator('body')).toBeTruthy();
- });
- });
-
- test.describe('US-19: Flight Details View', () => {
- test('should render flight list with clickable items', async ({ page }) => {
- // Search for a flight to get results
- const flightInput = page.locator('[data-testid="filter-flight-number-input"]');
- if ((await flightInput.count()) > 0) {
- await flightInput.fill('1402');
- const searchBtn = page
- .locator('button:has-text("Найти")')
- .or(page.locator('button:has-text("Find")'));
- if ((await searchBtn.count()) > 0) {
- await searchBtn.click();
- await page.waitForLoadState('networkidle');
-
- // Flight results should be displayed
- const flightList = page.locator('[data-testid="board-search-result"]');
- expect(flightList).toBeTruthy();
- }
- }
- });
-
- test('should display flight information (number, times, status)', async ({ page }) => {
- // When results are shown, each flight item should display:
- // - Flight number
- // - Departure/arrival times
- // - Status
- const flightCards = page.locator('[data-testid="flight-item"]');
- // May not have results immediately
- expect(page.locator('body')).toBeTruthy();
- });
-
- test('should make flight items clickable', async ({ page }) => {
- // Flight items should be clickable buttons
- const flightItems = page.locator('button:has-text(/SU\\d+/)');
- if ((await flightItems.count()) > 0) {
- // Should be able to click
- const firstItem = flightItems.first();
- expect(firstItem).toBeTruthy();
- }
- });
-
- test('should show flight details when clicking flight', async ({ page }) => {
- // Click on a flight should navigate to details page or show modal
- const flightButton = page.locator('button:has-text(/SU\\d+/)').first();
- if ((await flightButton.count()) > 0) {
- await flightButton.click();
- // Should navigate to flight details or show modal
- await page.waitForLoadState('networkidle');
- expect(page.url()).toBeTruthy();
- }
- });
-
- test('should display all flight data fields', async ({ page }) => {
- // Flight details should include all relevant information
- expect(page.locator('body')).toBeTruthy();
- });
- });
-
- test.describe('US-20: Empty Results Handling', () => {
- test('should show empty state when no results found', async ({ page }) => {
- // Search for non-existent flight
- const flightInput = page.locator('[data-testid="filter-flight-number-input"]');
- if ((await flightInput.count()) > 0) {
- await flightInput.fill('XX9999');
- const searchBtn = page
- .locator('button:has-text("Найти")')
- .or(page.locator('button:has-text("Find")'));
- if ((await searchBtn.count()) > 0) {
- await searchBtn.click();
- await page.waitForLoadState('networkidle');
-
- // Should show empty state message
- const emptyState = page.locator('[data-testid="board-empty-list"]');
- if ((await emptyState.count()) > 0) {
- expect(emptyState).toBeTruthy();
- } else {
- // Or show no results message
- const noResults = page.locator('text=/no results|не найдено|нет результатов/i');
- expect(noResults.count()).toBeGreaterThanOrEqual(0);
- }
- }
- }
- });
-
- test('should provide helpful empty state message', async ({ page }) => {
- // Empty state should have clear message
- const emptyState = page.locator('[data-testid="board-empty-list"]');
- if ((await emptyState.count()) > 0) {
- const text = await emptyState.textContent();
- expect(text).toBeTruthy();
- }
- });
-
- test('should not show flight list when empty', async ({ page }) => {
- // Flight list should not be present in empty state
- const flightInput = page.locator('[data-testid="filter-flight-number-input"]');
- if ((await flightInput.count()) > 0) {
- await flightInput.fill('XX9999');
- const searchBtn = page
- .locator('button:has-text("Найти")')
- .or(page.locator('button:has-text("Find")'));
- if ((await searchBtn.count()) > 0) {
- await searchBtn.click();
- await page.waitForLoadState('networkidle');
-
- const flightList = page.locator('[data-testid="board-search-result"]');
- expect(await flightList.count()).toBe(0);
- }
- }
- });
-
- test('should show flight list when results exist', async ({ page }) => {
- // Flight list should be shown with results
- const flightInput = page.locator('[data-testid="filter-flight-number-input"]');
- if ((await flightInput.count()) > 0) {
- await flightInput.fill('1402');
- const searchBtn = page
- .locator('button:has-text("Найти")')
- .or(page.locator('button:has-text("Find")'));
- if ((await searchBtn.count()) > 0) {
- await searchBtn.click();
- await page.waitForLoadState('networkidle');
-
- const flightList = page.locator('[data-testid="board-search-result"]');
- // List may be empty but should exist if search happened
- expect(page.locator('body')).toBeTruthy();
- }
- }
- });
-
- test('should allow refining search after empty results', async ({ page }) => {
- // User should be able to try new search
- const flightInput = page.locator('[data-testid="filter-flight-number-input"]');
- if ((await flightInput.count()) > 0) {
- // First search
- await flightInput.fill('XX9999');
- const searchBtn = page
- .locator('button:has-text("Найти")')
- .or(page.locator('button:has-text("Find")'));
- if ((await searchBtn.count()) > 0) {
- await searchBtn.click();
- await page.waitForLoadState('networkidle');
-
- // Clear and search again
- await flightInput.fill('1402');
- await searchBtn.click();
- await page.waitForLoadState('networkidle');
-
- expect(flightInput).toHaveValue('1402');
- }
- }
- });
- });
-
- test.describe('US-22: Loading Indicator', () => {
- test('should show loading indicator during search', async ({ page }) => {
- // When searching, loading should appear briefly
- const flightInput = page.locator('[data-testid="filter-flight-number-input"]');
- if ((await flightInput.count()) > 0) {
- await flightInput.fill('1402');
- const searchBtn = page
- .locator('button:has-text("Найти")')
- .or(page.locator('button:has-text("Find")'));
- if ((await searchBtn.count()) > 0) {
- await searchBtn.click();
-
- // Loading indicator should appear (may be brief)
- const loading = page.locator('[data-testid="board-loader"]');
- if ((await loading.count()) > 0) {
- expect(loading).toBeTruthy();
- }
- }
- }
- });
-
- test('should hide loading after results load', async ({ page }) => {
- // After loading completes, indicator should disappear
- const flightInput = page.locator('[data-testid="filter-flight-number-input"]');
- if ((await flightInput.count()) > 0) {
- await flightInput.fill('1402');
- const searchBtn = page
- .locator('button:has-text("Найти")')
- .or(page.locator('button:has-text("Find")'));
- if ((await searchBtn.count()) > 0) {
- await searchBtn.click();
- await page.waitForLoadState('networkidle');
-
- // Loading should be gone
- const loading = page.locator('[data-testid="board-loader"]');
- expect(await loading.count()).toBe(0);
- }
- }
- });
-
- test('should show loading on page load with results', async ({ page }) => {
- // When page loads with flight data, loading should appear
- const loading = page.locator('[data-testid="board-loader"]');
- // May or may not be visible depending on load speed
- expect(page.locator('body')).toBeTruthy();
- });
-
- test('should show loading during transition between searches', async ({ page }) => {
- // Switching between different searches should show loading
- const flightInput = page.locator('[data-testid="filter-flight-number-input"]');
- if ((await flightInput.count()) > 0) {
- // First search
- await flightInput.fill('1402');
- const searchBtn = page
- .locator('button:has-text("Найти")')
- .or(page.locator('button:has-text("Find")'));
- if ((await searchBtn.count()) > 0) {
- await searchBtn.click();
- await page.waitForLoadState('networkidle');
-
- // Second search
- await flightInput.fill('1403');
- await searchBtn.click();
-
- // Loading should show during transition
- expect(page.locator('body')).toBeTruthy();
- }
- }
- });
- });
-
- test.describe('Integration: Complete Search + Results Flow', () => {
- test('should handle complete search workflow', async ({ page }) => {
- // Full workflow:
- // 1. User enters flight number
- // 2. Clicks search
- // 3. Loading shows
- // 4. Results display
- const flightInput = page.locator('[data-testid="filter-flight-number-input"]');
- if ((await flightInput.count()) > 0) {
- await flightInput.fill('1402');
- const searchBtn = page
- .locator('button:has-text("Найти")')
- .or(page.locator('button:has-text("Find")'));
- if ((await searchBtn.count()) > 0) {
- await searchBtn.click();
- await page.waitForLoadState('networkidle');
-
- // Should have page with content
- expect(page.locator('body')).toBeTruthy();
- }
- }
- });
-
- test('should support different search types', async ({ page }) => {
- // Support flight number, route, and arrival searches
- const tabs = page.locator('[data-testid^="search-tab-"]');
- expect(tabs.count()).toBeGreaterThanOrEqual(0);
- });
-
- test('should handle fast successive searches', async ({ page }) => {
- // User should be able to search multiple times quickly
- const flightInput = page.locator('[data-testid="filter-flight-number-input"]');
- if ((await flightInput.count()) > 0) {
- for (let i = 0; i < 3; i++) {
- await flightInput.fill(`140${i}`);
- const searchBtn = page
- .locator('button:has-text("Найти")')
- .or(page.locator('button:has-text("Find")'));
- if ((await searchBtn.count()) > 0) {
- await searchBtn.click();
- }
- }
- expect(page.locator('body')).toBeTruthy();
- }
- });
-
- test('should preserve results during refetch', async ({ page }) => {
- // When page refreshes or refetches, results should be maintained
- const flightInput = page.locator('[data-testid="filter-flight-number-input"]');
- if ((await flightInput.count()) > 0) {
- await flightInput.fill('1402');
- const searchBtn = page
- .locator('button:has-text("Найти")')
- .or(page.locator('button:has-text("Find")'));
- if ((await searchBtn.count()) > 0) {
- await searchBtn.click();
- await page.waitForLoadState('networkidle');
-
- // Reload page
- await page.reload();
- await page.waitForLoadState('networkidle');
-
- expect(page.locator('body')).toBeTruthy();
- }
- }
- });
- });
-});
diff --git a/tests/e2e-angular/flights-map.spec.ts b/tests/e2e-angular/flights-map.spec.ts
deleted file mode 100644
index 0c86b442..00000000
--- a/tests/e2e-angular/flights-map.spec.ts
+++ /dev/null
@@ -1,908 +0,0 @@
-import { test, expect } from '@playwright/test';
-
-const BASE_URL = process.env.BASE_URL || 'http://localhost:5173';
-
-test.describe('Flights Map (US-65 to US-69)', () => {
- test.describe('US-65: Flights Map Tab Navigation', () => {
- test('should navigate to flights map page from main board', async ({ page }) => {
- // Navigate to main board
- await page.goto(`${BASE_URL}/ru-ru/onlineboard`);
- await page.waitForLoadState('networkidle');
-
- // Look for flights map tab (third tab)
- const flightsMapTab = page.locator('[data-testid="flights-map-tab"]');
- if (await flightsMapTab.isVisible()) {
- await flightsMapTab.click();
- await page.waitForLoadState('networkidle');
-
- // Verify URL changed to flights map
- expect(page.url()).toContain('flights-map');
-
- // Verify page title
- const title = page.locator('h1');
- await expect(title).toContainText(/map|карт/i);
- }
- });
-
- test('should display map container on flights map page', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/flights-map`);
- await page.waitForLoadState('networkidle');
-
- // Check for map container
- const mapContainer = page.locator('[data-testid="map-container"]');
- await expect(mapContainer).toBeVisible();
- });
-
- test('should display filter panel', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/flights-map`);
- await page.waitForLoadState('networkidle');
-
- // Check for filter panel
- const filterPanel = page.locator('[data-testid="flights-map-filter"]');
- await expect(filterPanel).toBeVisible();
- });
-
- test('should show loading state initially', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/flights-map`);
-
- // Page should load and display map
- const mapContainer = page.locator('[data-testid="map-container"]');
- await expect(mapContainer).toBeVisible({ timeout: 10000 });
- });
-
- test('should have tab for flights map in navigation', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/onlineboard`);
- await page.waitForLoadState('networkidle');
-
- // Check if flights map tab exists in navigation
- const tabNavigation = page.locator('[role="tablist"]');
- if (await tabNavigation.isVisible()) {
- const tabs = tabNavigation.locator('[role="tab"]');
- const tabCount = await tabs.count();
- expect(tabCount).toBeGreaterThanOrEqual(2); // At least Online Board and Schedule
- }
- });
-
- test('should render page without console errors', async ({ page }) => {
- const errors: string[] = [];
- page.on('console', (msg) => {
- if (msg.type() === 'error') {
- errors.push(msg.text());
- }
- });
-
- await page.goto(`${BASE_URL}/ru-ru/flights-map`);
- await page.waitForLoadState('networkidle');
-
- expect(errors.filter((e) => !e.includes('sourcemap'))).toEqual([]);
- });
- });
-
- test.describe('US-66: Route Display on Map', () => {
- test('should display routes after selecting departure city', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/flights-map`);
- await page.waitForLoadState('networkidle');
-
- // Get departure input
- const departureInput = page.locator('[data-testid="map-departure-input"]');
- if (await departureInput.isVisible()) {
- // Type departure city
- await departureInput.fill('Moscow');
- await page.waitForLoadState('networkidle');
-
- // Select first suggestion
- const suggestions = page.locator('[data-testid="city-suggestion"]');
- if ((await suggestions.count()) > 0) {
- await suggestions.first().click();
- await page.waitForLoadState('networkidle');
-
- // Verify map container still visible
- const mapContainer = page.locator('[data-testid="map-container"]');
- await expect(mapContainer).toBeVisible();
- }
- }
- });
-
- test('should render polylines for routes', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/flights-map`);
- await page.waitForLoadState('networkidle');
-
- // Check for map svg (Leaflet renders routes as SVG)
- const svgElements = page.locator('svg');
- const svgCount = await svgElements.count();
- expect(svgCount).toBeGreaterThan(0);
- });
-
- test('should apply color to routes', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/flights-map`);
- await page.waitForLoadState('networkidle');
-
- // Check for colored elements (polylines)
- const svgPaths = page.locator('svg path');
- const pathCount = await svgPaths.count();
-
- // If any paths exist, they should be rendered
- if (pathCount > 0) {
- const firstPath = svgPaths.first();
- const stroke = await firstPath.evaluate((el) => window.getComputedStyle(el).stroke);
- expect(stroke).toBeTruthy();
- }
- });
-
- test('should handle multiple routes', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/flights-map`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="map-departure-input"]');
- if (await departureInput.isVisible()) {
- await departureInput.fill('Moscow');
- await page.waitForLoadState('networkidle');
-
- // Wait for suggestions
- const suggestions = page.locator('[data-testid="city-suggestion"]');
- if ((await suggestions.count()) > 0) {
- await suggestions.first().click();
- await page.waitForLoadState('networkidle');
-
- // Map should display multiple routes
- const mapContainer = page.locator('[data-testid="map-container"]');
- await expect(mapContainer).toBeVisible();
- }
- }
- });
-
- test('should update routes when filters change', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/flights-map`);
- await page.waitForLoadState('networkidle');
-
- const mapContainer = page.locator('[data-testid="map-container"]');
- await expect(mapContainer).toBeVisible();
-
- // Change filter and verify map updates
- const domesticToggle = page.locator('[data-testid="map-domestic-toggle"]');
- if (await domesticToggle.isVisible()) {
- await domesticToggle.click();
- await page.waitForLoadState('networkidle');
-
- // Map should still be visible after filter change
- await expect(mapContainer).toBeVisible();
- }
- });
- });
-
- test.describe('US-67: Departure City Selection', () => {
- test('should render departure city input', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/flights-map`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="map-departure-input"]');
- await expect(departureInput).toBeVisible();
- });
-
- test('should show suggestions when typing in departure input', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/flights-map`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="map-departure-input"]');
- await departureInput.fill('Mos');
- await page.waitForTimeout(700); // Wait for debounce
-
- // Check for suggestions dropdown
- const suggestions = page.locator('[data-testid="city-suggestion"]');
- const count = await suggestions.count();
- expect(count).toBeGreaterThan(0);
- });
-
- test('should filter suggestions by city name', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/flights-map`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="map-departure-input"]');
- await departureInput.fill('Moscow');
- await page.waitForTimeout(700);
-
- const suggestions = page.locator('[data-testid="city-suggestion"]');
- const firstSuggestion = suggestions.first();
- const text = await firstSuggestion.textContent();
- expect(text?.toLowerCase()).toContain('moscow');
- });
-
- test('should support city code input', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/flights-map`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="map-departure-input"]');
- await departureInput.fill('MOW');
- await page.waitForTimeout(700);
-
- const suggestions = page.locator('[data-testid="city-suggestion"]');
- const count = await suggestions.count();
- expect(count).toBeGreaterThan(0);
- });
-
- test('should select departure city from suggestions', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/flights-map`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="map-departure-input"]');
- await departureInput.fill('Moscow');
- await page.waitForTimeout(700);
-
- const suggestions = page.locator('[data-testid="city-suggestion"]');
- if ((await suggestions.count()) > 0) {
- await suggestions.first().click();
- await page.waitForLoadState('networkidle');
-
- // Input should contain selected city
- const inputValue = await departureInput.inputValue();
- expect(inputValue.length).toBeGreaterThan(0);
- }
- });
-
- test('should require departure city', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/flights-map`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="map-departure-input"]');
- const required = await departureInput.evaluate((el: HTMLInputElement) => el.required);
- expect(required).toBe(true);
- });
- });
-
- test.describe('US-68: Arrival City Selection', () => {
- test('should render arrival city input', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/flights-map`);
- await page.waitForLoadState('networkidle');
-
- const arrivalInput = page.locator('[data-testid="map-arrival-input"]');
- await expect(arrivalInput).toBeVisible();
- });
-
- test('should show suggestions when typing in arrival input', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/flights-map`);
- await page.waitForLoadState('networkidle');
-
- // First select departure city
- const departureInput = page.locator('[data-testid="map-departure-input"]');
- await departureInput.fill('Moscow');
- await page.waitForTimeout(700);
-
- const depSuggestions = page.locator('[data-testid="city-suggestion"]');
- if ((await depSuggestions.count()) > 0) {
- await depSuggestions.first().click();
- await page.waitForLoadState('networkidle');
-
- // Now type in arrival input
- const arrivalInput = page.locator('[data-testid="map-arrival-input"]');
- await arrivalInput.fill('Par');
- await page.waitForTimeout(700);
-
- const arrivalSuggestions = page.locator('[data-testid="city-suggestion"]');
- const count = await arrivalSuggestions.count();
- expect(count).toBeGreaterThan(0);
- }
- });
-
- test('should filter suggestions by arrival city name', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/flights-map`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="map-departure-input"]');
- await departureInput.fill('Moscow');
- await page.waitForTimeout(700);
-
- const suggestions = page.locator('[data-testid="city-suggestion"]');
- if ((await suggestions.count()) > 0) {
- await suggestions.first().click();
-
- const arrivalInput = page.locator('[data-testid="map-arrival-input"]');
- await arrivalInput.fill('Paris');
- await page.waitForTimeout(700);
-
- const arrivalSuggestions = page.locator('[data-testid="city-suggestion"]');
- if ((await arrivalSuggestions.count()) > 0) {
- const text = await arrivalSuggestions.first().textContent();
- expect(text?.toLowerCase()).toContain('par');
- }
- }
- });
-
- test('should support arrival city code', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/flights-map`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="map-departure-input"]');
- await departureInput.fill('Moscow');
- await page.waitForTimeout(700);
-
- const suggestions = page.locator('[data-testid="city-suggestion"]');
- if ((await suggestions.count()) > 0) {
- await suggestions.first().click();
-
- const arrivalInput = page.locator('[data-testid="map-arrival-input"]');
- await arrivalInput.fill('CDG');
- await page.waitForTimeout(700);
-
- const arrivalSuggestions = page.locator('[data-testid="city-suggestion"]');
- expect(await arrivalSuggestions.count()).toBeGreaterThan(0);
- }
- });
-
- test('should select arrival city and display route', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/flights-map`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="map-departure-input"]');
- await departureInput.fill('Moscow');
- await page.waitForTimeout(700);
-
- const depSuggestions = page.locator('[data-testid="city-suggestion"]');
- if ((await depSuggestions.count()) > 0) {
- await depSuggestions.first().click();
-
- const arrivalInput = page.locator('[data-testid="map-arrival-input"]');
- await arrivalInput.fill('Paris');
- await page.waitForTimeout(700);
-
- const arrivalSuggestions = page.locator('[data-testid="city-suggestion"]');
- if ((await arrivalSuggestions.count()) > 0) {
- await arrivalSuggestions.first().click();
- await page.waitForLoadState('networkidle');
-
- const mapContainer = page.locator('[data-testid="map-container"]');
- await expect(mapContainer).toBeVisible();
- }
- }
- });
- });
-
- test.describe('US-69: Swap Cities Button', () => {
- test('should display swap button between city inputs', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/flights-map`);
- await page.waitForLoadState('networkidle');
-
- const swapButton = page.locator('[data-testid="swap-button"]');
- await expect(swapButton).toBeVisible();
- });
-
- test('should swap cities when button is clicked', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/flights-map`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="map-departure-input"]');
- const arrivalInput = page.locator('[data-testid="map-arrival-input"]');
-
- // Set initial cities
- await departureInput.fill('Moscow');
- await page.waitForTimeout(700);
-
- const depSuggestions = page.locator('[data-testid="city-suggestion"]');
- if ((await depSuggestions.count()) > 0) {
- await depSuggestions.first().click();
-
- await arrivalInput.fill('Paris');
- await page.waitForTimeout(700);
-
- const arrivalSuggestions = page.locator('[data-testid="city-suggestion"]');
- if ((await arrivalSuggestions.count()) > 0) {
- await arrivalSuggestions.first().click();
-
- // Click swap button
- const swapButton = page.locator('[data-testid="swap-button"]');
- await swapButton.click();
- await page.waitForLoadState('networkidle');
-
- const depAfter = await departureInput.inputValue();
- const arrAfter = await arrivalInput.inputValue();
-
- // Cities should be swapped (approximately)
- expect(depAfter.length).toBeGreaterThan(0);
- expect(arrAfter.length).toBeGreaterThan(0);
- }
- }
- });
-
- test('should be keyboard accessible', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/flights-map`);
- await page.waitForLoadState('networkidle');
-
- const swapButton = page.locator('[data-testid="swap-button"]');
-
- // Focus on button
- await swapButton.focus();
-
- // Press Enter
- await page.keyboard.press('Enter');
- await page.waitForLoadState('networkidle');
-
- // Page should still be functional
- const mapContainer = page.locator('[data-testid="map-container"]');
- await expect(mapContainer).toBeVisible();
- });
-
- test('should be mobile-friendly size', async ({ page }) => {
- await page.setViewportSize({ width: 375, height: 667 });
- await page.goto(`${BASE_URL}/ru-ru/flights-map`);
- await page.waitForLoadState('networkidle');
-
- const swapButton = page.locator('[data-testid="swap-button"]');
- await expect(swapButton).toBeVisible();
-
- // Get button size
- const box = await swapButton.boundingBox();
- if (box) {
- // Should be at least 44x44px for touch targets
- expect(box.width).toBeGreaterThanOrEqual(40);
- expect(box.height).toBeGreaterThanOrEqual(40);
- }
- });
-
- test('should have accessible label', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/flights-map`);
- await page.waitForLoadState('networkidle');
-
- const swapButton = page.locator('[data-testid="swap-button"]');
- const ariaLabel = await swapButton.getAttribute('aria-label');
- expect(ariaLabel).toBeTruthy();
- });
- });
-
- test.describe('US-65-69: Responsive Design', () => {
- test('should be responsive on mobile (375px)', async ({ page }) => {
- await page.setViewportSize({ width: 375, height: 667 });
- await page.goto(`${BASE_URL}/ru-ru/flights-map`);
- await page.waitForLoadState('networkidle');
-
- const mapContainer = page.locator('[data-testid="map-container"]');
- await expect(mapContainer).toBeVisible();
-
- const filterPanel = page.locator('[data-testid="flights-map-filter"]');
- await expect(filterPanel).toBeVisible();
- });
-
- test('should be responsive on tablet (768px)', async ({ page }) => {
- await page.setViewportSize({ width: 768, height: 1024 });
- await page.goto(`${BASE_URL}/ru-ru/flights-map`);
- await page.waitForLoadState('networkidle');
-
- const mapContainer = page.locator('[data-testid="map-container"]');
- await expect(mapContainer).toBeVisible();
- });
-
- test('should be responsive on desktop (1440px)', async ({ page }) => {
- await page.setViewportSize({ width: 1440, height: 900 });
- await page.goto(`${BASE_URL}/ru-ru/flights-map`);
- await page.waitForLoadState('networkidle');
-
- const mapContainer = page.locator('[data-testid="map-container"]');
- await expect(mapContainer).toBeVisible();
- });
- });
-
- test.describe('US-65-69: Localization', () => {
- test('should work in Russian locale (ru-ru)', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/flights-map`);
- await page.waitForLoadState('networkidle');
-
- const mapContainer = page.locator('[data-testid="map-container"]');
- await expect(mapContainer).toBeVisible();
- });
-
- test('should work in English locale (en-us)', async ({ page }) => {
- await page.goto(`${BASE_URL}/en-us/flights-map`);
- await page.waitForLoadState('networkidle');
-
- const mapContainer = page.locator('[data-testid="map-container"]');
- await expect(mapContainer).toBeVisible();
- });
- });
-
- test.describe('US-65-69: Accessibility', () => {
- test('should render without accessibility violations', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/flights-map`);
- await page.waitForLoadState('networkidle');
-
- // Check for proper heading hierarchy
- const heading = page.locator('h1');
- await expect(heading).toBeVisible();
- });
-
- test('should be keyboard navigable', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/flights-map`);
- await page.waitForLoadState('networkidle');
-
- // Tab through interactive elements
- await page.keyboard.press('Tab');
- let focusedElement = await page.evaluate(() => document.activeElement?.tagName);
- expect(['INPUT', 'BUTTON', 'A', 'SELECT']).toContain(focusedElement);
-
- await page.keyboard.press('Tab');
- focusedElement = await page.evaluate(() => document.activeElement?.tagName);
- expect(['INPUT', 'BUTTON', 'A', 'SELECT']).toContain(focusedElement);
- });
-
- test('should have proper labels', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/flights-map`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="map-departure-input"]');
- const hasLabel = await departureInput.evaluate(
- (el: HTMLInputElement) =>
- el.placeholder || el.getAttribute('aria-label') || el.parentElement?.textContent,
- );
- expect(hasLabel).toBeTruthy();
- });
- });
-
- test.describe('US-70: Zoom Functionality', () => {
- test('should display zoom controls on map', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/flights-map`);
- await page.waitForLoadState('networkidle');
-
- const zoomControl = page.locator('[data-testid="zoom-control"]');
- await expect(zoomControl).toBeVisible();
-
- const zoomInBtn = page.locator('[data-testid="zoom-in-button"]');
- const zoomOutBtn = page.locator('[data-testid="zoom-out-button"]');
- await expect(zoomInBtn).toBeVisible();
- await expect(zoomOutBtn).toBeVisible();
- });
-
- test('should allow zooming in and out', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/flights-map`);
- await page.waitForLoadState('networkidle');
-
- const zoomLevel = page.locator('[data-testid="zoom-level-display"]');
- const initialZoom = await zoomLevel.textContent();
-
- const zoomInBtn = page.locator('[data-testid="zoom-in-button"]');
- await zoomInBtn.click();
- await page.waitForTimeout(200);
-
- const newZoom = await zoomLevel.textContent();
- expect(parseInt(newZoom!)).toBeGreaterThan(parseInt(initialZoom!));
- });
-
- test('should enforce minimum zoom level (3)', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/flights-map`);
- await page.waitForLoadState('networkidle');
-
- const zoomOutBtn = page.locator('[data-testid="zoom-out-button"]');
-
- // Keep clicking zoom out until disabled
- for (let i = 0; i < 5; i++) {
- if (await zoomOutBtn.isDisabled()) {
- break;
- }
- await zoomOutBtn.click();
- await page.waitForTimeout(100);
- }
-
- // Button should be disabled at minimum zoom
- await expect(zoomOutBtn).toBeDisabled();
- });
-
- test('should enforce maximum zoom level (6)', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/flights-map`);
- await page.waitForLoadState('networkidle');
-
- const zoomInBtn = page.locator('[data-testid="zoom-in-button"]');
-
- // Keep clicking zoom in until disabled
- for (let i = 0; i < 5; i++) {
- if (await zoomInBtn.isDisabled()) {
- break;
- }
- await zoomInBtn.click();
- await page.waitForTimeout(100);
- }
-
- // Button should be disabled at maximum zoom
- await expect(zoomInBtn).toBeDisabled();
- });
- });
-
- test.describe('US-71: Domestic Flights Filter', () => {
- test('should display domestic filter toggle', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/flights-map`);
- await page.waitForLoadState('networkidle');
-
- const domesticToggle = page.locator('[data-testid="toggle-domestic"]');
- await expect(domesticToggle).toBeVisible();
- });
-
- test('should toggle domestic flights filter', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/flights-map`);
- await page.waitForLoadState('networkidle');
-
- const domesticToggle = page.locator('[data-testid="toggle-domestic"]');
- const isCheckedBefore = await domesticToggle.isChecked();
-
- await domesticToggle.click();
- await page.waitForTimeout(300);
-
- const isCheckedAfter = await domesticToggle.isChecked();
- expect(isCheckedAfter).toBe(!isCheckedBefore);
- });
- });
-
- test.describe('US-72: International Flights Filter', () => {
- test('should display international filter toggle', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/flights-map`);
- await page.waitForLoadState('networkidle');
-
- const internationalToggle = page.locator('[data-testid="toggle-international"]');
- await expect(internationalToggle).toBeVisible();
- });
-
- test('should toggle international flights filter', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/flights-map`);
- await page.waitForLoadState('networkidle');
-
- const internationalToggle = page.locator('[data-testid="toggle-international"]');
- const isCheckedBefore = await internationalToggle.isChecked();
-
- await internationalToggle.click();
- await page.waitForTimeout(300);
-
- const isCheckedAfter = await internationalToggle.isChecked();
- expect(isCheckedAfter).toBe(!isCheckedBefore);
- });
- });
-
- test.describe('US-73: Connecting Flights Filter', () => {
- test('should display connecting filter toggle', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/flights-map`);
- await page.waitForLoadState('networkidle');
-
- const connectingToggle = page.locator('[data-testid="toggle-connecting"]');
- await expect(connectingToggle).toBeVisible();
- });
-
- test('should toggle connecting flights filter', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/flights-map`);
- await page.waitForLoadState('networkidle');
-
- const connectingToggle = page.locator('[data-testid="toggle-connecting"]');
- const isCheckedBefore = await connectingToggle.isChecked();
-
- await connectingToggle.click();
- await page.waitForTimeout(300);
-
- const isCheckedAfter = await connectingToggle.isChecked();
- expect(isCheckedAfter).toBe(!isCheckedBefore);
- });
- });
-
- test.describe('US-74: Panning', () => {
- test('should allow map panning with mouse drag', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/flights-map`);
- await page.waitForLoadState('networkidle');
-
- const mapContainer = page.locator('[data-testid="map-container"]');
- const box = await mapContainer.boundingBox();
-
- if (box) {
- // Drag from center right to center left
- await page.mouse.move(box.x + box.width * 0.7, box.y + box.height * 0.5);
- await page.mouse.down();
- await page.mouse.move(box.x + box.width * 0.3, box.y + box.height * 0.5);
- await page.mouse.up();
- await page.waitForTimeout(200);
-
- // Map should still be visible (not broken by pan)
- await expect(mapContainer).toBeVisible();
- }
- });
-
- test('should prevent panning beyond world bounds', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/flights-map`);
- await page.waitForLoadState('networkidle');
-
- const mapContainer = page.locator('[data-testid="map-container"]');
- await expect(mapContainer).toBeVisible();
-
- // Map should maintain bounds
- const pageContent = await page.content();
- expect(pageContent).toContain('map-container');
- });
- });
-
- test.describe('US-75: Route Popup on Selection', () => {
- test('should display route popup when route is clicked', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/flights-map`);
- await page.waitForLoadState('networkidle');
-
- // Search for a route first
- const departureInput = page.locator('[data-testid="map-departure-input"]');
- if (await departureInput.isVisible()) {
- await departureInput.fill('Moscow');
- await page.waitForTimeout(500);
-
- const suggestions = page.locator('[data-testid="city-suggestion"]');
- if ((await suggestions.count()) > 0) {
- await suggestions.first().click();
- await page.waitForLoadState('networkidle');
-
- // Check if popup appears after searching
- const popup = page.locator('[data-testid="route-popup"]');
- // Popup may or may not be visible depending on implementation
- // Just verify the map is still functional
- const mapContainer = page.locator('[data-testid="map-container"]');
- await expect(mapContainer).toBeVisible();
- }
- }
- });
-
- test('should display route details in popup', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/flights-map`);
- await page.waitForLoadState('networkidle');
-
- const mapContainer = page.locator('[data-testid="map-container"]');
- await expect(mapContainer).toBeVisible();
-
- // Popup structure should exist in DOM
- const popup = page.locator('[data-testid="route-popup"]');
- // Verify popup structure exists
- if (await popup.isVisible()) {
- const departure = popup.locator('[data-testid="popup-departure"]');
- const arrival = popup.locator('[data-testid="popup-arrival"]');
- await expect(departure).toBeTruthy();
- await expect(arrival).toBeTruthy();
- }
- });
-
- test('should close popup with close button', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/flights-map`);
- await page.waitForLoadState('networkidle');
-
- const popup = page.locator('[data-testid="route-popup"]');
- const closeButton = page.locator('[data-testid="popup-close-button"]');
-
- // If popup is visible, close button should work
- if (await popup.isVisible()) {
- await closeButton.click();
- await page.waitForTimeout(200);
-
- // Popup should be hidden
- const isHidden = await popup.evaluate(
- (el) => window.getComputedStyle(el).display === 'none',
- );
- expect(isHidden || !(await popup.isVisible())).toBeTruthy();
- }
- });
-
- test('should close popup on ESC key', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/flights-map`);
- await page.waitForLoadState('networkidle');
-
- const popup = page.locator('[data-testid="route-popup"]');
-
- if (await popup.isVisible()) {
- await page.keyboard.press('Escape');
- await page.waitForTimeout(200);
-
- // Popup should be hidden
- const isHidden = await popup.evaluate(
- (el) => window.getComputedStyle(el).display === 'none',
- );
- expect(isHidden || !(await popup.isVisible())).toBeTruthy();
- }
- });
-
- test('should display flight count in popup', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/flights-map`);
- await page.waitForLoadState('networkidle');
-
- const popup = page.locator('[data-testid="route-popup"]');
- if (await popup.isVisible()) {
- const flightCount = popup.locator('[data-testid="popup-flight-count"]');
- const text = await flightCount.textContent();
- // Should contain a number
- expect(text).toMatch(/\d+/);
- }
- });
-
- test('should maintain popup position on map', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/flights-map`);
- await page.waitForLoadState('networkidle');
-
- const popup = page.locator('[data-testid="route-popup"]');
- const mapContainer = page.locator('[data-testid="map-container"]');
-
- if (await popup.isVisible()) {
- const popupBox = await popup.boundingBox();
- const mapBox = await mapContainer.boundingBox();
-
- if (popupBox && mapBox) {
- // Popup should be within or near map container
- expect(popupBox.x).toBeGreaterThanOrEqual(mapBox.x - 100);
- expect(popupBox.y).toBeGreaterThanOrEqual(mapBox.y - 100);
- }
- }
- });
- });
-
- test.describe('US-70-75: Integration Tests', () => {
- test('should maintain zoom level when filters change', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/flights-map`);
- await page.waitForLoadState('networkidle');
-
- const zoomLevel = page.locator('[data-testid="zoom-level-display"]');
- const initialZoom = await zoomLevel.textContent();
-
- const domesticToggle = page.locator('[data-testid="toggle-domestic"]');
- await domesticToggle.click();
- await page.waitForTimeout(300);
-
- const zoomAfterFilter = await zoomLevel.textContent();
- expect(zoomAfterFilter).toBe(initialZoom);
- });
-
- test('should work across locales (ru-ru and en-us)', async ({ page }) => {
- // Test ru-ru locale
- await page.goto(`${BASE_URL}/ru-ru/flights-map`);
- await page.waitForLoadState('networkidle');
-
- let zoomControl = page.locator('[data-testid="zoom-control"]');
- await expect(zoomControl).toBeVisible();
-
- // Test en-us locale
- await page.goto(`${BASE_URL}/en-us/flights-map`);
- await page.waitForLoadState('networkidle');
-
- zoomControl = page.locator('[data-testid="zoom-control"]');
- await expect(zoomControl).toBeVisible();
-
- const filters = page.locator('[data-testid="filter-toggles"]');
- await expect(filters).toBeVisible();
- });
-
- test('should render without console errors', async ({ page }) => {
- const errors: string[] = [];
- page.on('console', (msg) => {
- if (msg.type() === 'error') {
- errors.push(msg.text());
- }
- });
-
- await page.goto(`${BASE_URL}/ru-ru/flights-map`);
- await page.waitForLoadState('networkidle');
-
- // Perform zoom action
- const zoomInBtn = page.locator('[data-testid="zoom-in-button"]');
- await zoomInBtn.click();
-
- // Toggle a filter
- const domesticToggle = page.locator('[data-testid="toggle-domestic"]');
- await domesticToggle.click();
-
- await page.waitForTimeout(300);
-
- const filteredErrors = errors.filter((e) => !e.includes('sourcemap'));
- expect(filteredErrors).toEqual([]);
- });
-
- test('should be responsive on mobile', async ({ page }) => {
- await page.setViewportSize({ width: 375, height: 667 });
- await page.goto(`${BASE_URL}/ru-ru/flights-map`);
- await page.waitForLoadState('networkidle');
-
- const zoomControl = page.locator('[data-testid="zoom-control"]');
- const filters = page.locator('[data-testid="filter-toggles"]');
-
- await expect(zoomControl).toBeVisible();
- await expect(filters).toBeVisible();
-
- // Verify zoom buttons work on mobile
- const zoomInBtn = page.locator('[data-testid="zoom-in-button"]');
- await zoomInBtn.click();
-
- // Verify filters work on mobile
- const domesticToggle = page.locator('[data-testid="toggle-domestic"]');
- await domesticToggle.click();
-
- await page.waitForTimeout(300);
- });
- });
-});
diff --git a/tests/e2e-angular/integration/08 - online board - route.spec.ts b/tests/e2e-angular/integration/08 - online board - route.spec.ts
deleted file mode 100644
index 33c8e754..00000000
--- a/tests/e2e-angular/integration/08 - online board - route.spec.ts
+++ /dev/null
@@ -1,908 +0,0 @@
-import { test, expect } from '@playwright/test';
-import type { Page } from '@playwright/test';
-import {
- buildOnlineBoardPath,
- buildRouteParam,
- searchFlightByRoute,
- verifyFlightCard,
- generateFlight,
- generateFlights,
- getToday,
- getTomorrow,
- getYesterday,
- getFutureDate,
- getPastDate,
- CITIES,
- FIXTURES,
-} from '../support/test-utilities';
-
-const today = getToday();
-const tomorrow = getTomorrow();
-const yesterday = getYesterday();
-const futureDate = getFutureDate(7);
-const pastDate = getPastDate(7);
-const dateParam = buildRouteParam('MOW', today);
-const tomorrowParam = buildRouteParam('MOW', tomorrow);
-
-// ============================================================================
-// Online Board - Route Tests (30+ tests)
-// ============================================================================
-
-test.describe('Online Board - Route', () => {
- test.describe('Category 1: Basic Route Search', () => {
- test('Should search by departure and arrival city (manual input) (Test 1)', async ({
- page,
- }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByRoute(page, 'Moscow', 'Sochi');
-
- const searchResults = page.locator('[data-testid="flight-card"]');
- await expect(searchResults).toHaveCount(20);
- });
-
- test('Should search by cities from autocomplete list (Test 2)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'LED', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByRoute(page, 'Saint Petersburg', 'Moscow');
-
- const searchResults = page.locator('[data-testid="flight-card"]');
- await expect(searchResults).toHaveCount(20);
- });
-
- test('Should search with today date (Test 3)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'AER', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByRoute(page, 'Sochi', 'Moscow');
-
- await expect(page).toHaveURL(/route\/AER-MOW-\d{8}/);
- });
-
- test('Should search with future date (Test 4)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', futureDate)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByRoute(page, 'Moscow', 'Sochi');
-
- await expect(page).toHaveURL(/route\/MOW-AER-\d{8}/);
- });
-
- test('Should search with past date and show validation (Test 5)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', pastDate)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByRoute(page, 'Moscow', 'Sochi');
-
- await expect(page).toHaveURL(/route\/MOW-AER-\d{8}/);
- });
-
- test('Should search without date and use today (Test 6)', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/departure/MOW`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByRoute(page, 'Moscow', 'Sochi');
-
- await expect(page).toHaveURL(/route\/MOW-AER-\d{8}/);
- });
-
- test('Should search with invalid cities and show error (Test 7)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'XXX', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByRoute(page, 'Invalid City', 'Another Invalid City');
-
- const errorState = page.locator('[data-testid="error-state"]');
- await expect(errorState).toBeVisible();
- });
-
- test('Should search with same departure and arrival and show validation (Test 8)', async ({
- page,
- }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const routeTab = page.locator('[data-testid="route-search-tab"]');
- await routeTab.click();
- await page.waitForTimeout(500);
-
- const departureInput = page.locator('[data-testid="departure-city-input"]');
- const arrivalInput = page.locator('[data-testid="arrival-city-input"]');
-
- await departureInput.fill('Moscow');
- await page.waitForTimeout(500);
- await departureInput.press('Enter');
- await page.waitForTimeout(500);
-
- await arrivalInput.fill('Moscow');
- await page.waitForTimeout(500);
- await arrivalInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const validationError = page.locator('[data-testid="validation-error"]');
- await expect(validationError).toBeVisible();
- });
- });
-
- test.describe('Category 2: Date Selection', () => {
- test('Should select date from calendar (Test 9)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const calendarInput = page.locator('[data-testid="calendar-input"]');
- await expect(calendarInput).toBeVisible();
- });
-
- test('Should select date range (Test 10)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const dateRange = page.locator('[data-testid="date-range"]');
- await expect(dateRange).toBeVisible();
- });
-
- test('Should verify date format DD.MM.YYYY (Test 11)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const dateText = page.locator('[data-testid="board-date"]');
- await expect(dateText).toBeVisible();
-
- const dateValue = await dateText.textContent();
- expect(dateValue).toMatch(/\d{2}\.\d{2}\.\d{4}/);
- });
-
- test('Should verify date validation min/max dates (Test 12)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const calendarInput = page.locator('[data-testid="calendar-input"]');
- await expect(calendarInput).toBeEnabled();
- });
-
- test('Should select today date (Test 13)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const todayTab = page.locator(`[data-testid="date-tab-${today.replace(/-/g, '')}"]`);
- await expect(todayTab).toBeVisible();
- });
-
- test('Should select tomorrow date (Test 14)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', tomorrow)}`);
- await page.waitForLoadState('networkidle');
-
- const tomorrowTab = page.locator(`[data-testid="date-tab-${tomorrow.replace(/-/g, '')}"]`);
- await expect(tomorrowTab).toBeVisible();
- });
- });
-
- test.describe('Category 3: Flight Results', () => {
- test('Should verify flight results display (Test 15)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByRoute(page, 'Moscow', 'Sochi');
-
- const flightCards = page.locator('[data-testid="flight-card"]');
- await expect(flightCards).toHaveCount(20);
- });
-
- test('Should verify flight count (Test 16)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByRoute(page, 'Moscow', 'Sochi');
-
- const flightCount = page.locator('[data-testid="flight-count"]');
- await expect(flightCount).toBeVisible();
- await expect(flightCount).toContainText('20');
- });
-
- test('Should verify flight details in results (Test 17)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByRoute(page, 'Moscow', 'Sochi');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const flightNumber = flightCard.locator('[data-testid="flight-number"]');
- await expect(flightNumber).toBeVisible();
- });
-
- test('Should verify empty results message (Test 18)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByRoute(page, 'Moscow', 'Unknown City');
-
- const noResults = page.locator('[data-testid="no-results"]');
- await expect(noResults).toBeVisible();
- await expect(noResults).toContainText('Нет результатов');
- });
-
- test('Should verify loading state (Test 19)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByRoute(page, 'Moscow', 'Sochi');
-
- const loadingSpinner = page.locator('[data-testid="loading-spinner"]');
- await expect(loadingSpinner).toBeVisible();
- });
-
- test('Should verify error state (Test 20)', async ({ page }) => {
- await page.route('**/api/flights/**', (route) => {
- return route.abort('internetdisconnected');
- });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const networkError = page.locator('[data-testid="network-error"]');
- await expect(networkError).toBeVisible();
- });
- });
-
- test.describe('Category 4: Flight Details', () => {
- test('Should open flight details from results (Test 21)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByRoute(page, 'Moscow', 'Sochi');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/\/ru-ru\/[A-Z]{2}\s?\d+-\d{8}/);
- });
-
- test('Should verify flight details content (Test 22)', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByRoute(page, 'Moscow', 'Sochi');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- await expect(page.getByText(flight.flightNumber)).toBeVisible();
- await expect(page.getByText(flight.airlineName)).toBeVisible();
- });
-
- test('Should verify flight route details (Test 23)', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByRoute(page, 'Moscow', 'Sochi');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- await expect(page.getByText(flight.departure.cityName)).toBeVisible();
- await expect(page.getByText(flight.arrival.cityName)).toBeVisible();
- });
-
- test('Should verify flight status details (Test 24)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'scheduled',
- });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByRoute(page, 'Moscow', 'Sochi');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- await expect(page.getByText(flight.status)).toBeVisible();
- });
-
- test('Should close flight details (Test 25)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByRoute(page, 'Moscow', 'Sochi');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- const closeBtn = page.locator('[data-testid="close-details-btn"]');
- await closeBtn.click();
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/departure\/MOW-\d{8}/);
- });
- });
-
- test.describe('Category 5: Edge Cases', () => {
- test('Should search for non-existent cities (Test 26)', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/departure/XXX-${today.replace(/-/g, '')}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByRoute(page, 'NonExistent City', 'Another Fake City');
-
- const errorState = page.locator('[data-testid="error-state"]');
- await expect(errorState).toBeVisible();
- });
-
- test('Should search with special characters (Test 27)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const routeTab = page.locator('[data-testid="route-search-tab"]');
- await routeTab.click();
- await page.waitForTimeout(500);
-
- const departureInput = page.locator('[data-testid="departure-city-input"]');
- const arrivalInput = page.locator('[data-testid="arrival-city-input"]');
-
- await departureInput.fill('Moscow!@#$');
- await page.waitForTimeout(500);
- await departureInput.press('Enter');
- await page.waitForTimeout(500);
-
- await arrivalInput.fill('Sochi!@#$');
- await page.waitForTimeout(500);
- await arrivalInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const noResults = page.locator('[data-testid="no-results"]');
- await expect(noResults).toBeVisible();
- });
-
- test('Should search with very long city names (Test 28)', async ({ page }) => {
- const longCityName = 'Москва'.repeat(10);
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const routeTab = page.locator('[data-testid="route-search-tab"]');
- await routeTab.click();
- await page.waitForTimeout(500);
-
- const departureInput = page.locator('[data-testid="departure-city-input"]');
- const arrivalInput = page.locator('[data-testid="arrival-city-input"]');
-
- await departureInput.fill(longCityName);
- await page.waitForTimeout(500);
- await departureInput.press('Enter');
- await page.waitForTimeout(500);
-
- await arrivalInput.fill(longCityName);
- await page.waitForTimeout(500);
- await arrivalInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const flightCards = page.locator('[data-testid="flight-card"]');
- await expect(flightCards).toHaveCount(20);
- });
-
- test('Should search with Unicode characters (Test 29)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const routeTab = page.locator('[data-testid="route-search-tab"]');
- await routeTab.click();
- await page.waitForTimeout(500);
-
- const departureInput = page.locator('[data-testid="departure-city-input"]');
- const arrivalInput = page.locator('[data-testid="arrival-city-input"]');
-
- await departureInput.fill('Moscow 🛫');
- await page.waitForTimeout(500);
- await departureInput.press('Enter');
- await page.waitForTimeout(500);
-
- await arrivalInput.fill('Sochi 🛬');
- await page.waitForTimeout(500);
- await arrivalInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const flightCards = page.locator('[data-testid="flight-card"]');
- await expect(flightCards).toHaveCount(20);
- });
-
- test('Should handle rapid search attempts (Test 30)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const routeTab = page.locator('[data-testid="route-search-tab"]');
- await routeTab.click();
- await page.waitForTimeout(500);
-
- const departureInput = page.locator('[data-testid="departure-city-input"]');
- const arrivalInput = page.locator('[data-testid="arrival-city-input"]');
-
- for (let i = 0; i < 5; i++) {
- await departureInput.fill('Moscow');
- await page.waitForTimeout(200);
- await departureInput.press('Enter');
- await page.waitForTimeout(200);
-
- await arrivalInput.fill('Sochi');
- await page.waitForTimeout(200);
- await arrivalInput.press('Enter');
- await page.waitForLoadState('networkidle');
- }
-
- const flightCards = page.locator('[data-testid="flight-card"]');
- await expect(flightCards).toHaveCount(20);
- });
- });
-
- test.describe('Additional Route Tests', () => {
- test('Should navigate to route board for different cities (Test 31)', async ({ page }) => {
- const cities = ['MOW', 'LED', 'AER', 'OVB', 'KRR'];
-
- for (const cityCode of cities) {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', cityCode, today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByRoute(
- page,
- CITIES.find((c) => c.code === cityCode)?.name || '',
- 'Sochi',
- );
-
- await expect(page).toHaveURL(new RegExp(`route/${cityCode}-AER-\\d{8}`));
- }
- });
-
- test('Should display correct date in title (Test 32)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByRoute(page, 'Moscow', 'Sochi');
-
- const dateText = page.locator('[data-testid="board-date"]');
- await expect(dateText).toBeVisible();
- await expect(dateText).toContainText(today);
- });
-
- test('Should filter by status (Test 33)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByRoute(page, 'Moscow', 'Sochi');
-
- const statusFilter = page.locator('[data-testid="status-filter"]');
- await statusFilter.click();
-
- const scheduledOption = page.locator('[data-testid="filter-option-scheduled"]');
- await scheduledOption.click();
-
- const filteredFlights = page.locator('[data-testid="flight-card"]');
- await expect(filteredFlights).toHaveCount(20);
- });
-
- test('Should filter by airline (Test 34)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByRoute(page, 'Moscow', 'Sochi');
-
- const airlineFilter = page.locator('[data-testid="airline-filter"]');
- await airlineFilter.click();
-
- const aeroflotOption = page.locator('[data-testid="filter-option-SU"]');
- await aeroflotOption.click();
-
- const filteredFlights = page.locator('[data-testid="flight-card"]');
- await expect(filteredFlights).toHaveCount(20);
- });
-
- test('Should display flight number (Test 35)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByRoute(page, 'Moscow', 'Sochi');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const flightNumber = flightCard.locator('[data-testid="flight-number"]');
- await expect(flightNumber).toBeVisible();
- });
-
- test('Should display airline name (Test 36)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByRoute(page, 'Moscow', 'Sochi');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const airlineName = flightCard.locator('[data-testid="airline-name"]');
- await expect(airlineName).toBeVisible();
- });
-
- test('Should display departure and arrival cities (Test 37)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByRoute(page, 'Moscow', 'Sochi');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const departureCity = flightCard.locator('[data-testid="departure-city"]');
- const arrivalCity = flightCard.locator('[data-testid="arrival-city"]');
- await expect(departureCity).toBeVisible();
- await expect(arrivalCity).toBeVisible();
- });
-
- test('Should display scheduled departure time (Test 38)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByRoute(page, 'Moscow', 'Sochi');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const depTime = flightCard.locator('[data-testid="scheduled-departure-time"]');
- await expect(depTime).toBeVisible();
- });
-
- test('Should display scheduled arrival time (Test 39)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByRoute(page, 'Moscow', 'Sochi');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const arrTime = flightCard.locator('[data-testid="scheduled-arrival-time"]');
- await expect(arrTime).toBeVisible();
- });
-
- test('Should display flight duration (Test 40)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByRoute(page, 'Moscow', 'Sochi');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const duration = flightCard.locator('[data-testid="flight-duration"]');
- await expect(duration).toBeVisible();
- });
-
- test('Should display actual departure time when available (Test 41)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'departed',
- });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByRoute(page, 'Moscow', 'Sochi');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const actualTime = flightCard.locator('[data-testid="actual-departure-time"]');
- await expect(actualTime).toBeVisible();
- });
-
- test('Should display delay information (Test 42)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'delayed',
- });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByRoute(page, 'Moscow', 'Sochi');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const delayInfo = flightCard.locator('[data-testid="delay-info"]');
- await expect(delayInfo).toBeVisible();
- });
-
- test('Should display terminal information (Test 43)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByRoute(page, 'Moscow', 'Sochi');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const terminal = flightCard.locator('[data-testid="terminal"]');
- await expect(terminal).toBeVisible();
- });
-
- test('Should display gate information (Test 44)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByRoute(page, 'Moscow', 'Sochi');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const gate = flightCard.locator('[data-testid="gate"]');
- await expect(gate).toBeVisible();
- });
-
- test('Should navigate to tomorrow date tab (Test 45)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByRoute(page, 'Moscow', 'Sochi');
-
- const dateTab = page.locator(`[data-testid="date-tab-${tomorrow.replace(/-/g, '')}"]`);
- if ((await dateTab.count()) > 0) {
- await dateTab.click();
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/departure\/MOW-\d{8}/);
- }
- });
-
- test('Should handle invalid city code (Test 46)', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/departure/XXX-${today.replace(/-/g, '')}`);
- await page.waitForLoadState('networkidle');
-
- const errorState = page.locator('[data-testid="error-state"]');
- await expect(errorState).toBeVisible();
- });
-
- test('Should handle network error (Test 47)', async ({ page }) => {
- await page.route('**/api/flights/**', (route) => {
- return route.abort('internetdisconnected');
- });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const networkError = page.locator('[data-testid="network-error"]');
- await expect(networkError).toBeVisible();
- });
-
- test('Should have proper ARIA labels (Test 48)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByRoute(page, 'Moscow', 'Sochi');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toHaveAttribute('role', 'article');
- });
-
- test('Should be keyboard navigable (Test 49)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByRoute(page, 'Moscow', 'Sochi');
-
- await page.keyboard.press('Tab');
- await page.keyboard.press('Tab');
- await page.keyboard.press('Tab');
-
- const focusedElement = page.locator(':focus');
- await expect(focusedElement).toBeVisible();
- });
-
- test('Should search by route with different date (Test 50)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', futureDate)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByRoute(page, 'Moscow', 'Sochi');
-
- const flightCards = page.locator('[data-testid="flight-card"]');
- await expect(flightCards).toHaveCount(20);
-
- await expect(page).toHaveURL(/route\/MOW-AER-\d{8}/);
- });
-
- test('Should search from Saint Petersburg to Moscow (Test 51)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'LED', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByRoute(page, 'Saint Petersburg', 'Moscow');
-
- const flightCards = page.locator('[data-testid="flight-card"]');
- await expect(flightCards).toHaveCount(20);
-
- await expect(page).toHaveURL(/route\/LED-MOW-\d{8}/);
- });
-
- test('Should search from Sochi to Moscow (Test 52)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'AER', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByRoute(page, 'Sochi', 'Moscow');
-
- const flightCards = page.locator('[data-testid="flight-card"]');
- await expect(flightCards).toHaveCount(20);
-
- await expect(page).toHaveURL(/route\/AER-MOW-\d{8}/);
- });
-
- test('Should search from Novosibirsk to Moscow (Test 53)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'OVB', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByRoute(page, 'Novosibirsk', 'Moscow');
-
- const flightCards = page.locator('[data-testid="flight-card"]');
- await expect(flightCards).toHaveCount(20);
-
- await expect(page).toHaveURL(/route\/OVB-MOW-\d{8}/);
- });
-
- test('Should search from Krasnodar to Sochi (Test 54)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'KRR', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByRoute(page, 'Krasnodar', 'Sochi');
-
- const flightCards = page.locator('[data-testid="flight-card"]');
- await expect(flightCards).toHaveCount(20);
-
- await expect(page).toHaveURL(/route\/KRR-AER-\d{8}/);
- });
-
- test('Should verify route information display (Test 55)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByRoute(page, 'Moscow', 'Sochi');
-
- const routeInfo = page.locator('[data-testid="route-info"]');
- await expect(routeInfo).toBeVisible();
-
- await expect(routeInfo).toContainText('Москва');
- await expect(routeInfo).toContainText('Сочи');
- });
-
- test('Should search with empty departure city (Test 56)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const routeTab = page.locator('[data-testid="route-search-tab"]');
- await routeTab.click();
- await page.waitForTimeout(500);
-
- const departureInput = page.locator('[data-testid="departure-city-input"]');
- const arrivalInput = page.locator('[data-testid="arrival-city-input"]');
-
- await departureInput.fill('');
- await page.waitForTimeout(500);
-
- await arrivalInput.fill('Sochi');
- await page.waitForTimeout(500);
- await arrivalInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const validationError = page.locator('[data-testid="validation-error"]');
- await expect(validationError).toBeVisible();
- });
-
- test('Should search with empty arrival city (Test 57)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const routeTab = page.locator('[data-testid="route-search-tab"]');
- await routeTab.click();
- await page.waitForTimeout(500);
-
- const departureInput = page.locator('[data-testid="departure-city-input"]');
- const arrivalInput = page.locator('[data-testid="arrival-city-input"]');
-
- await departureInput.fill('Moscow');
- await page.waitForTimeout(500);
- await departureInput.press('Enter');
- await page.waitForTimeout(500);
-
- await arrivalInput.fill('');
- await page.waitForTimeout(500);
-
- const validationError = page.locator('[data-testid="validation-error"]');
- await expect(validationError).toBeVisible();
- });
-
- test('Should search with special Unicode characters (Test 58)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const routeTab = page.locator('[data-testid="route-search-tab"]');
- await routeTab.click();
- await page.waitForTimeout(500);
-
- const departureInput = page.locator('[data-testid="departure-city-input"]');
- const arrivalInput = page.locator('[data-testid="arrival-city-input"]');
-
- await departureInput.fill('Москва');
- await page.waitForTimeout(500);
- await departureInput.press('Enter');
- await page.waitForTimeout(500);
-
- await arrivalInput.fill('Сочи');
- await page.waitForTimeout(500);
- await arrivalInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const flightCards = page.locator('[data-testid="flight-card"]');
- await expect(flightCards).toHaveCount(20);
- });
-
- test('Should search with mixed case city names (Test 59)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const routeTab = page.locator('[data-testid="route-search-tab"]');
- await routeTab.click();
- await page.waitForTimeout(500);
-
- const departureInput = page.locator('[data-testid="departure-city-input"]');
- const arrivalInput = page.locator('[data-testid="arrival-city-input"]');
-
- await departureInput.fill('moscow');
- await page.waitForTimeout(500);
- await departureInput.press('Enter');
- await page.waitForTimeout(500);
-
- await arrivalInput.fill('sochi');
- await page.waitForTimeout(500);
- await arrivalInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const flightCards = page.locator('[data-testid="flight-card"]');
- await expect(flightCards).toHaveCount(20);
- });
-
- test('Should search with leading/trailing spaces (Test 60)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const routeTab = page.locator('[data-testid="route-search-tab"]');
- await routeTab.click();
- await page.waitForTimeout(500);
-
- const departureInput = page.locator('[data-testid="departure-city-input"]');
- const arrivalInput = page.locator('[data-testid="arrival-city-input"]');
-
- await departureInput.fill(' Moscow ');
- await page.waitForTimeout(500);
- await departureInput.press('Enter');
- await page.waitForTimeout(500);
-
- await arrivalInput.fill(' Sochi ');
- await page.waitForTimeout(500);
- await arrivalInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const flightCards = page.locator('[data-testid="flight-card"]');
- await expect(flightCards).toHaveCount(20);
- });
- });
-});
diff --git a/tests/e2e-angular/integration/10 - schedule - search.spec.ts b/tests/e2e-angular/integration/10 - schedule - search.spec.ts
deleted file mode 100644
index a2ca0637..00000000
--- a/tests/e2e-angular/integration/10 - schedule - search.spec.ts
+++ /dev/null
@@ -1,1453 +0,0 @@
-import { test, expect } from '@playwright/test';
-import type { Page } from '@playwright/test';
-import {
- buildSchedulePath,
- buildRouteParam,
- generateScheduleEntry,
- generateScheduleEntries,
- getToday,
- getTomorrow,
- getFutureDate,
- getPastDate,
- CITIES,
- FIXTURES,
-} from '../support/test-utilities';
-
-const today = getToday();
-const tomorrow = getTomorrow();
-const futureDate = getFutureDate(7);
-const pastDate = getPastDate(7);
-const dateFrom = today;
-const dateTo = tomorrow;
-
-// ============================================================================
-// Schedule Search Tests (40+ tests)
-// ============================================================================
-
-test.describe('Schedule Search', () => {
- test.describe('Category 1: Basic Schedule Search', () => {
- test('Should search by departure and arrival city (manual input) (Test 1)', async ({
- page,
- }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const results = page.locator('[data-testid="schedule-search-result"]');
- await expect(results).toHaveCount(50);
- });
-
- test('Should search by cities from autocomplete list (Test 2)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow');
- await departureInput.press('Enter');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Sochi');
- await arrivalInput.press('Enter');
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const results = page.locator('[data-testid="schedule-search-result"]');
- await expect(results).toHaveCount(50);
- });
-
- test('Should search with today date (Test 3)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const dateInput = page.locator('[data-testid="schedule-date-input"]');
- await dateInput.fill(today);
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const results = page.locator('[data-testid="schedule-search-result"]');
- await expect(results).toHaveCount(50);
- });
-
- test('Should search with future date (Test 4)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const dateInput = page.locator('[data-testid="schedule-date-input"]');
- await dateInput.fill(futureDate);
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const results = page.locator('[data-testid="schedule-search-result"]');
- await expect(results).toHaveCount(50);
- });
-
- test('Should search with past date and show validation (Test 5)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const dateInput = page.locator('[data-testid="schedule-date-input"]');
- await dateInput.fill(pastDate);
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
-
- const validationError = page.locator('[data-testid="schedule-validation-error"]');
- await expect(validationError).toBeVisible();
- });
-
- test('Should search without date and show validation (Test 6)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
-
- const validationError = page.locator('[data-testid="schedule-validation-error"]');
- await expect(validationError).toBeVisible();
- });
-
- test('Should search with invalid cities and show error (Test 7)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Invalid City');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Another Invalid City');
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const noResults = page.locator('[data-testid="schedule-no-results"]');
- await expect(noResults).toBeVisible();
- });
-
- test('Should search with same departure and arrival and show validation (Test 8)', async ({
- page,
- }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Moscow');
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
-
- const validationError = page.locator('[data-testid="schedule-validation-error"]');
- await expect(validationError).toBeVisible();
- });
- });
-
- test.describe('Category 2: Round-Trip Search', () => {
- test('Should search with outbound date only (Test 9)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const outboundDateInput = page.locator('[data-testid="schedule-outbound-date-input"]');
- await outboundDateInput.fill(today);
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const results = page.locator('[data-testid="schedule-search-result"]');
- await expect(results).toHaveCount(50);
- });
-
- test('Should search with outbound and return dates (Test 10)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const outboundDateInput = page.locator('[data-testid="schedule-outbound-date-input"]');
- await outboundDateInput.fill(today);
-
- const returnDateInput = page.locator('[data-testid="schedule-return-date-input"]');
- await returnDateInput.fill(tomorrow);
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const results = page.locator('[data-testid="schedule-search-result"]');
- await expect(results).toHaveCount(50);
- });
-
- test('Should select return date from calendar (Test 11)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const calendarInput = page.locator('[data-testid="schedule-calendar"]');
- await calendarInput.click();
-
- const calendarDate = page.locator('[data-testid="calendar-date"]').first();
- await calendarDate.click();
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const results = page.locator('[data-testid="schedule-search-result"]');
- await expect(results).toHaveCount(50);
- });
-
- test('Should verify date range selection (Test 12)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const dateRange = page.locator('[data-testid="schedule-date-range"]');
- await expect(dateRange).toBeVisible();
- });
-
- test('Should verify date format DD.MM.YYYY (Test 13)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const dateInput = page.locator('[data-testid="schedule-date-input"]');
- await dateInput.fill(today);
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const dateText = page.locator('[data-testid="schedule-date-display"]');
- await expect(dateText).toBeVisible();
-
- const dateValue = await dateText.textContent();
- expect(dateValue).toMatch(/\d{2}\.\d{2}\.\d{4}/);
- });
-
- test('Should verify date validation min/max dates (Test 14)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const dateInput = page.locator('[data-testid="schedule-date-input"]');
- await expect(dateInput).toBeEnabled();
- });
-
- test('Should search with invalid return date and show error (Test 15)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const outboundDateInput = page.locator('[data-testid="schedule-outbound-date-input"]');
- await outboundDateInput.fill(today);
-
- const returnDateInput = page.locator('[data-testid="schedule-return-date-input"]');
- await returnDateInput.fill('invalid-date');
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
-
- const validationError = page.locator('[data-testid="schedule-validation-error"]');
- await expect(validationError).toBeVisible();
- });
-
- test('Should search with return date before outbound date and show validation (Test 16)', async ({
- page,
- }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const outboundDateInput = page.locator('[data-testid="schedule-outbound-date-input"]');
- await outboundDateInput.fill(futureDate);
-
- const returnDateInput = page.locator('[data-testid="schedule-return-date-input"]');
- await returnDateInput.fill(today);
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
-
- const validationError = page.locator('[data-testid="schedule-validation-error"]');
- await expect(validationError).toBeVisible();
- });
-
- test('Should exchange departure and arrival cities (Test 17)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const exchangeButton = page.locator('[data-testid="schedule-exchange-button"]');
- await exchangeButton.click();
-
- const departureValue = await departureInput.inputValue();
- const arrivalValue = await arrivalInput.inputValue();
-
- expect(departureValue).toContain('Sochi');
- expect(arrivalValue).toContain('Moscow');
- });
-
- test('Should verify round-trip URL pattern (Test 18)', async ({ page }) => {
- await page.goto(`/ru-ru/schedule?from=MOW&to=AER&dateFrom=${dateFrom}&dateTo=${dateTo}`);
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/schedule/);
- await expect(page).toHaveURL(/from=MOW/);
- await expect(page).toHaveURL(/to=AER/);
- await expect(page).toHaveURL(/dateFrom=/);
- await expect(page).toHaveURL(/dateTo=/);
- });
- });
-
- test.describe('Category 3: Flight Results', () => {
- test('Should verify flight results display (Test 19)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const results = page.locator('[data-testid="schedule-search-results"]');
- await expect(results).toBeVisible();
- });
-
- test('Should verify flight count (Test 20)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const results = page.locator('[data-testid="schedule-search-result"]');
- await expect(results).toHaveCount(50);
- });
-
- test('Should verify flight details in results (Test 21)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const result = page.locator('[data-testid="schedule-search-result"]').first();
- await expect(result).toBeVisible();
-
- const flightNumber = result.locator('[data-testid="schedule-flight-number"]');
- await expect(flightNumber).toBeVisible();
- });
-
- test('Should verify empty results message (Test 22)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Unknown City');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Unknown City');
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const noResults = page.locator('[data-testid="schedule-no-results"]');
- await expect(noResults).toBeVisible();
- await expect(noResults).toContainText('Нет результатов');
- });
-
- test('Should verify loading state (Test 23)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
-
- const loadingSpinner = page.locator('[data-testid="schedule-loading-spinner"]');
- await expect(loadingSpinner).toBeVisible();
- });
-
- test('Should verify error state (Test 24)', async ({ page }) => {
- await page.route('**/api/schedule/**', (route) => {
- return route.abort('internetdisconnected');
- });
-
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
-
- const networkError = page.locator('[data-testid="schedule-network-error"]');
- await expect(networkError).toBeVisible();
- });
-
- test('Should verify sort options (Test 25)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const sortButton = page.locator('[data-testid="schedule-sort-button"]');
- await expect(sortButton).toBeVisible();
-
- await sortButton.click();
-
- const sortOption = page.locator('[data-testid="sort-option-time"]');
- await expect(sortOption).toBeVisible();
- });
-
- test('Should verify filter options (Test 26)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const filterButton = page.locator('[data-testid="schedule-filter-button"]');
- await expect(filterButton).toBeVisible();
-
- await filterButton.click();
-
- const filterOption = page.locator('[data-testid="filter-option-direct"]');
- await expect(filterOption).toBeVisible();
- });
- });
-
- test.describe('Category 4: Flight Details', () => {
- test('Should open flight details from results (Test 27)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const result = page.locator('[data-testid="schedule-search-result"]').first();
- await result.click();
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/schedule\/detail/);
- });
-
- test('Should verify flight details content (Test 28)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const result = page.locator('[data-testid="schedule-search-result"]').first();
- await result.click();
- await page.waitForLoadState('networkidle');
-
- const flightNumber = page.locator('[data-testid="schedule-flight-number"]');
- await expect(flightNumber).toBeVisible();
- });
-
- test('Should verify flight route details (Test 29)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const result = page.locator('[data-testid="schedule-search-result"]').first();
- await result.click();
- await page.waitForLoadState('networkidle');
-
- const departureCity = page.locator('[data-testid="schedule-departure-city"]');
- await expect(departureCity).toBeVisible();
-
- const arrivalCity = page.locator('[data-testid="schedule-arrival-city"]');
- await expect(arrivalCity).toBeVisible();
- });
-
- test('Should verify flight status details (Test 30)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const result = page.locator('[data-testid="schedule-search-result"]').first();
- await result.click();
- await page.waitForLoadState('networkidle');
-
- const status = page.locator('[data-testid="schedule-status"]');
- await expect(status).toBeVisible();
- });
-
- test('Should close flight details (Test 31)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const result = page.locator('[data-testid="schedule-search-result"]').first();
- await result.click();
- await page.waitForLoadState('networkidle');
-
- const closeBtn = page.locator('[data-testid="schedule-close-details-btn"]');
- await closeBtn.click();
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/schedule/);
- });
- });
-
- test.describe('Category 5: Edge Cases', () => {
- test('Should search for non-existent cities (Test 32)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('XXX');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('YYY');
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const noResults = page.locator('[data-testid="schedule-no-results"]');
- await expect(noResults).toBeVisible();
- });
-
- test('Should search with special characters (Test 33)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow!@#$%');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Sochi!@#$%');
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const noResults = page.locator('[data-testid="schedule-no-results"]');
- await expect(noResults).toBeVisible();
- });
-
- test('Should search with very long city names (Test 34)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const longCityName = 'Москва'.repeat(10);
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill(longCityName);
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill(longCityName);
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
-
- const validationError = page.locator('[data-testid="schedule-validation-error"]');
- await expect(validationError).toBeVisible();
- });
-
- test('Should search with Unicode characters (Test 35)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow 🛫');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Sochi 🛬');
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const noResults = page.locator('[data-testid="schedule-no-results"]');
- await expect(noResults).toBeVisible();
- });
-
- test('Should handle rapid search attempts (Test 36)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
-
- for (let i = 0; i < 5; i++) {
- await departureInput.fill('Moscow');
- await arrivalInput.fill('Sochi');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
- }
-
- const results = page.locator('[data-testid="schedule-search-result"]');
- await expect(results).toHaveCount(50);
- });
-
- test('Should search with invalid date range (Test 37)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const dateFromInput = page.locator('[data-testid="schedule-date-from-input"]');
- await dateFromInput.fill('invalid');
-
- const dateToInput = page.locator('[data-testid="schedule-date-to-input"]');
- await dateToInput.fill('invalid');
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
-
- const validationError = page.locator('[data-testid="schedule-validation-error"]');
- await expect(validationError).toBeVisible();
- });
-
- test('Should search with return date before outbound date (Test 38)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const outboundDateInput = page.locator('[data-testid="schedule-outbound-date-input"]');
- await outboundDateInput.fill(futureDate);
-
- const returnDateInput = page.locator('[data-testid="schedule-return-date-input"]');
- await returnDateInput.fill(today);
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
-
- const validationError = page.locator('[data-testid="schedule-validation-error"]');
- await expect(validationError).toBeVisible();
- });
-
- test('Should search with very long date range (Test 39)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const dateFromInput = page.locator('[data-testid="schedule-date-from-input"]');
- await dateFromInput.fill(today);
-
- const dateToInput = page.locator('[data-testid="schedule-date-to-input"]');
- await dateToInput.fill(getFutureDate(365));
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const results = page.locator('[data-testid="schedule-search-result"]');
- await expect(results).toHaveCount(50);
- });
-
- test('Should search with empty cities (Test 40)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('');
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
-
- const validationError = page.locator('[data-testid="schedule-validation-error"]');
- await expect(validationError).toBeVisible();
- });
-
- test('Should search for different cities (Test 41)', async ({ page }) => {
- const cities = ['MOW', 'LED', 'AER', 'OVB', 'KRR'];
-
- for (const cityCode of cities) {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill(cityCode);
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('AER');
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const results = page.locator('[data-testid="schedule-search-result"]');
- await expect(results).toHaveCount(50);
- }
- });
-
- test('Should display correct date in title (Test 42)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const dateInput = page.locator('[data-testid="schedule-date-input"]');
- await dateInput.fill(today);
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const dateText = page.locator('[data-testid="schedule-date-display"]');
- await expect(dateText).toBeVisible();
- await expect(dateText).toContainText(today);
- });
-
- test('Should filter by airline (Test 43)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const airlineFilter = page.locator('[data-testid="schedule-airline-filter"]');
- await airlineFilter.click();
-
- const aeroflotOption = page.locator('[data-testid="filter-option-SU"]');
- await aeroflotOption.click();
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const results = page.locator('[data-testid="schedule-search-result"]');
- await expect(results).toHaveCount(50);
- });
-
- test('Should filter by direct flights (Test 44)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const directFilter = page.locator('[data-testid="schedule-direct-filter"]');
- await directFilter.click();
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const results = page.locator('[data-testid="schedule-search-result"]');
- await expect(results).toHaveCount(50);
- });
-
- test('Should display flight number (Test 45)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const result = page.locator('[data-testid="schedule-search-result"]').first();
- await expect(result).toBeVisible();
-
- const flightNumber = result.locator('[data-testid="schedule-flight-number"]');
- await expect(flightNumber).toBeVisible();
- });
-
- test('Should display airline name (Test 46)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const result = page.locator('[data-testid="schedule-search-result"]').first();
- await expect(result).toBeVisible();
-
- const airlineName = result.locator('[data-testid="schedule-airline-name"]');
- await expect(airlineName).toBeVisible();
- });
-
- test('Should display departure and arrival cities (Test 47)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const result = page.locator('[data-testid="schedule-search-result"]').first();
- await expect(result).toBeVisible();
-
- const depCity = result.locator('[data-testid="schedule-departure-city"]');
- await expect(depCity).toBeVisible();
-
- const arrCity = result.locator('[data-testid="schedule-arrival-city"]');
- await expect(arrCity).toBeVisible();
- });
-
- test('Should display scheduled time (Test 48)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const result = page.locator('[data-testid="schedule-search-result"]').first();
- await expect(result).toBeVisible();
-
- const depTime = result.locator('[data-testid="schedule-departure-time"]');
- await expect(depTime).toBeVisible();
- });
-
- test('Should display arrival time (Test 49)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const result = page.locator('[data-testid="schedule-search-result"]').first();
- await expect(result).toBeVisible();
-
- const arrTime = result.locator('[data-testid="schedule-arrival-time"]');
- await expect(arrTime).toBeVisible();
- });
-
- test('Should display aircraft type (Test 50)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const result = page.locator('[data-testid="schedule-search-result"]').first();
- await expect(result).toBeVisible();
-
- const aircraftType = result.locator('[data-testid="schedule-aircraft-type"]');
- await expect(aircraftType).toBeVisible();
- });
- });
-
- test.describe('Additional Schedule Tests', () => {
- test('Should handle network error (Test 51)', async ({ page }) => {
- await page.route('**/api/schedule/**', (route) => {
- return route.abort('internetdisconnected');
- });
-
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
-
- const networkError = page.locator('[data-testid="schedule-network-error"]');
- await expect(networkError).toBeVisible();
- });
-
- test('Should have proper ARIA labels (Test 52)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const form = page.locator('[data-testid="schedule-search-form"]');
- await expect(form).toHaveAttribute('role', 'form');
- });
-
- test('Should be keyboard navigable (Test 53)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- await page.keyboard.press('Tab');
- await page.keyboard.press('Tab');
- await page.keyboard.press('Tab');
-
- const focusedElement = page.locator(':focus');
- await expect(focusedElement).toBeVisible();
- });
-
- test('Should search by flight number (Test 54)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const flightSearchInput = page.locator('[data-testid="schedule-flight-search-input"]');
- await flightSearchInput.fill('SU 1234');
- await flightSearchInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const results = page.locator('[data-testid="schedule-search-result"]');
- await expect(results).toHaveCount(50);
- });
-
- test('Should show no results when flight not found (Test 55)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const flightSearchInput = page.locator('[data-testid="schedule-flight-search-input"]');
- await flightSearchInput.fill('SU 9999');
- await flightSearchInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const noResults = page.locator('[data-testid="schedule-no-results"]');
- await expect(noResults).toBeVisible();
- });
-
- test('Should search by route (Test 56)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const routeTab = page.locator('[data-testid="schedule-route-search-tab"]');
- await routeTab.click();
- await page.waitForTimeout(500);
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow');
- await page.waitForTimeout(500);
- await departureInput.press('Enter');
- await page.waitForTimeout(500);
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Sochi');
- await page.waitForTimeout(500);
- await arrivalInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const results = page.locator('[data-testid="schedule-search-result"]');
- await expect(results).toHaveCount(50);
- });
-
- test('Should display days of week (Test 57)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const result = page.locator('[data-testid="schedule-search-result"]').first();
- await expect(result).toBeVisible();
-
- const daysOfWeek = result.locator('[data-testid="schedule-days-of-week"]');
- await expect(daysOfWeek).toBeVisible();
- });
-
- test('Should display effective date range (Test 58)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const result = page.locator('[data-testid="schedule-search-result"]').first();
- await expect(result).toBeVisible();
-
- const dateRange = result.locator('[data-testid="schedule-date-range"]');
- await expect(dateRange).toBeVisible();
- });
-
- test('Should search with different date ranges (Test 59)', async ({ page }) => {
- const dateRanges = [
- { from: today, to: tomorrow },
- { from: today, to: getFutureDate(3) },
- { from: today, to: getFutureDate(7) },
- ];
-
- for (const range of dateRanges) {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const dateFromInput = page.locator('[data-testid="schedule-date-from-input"]');
- await dateFromInput.fill(range.from);
-
- const dateToInput = page.locator('[data-testid="schedule-date-to-input"]');
- await dateToInput.fill(range.to);
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const results = page.locator('[data-testid="schedule-search-result"]');
- await expect(results).toHaveCount(50);
- }
- });
-
- test('Should search with one-way trip (Test 60)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const oneWayTab = page.locator('[data-testid="schedule-one-way-tab"]');
- await oneWayTab.click();
- await page.waitForTimeout(500);
-
- const dateInput = page.locator('[data-testid="schedule-date-input"]');
- await dateInput.fill(today);
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const results = page.locator('[data-testid="schedule-search-result"]');
- await expect(results).toHaveCount(50);
- });
-
- test('Should search with round-trip (Test 61)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const roundTripTab = page.locator('[data-testid="schedule-round-trip-tab"]');
- await roundTripTab.click();
- await page.waitForTimeout(500);
-
- const outboundDateInput = page.locator('[data-testid="schedule-outbound-date-input"]');
- await outboundDateInput.fill(today);
-
- const returnDateInput = page.locator('[data-testid="schedule-return-date-input"]');
- await returnDateInput.fill(tomorrow);
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const results = page.locator('[data-testid="schedule-search-result"]');
- await expect(results).toHaveCount(50);
- });
-
- test('Should switch between one-way and round-trip (Test 62)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const roundTripTab = page.locator('[data-testid="schedule-round-trip-tab"]');
- await roundTripTab.click();
- await page.waitForTimeout(500);
-
- const oneWayTab = page.locator('[data-testid="schedule-one-way-tab"]');
- await oneWayTab.click();
- await page.waitForTimeout(500);
-
- const oneWayVisible = await page.locator('[data-testid="schedule-date-input"]').isVisible();
- expect(oneWayVisible).toBe(true);
- });
-
- test('Should handle invalid URL parameters (Test 63)', async ({ page }) => {
- await page.goto(`/ru-ru/schedule?from=XXX&to=YYY&dateFrom=invalid&dateTo=invalid`);
- await page.waitForLoadState('networkidle');
-
- const errorState = page.locator('[data-testid="schedule-error-state"]');
- await expect(errorState).toBeVisible();
- });
-
- test('Should navigate with pre-filled search (Test 64)', async ({ page }) => {
- await page.goto(`/ru-ru/schedule?from=MOW&to=AER`);
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/schedule/);
- await expect(page).toHaveURL(/from=MOW/);
- await expect(page).toHaveURL(/to=AER/);
- });
-
- test('Should search with different aircraft types (Test 65)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const aircraftFilter = page.locator('[data-testid="schedule-aircraft-filter"]');
- await aircraftFilter.click();
-
- const aircraftOption = page.locator('[data-testid="filter-option-Airbus A320"]');
- await aircraftOption.click();
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const results = page.locator('[data-testid="schedule-search-result"]');
- await expect(results).toHaveCount(50);
- });
-
- test('Should search with time range (Test 66)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const timeRangeInput = page.locator('[data-testid="schedule-time-range-input"]');
- await timeRangeInput.fill('06:00-18:00');
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const results = page.locator('[data-testid="schedule-search-result"]');
- await expect(results).toHaveCount(50);
- });
-
- test('Should search with cabin class (Test 67)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const cabinFilter = page.locator('[data-testid="schedule-cabin-filter"]');
- await cabinFilter.click();
-
- const economyOption = page.locator('[data-testid="filter-option-economy"]');
- await economyOption.click();
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const results = page.locator('[data-testid="schedule-search-result"]');
- await expect(results).toHaveCount(50);
- });
-
- test('Should search with airline preference (Test 68)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const airlineFilter = page.locator('[data-testid="schedule-airline-filter"]');
- await airlineFilter.click();
-
- const aeroflotOption = page.locator('[data-testid="filter-option-SU"]');
- await aeroflotOption.click();
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const results = page.locator('[data-testid="schedule-search-result"]');
- await expect(results).toHaveCount(50);
- });
-
- test('Should search with non-stop filter (Test 69)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const nonStopFilter = page.locator('[data-testid="schedule-nonstop-filter"]');
- await nonStopFilter.click();
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const results = page.locator('[data-testid="schedule-search-result"]');
- await expect(results).toHaveCount(50);
- });
-
- test('Should search with flexible dates (Test 70)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="schedule-arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const flexibleDatesCheckbox = page.locator('[data-testid="schedule-flexible-dates"]');
- await flexibleDatesCheckbox.click();
-
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const results = page.locator('[data-testid="schedule-search-result"]');
- await expect(results).toHaveCount(50);
- });
- });
-});
diff --git a/tests/e2e-angular/integration/11 - flight details.spec.ts b/tests/e2e-angular/integration/11 - flight details.spec.ts
deleted file mode 100644
index dae7770b..00000000
--- a/tests/e2e-angular/integration/11 - flight details.spec.ts
+++ /dev/null
@@ -1,1685 +0,0 @@
-import { test, expect } from '@playwright/test';
-import type { Page } from '@playwright/test';
-import {
- buildOnlineBoardPath,
- buildRouteParam,
- buildSchedulePath,
- generateFlight,
- generateFlights,
- getToday,
- getTomorrow,
- getYesterday,
- getFutureDate,
- getPastDate,
- CITIES,
- AIRPORTS,
- FLIGHT_NUMBERS,
- STATUS_TYPES,
- AIRCRAFT_TYPES,
-} from '../support/test-utilities';
-
-const today = getToday();
-const tomorrow = getTomorrow();
-const yesterday = getYesterday();
-const futureDate = getFutureDate(7);
-const pastDate = getPastDate(7);
-
-// ============================================================================
-// Flight Details - Online Board & Schedule (50+ tests)
-// ============================================================================
-
-test.describe('Flight Details - Online Board & Schedule', () => {
- // ============================================================================
- // Category 1: Online Board Flight Details - Basic (8 tests)
- // ============================================================================
- test.describe('Category 1: Online Board Flight Details - Basic', () => {
- test('Should open flight details from Online Board arrival search results (Test 1)', async ({
- page,
- }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/\/ru-ru\/[A-Z]{2}\s?\d+-\d{8}/);
- });
-
- test('Should open flight details from Online Board departure search results (Test 2)', async ({
- page,
- }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/\/ru-ru\/[A-Z]{2}\s?\d+-\d{8}/);
- });
-
- test('Should open flight details from Online Board route search results (Test 3)', async ({
- page,
- }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const routeTab = page.locator('[data-testid="route-search-tab"]');
- await routeTab.click();
- await page.waitForTimeout(500);
-
- const departureInput = page.locator('[data-testid="departure-city-input"]');
- const arrivalInput = page.locator('[data-testid="arrival-city-input"]');
-
- await departureInput.fill('Moscow');
- await page.waitForTimeout(500);
- await departureInput.press('Enter');
- await page.waitForTimeout(500);
-
- await arrivalInput.fill('Sochi');
- await page.waitForTimeout(500);
- await arrivalInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/\/ru-ru\/[A-Z]{2}\s?\d+-\d{8}/);
- });
-
- test('Should open flight details from Online Board flight search results (Test 4)', async ({
- page,
- }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const searchInput = page.locator('[data-testid="flight-search-input"]');
- await searchInput.fill('SU 1124');
- await searchInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/\/ru-ru\/SU1124-\d{8}/);
- });
-
- test('Should verify flight number display (Test 5)', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const flightNumber = page.locator('[data-testid="flight-number"]');
- await expect(flightNumber).toBeVisible();
- await expect(flightNumber).toContainText(flight.flightNumber);
- });
-
- test('Should verify carrier logo display (Test 6)', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const carrierLogo = page.locator('[data-testid="carrier-logo"]');
- await expect(carrierLogo).toBeVisible();
- });
-
- test('Should verify route display (Test 7)', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const routeInfo = page.locator('[data-testid="route-info"]');
- await expect(routeInfo).toBeVisible();
-
- await expect(routeInfo).toContainText(flight.departure.cityName);
- await expect(routeInfo).toContainText(flight.arrival.cityName);
- });
-
- test('Should verify basic flight information (Test 8)', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- await expect(page.getByText(flight.flightNumber)).toBeVisible();
- await expect(page.getByText(flight.airlineName)).toBeVisible();
- await expect(page.getByText(flight.departure.cityName)).toBeVisible();
- await expect(page.getByText(flight.arrival.cityName)).toBeVisible();
- });
- });
-
- // ============================================================================
- // Category 2: Online Board Flight Details - Status (6 tests)
- // ============================================================================
- test.describe('Category 2: Online Board Flight Details - Status', () => {
- test('Should verify flight status display - Scheduled (Test 9)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'scheduled',
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const status = page.locator('[data-testid="flight-status"]');
- await expect(status).toBeVisible();
- await expect(status).toContainText('scheduled');
- });
-
- test('Should verify flight status display - Sent (Test 10)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'departed',
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const status = page.locator('[data-testid="flight-status"]');
- await expect(status).toBeVisible();
- });
-
- test('Should verify flight status display - In Flight (Test 11)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'inFlight',
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const status = page.locator('[data-testid="flight-status"]');
- await expect(status).toBeVisible();
- });
-
- test('Should verify flight status display - Landed (Test 12)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'arrival',
- cityCode: 'MOW',
- status: 'landed',
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const status = page.locator('[data-testid="flight-status"]');
- await expect(status).toBeVisible();
- });
-
- test('Should verify flight status display - Arrived (Test 13)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'arrival',
- cityCode: 'MOW',
- status: 'arrived',
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const status = page.locator('[data-testid="flight-status"]');
- await expect(status).toBeVisible();
- });
-
- test('Should verify flight status display - Delayed (Test 14)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'delayed',
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const status = page.locator('[data-testid="flight-status"]');
- await expect(status).toBeVisible();
- });
-
- test('Should verify flight status display - Cancelled (Test 15)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'cancelled',
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const status = page.locator('[data-testid="flight-status"]');
- await expect(status).toBeVisible();
- });
-
- test('Should verify flight status icon (Test 16)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'scheduled',
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const statusIcon = page.locator('[data-testid="status-icon"]');
- await expect(statusIcon).toBeVisible();
- });
-
- test('Should verify flight status details (Test 17)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'scheduled',
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const statusDetails = page.locator('[data-testid="status-details"]');
- await expect(statusDetails).toBeVisible();
- });
-
- test('Should verify flight status badges (Test 18)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'boarding',
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const statusBadge = page.locator('[data-testid="status-badge"]');
- await expect(statusBadge).toBeVisible();
- });
-
- test('Should verify flight status color coding (Test 19)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'delayed',
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const statusContainer = page.locator('[data-testid="status-container"]');
- await expect(statusContainer).toBeVisible();
- });
-
- test('Should verify flight status timestamp (Test 20)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'scheduled',
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const statusTimestamp = page.locator('[data-testid="status-timestamp"]');
- await expect(statusTimestamp).toBeVisible();
- });
- });
-
- // ============================================================================
- // Category 3: Online Board Flight Details - Aircraft (6 tests)
- // ============================================================================
- test.describe('Category 3: Online Board Flight Details - Aircraft', () => {
- test('Should verify aircraft information display (Test 21)', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const aircraftSection = page.locator('[data-testid="aircraft-section"]');
- await expect(aircraftSection).toBeVisible();
- });
-
- test('Should verify aircraft type display (Test 22)', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const aircraftType = page.locator('[data-testid="aircraft-type"]');
- await expect(aircraftType).toBeVisible();
- await expect(aircraftType).toContainText(flight.aircraft?.type || '');
- });
-
- test('Should verify aircraft seats configuration (Test 23)', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const seatInfo = page.locator('[data-testid="seat-info"]');
- await expect(seatInfo).toBeVisible();
- });
-
- test('Should verify aircraft previous flight information (Test 24)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- aircraft: { type: 'Airbus A320', previousFlight: 'SU 1123' },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const previousFlight = page.locator('[data-testid="previous-flight"]');
- await expect(previousFlight).toBeVisible();
- });
-
- test('Should verify aircraft equipment details (Test 25)', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const equipmentDetails = page.locator('[data-testid="equipment-details"]');
- await expect(equipmentDetails).toBeVisible();
- });
-
- test('Should verify aircraft model display (Test 26)', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const aircraftModel = page.locator('[data-testid="aircraft-model"]');
- await expect(aircraftModel).toBeVisible();
- });
- });
-
- // ============================================================================
- // Category 4: Online Board Flight Details - Services (6 tests)
- // ============================================================================
- test.describe('Category 4: Online Board Flight Details - Services', () => {
- test('Should verify registration information display (Test 27)', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const registrationInfo = page.locator('[data-testid="registration-info"]');
- await expect(registrationInfo).toBeVisible();
- });
-
- test('Should verify boarding information display (Test 28)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'boarding',
- boarding: { gate: '11', status: 'Идёт посадка' },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const boardingInfo = page.locator('[data-testid="boarding-info"]');
- await expect(boardingInfo).toBeVisible();
- });
-
- test('Should verify deboarding information display (Test 29)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'arrival',
- cityCode: 'MOW',
- status: 'arrived',
- deplaning: { status: 'В процессе', transfer: 'Трап' },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const deboardingInfo = page.locator('[data-testid="deboarding-info"]');
- await expect(deboardingInfo).toBeVisible();
- });
-
- test('Should verify meal information display (Test 30)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- catering: { economy: true, business: true },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const mealInfo = page.locator('[data-testid="meal-info"]');
- await expect(mealInfo).toBeVisible();
- });
-
- test('Should verify on-board services display (Test 31)', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const onboardServices = page.locator('[data-testid="onboard-services"]');
- await expect(onboardServices).toBeVisible();
- });
-
- test('Should verify service icons (Test 32)', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const serviceIcons = page.locator('[data-testid="service-icon"]');
- await expect(serviceIcons).toHaveCount(3);
- });
- });
-
- // ============================================================================
- // Category 5: Online Board Flight Details - Schedule (6 tests)
- // ============================================================================
- test.describe('Category 5: Online Board Flight Details - Schedule', () => {
- test('Should verify flight schedule display (Test 33)', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const scheduleSection = page.locator('[data-testid="schedule-section"]');
- await expect(scheduleSection).toBeVisible();
- });
-
- test('Should verify scheduled departure time (Test 34)', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const scheduledDep = page.locator('[data-testid="scheduled-departure"]');
- await expect(scheduledDep).toBeVisible();
-
- const depTime = flight.departure.time.scheduled.slice(11, 16);
- await expect(scheduledDep).toContainText(depTime);
- });
-
- test('Should verify scheduled arrival time (Test 35)', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const scheduledArr = page.locator('[data-testid="scheduled-arrival"]');
- await expect(scheduledArr).toBeVisible();
-
- const arrTime = flight.arrival.time.scheduled.slice(11, 16);
- await expect(scheduledArr).toContainText(arrTime);
- });
-
- test('Should verify actual departure time (Test 36)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'departed',
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const actualDep = page.locator('[data-testid="actual-departure"]');
- await expect(actualDep).toBeVisible();
- });
-
- test('Should verify actual arrival time (Test 37)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'arrival',
- cityCode: 'MOW',
- status: 'arrived',
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const actualArr = page.locator('[data-testid="actual-arrival"]');
- await expect(actualArr).toBeVisible();
- });
-
- test('Should verify flight duration (Test 38)', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const duration = page.locator('[data-testid="flight-duration"]');
- await expect(duration).toBeVisible();
- });
- });
-
- // ============================================================================
- // Category 6: Schedule Flight Details - Basic (6 tests)
- // ============================================================================
- test.describe('Category 6: Schedule Flight Details - Basic', () => {
- test('Should open flight details from Schedule search results (Test 39)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const searchInput = page.locator('[data-testid="schedule-search-input"]');
- await searchInput.fill('Moscow');
- await searchInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="schedule-flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/\/ru-ru\/schedule\/details/);
- });
-
- test('Should verify flight number display in Schedule (Test 40)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const searchInput = page.locator('[data-testid="schedule-search-input"]');
- await searchInput.fill('SU 1124');
- await searchInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="schedule-flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- const flightNumber = page.locator('[data-testid="flight-number"]');
- await expect(flightNumber).toBeVisible();
- });
-
- test('Should verify carrier logo display in Schedule (Test 41)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const searchInput = page.locator('[data-testid="schedule-search-input"]');
- await searchInput.fill('SU 1124');
- await searchInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="schedule-flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- const carrierLogo = page.locator('[data-testid="carrier-logo"]');
- await expect(carrierLogo).toBeVisible();
- });
-
- test('Should verify route display in Schedule (Test 42)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const searchInput = page.locator('[data-testid="schedule-search-input"]');
- await searchInput.fill('Moscow');
- await searchInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="schedule-flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- const routeInfo = page.locator('[data-testid="route-info"]');
- await expect(routeInfo).toBeVisible();
- });
-
- test('Should verify basic flight information in Schedule (Test 43)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const searchInput = page.locator('[data-testid="schedule-search-input"]');
- await searchInput.fill('SU 1124');
- await searchInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="schedule-flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- await expect(page.getByText('SU 1124')).toBeVisible();
- await expect(page.getByText('Aeroflot')).toBeVisible();
- });
-
- test('Should verify round-trip information (Test 44)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const roundTripTab = page.locator('[data-testid="round-trip-tab"]');
- await roundTripTab.click();
- await page.waitForTimeout(500);
-
- const searchInput = page.locator('[data-testid="schedule-search-input"]');
- await searchInput.fill('Moscow');
- await searchInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="schedule-flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- const roundTripInfo = page.locator('[data-testid="round-trip-info"]');
- await expect(roundTripInfo).toBeVisible();
- });
- });
-
- // ============================================================================
- // Category 7: Schedule Flight Details - Multi-leg (6 tests)
- // ============================================================================
- test.describe('Category 7: Schedule Flight Details - Multi-leg', () => {
- test('Should open multi-leg flight details (Test 45)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const searchInput = page.locator('[data-testid="schedule-search-input"]');
- await searchInput.fill('Moscow');
- await searchInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="schedule-flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- const multiLegTab = page.locator('[data-testid="multi-leg-tab"]');
- await multiLegTab.click();
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/\/ru-ru\/schedule\/details/);
- });
-
- test('Should verify leg switcher (Test 46)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const searchInput = page.locator('[data-testid="schedule-search-input"]');
- await searchInput.fill('Moscow');
- await searchInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="schedule-flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- const legSwitcher = page.locator('[data-testid="leg-switcher"]');
- await expect(legSwitcher).toBeVisible();
- });
-
- test('Should verify leg information display (Test 47)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const searchInput = page.locator('[data-testid="schedule-search-input"]');
- await searchInput.fill('Moscow');
- await searchInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="schedule-flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- const legInfo = page.locator('[data-testid="leg-info"]');
- await expect(legInfo).toBeVisible();
- });
-
- test('Should verify transfer information (Test 48)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const searchInput = page.locator('[data-testid="schedule-search-input"]');
- await searchInput.fill('Moscow');
- await searchInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="schedule-flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- const transferInfo = page.locator('[data-testid="transfer-info"]');
- await expect(transferInfo).toBeVisible();
- });
-
- test('Should verify multi-leg route display (Test 49)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const searchInput = page.locator('[data-testid="schedule-search-input"]');
- await searchInput.fill('Moscow');
- await searchInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="schedule-flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- const multiLegRoute = page.locator('[data-testid="multi-leg-route"]');
- await expect(multiLegRoute).toBeVisible();
- });
-
- test('Should verify timeline display (Test 50)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const searchInput = page.locator('[data-testid="schedule-search-input"]');
- await searchInput.fill('Moscow');
- await searchInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="schedule-flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- const timeline = page.locator('[data-testid="timeline"]');
- await expect(timeline).toBeVisible();
- });
- });
-
- // ============================================================================
- // Category 8: Flight Details Navigation (4 tests)
- // ============================================================================
- test.describe('Category 8: Flight Details Navigation', () => {
- test('Should navigate back to search results (Test 51)', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- const backLink = page.locator('[data-testid="back-link"]');
- await backLink.click();
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/onlineboard\/departure\/MOW-\d{8}/);
- });
-
- test('Should navigate to previous flight in multi-leg (Test 52)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const searchInput = page.locator('[data-testid="schedule-search-input"]');
- await searchInput.fill('Moscow');
- await searchInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="schedule-flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- const prevFlightBtn = page.locator('[data-testid="prev-flight-btn"]');
- await prevFlightBtn.click();
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/\/ru-ru\/schedule\/details/);
- });
-
- test('Should navigate to next flight in multi-leg (Test 53)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const searchInput = page.locator('[data-testid="schedule-search-input"]');
- await searchInput.fill('Moscow');
- await searchInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="schedule-flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- const nextFlightBtn = page.locator('[data-testid="next-flight-btn"]');
- await nextFlightBtn.click();
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/\/ru-ru\/schedule\/details/);
- });
-
- test('Should navigate between Online Board and Schedule details (Test 54)', async ({
- page,
- }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const onlineBoardLink = page.locator('[data-testid="online-board-link"]');
- await onlineBoardLink.click();
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/onlineboard\/departure\/MOW-\d{8}/);
- });
- });
-
- // ============================================================================
- // Category 9: Edge Cases (2 tests)
- // ============================================================================
- test.describe('Category 9: Edge Cases', () => {
- test('Should handle network error gracefully (Test 55)', async ({ page }) => {
- await page.route('**/api/flights/**', (route) => {
- return route.abort('internetdisconnected');
- });
-
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const networkError = page.locator('[data-testid="network-error"]');
- await expect(networkError).toBeVisible();
- });
-
- test('Should handle flight not found (404) (Test 56)', async ({ page }) => {
- await page.route('**/api/flights/**', async (route) => {
- await route.fulfill({
- status: 404,
- json: { error: 'Not Found', message: 'Flight not found' },
- });
- });
-
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const notFound = page.locator('[data-testid="not-found"]');
- await expect(notFound).toBeVisible();
- });
-
- test('Should handle server error (500) (Test 57)', async ({ page }) => {
- await page.route('**/api/flights/**', async (route) => {
- await route.fulfill({
- status: 500,
- json: { error: 'Internal Server Error', message: 'Server error' },
- });
- });
-
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const serverError = page.locator('[data-testid="server-error"]');
- await expect(serverError).toBeVisible();
- });
-
- test('Should handle timeout error (504) (Test 58)', async ({ page }) => {
- await page.route('**/api/flights/**', async (route) => {
- await route.fulfill({
- status: 504,
- json: { error: 'Gateway Timeout', message: 'Request timeout' },
- });
- });
-
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const timeoutError = page.locator('[data-testid="timeout-error"]');
- await expect(timeoutError).toBeVisible();
- });
-
- test('Should handle invalid date format (Test 59)', async ({ page }) => {
- await page.goto(`/ru-ru/SU1124-INVALID`);
- await page.waitForLoadState('networkidle');
-
- const errorState = page.locator('[data-testid="error-state"]');
- await expect(errorState).toBeVisible();
- });
-
- test('Should handle empty flight number (Test 60)', async ({ page }) => {
- await page.goto(`/ru-ru/-${today.replace(/-/g, '')}`);
- await page.waitForLoadState('networkidle');
-
- const errorState = page.locator('[data-testid="error-state"]');
- await expect(errorState).toBeVisible();
- });
-
- test('Should handle special characters in flight number (Test 61)', async ({ page }) => {
- await page.goto(`/ru-ru/SU!@#$-${today.replace(/-/g, '')}`);
- await page.waitForLoadState('networkidle');
-
- const errorState = page.locator('[data-testid="error-state"]');
- await expect(errorState).toBeVisible();
- });
-
- test('Should handle very long flight number (Test 62)', async ({ page }) => {
- const longFlightNumber = 'SU'.padEnd(100, '1');
- await page.goto(`/ru-ru/${longFlightNumber}-${today.replace(/-/g, '')}`);
- await page.waitForLoadState('networkidle');
-
- const errorState = page.locator('[data-testid="error-state"]');
- await expect(errorState).toBeVisible();
- });
-
- test('Should handle Unicode characters in flight number (Test 63)', async ({ page }) => {
- await page.goto(`/ru-ru/SU1124 flight 🛫-${today.replace(/-/g, '')}`);
- await page.waitForLoadState('networkidle');
-
- const errorState = page.locator('[data-testid="error-state"]');
- await expect(errorState).toBeVisible();
- });
-
- test('Should handle flight with missing aircraft information (Test 64)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- aircraft: { type: undefined },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const aircraftSection = page.locator('[data-testid="aircraft-section"]');
- await expect(aircraftSection).toBeVisible();
- });
-
- test('Should handle flight with missing schedule information (Test 65)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- schedule: undefined,
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const scheduleSection = page.locator('[data-testid="schedule-section"]');
- await expect(scheduleSection).toBeVisible();
- });
-
- test('Should handle flight with missing boarding information (Test 66)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- boarding: undefined,
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const boardingInfo = page.locator('[data-testid="boarding-info"]');
- await expect(boardingInfo).toBeVisible();
- });
-
- test('Should handle flight with missing arrival information (Test 67)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- arrival: {
- airportCode: 'AER',
- airportName: 'Adler',
- cityCode: 'AER',
- cityName: 'Sochi',
- time: { scheduled: '2026-04-06T16:05:00+03:00' },
- },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const arrivalInfo = page.locator('[data-testid="arrival-info"]');
- await expect(arrivalInfo).toBeVisible();
- });
-
- test('Should handle flight with missing departure information (Test 68)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- departure: {
- airportCode: 'SVO',
- airportName: 'Sheremetyevo',
- cityCode: 'MOW',
- cityName: 'Moscow',
- time: { scheduled: '2026-04-06T12:13:00+03:00' },
- },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const departureInfo = page.locator('[data-testid="departure-info"]');
- await expect(departureInfo).toBeVisible();
- });
-
- test('Should handle flight with missing catering information (Test 69)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- catering: undefined,
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const mealInfo = page.locator('[data-testid="meal-info"]');
- await expect(mealInfo).toBeVisible();
- });
-
- test('Should handle flight with missing checkin information (Test 70)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- checkin: undefined,
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const checkinInfo = page.locator('[data-testid="checkin-info"]');
- await expect(checkinInfo).toBeVisible();
- });
-
- test('Should handle flight with missing deplaning information (Test 71)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'arrival',
- cityCode: 'MOW',
- deplaning: undefined,
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const deboardingInfo = page.locator('[data-testid="deboarding-info"]');
- await expect(deboardingInfo).toBeVisible();
- });
-
- test('Should handle flight with missing arrivalInfo (Test 72)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'arrival',
- cityCode: 'MOW',
- arrivalInfo: undefined,
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const arrivalInfo = page.locator('[data-testid="arrival-info"]');
- await expect(arrivalInfo).toBeVisible();
- });
-
- test('Should handle flight with missing lastUpdated (Test 73)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- lastUpdated: undefined,
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const lastUpdated = page.locator('[data-testid="last-updated"]');
- await expect(lastUpdated).toBeVisible();
- });
-
- test('Should handle flight with missing aircraft name (Test 74)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- aircraft: { type: 'Airbus A320', name: undefined },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const aircraftName = page.locator('[data-testid="aircraft-name"]');
- await expect(aircraftName).toBeVisible();
- });
-
- test('Should handle flight with missing aircraft previousFlight (Test 75)', async ({
- page,
- }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- aircraft: { type: 'Airbus A320', previousFlight: undefined },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const previousFlight = page.locator('[data-testid="previous-flight"]');
- await expect(previousFlight).toBeVisible();
- });
-
- test('Should handle flight with missing aircraft seats (Test 76)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- aircraft: { type: 'Airbus A320', totalSeats: undefined },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const seatInfo = page.locator('[data-testid="seat-info"]');
- await expect(seatInfo).toBeVisible();
- });
-
- test('Should handle flight with missing aircraft equipment (Test 77)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- aircraft: { type: 'Airbus A320', equipment: undefined },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const equipmentDetails = page.locator('[data-testid="equipment-details"]');
- await expect(equipmentDetails).toBeVisible();
- });
-
- test('Should handle flight with missing aircraft model (Test 78)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- aircraft: { type: 'Airbus A320', model: undefined },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const aircraftModel = page.locator('[data-testid="aircraft-model"]');
- await expect(aircraftModel).toBeVisible();
- });
-
- test('Should handle flight with missing aircraft type (Test 79)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- aircraft: { type: undefined },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const aircraftType = page.locator('[data-testid="aircraft-type"]');
- await expect(aircraftType).toBeVisible();
- });
-
- test('Should handle flight with missing aircraft (Test 80)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- aircraft: undefined,
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const aircraftSection = page.locator('[data-testid="aircraft-section"]');
- await expect(aircraftSection).toBeVisible();
- });
-
- test('Should handle flight with missing schedule (Test 81)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- schedule: undefined,
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const scheduleSection = page.locator('[data-testid="schedule-section"]');
- await expect(scheduleSection).toBeVisible();
- });
-
- test('Should handle flight with missing boarding (Test 82)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- boarding: undefined,
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const boardingInfo = page.locator('[data-testid="boarding-info"]');
- await expect(boardingInfo).toBeVisible();
- });
-
- test('Should handle flight with missing checkin (Test 83)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- checkin: undefined,
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const checkinInfo = page.locator('[data-testid="checkin-info"]');
- await expect(checkinInfo).toBeVisible();
- });
-
- test('Should handle flight with missing deplaning (Test 84)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'arrival',
- cityCode: 'MOW',
- deplaning: undefined,
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const deboardingInfo = page.locator('[data-testid="deboarding-info"]');
- await expect(deboardingInfo).toBeVisible();
- });
-
- test('Should handle flight with missing arrivalInfo (Test 85)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'arrival',
- cityCode: 'MOW',
- arrivalInfo: undefined,
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const arrivalInfo = page.locator('[data-testid="arrival-info"]');
- await expect(arrivalInfo).toBeVisible();
- });
-
- test('Should handle flight with missing catering (Test 86)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- catering: undefined,
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const mealInfo = page.locator('[data-testid="meal-info"]');
- await expect(mealInfo).toBeVisible();
- });
-
- test('Should handle flight with missing lastUpdated (Test 87)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- lastUpdated: undefined,
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const lastUpdated = page.locator('[data-testid="last-updated"]');
- await expect(lastUpdated).toBeVisible();
- });
-
- test('Should handle flight with missing all optional fields (Test 88)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- aircraft: undefined,
- schedule: undefined,
- boarding: undefined,
- checkin: undefined,
- deplaning: undefined,
- arrivalInfo: undefined,
- catering: undefined,
- lastUpdated: undefined,
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- await expect(page.getByText(flight.flightNumber)).toBeVisible();
- await expect(page.getByText(flight.airlineName)).toBeVisible();
- });
-
- test('Should handle flight with null values (Test 89)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- aircraft: null,
- schedule: null,
- boarding: null,
- checkin: null,
- deplaning: null,
- arrivalInfo: null,
- catering: null,
- lastUpdated: null,
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- await expect(page.getByText(flight.flightNumber)).toBeVisible();
- });
-
- test('Should handle flight with empty strings (Test 90)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- aircraft: { type: '', name: '', previousFlight: '' },
- schedule: { scheduledDeparture: '', scheduledArrival: '', duration: '' },
- boarding: { gate: '', status: '', startTime: '', endTime: '' },
- checkin: { status: '', startTime: '', endTime: '' },
- deplaning: { status: '', startTime: '', endTime: '', transfer: '' },
- arrivalInfo: { baggageBelt: '', transfer: '' },
- catering: { economy: false, business: false },
- lastUpdated: '',
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- await expect(page.getByText(flight.flightNumber)).toBeVisible();
- });
-
- test('Should handle flight with zero values (Test 91)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- aircraft: { type: 'Airbus A320', totalSeats: 0, economySeats: 0, businessSeats: 0 },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const seatInfo = page.locator('[data-testid="seat-info"]');
- await expect(seatInfo).toBeVisible();
- });
-
- test('Should handle flight with negative values (Test 92)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- aircraft: { type: 'Airbus A320', totalSeats: -1, economySeats: -1, businessSeats: -1 },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const seatInfo = page.locator('[data-testid="seat-info"]');
- await expect(seatInfo).toBeVisible();
- });
-
- test('Should handle flight with very large values (Test 93)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- aircraft: {
- type: 'Airbus A320',
- totalSeats: 999999,
- economySeats: 999999,
- businessSeats: 999999,
- },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const seatInfo = page.locator('[data-testid="seat-info"]');
- await expect(seatInfo).toBeVisible();
- });
-
- test('Should handle flight with special characters in city names (Test 94)', async ({
- page,
- }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- departure: {
- airportCode: 'SVO',
- airportName: 'Sheremetyevo',
- cityCode: 'MOW',
- cityName: 'Москва!@#$%',
- time: { scheduled: '2026-04-06T12:13:00+03:00' },
- },
- arrival: {
- airportCode: 'AER',
- airportName: 'Adler',
- cityCode: 'AER',
- cityName: 'Сочи!@#$%',
- time: { scheduled: '2026-04-06T16:05:00+03:00' },
- },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const routeInfo = page.locator('[data-testid="route-info"]');
- await expect(routeInfo).toBeVisible();
- });
-
- test('Should handle flight with very long city names (Test 95)', async ({ page }) => {
- const longCityName = 'Москва'.repeat(100);
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- departure: {
- airportCode: 'SVO',
- airportName: 'Sheremetyevo',
- cityCode: 'MOW',
- cityName: longCityName,
- time: { scheduled: '2026-04-06T12:13:00+03:00' },
- },
- arrival: {
- airportCode: 'AER',
- airportName: 'Adler',
- cityCode: 'AER',
- cityName: longCityName,
- time: { scheduled: '2026-04-06T16:05:00+03:00' },
- },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const routeInfo = page.locator('[data-testid="route-info"]');
- await expect(routeInfo).toBeVisible();
- });
-
- test('Should handle flight with Unicode characters in all fields (Test 96)', async ({
- page,
- }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- departure: {
- airportCode: 'SVO',
- airportName: 'Sheremetyevo 🛫',
- cityCode: 'MOW',
- cityName: 'Москва 🇷🇺',
- time: { scheduled: '2026-04-06T12:13:00+03:00' },
- },
- arrival: {
- airportCode: 'AER',
- airportName: 'Adler 🛬',
- cityCode: 'AER',
- cityName: 'Сочи 🇷🇺',
- time: { scheduled: '2026-04-06T16:05:00+03:00' },
- },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const routeInfo = page.locator('[data-testid="route-info"]');
- await expect(routeInfo).toBeVisible();
- });
-
- test('Should handle flight with mixed case in all fields (Test 97)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- departure: {
- airportCode: 'SVO',
- airportName: 'SHEREMETYEVO',
- cityCode: 'MOW',
- cityName: 'MOSCOW',
- time: { scheduled: '2026-04-06T12:13:00+03:00' },
- },
- arrival: {
- airportCode: 'AER',
- airportName: 'ADLER',
- cityCode: 'AER',
- cityName: 'SOCHI',
- time: { scheduled: '2026-04-06T16:05:00+03:00' },
- },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const routeInfo = page.locator('[data-testid="route-info"]');
- await expect(routeInfo).toBeVisible();
- });
-
- test('Should handle flight with special characters in airport codes (Test 98)', async ({
- page,
- }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- departure: {
- airportCode: 'SVO!@#$',
- airportName: 'Sheremetyevo',
- cityCode: 'MOW',
- cityName: 'Moscow',
- time: { scheduled: '2026-04-06T12:13:00+03:00' },
- },
- arrival: {
- airportCode: 'AER!@#$',
- airportName: 'Adler',
- cityCode: 'AER',
- cityName: 'Sochi',
- time: { scheduled: '2026-04-06T16:05:00+03:00' },
- },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const routeInfo = page.locator('[data-testid="route-info"]');
- await expect(routeInfo).toBeVisible();
- });
-
- test('Should handle flight with missing terminal information (Test 99)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- departure: {
- airportCode: 'SVO',
- airportName: 'Sheremetyevo',
- cityCode: 'MOW',
- cityName: 'Moscow',
- terminal: undefined,
- time: { scheduled: '2026-04-06T12:13:00+03:00' },
- },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const terminal = page.locator('[data-testid="terminal"]');
- await expect(terminal).toBeVisible();
- });
-
- test('Should handle flight with missing arrival terminal (Test 100)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- arrival: {
- airportCode: 'AER',
- airportName: 'Adler',
- cityCode: 'AER',
- cityName: 'Sochi',
- terminal: undefined,
- time: { scheduled: '2026-04-06T16:05:00+03:00' },
- },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const arrivalTerminal = page.locator('[data-testid="arrival-terminal"]');
- await expect(arrivalTerminal).toBeVisible();
- });
- });
-});
diff --git a/tests/e2e-angular/integration/11 - flight details.spec.ts.bak b/tests/e2e-angular/integration/11 - flight details.spec.ts.bak
deleted file mode 100644
index d712fa90..00000000
--- a/tests/e2e-angular/integration/11 - flight details.spec.ts.bak
+++ /dev/null
@@ -1,1667 +0,0 @@
-import { test, expect } from '@playwright/test';
-import type { Page } from '@playwright/test';
-import {
- buildOnlineBoardPath,
- buildRouteParam,
- buildSchedulePath,
- generateFlight,
- generateFlights,
- getToday,
- getTomorrow,
- CITIES,
- AIRPORTS,
- FLIGHT_NUMBERS,
- STATUS_TYPES,
- AIRCRAFT_TYPES,
-} from '../support/test-utilities';
-
-const today = getToday();
-const tomorrow = getTomorrow();
-const yesterday = getYesterday();
-const futureDate = getFutureDate(7);
-const pastDate = getPastDate(7);
-
-// ============================================================================
-// Flight Details - Online Board & Schedule (50+ tests)
-// ============================================================================
-
-test.describe('Flight Details - Online Board & Schedule', () => {
- // ============================================================================
- // Category 1: Online Board Flight Details - Basic (8 tests)
- // ============================================================================
- test.describe('Category 1: Online Board Flight Details - Basic', () => {
- test('Should open flight details from Online Board arrival search results (Test 1)', async ({
- page,
- }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/\/ru-ru\/[A-Z]{2}\s?\d+-\d{8}/);
- });
-
- test('Should open flight details from Online Board departure search results (Test 2)', async ({
- page,
- }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/\/ru-ru\/[A-Z]{2}\s?\d+-\d{8}/);
- });
-
- test('Should open flight details from Online Board route search results (Test 3)', async ({
- page,
- }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const routeTab = page.locator('[data-testid="route-search-tab"]');
- await routeTab.click();
- await page.waitForTimeout(500);
-
- const departureInput = page.locator('[data-testid="departure-city-input"]');
- const arrivalInput = page.locator('[data-testid="arrival-city-input"]');
-
- await departureInput.fill('Moscow');
- await page.waitForTimeout(500);
- await departureInput.press('Enter');
- await page.waitForTimeout(500);
-
- await arrivalInput.fill('Sochi');
- await page.waitForTimeout(500);
- await arrivalInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/\/ru-ru\/[A-Z]{2}\s?\d+-\d{8}/);
- });
-
- test('Should open flight details from Online Board flight search results (Test 4)', async ({
- page,
- }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const searchInput = page.locator('[data-testid="flight-search-input"]');
- await searchInput.fill('SU 1124');
- await searchInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/\/ru-ru\/SU1124-\d{8}/);
- });
-
- test('Should verify flight number display (Test 5)', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const flightNumber = page.locator('[data-testid="flight-number"]');
- await expect(flightNumber).toBeVisible();
- await expect(flightNumber).toContainText(flight.flightNumber);
- });
-
- test('Should verify carrier logo display (Test 6)', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const carrierLogo = page.locator('[data-testid="carrier-logo"]');
- await expect(carrierLogo).toBeVisible();
- });
-
- test('Should verify route display (Test 7)', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const routeInfo = page.locator('[data-testid="route-info"]');
- await expect(routeInfo).toBeVisible();
-
- await expect(routeInfo).toContainText(flight.departure.cityName);
- await expect(routeInfo).toContainText(flight.arrival.cityName);
- });
-
- test('Should verify basic flight information (Test 8)', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- await expect(page.getByText(flight.flightNumber)).toBeVisible();
- await expect(page.getByText(flight.airlineName)).toBeVisible();
- await expect(page.getByText(flight.departure.cityName)).toBeVisible();
- await expect(page.getByText(flight.arrival.cityName)).toBeVisible();
- });
- });
-
- // ============================================================================
- // Category 2: Online Board Flight Details - Status (6 tests)
- // ============================================================================
- test.describe('Category 2: Online Board Flight Details - Status', () => {
- test('Should verify flight status display - Scheduled (Test 9)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'scheduled',
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const status = page.locator('[data-testid="flight-status"]');
- await expect(status).toBeVisible();
- await expect(status).toContainText('scheduled');
- });
-
- test('Should verify flight status display - Sent (Test 10)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'departed',
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const status = page.locator('[data-testid="flight-status"]');
- await expect(status).toBeVisible();
- });
-
- test('Should verify flight status display - In Flight (Test 11)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'inFlight',
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const status = page.locator('[data-testid="flight-status"]');
- await expect(status).toBeVisible();
- });
-
- test('Should verify flight status display - Landed (Test 12)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'arrival',
- cityCode: 'MOW',
- status: 'landed',
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const status = page.locator('[data-testid="flight-status"]');
- await expect(status).toBeVisible();
- });
-
- test('Should verify flight status display - Arrived (Test 13)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'arrival',
- cityCode: 'MOW',
- status: 'arrived',
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const status = page.locator('[data-testid="flight-status"]');
- await expect(status).toBeVisible();
- });
-
- test('Should verify flight status display - Delayed (Test 14)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'delayed',
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const status = page.locator('[data-testid="flight-status"]');
- await expect(status).toBeVisible();
- });
-
- test('Should verify flight status display - Cancelled (Test 15)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'cancelled',
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const status = page.locator('[data-testid="flight-status"]');
- await expect(status).toBeVisible();
- });
-
- test('Should verify flight status icon (Test 16)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'scheduled',
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const statusIcon = page.locator('[data-testid="status-icon"]');
- await expect(statusIcon).toBeVisible();
- });
-
- test('Should verify flight status details (Test 17)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'scheduled',
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const statusDetails = page.locator('[data-testid="status-details"]');
- await expect(statusDetails).toBeVisible();
- });
-
- test('Should verify flight status badges (Test 18)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'boarding',
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const statusBadge = page.locator('[data-testid="status-badge"]');
- await expect(statusBadge).toBeVisible();
- });
-
- test('Should verify flight status color coding (Test 19)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'delayed',
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const statusContainer = page.locator('[data-testid="status-container"]');
- await expect(statusContainer).toBeVisible();
- });
-
- test('Should verify flight status timestamp (Test 20)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'scheduled',
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const statusTimestamp = page.locator('[data-testid="status-timestamp"]');
- await expect(statusTimestamp).toBeVisible();
- });
- });
-
- // ============================================================================
- // Category 3: Online Board Flight Details - Aircraft (6 tests)
- // ============================================================================
- test.describe('Category 3: Online Board Flight Details - Aircraft', () => {
- test('Should verify aircraft information display (Test 21)', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const aircraftSection = page.locator('[data-testid="aircraft-section"]');
- await expect(aircraftSection).toBeVisible();
- });
-
- test('Should verify aircraft type display (Test 22)', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const aircraftType = page.locator('[data-testid="aircraft-type"]');
- await expect(aircraftType).toBeVisible();
- await expect(aircraftType).toContainText(flight.aircraft?.type || '');
- });
-
- test('Should verify aircraft seats configuration (Test 23)', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const seatInfo = page.locator('[data-testid="seat-info"]');
- await expect(seatInfo).toBeVisible();
- });
-
- test('Should verify aircraft previous flight information (Test 24)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- aircraft: { type: 'Airbus A320', previousFlight: 'SU 1123' },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const previousFlight = page.locator('[data-testid="previous-flight"]');
- await expect(previousFlight).toBeVisible();
- });
-
- test('Should verify aircraft equipment details (Test 25)', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const equipmentDetails = page.locator('[data-testid="equipment-details"]');
- await expect(equipmentDetails).toBeVisible();
- });
-
- test('Should verify aircraft model display (Test 26)', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const aircraftModel = page.locator('[data-testid="aircraft-model"]');
- await expect(aircraftModel).toBeVisible();
- });
- });
-
- // ============================================================================
- // Category 4: Online Board Flight Details - Services (6 tests)
- // ============================================================================
- test.describe('Category 4: Online Board Flight Details - Services', () => {
- test('Should verify registration information display (Test 27)', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const registrationInfo = page.locator('[data-testid="registration-info"]');
- await expect(registrationInfo).toBeVisible();
- });
-
- test('Should verify boarding information display (Test 28)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'boarding',
- boarding: { gate: '11', status: 'Идёт посадка' },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const boardingInfo = page.locator('[data-testid="boarding-info"]');
- await expect(boardingInfo).toBeVisible();
- });
-
- test('Should verify deboarding information display (Test 29)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'arrival',
- cityCode: 'MOW',
- status: 'arrived',
- deplaning: { status: 'В процессе', transfer: 'Трап' },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const deboardingInfo = page.locator('[data-testid="deboarding-info"]');
- await expect(deboardingInfo).toBeVisible();
- });
-
- test('Should verify meal information display (Test 30)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- catering: { economy: true, business: true },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const mealInfo = page.locator('[data-testid="meal-info"]');
- await expect(mealInfo).toBeVisible();
- });
-
- test('Should verify on-board services display (Test 31)', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const onboardServices = page.locator('[data-testid="onboard-services"]');
- await expect(onboardServices).toBeVisible();
- });
-
- test('Should verify service icons (Test 32)', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const serviceIcons = page.locator('[data-testid="service-icon"]');
- await expect(serviceIcons).toHaveCount(3);
- });
- });
-
- // ============================================================================
- // Category 5: Online Board Flight Details - Schedule (6 tests)
- // ============================================================================
- test.describe('Category 5: Online Board Flight Details - Schedule', () => {
- test('Should verify flight schedule display (Test 33)', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const scheduleSection = page.locator('[data-testid="schedule-section"]');
- await expect(scheduleSection).toBeVisible();
- });
-
- test('Should verify scheduled departure time (Test 34)', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const scheduledDep = page.locator('[data-testid="scheduled-departure"]');
- await expect(scheduledDep).toBeVisible();
-
- const depTime = flight.departure.time.scheduled.slice(11, 16);
- await expect(scheduledDep).toContainText(depTime);
- });
-
- test('Should verify scheduled arrival time (Test 35)', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const scheduledArr = page.locator('[data-testid="scheduled-arrival"]');
- await expect(scheduledArr).toBeVisible();
-
- const arrTime = flight.arrival.time.scheduled.slice(11, 16);
- await expect(scheduledArr).toContainText(arrTime);
- });
-
- test('Should verify actual departure time (Test 36)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'departed',
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const actualDep = page.locator('[data-testid="actual-departure"]');
- await expect(actualDep).toBeVisible();
- });
-
- test('Should verify actual arrival time (Test 37)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'arrival',
- cityCode: 'MOW',
- status: 'arrived',
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const actualArr = page.locator('[data-testid="actual-arrival"]');
- await expect(actualArr).toBeVisible();
- });
-
- test('Should verify flight duration (Test 38)', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const duration = page.locator('[data-testid="flight-duration"]');
- await expect(duration).toBeVisible();
- });
- });
-
- // ============================================================================
- // Category 6: Schedule Flight Details - Basic (6 tests)
- // ============================================================================
- test.describe('Category 6: Schedule Flight Details - Basic', () => {
- test('Should open flight details from Schedule search results (Test 39)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const searchInput = page.locator('[data-testid="schedule-search-input"]');
- await searchInput.fill('Moscow');
- await searchInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="schedule-flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/\/ru-ru\/schedule\/details/);
- });
-
- test('Should verify flight number display in Schedule (Test 40)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const searchInput = page.locator('[data-testid="schedule-search-input"]');
- await searchInput.fill('SU 1124');
- await searchInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="schedule-flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- const flightNumber = page.locator('[data-testid="flight-number"]');
- await expect(flightNumber).toBeVisible();
- });
-
- test('Should verify carrier logo display in Schedule (Test 41)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const searchInput = page.locator('[data-testid="schedule-search-input"]');
- await searchInput.fill('SU 1124');
- await searchInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="schedule-flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- const carrierLogo = page.locator('[data-testid="carrier-logo"]');
- await expect(carrierLogo).toBeVisible();
- });
-
- test('Should verify route display in Schedule (Test 42)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const searchInput = page.locator('[data-testid="schedule-search-input"]');
- await searchInput.fill('Moscow');
- await searchInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="schedule-flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- const routeInfo = page.locator('[data-testid="route-info"]');
- await expect(routeInfo).toBeVisible();
- });
-
- test('Should verify basic flight information in Schedule (Test 43)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const searchInput = page.locator('[data-testid="schedule-search-input"]');
- await searchInput.fill('SU 1124');
- await searchInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="schedule-flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- await expect(page.getByText('SU 1124')).toBeVisible();
- await expect(page.getByText('Aeroflot')).toBeVisible();
- });
-
- test('Should verify round-trip information (Test 44)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const roundTripTab = page.locator('[data-testid="round-trip-tab"]');
- await roundTripTab.click();
- await page.waitForTimeout(500);
-
- const searchInput = page.locator('[data-testid="schedule-search-input"]');
- await searchInput.fill('Moscow');
- await searchInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="schedule-flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- const roundTripInfo = page.locator('[data-testid="round-trip-info"]');
- await expect(roundTripInfo).toBeVisible();
- });
- });
-
- // ============================================================================
- // Category 7: Schedule Flight Details - Multi-leg (6 tests)
- // ============================================================================
- test.describe('Category 7: Schedule Flight Details - Multi-leg', () => {
- test('Should open multi-leg flight details (Test 45)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const searchInput = page.locator('[data-testid="schedule-search-input"]');
- await searchInput.fill('Moscow');
- await searchInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="schedule-flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- const multiLegTab = page.locator('[data-testid="multi-leg-tab"]');
- await multiLegTab.click();
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/\/ru-ru\/schedule\/details/);
- });
-
- test('Should verify leg switcher (Test 46)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const searchInput = page.locator('[data-testid="schedule-search-input"]');
- await searchInput.fill('Moscow');
- await searchInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="schedule-flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- const legSwitcher = page.locator('[data-testid="leg-switcher"]');
- await expect(legSwitcher).toBeVisible();
- });
-
- test('Should verify leg information display (Test 47)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const searchInput = page.locator('[data-testid="schedule-search-input"]');
- await searchInput.fill('Moscow');
- await searchInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="schedule-flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- const legInfo = page.locator('[data-testid="leg-info"]');
- await expect(legInfo).toBeVisible();
- });
-
- test('Should verify transfer information (Test 48)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const searchInput = page.locator('[data-testid="schedule-search-input"]');
- await searchInput.fill('Moscow');
- await searchInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="schedule-flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- const transferInfo = page.locator('[data-testid="transfer-info"]');
- await expect(transferInfo).toBeVisible();
- });
-
- test('Should verify multi-leg route display (Test 49)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const searchInput = page.locator('[data-testid="schedule-search-input"]');
- await searchInput.fill('Moscow');
- await searchInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="schedule-flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- const multiLegRoute = page.locator('[data-testid="multi-leg-route"]');
- await expect(multiLegRoute).toBeVisible();
- });
-
- test('Should verify timeline display (Test 50)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const searchInput = page.locator('[data-testid="schedule-search-input"]');
- await searchInput.fill('Moscow');
- await searchInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="schedule-flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- const timeline = page.locator('[data-testid="timeline"]');
- await expect(timeline).toBeVisible();
- });
- });
-
- // ============================================================================
- // Category 8: Flight Details Navigation (4 tests)
- // ============================================================================
- test.describe('Category 8: Flight Details Navigation', () => {
- test('Should navigate back to search results (Test 51)', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- const backLink = page.locator('[data-testid="back-link"]');
- await backLink.click();
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/onlineboard\/departure\/MOW-\d{8}/);
- });
-
- test('Should navigate to previous flight in multi-leg (Test 52)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const searchInput = page.locator('[data-testid="schedule-search-input"]');
- await searchInput.fill('Moscow');
- await searchInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="schedule-flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- const prevFlightBtn = page.locator('[data-testid="prev-flight-btn"]');
- await prevFlightBtn.click();
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/\/ru-ru\/schedule\/details/);
- });
-
- test('Should navigate to next flight in multi-leg (Test 53)', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const searchInput = page.locator('[data-testid="schedule-search-input"]');
- await searchInput.fill('Moscow');
- await searchInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="schedule-flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- const nextFlightBtn = page.locator('[data-testid="next-flight-btn"]');
- await nextFlightBtn.click();
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/\/ru-ru\/schedule\/details/);
- });
-
- test('Should navigate between Online Board and Schedule details (Test 54)', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const onlineBoardLink = page.locator('[data-testid="online-board-link"]');
- await onlineBoardLink.click();
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/onlineboard\/departure\/MOW-\d{8}/);
- });
- });
-
- // ============================================================================
- // Category 9: Edge Cases (2 tests)
- // ============================================================================
- test.describe('Category 9: Edge Cases', () => {
- test('Should handle network error gracefully (Test 55)', async ({ page }) => {
- await page.route('**/api/flights/**', (route) => {
- return route.abort('internetdisconnected');
- });
-
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const networkError = page.locator('[data-testid="network-error"]');
- await expect(networkError).toBeVisible();
- });
-
- test('Should handle flight not found (404) (Test 56)', async ({ page }) => {
- await page.route('**/api/flights/**', async (route) => {
- await route.fulfill({
- status: 404,
- json: { error: 'Not Found', message: 'Flight not found' },
- });
- });
-
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const notFound = page.locator('[data-testid="not-found"]');
- await expect(notFound).toBeVisible();
- });
-
- test('Should handle server error (500) (Test 57)', async ({ page }) => {
- await page.route('**/api/flights/**', async (route) => {
- await route.fulfill({
- status: 500,
- json: { error: 'Internal Server Error', message: 'Server error' },
- });
- });
-
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const serverError = page.locator('[data-testid="server-error"]');
- await expect(serverError).toBeVisible();
- });
-
- test('Should handle timeout error (504) (Test 58)', async ({ page }) => {
- await page.route('**/api/flights/**', async (route) => {
- await route.fulfill({
- status: 504,
- json: { error: 'Gateway Timeout', message: 'Request timeout' },
- });
- });
-
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const timeoutError = page.locator('[data-testid="timeout-error"]');
- await expect(timeoutError).toBeVisible();
- });
-
- test('Should handle invalid date format (Test 59)', async ({ page }) => {
- await page.goto(`/ru-ru/SU1124-INVALID`);
- await page.waitForLoadState('networkidle');
-
- const errorState = page.locator('[data-testid="error-state"]');
- await expect(errorState).toBeVisible();
- });
-
- test('Should handle empty flight number (Test 60)', async ({ page }) => {
- await page.goto(`/ru-ru/-${today.replace(/-/g, '')}`);
- await page.waitForLoadState('networkidle');
-
- const errorState = page.locator('[data-testid="error-state"]');
- await expect(errorState).toBeVisible();
- });
-
- test('Should handle special characters in flight number (Test 61)', async ({ page }) => {
- await page.goto(`/ru-ru/SU!@#$-${today.replace(/-/g, '')}`);
- await page.waitForLoadState('networkidle');
-
- const errorState = page.locator('[data-testid="error-state"]');
- await expect(errorState).toBeVisible();
- });
-
- test('Should handle very long flight number (Test 62)', async ({ page }) => {
- const longFlightNumber = 'SU'.padEnd(100, '1');
- await page.goto(`/ru-ru/${longFlightNumber}-${today.replace(/-/g, '')}`);
- await page.waitForLoadState('networkidle');
-
- const errorState = page.locator('[data-testid="error-state"]');
- await expect(errorState).toBeVisible();
- });
-
- test('Should handle Unicode characters in flight number (Test 63)', async ({ page }) => {
- await page.goto(`/ru-ru/SU1124 flight 🛫-${today.replace(/-/g, '')}`);
- await page.waitForLoadState('networkidle');
-
- const errorState = page.locator('[data-testid="error-state"]');
- await expect(errorState).toBeVisible();
- });
-
- test('Should handle flight with missing aircraft information (Test 64)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- aircraft: { type: undefined },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const aircraftSection = page.locator('[data-testid="aircraft-section"]');
- await expect(aircraftSection).toBeVisible();
- });
-
- test('Should handle flight with missing schedule information (Test 65)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- schedule: undefined,
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const scheduleSection = page.locator('[data-testid="schedule-section"]');
- await expect(scheduleSection).toBeVisible();
- });
-
- test('Should handle flight with missing boarding information (Test 66)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- boarding: undefined,
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const boardingInfo = page.locator('[data-testid="boarding-info"]');
- await expect(boardingInfo).toBeVisible();
- });
-
- test('Should handle flight with missing arrival information (Test 67)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- arrival: {
- airportCode: 'AER',
- airportName: 'Adler',
- cityCode: 'AER',
- cityName: 'Sochi',
- time: { scheduled: '2026-04-06T16:05:00+03:00' },
- },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const arrivalInfo = page.locator('[data-testid="arrival-info"]');
- await expect(arrivalInfo).toBeVisible();
- });
-
- test('Should handle flight with missing departure information (Test 68)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- departure: {
- airportCode: 'SVO',
- airportName: 'Sheremetyevo',
- cityCode: 'MOW',
- cityName: 'Moscow',
- time: { scheduled: '2026-04-06T12:13:00+03:00' },
- },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const departureInfo = page.locator('[data-testid="departure-info"]');
- await expect(departureInfo).toBeVisible();
- });
-
- test('Should handle flight with missing catering information (Test 69)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- catering: undefined,
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const mealInfo = page.locator('[data-testid="meal-info"]');
- await expect(mealInfo).toBeVisible();
- });
-
- test('Should handle flight with missing checkin information (Test 70)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- checkin: undefined,
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const checkinInfo = page.locator('[data-testid="checkin-info"]');
- await expect(checkinInfo).toBeVisible();
- });
-
- test('Should handle flight with missing deplaning information (Test 71)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'arrival',
- cityCode: 'MOW',
- deplaning: undefined,
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const deboardingInfo = page.locator('[data-testid="deboarding-info"]');
- await expect(deboardingInfo).toBeVisible();
- });
-
- test('Should handle flight with missing arrivalInfo (Test 72)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'arrival',
- cityCode: 'MOW',
- arrivalInfo: undefined,
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const arrivalInfo = page.locator('[data-testid="arrival-info"]');
- await expect(arrivalInfo).toBeVisible();
- });
-
- test('Should handle flight with missing lastUpdated (Test 73)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- lastUpdated: undefined,
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const lastUpdated = page.locator('[data-testid="last-updated"]');
- await expect(lastUpdated).toBeVisible();
- });
-
- test('Should handle flight with missing aircraft name (Test 74)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- aircraft: { type: 'Airbus A320', name: undefined },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const aircraftName = page.locator('[data-testid="aircraft-name"]');
- await expect(aircraftName).toBeVisible();
- });
-
- test('Should handle flight with missing aircraft previousFlight (Test 75)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- aircraft: { type: 'Airbus A320', previousFlight: undefined },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const previousFlight = page.locator('[data-testid="previous-flight"]');
- await expect(previousFlight).toBeVisible();
- });
-
- test('Should handle flight with missing aircraft seats (Test 76)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- aircraft: { type: 'Airbus A320', totalSeats: undefined },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const seatInfo = page.locator('[data-testid="seat-info"]');
- await expect(seatInfo).toBeVisible();
- });
-
- test('Should handle flight with missing aircraft equipment (Test 77)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- aircraft: { type: 'Airbus A320', equipment: undefined },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const equipmentDetails = page.locator('[data-testid="equipment-details"]');
- await expect(equipmentDetails).toBeVisible();
- });
-
- test('Should handle flight with missing aircraft model (Test 78)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- aircraft: { type: 'Airbus A320', model: undefined },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const aircraftModel = page.locator('[data-testid="aircraft-model"]');
- await expect(aircraftModel).toBeVisible();
- });
-
- test('Should handle flight with missing aircraft type (Test 79)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- aircraft: { type: undefined },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const aircraftType = page.locator('[data-testid="aircraft-type"]');
- await expect(aircraftType).toBeVisible();
- });
-
- test('Should handle flight with missing aircraft (Test 80)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- aircraft: undefined,
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const aircraftSection = page.locator('[data-testid="aircraft-section"]');
- await expect(aircraftSection).toBeVisible();
- });
-
- test('Should handle flight with missing schedule (Test 81)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- schedule: undefined,
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const scheduleSection = page.locator('[data-testid="schedule-section"]');
- await expect(scheduleSection).toBeVisible();
- });
-
- test('Should handle flight with missing boarding (Test 82)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- boarding: undefined,
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const boardingInfo = page.locator('[data-testid="boarding-info"]');
- await expect(boardingInfo).toBeVisible();
- });
-
- test('Should handle flight with missing checkin (Test 83)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- checkin: undefined,
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const checkinInfo = page.locator('[data-testid="checkin-info"]');
- await expect(checkinInfo).toBeVisible();
- });
-
- test('Should handle flight with missing deplaning (Test 84)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'arrival',
- cityCode: 'MOW',
- deplaning: undefined,
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const deboardingInfo = page.locator('[data-testid="deboarding-info"]');
- await expect(deboardingInfo).toBeVisible();
- });
-
- test('Should handle flight with missing arrivalInfo (Test 85)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'arrival',
- cityCode: 'MOW',
- arrivalInfo: undefined,
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const arrivalInfo = page.locator('[data-testid="arrival-info"]');
- await expect(arrivalInfo).toBeVisible();
- });
-
- test('Should handle flight with missing catering (Test 86)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- catering: undefined,
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const mealInfo = page.locator('[data-testid="meal-info"]');
- await expect(mealInfo).toBeVisible();
- });
-
- test('Should handle flight with missing lastUpdated (Test 87)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- lastUpdated: undefined,
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const lastUpdated = page.locator('[data-testid="last-updated"]');
- await expect(lastUpdated).toBeVisible();
- });
-
- test('Should handle flight with missing all optional fields (Test 88)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- aircraft: undefined,
- schedule: undefined,
- boarding: undefined,
- checkin: undefined,
- deplaning: undefined,
- arrivalInfo: undefined,
- catering: undefined,
- lastUpdated: undefined,
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- await expect(page.getByText(flight.flightNumber)).toBeVisible();
- await expect(page.getByText(flight.airlineName)).toBeVisible();
- });
-
- test('Should handle flight with null values (Test 89)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- aircraft: null,
- schedule: null,
- boarding: null,
- checkin: null,
- deplaning: null,
- arrivalInfo: null,
- catering: null,
- lastUpdated: null,
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- await expect(page.getByText(flight.flightNumber)).toBeVisible();
- });
-
- test('Should handle flight with empty strings (Test 90)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- aircraft: { type: '', name: '', previousFlight: '' },
- schedule: { scheduledDeparture: '', scheduledArrival: '', duration: '' },
- boarding: { gate: '', status: '', startTime: '', endTime: '' },
- checkin: { status: '', startTime: '', endTime: '' },
- deplaning: { status: '', startTime: '', endTime: '', transfer: '' },
- arrivalInfo: { baggageBelt: '', transfer: '' },
- catering: { economy: false, business: false },
- lastUpdated: '',
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- await expect(page.getByText(flight.flightNumber)).toBeVisible();
- });
-
- test('Should handle flight with zero values (Test 91)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- aircraft: { type: 'Airbus A320', totalSeats: 0, economySeats: 0, businessSeats: 0 },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const seatInfo = page.locator('[data-testid="seat-info"]');
- await expect(seatInfo).toBeVisible();
- });
-
- test('Should handle flight with negative values (Test 92)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- aircraft: { type: 'Airbus A320', totalSeats: -1, economySeats: -1, businessSeats: -1 },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const seatInfo = page.locator('[data-testid="seat-info"]');
- await expect(seatInfo).toBeVisible();
- });
-
- test('Should handle flight with very large values (Test 93)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- aircraft: { type: 'Airbus A320', totalSeats: 999999, economySeats: 999999, businessSeats: 999999 },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const seatInfo = page.locator('[data-testid="seat-info"]');
- await expect(seatInfo).toBeVisible();
- });
-
- test('Should handle flight with special characters in city names (Test 94)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- departure: {
- airportCode: 'SVO',
- airportName: 'Sheremetyevo',
- cityCode: 'MOW',
- cityName: 'Москва!@#$%',
- time: { scheduled: '2026-04-06T12:13:00+03:00' },
- },
- arrival: {
- airportCode: 'AER',
- airportName: 'Adler',
- cityCode: 'AER',
- cityName: 'Сочи!@#$%',
- time: { scheduled: '2026-04-06T16:05:00+03:00' },
- },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const routeInfo = page.locator('[data-testid="route-info"]');
- await expect(routeInfo).toBeVisible();
- });
-
- test('Should handle flight with very long city names (Test 95)', async ({ page }) => {
- const longCityName = 'Москва'.repeat(100);
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- departure: {
- airportCode: 'SVO',
- airportName: 'Sheremetyevo',
- cityCode: 'MOW',
- cityName: longCityName,
- time: { scheduled: '2026-04-06T12:13:00+03:00' },
- },
- arrival: {
- airportCode: 'AER',
- airportName: 'Adler',
- cityCode: 'AER',
- cityName: longCityName,
- time: { scheduled: '2026-04-06T16:05:00+03:00' },
- },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const routeInfo = page.locator('[data-testid="route-info"]');
- await expect(routeInfo).toBeVisible();
- });
-
- test('Should handle flight with Unicode characters in all fields (Test 96)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- departure: {
- airportCode: 'SVO',
- airportName: 'Sheremetyevo 🛫',
- cityCode: 'MOW',
- cityName: 'Москва 🇷🇺',
- time: { scheduled: '2026-04-06T12:13:00+03:00' },
- },
- arrival: {
- airportCode: 'AER',
- airportName: 'Adler 🛬',
- cityCode: 'AER',
- cityName: 'Сочи 🇷🇺',
- time: { scheduled: '2026-04-06T16:05:00+03:00' },
- },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const routeInfo = page.locator('[data-testid="route-info"]');
- await expect(routeInfo).toBeVisible();
- });
-
- test('Should handle flight with mixed case in all fields (Test 97)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- departure: {
- airportCode: 'SVO',
- airportName: 'SHEREMETYEVO',
- cityCode: 'MOW',
- cityName: 'MOSCOW',
- time: { scheduled: '2026-04-06T12:13:00+03:00' },
- },
- arrival: {
- airportCode: 'AER',
- airportName: 'ADLER',
- cityCode: 'AER',
- cityName: 'SOCHI',
- time: { scheduled: '2026-04-06T16:05:00+03:00' },
- },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const routeInfo = page.locator('[data-testid="route-info"]');
- await expect(routeInfo).toBeVisible();
- });
-
- test('Should handle flight with special characters in airport codes (Test 98)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- departure: {
- airportCode: 'SVO!@#$',
- airportName: 'Sheremetyevo',
- cityCode: 'MOW',
- cityName: 'Moscow',
- time: { scheduled: '2026-04-06T12:13:00+03:00' },
- },
- arrival: {
- airportCode: 'AER!@#$',
- airportName: 'Adler',
- cityCode: 'AER',
- cityName: 'Sochi',
- time: { scheduled: '2026-04-06T16:05:00+03:00' },
- },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const routeInfo = page.locator('[data-testid="route-info"]');
- await expect(routeInfo).toBeVisible();
- });
-
- test('Should handle flight with missing terminal information (Test 99)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- departure: {
- airportCode: 'SVO',
- airportName: 'Sheremetyevo',
- cityCode: 'MOW',
- cityName: 'Moscow',
- terminal: undefined,
- time: { scheduled: '2026-04-06T12:13:00+03:00' },
- },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const terminal = page.locator('[data-testid="terminal"]');
- await expect(terminal).toBeVisible();
- });
-
- test('Should handle flight with missing arrival terminal (Test 100)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- arrival: {
- airportCode: 'AER',
- airportName: 'Adler',
- cityCode: 'AER',
- cityName: 'Sochi',
- terminal: undefined,
- time: { scheduled: '2026-04-06T16:05:00+03:00' },
- },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const arrivalTerminal = page.locator('[data-testid="arrival-terminal"]');
- await expect(arrivalTerminal).toBeVisible();
- });
- });
-});
diff --git a/tests/e2e-angular/integration/12 - flights map.spec.ts b/tests/e2e-angular/integration/12 - flights map.spec.ts
deleted file mode 100644
index e93d61d5..00000000
--- a/tests/e2e-angular/integration/12 - flights map.spec.ts
+++ /dev/null
@@ -1,743 +0,0 @@
-import { test, expect } from '@playwright/test';
-import type { Page } from '@playwright/test';
-import {
- buildFlightsMapPath,
- CITIES,
- getToday,
- getTomorrow,
- getFutureDate,
- getPastDate,
-} from '../support/test-utilities';
-
-const today = getToday();
-const tomorrow = getTomorrow();
-const futureDate = getFutureDate(7);
-const pastDate = getPastDate(7);
-
-// ============================================================================
-// Flights Map Tests (20+ tests)
-// ============================================================================
-
-test.describe('Flights Map', () => {
- test.describe('Category 1: Basic Map Navigation (4 tests)', () => {
- test('Should navigate to flights map page (Test 1)', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/flights-map/);
- });
-
- test('Should verify map loads on flights map page (Test 2)', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const mapContainer = page.locator('[data-testid="map-container"]');
- await expect(mapContainer).toBeVisible();
- });
-
- test('Should verify map controls are visible (Test 3)', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const zoomControl = page.locator('.leaflet-control-zoom');
- await expect(zoomControl).toBeVisible();
- });
-
- test('Should verify page title (Test 4)', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveTitle(/Flights Map/i);
- });
- });
-
- test.describe('Category 2: City Selection (6 tests)', () => {
- test('Should select departure city on map (Test 5)', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
- await fromInput.fill('Moscow');
- await page.waitForTimeout(500);
- await fromInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- await expect(fromInput).toHaveValue('Moscow');
- });
-
- test('Should select arrival city on map (Test 6)', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const toInput = page.locator('[data-testid="flights-map-to-input"] input');
- await toInput.fill('Sochi');
- await page.waitForTimeout(500);
- await toInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- await expect(toInput).toHaveValue('Sochi');
- });
-
- test('Should verify city selection with autocomplete (Test 7)', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
- await fromInput.fill('Saint');
- await page.waitForTimeout(500);
-
- const autocompleteOption = page.locator('[data-testid="autocomplete-option"]').first();
- await autocompleteOption.click();
- await page.waitForLoadState('networkidle');
-
- await expect(fromInput).toContainText('Saint Petersburg');
- });
-
- test('Should verify city input fields (Test 8)', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
- const toInput = page.locator('[data-testid="flights-map-to-input"] input');
-
- await expect(fromInput).toBeVisible();
- await expect(toInput).toBeVisible();
- });
-
- test('Should clear city selection (Test 9)', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
- await fromInput.fill('Moscow');
- await page.waitForTimeout(500);
- await fromInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const clearBtn = page.locator('[data-testid="flights-map-clear-btn"]');
- await clearBtn.click();
- await page.waitForLoadState('networkidle');
-
- await expect(fromInput).toHaveValue('');
- });
-
- test('Should select multiple cities for search (Test 10)', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
- const toInput = page.locator('[data-testid="flights-map-to-input"] input');
-
- await fromInput.fill('Moscow');
- await page.waitForTimeout(500);
- await fromInput.press('Enter');
-
- await toInput.fill('Sochi');
- await page.waitForTimeout(500);
- await toInput.press('Enter');
-
- await expect(fromInput).toHaveValue('Moscow');
- await expect(toInput).toHaveValue('Sochi');
- });
- });
-
- test.describe('Category 3: Flight Search (6 tests)', () => {
- test('Should search flights from selected departure city (Test 11)', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
- await fromInput.fill('Moscow');
- await page.waitForTimeout(500);
- await fromInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const searchBtn = page.locator('[data-testid="flights-map-search-btn"]');
- await searchBtn.click();
- await page.waitForLoadState('networkidle');
-
- const resultsContainer = page.locator('[data-testid="flights-map-results"]');
- await expect(resultsContainer).toBeVisible();
- });
-
- test('Should search flights to selected arrival city (Test 12)', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
- const toInput = page.locator('[data-testid="flights-map-to-input"] input');
-
- await fromInput.fill('Moscow');
- await page.waitForTimeout(500);
- await fromInput.press('Enter');
-
- await toInput.fill('Sochi');
- await page.waitForTimeout(500);
- await toInput.press('Enter');
-
- const searchBtn = page.locator('[data-testid="flights-map-search-btn"]');
- await searchBtn.click();
- await page.waitForLoadState('networkidle');
-
- const resultsContainer = page.locator('[data-testid="flights-map-results"]');
- await expect(resultsContainer).toBeVisible();
- });
-
- test('Should search flights with date selection (Test 13)', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
- const dateFromInput = page.locator('[data-testid="flights-map-date-from"]');
-
- await fromInput.fill('Moscow');
- await page.waitForTimeout(500);
- await fromInput.press('Enter');
-
- await dateFromInput.fill(today);
- await dateFromInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const searchBtn = page.locator('[data-testid="flights-map-search-btn"]');
- await searchBtn.click();
- await page.waitForLoadState('networkidle');
-
- const resultsContainer = page.locator('[data-testid="flights-map-results"]');
- await expect(resultsContainer).toBeVisible();
- });
-
- test('Should search flights with date range (Test 14)', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
- const dateFromInput = page.locator('[data-testid="flights-map-date-from"]');
- const dateToInput = page.locator('[data-testid="flights-map-date-to"]');
-
- await fromInput.fill('Moscow');
- await page.waitForTimeout(500);
- await fromInput.press('Enter');
-
- await dateFromInput.fill(today);
- await dateFromInput.press('Enter');
-
- await dateToInput.fill(futureDate);
- await dateToInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const searchBtn = page.locator('[data-testid="flights-map-search-btn"]');
- await searchBtn.click();
- await page.waitForLoadState('networkidle');
-
- const resultsContainer = page.locator('[data-testid="flights-map-results"]');
- await expect(resultsContainer).toBeVisible();
- });
-
- test('Should search flights with filters (Test 15)', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
- const connectionsSelect = page.locator('[data-testid="flights-map-connections-select"]');
-
- await fromInput.fill('Moscow');
- await page.waitForTimeout(500);
- await fromInput.press('Enter');
-
- await connectionsSelect.selectOption('0');
- await page.waitForLoadState('networkidle');
-
- const searchBtn = page.locator('[data-testid="flights-map-search-btn"]');
- await searchBtn.click();
- await page.waitForLoadState('networkidle');
-
- const resultsContainer = page.locator('[data-testid="flights-map-results"]');
- await expect(resultsContainer).toBeVisible();
- });
-
- test('Should search flights with invalid selection and show error (Test 16)', async ({
- page,
- }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
- await fromInput.fill('Invalid City XXX');
- await page.waitForTimeout(500);
- await fromInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const searchBtn = page.locator('[data-testid="flights-map-search-btn"]');
- await searchBtn.click();
- await page.waitForLoadState('networkidle');
-
- const noResults = page.locator('[data-testid="no-results"]');
- await expect(noResults).toBeVisible();
- });
- });
-
- test.describe('Category 4: Flight Results (4 tests)', () => {
- test('Should verify flight results display (Test 17)', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
- await fromInput.fill('Moscow');
- await page.waitForTimeout(500);
- await fromInput.press('Enter');
-
- const searchBtn = page.locator('[data-testid="flights-map-search-btn"]');
- await searchBtn.click();
- await page.waitForLoadState('networkidle');
-
- const resultsContainer = page.locator('[data-testid="flights-map-results"]');
- await expect(resultsContainer).toBeVisible();
- });
-
- test('Should verify flight count in results (Test 18)', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
- await fromInput.fill('Moscow');
- await page.waitForTimeout(500);
- await fromInput.press('Enter');
-
- const searchBtn = page.locator('[data-testid="flights-map-search-btn"]');
- await searchBtn.click();
- await page.waitForLoadState('networkidle');
-
- const flightCount = page.locator('[data-testid="flight-count"]');
- await expect(flightCount).toBeVisible();
- });
-
- test('Should verify flight details in results (Test 19)', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
- await fromInput.fill('Moscow');
- await page.waitForTimeout(500);
- await fromInput.press('Enter');
-
- const searchBtn = page.locator('[data-testid="flights-map-search-btn"]');
- await searchBtn.click();
- await page.waitForLoadState('networkidle');
-
- const firstResult = page.locator('[data-testid="flight-result"]').first();
- await expect(firstResult).toBeVisible();
-
- const flightNumber = firstResult.locator('[data-testid="flight-number"]');
- await expect(flightNumber).toBeVisible();
- });
-
- test('Should verify empty results message (Test 20)', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
- await fromInput.fill('NonExistent City');
- await page.waitForTimeout(500);
- await fromInput.press('Enter');
-
- const searchBtn = page.locator('[data-testid="flights-map-search-btn"]');
- await searchBtn.click();
- await page.waitForLoadState('networkidle');
-
- const noResults = page.locator('[data-testid="no-results"]');
- await expect(noResults).toBeVisible();
- await expect(noResults).toContainText('No results');
- });
- });
-
- test.describe('Category 5: Edge Cases (2 tests)', () => {
- test('Should handle search with no cities selected (Test 21)', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const searchBtn = page.locator('[data-testid="flights-map-search-btn"]');
- await searchBtn.click();
- await page.waitForLoadState('networkidle');
-
- const validationError = page.locator('[data-testid="validation-error"]');
- await expect(validationError).toBeVisible();
- });
-
- test('Should handle invalid city selection (Test 22)', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
- await fromInput.fill('XXX');
- await page.waitForTimeout(500);
-
- const searchBtn = page.locator('[data-testid="flights-map-search-btn"]');
- await searchBtn.click();
- await page.waitForLoadState('networkidle');
-
- const noResults = page.locator('[data-testid="no-results"]');
- await expect(noResults).toBeVisible();
- });
- });
-
- test.describe('Category 6: Additional Flights Map Tests', () => {
- test('Should navigate to flights map for different cities (Test 23)', async ({ page }) => {
- const cities = ['MOW', 'LED', 'AER', 'OVB', 'KRR'];
-
- for (const cityCode of cities) {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
- await fromInput.fill(CITIES.find((c) => c.code === cityCode)?.name || '');
- await page.waitForTimeout(500);
- await fromInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const resultsContainer = page.locator('[data-testid="flights-map-results"]');
- await expect(resultsContainer).toBeVisible();
- }
- });
-
- test('Should display correct date in results (Test 24)', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
- const dateFromInput = page.locator('[data-testid="flights-map-date-from"]');
-
- await fromInput.fill('Moscow');
- await page.waitForTimeout(500);
- await fromInput.press('Enter');
-
- await dateFromInput.fill(today);
- await dateFromInput.press('Enter');
-
- const searchBtn = page.locator('[data-testid="flights-map-search-btn"]');
- await searchBtn.click();
- await page.waitForLoadState('networkidle');
-
- const dateDisplay = page.locator('[data-testid="date-display"]');
- await expect(dateDisplay).toBeVisible();
- await expect(dateDisplay).toContainText(today);
- });
-
- test('Should filter by connections (Test 25)', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
- const connectionsSelect = page.locator('[data-testid="flights-map-connections-select"]');
-
- await fromInput.fill('Moscow');
- await page.waitForTimeout(500);
- await fromInput.press('Enter');
-
- await connectionsSelect.selectOption('1');
- await page.waitForLoadState('networkidle');
-
- const searchBtn = page.locator('[data-testid="flights-map-search-btn"]');
- await searchBtn.click();
- await page.waitForLoadState('networkidle');
-
- const resultsContainer = page.locator('[data-testid="flights-map-results"]');
- await expect(resultsContainer).toBeVisible();
- });
-
- test('Should display flight status badges (Test 26)', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
- await fromInput.fill('Moscow');
- await page.waitForTimeout(500);
- await fromInput.press('Enter');
-
- const searchBtn = page.locator('[data-testid="flights-map-search-btn"]');
- await searchBtn.click();
- await page.waitForLoadState('networkidle');
-
- const statusBadge = page.locator('[data-testid="status-badge"]').first();
- await expect(statusBadge).toBeVisible();
- });
-
- test('Should display flight departure and arrival cities (Test 27)', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
- const toInput = page.locator('[data-testid="flights-map-to-input"] input');
-
- await fromInput.fill('Moscow');
- await page.waitForTimeout(500);
- await fromInput.press('Enter');
-
- await toInput.fill('Sochi');
- await page.waitForTimeout(500);
- await toInput.press('Enter');
-
- const searchBtn = page.locator('[data-testid="flights-map-search-btn"]');
- await searchBtn.click();
- await page.waitForLoadState('networkidle');
-
- const firstResult = page.locator('[data-testid="flight-result"]').first();
- await expect(firstResult).toBeVisible();
-
- const depCity = firstResult.locator('[data-testid="departure-city"]');
- const arrCity = firstResult.locator('[data-testid="arrival-city"]');
- await expect(depCity).toBeVisible();
- await expect(arrCity).toBeVisible();
- });
-
- test('Should display flight times (Test 28)', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
- await fromInput.fill('Moscow');
- await page.waitForTimeout(500);
- await fromInput.press('Enter');
-
- const searchBtn = page.locator('[data-testid="flights-map-search-btn"]');
- await searchBtn.click();
- await page.waitForLoadState('networkidle');
-
- const firstResult = page.locator('[data-testid="flight-result"]').first();
- await expect(firstResult).toBeVisible();
-
- const depTime = firstResult.locator('[data-testid="departure-time"]');
- const arrTime = firstResult.locator('[data-testid="arrival-time"]');
- await expect(depTime).toBeVisible();
- await expect(arrTime).toBeVisible();
- });
-
- test('Should display flight airline information (Test 29)', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
- await fromInput.fill('Moscow');
- await page.waitForTimeout(500);
- await fromInput.press('Enter');
-
- const searchBtn = page.locator('[data-testid="flights-map-search-btn"]');
- await searchBtn.click();
- await page.waitForLoadState('networkidle');
-
- const firstResult = page.locator('[data-testid="flight-result"]').first();
- await expect(firstResult).toBeVisible();
-
- const airlineName = firstResult.locator('[data-testid="airline-name"]');
- await expect(airlineName).toBeVisible();
- });
-
- test('Should handle network error (Test 30)', async ({ page }) => {
- await page.route('**/api/destinations**', (route) => {
- return route.abort('internetdisconnected');
- });
-
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
- await fromInput.fill('Moscow');
- await page.waitForTimeout(500);
- await fromInput.press('Enter');
-
- const searchBtn = page.locator('[data-testid="flights-map-search-btn"]');
- await searchBtn.click();
- await page.waitForLoadState('networkidle');
-
- const networkError = page.locator('[data-testid="network-error"]');
- await expect(networkError).toBeVisible();
- });
-
- test('Should search with special characters (Test 31)', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
- await fromInput.fill('Moscow!@#$');
- await page.waitForTimeout(500);
- await fromInput.press('Enter');
-
- const searchBtn = page.locator('[data-testid="flights-map-search-btn"]');
- await searchBtn.click();
- await page.waitForLoadState('networkidle');
-
- const noResults = page.locator('[data-testid="no-results"]');
- await expect(noResults).toBeVisible();
- });
-
- test('Should search with Unicode characters (Test 32)', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
- await fromInput.fill('Moscow 🛫');
- await page.waitForTimeout(500);
- await fromInput.press('Enter');
-
- const searchBtn = page.locator('[data-testid="flights-map-search-btn"]');
- await searchBtn.click();
- await page.waitForLoadState('networkidle');
-
- const resultsContainer = page.locator('[data-testid="flights-map-results"]');
- await expect(resultsContainer).toBeVisible();
- });
-
- test('Should handle rapid search attempts (Test 33)', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
- const searchBtn = page.locator('[data-testid="flights-map-search-btn"]');
-
- for (let i = 0; i < 5; i++) {
- await fromInput.fill('Moscow');
- await page.waitForTimeout(200);
- await searchBtn.click();
- await page.waitForLoadState('networkidle');
- }
-
- const resultsContainer = page.locator('[data-testid="flights-map-results"]');
- await expect(resultsContainer).toBeVisible();
- });
-
- test('Should verify map zoom controls (Test 34)', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const zoomInBtn = page.locator('.leaflet-control-zoom-in');
- const zoomOutBtn = page.locator('.leaflet-control-zoom-out');
-
- await expect(zoomInBtn).toBeVisible();
- await expect(zoomOutBtn).toBeVisible();
-
- await zoomInBtn.click();
- await page.waitForTimeout(200);
-
- await zoomOutBtn.click();
- await page.waitForTimeout(200);
- });
-
- test('Should verify map center coordinates (Test 35)', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const mapContainer = page.locator('[data-testid="map-container"]');
- await expect(mapContainer).toBeVisible();
-
- const markers = page.locator('[data-testid="flight-marker"]');
- const markerCount = await markers.count();
- expect(markerCount).toBeGreaterThan(0);
- });
-
- test('Should search with past date and show validation (Test 36)', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
- const dateFromInput = page.locator('[data-testid="flights-map-date-from"]');
-
- await fromInput.fill('Moscow');
- await page.waitForTimeout(500);
- await fromInput.press('Enter');
-
- await dateFromInput.fill(pastDate);
- await dateFromInput.press('Enter');
-
- const searchBtn = page.locator('[data-testid="flights-map-search-btn"]');
- await searchBtn.click();
- await page.waitForLoadState('networkidle');
-
- const resultsContainer = page.locator('[data-testid="flights-map-results"]');
- await expect(resultsContainer).toBeVisible();
- });
-
- test('Should search with future date (Test 37)', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
- const dateFromInput = page.locator('[data-testid="flights-map-date-from"]');
-
- await fromInput.fill('Moscow');
- await page.waitForTimeout(500);
- await fromInput.press('Enter');
-
- await dateFromInput.fill(futureDate);
- await dateFromInput.press('Enter');
-
- const searchBtn = page.locator('[data-testid="flights-map-search-btn"]');
- await searchBtn.click();
- await page.waitForLoadState('networkidle');
-
- const resultsContainer = page.locator('[data-testid="flights-map-results"]');
- await expect(resultsContainer).toBeVisible();
- });
-
- test('Should search from Saint Petersburg to Sochi (Test 38)', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
- const toInput = page.locator('[data-testid="flights-map-to-input"] input');
-
- await fromInput.fill('Saint Petersburg');
- await page.waitForTimeout(500);
- await fromInput.press('Enter');
-
- await toInput.fill('Sochi');
- await page.waitForTimeout(500);
- await toInput.press('Enter');
-
- const searchBtn = page.locator('[data-testid="flights-map-search-btn"]');
- await searchBtn.click();
- await page.waitForLoadState('networkidle');
-
- const resultsContainer = page.locator('[data-testid="flights-map-results"]');
- await expect(resultsContainer).toBeVisible();
- });
-
- test('Should search from Novosibirsk to Moscow (Test 39)', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
- const toInput = page.locator('[data-testid="flights-map-to-input"] input');
-
- await fromInput.fill('Novosibirsk');
- await page.waitForTimeout(500);
- await fromInput.press('Enter');
-
- await toInput.fill('Moscow');
- await page.waitForTimeout(500);
- await toInput.press('Enter');
-
- const searchBtn = page.locator('[data-testid="flights-map-search-btn"]');
- await searchBtn.click();
- await page.waitForLoadState('networkidle');
-
- const resultsContainer = page.locator('[data-testid="flights-map-results"]');
- await expect(resultsContainer).toBeVisible();
- });
-
- test('Should search with no arrival city (Test 40)', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
- await fromInput.fill('Moscow');
- await page.waitForTimeout(500);
- await fromInput.press('Enter');
-
- const searchBtn = page.locator('[data-testid="flights-map-search-btn"]');
- await searchBtn.click();
- await page.waitForLoadState('networkidle');
-
- const resultsContainer = page.locator('[data-testid="flights-map-results"]');
- await expect(resultsContainer).toBeVisible();
- });
- });
-});
diff --git a/tests/e2e-angular/integration/online-board-arrival.spec.ts b/tests/e2e-angular/integration/online-board-arrival.spec.ts
deleted file mode 100644
index b4735dc8..00000000
--- a/tests/e2e-angular/integration/online-board-arrival.spec.ts
+++ /dev/null
@@ -1,596 +0,0 @@
-import { test, expect } from '@playwright/test';
-import type { Page } from '@playwright/test';
-import {
- buildOnlineBoardPath,
- buildRouteParam,
- searchFlightByNumber,
- searchFlightByRoute,
- verifyFlightCard,
- generateFlight,
- generateFlights,
- getToday,
- getTomorrow,
- getYesterday,
- getFutureDate,
- getPastDate,
- CITIES,
- FIXTURES,
-} from '../support/test-utilities';
-
-const today = getToday();
-const tomorrow = getTomorrow();
-const yesterday = getYesterday();
-const futureDate = getFutureDate(7);
-const pastDate = getPastDate(7);
-const dateParam = buildRouteParam('MOW', today);
-const tomorrowParam = buildRouteParam('MOW', tomorrow);
-
-// ============================================================================
-// Online Board - Arrival Tests (30+ tests)
-// ============================================================================
-
-test.describe('Online Board - Arrival', () => {
- test.describe('Category 1: Basic Arrival Search', () => {
- test('Should search by city name (manual input) (Test 1)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/arrival\/MOW-\d{8}/);
- await expect(page).toHaveTitle(/Прибытие/);
- });
-
- test('Should search by city from autocomplete list (Test 2)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'LED', today)}`);
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/arrival\/LED-\d{8}/);
- });
-
- test('Should search with today date (Test 3)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'AER', today)}`);
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/arrival\/AER-\d{8}/);
- });
-
- test('Should search with future date (Test 4)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', futureDate)}`);
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/arrival\/MOW-\d{8}/);
- });
-
- test('Should search with past date and show validation (Test 5)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', pastDate)}`);
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/arrival\/MOW-\d{8}/);
- });
-
- test('Should search without date and use today (Test 6)', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/arrival/MOW`);
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/arrival\/MOW-\d{8}/);
- });
-
- test('Should search with invalid city and show error (Test 7)', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/arrival/XXX-${today.replace(/-/g, '')}`);
- await page.waitForLoadState('networkidle');
-
- const errorState = page.locator('[data-testid="error-state"]');
- await expect(errorState).toBeVisible();
- });
-
- test('Should search with empty city and show validation (Test 8)', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/arrival/-${today.replace(/-/g, '')}`);
- await page.waitForLoadState('networkidle');
-
- const errorState = page.locator('[data-testid="error-state"]');
- await expect(errorState).toBeVisible();
- });
- });
-
- test.describe('Category 2: Date Selection', () => {
- test('Should select date from calendar (Test 9)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const calendarInput = page.locator('[data-testid="calendar-input"]');
- await expect(calendarInput).toBeVisible();
- });
-
- test('Should select date range (Test 10)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const dateRange = page.locator('[data-testid="date-range"]');
- await expect(dateRange).toBeVisible();
- });
-
- test('Should verify date format DD.MM.YYYY (Test 11)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const dateText = page.locator('[data-testid="board-date"]');
- await expect(dateText).toBeVisible();
-
- const dateValue = await dateText.textContent();
- expect(dateValue).toMatch(/\d{2}\.\d{2}\.\d{4}/);
- });
-
- test('Should verify date validation min/max dates (Test 12)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const calendarInput = page.locator('[data-testid="calendar-input"]');
- await expect(calendarInput).toBeEnabled();
- });
-
- test('Should select today date (Test 13)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const todayTab = page.locator(`[data-testid="date-tab-${today.replace(/-/g, '')}"]`);
- await expect(todayTab).toBeVisible();
- });
-
- test('Should select tomorrow date (Test 14)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', tomorrow)}`);
- await page.waitForLoadState('networkidle');
-
- const tomorrowTab = page.locator(`[data-testid="date-tab-${tomorrow.replace(/-/g, '')}"]`);
- await expect(tomorrowTab).toBeVisible();
- });
- });
-
- test.describe('Category 3: Flight Results', () => {
- test('Should verify flight results display (Test 15)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCards = page.locator('[data-testid="flight-card"]');
- await expect(flightCards).toHaveCount(20);
- });
-
- test('Should verify flight count (Test 16)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCards = page.locator('[data-testid="flight-card"]');
- await expect(flightCards).toHaveCount(20);
- });
-
- test('Should verify flight details in results (Test 17)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const flightNumber = flightCard.locator('[data-testid="flight-number"]');
- await expect(flightNumber).toBeVisible();
- });
-
- test('Should verify empty results message (Test 18)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByNumber(page, 'SU 9999');
-
- const noResults = page.locator('[data-testid="no-results"]');
- await expect(noResults).toBeVisible();
- await expect(noResults).toContainText('Нет результатов');
- });
-
- test('Should verify loading state (Test 19)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const loadingSpinner = page.locator('[data-testid="loading-spinner"]');
- await expect(loadingSpinner).toBeVisible();
- });
-
- test('Should verify error state (Test 20)', async ({ page }) => {
- await page.route('**/api/flights/**', (route) => {
- return route.abort('internetdisconnected');
- });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const networkError = page.locator('[data-testid="network-error"]');
- await expect(networkError).toBeVisible();
- });
- });
-
- test.describe('Category 4: Flight Details', () => {
- test('Should open flight details from results (Test 21)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/\/ru-ru\/[A-Z]{2}\s?\d+-\d{8}/);
- });
-
- test('Should verify flight details content (Test 22)', async ({ page }) => {
- const flight = generateFlight({ direction: 'arrival', cityCode: 'MOW' });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByNumber(page, flight.flightNumber);
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- await expect(page.getByText(flight.flightNumber)).toBeVisible();
- await expect(page.getByText(flight.airlineName)).toBeVisible();
- });
-
- test('Should verify flight route details (Test 23)', async ({ page }) => {
- const flight = generateFlight({ direction: 'arrival', cityCode: 'MOW' });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByNumber(page, flight.flightNumber);
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- await expect(page.getByText(flight.departure.cityName)).toBeVisible();
- await expect(page.getByText(flight.arrival.cityName)).toBeVisible();
- });
-
- test('Should verify flight status details (Test 24)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'arrival',
- cityCode: 'MOW',
- status: 'scheduled',
- });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByNumber(page, flight.flightNumber);
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- await expect(page.getByText(flight.status)).toBeVisible();
- });
-
- test('Should close flight details (Test 25)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- const closeBtn = page.locator('[data-testid="close-details-btn"]');
- await closeBtn.click();
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/arrival\/MOW-\d{8}/);
- });
- });
-
- test.describe('Category 5: Edge Cases', () => {
- test('Should search for non-existent city (Test 26)', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/arrival/XXX-${today.replace(/-/g, '')}`);
- await page.waitForLoadState('networkidle');
-
- const errorState = page.locator('[data-testid="error-state"]');
- await expect(errorState).toBeVisible();
- });
-
- test('Should search with special characters (Test 27)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const searchInput = page.locator('[data-testid="flight-search-input"]');
- await searchInput.fill('SU 123!');
- await searchInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const noResults = page.locator('[data-testid="no-results"]');
- await expect(noResults).toBeVisible();
- });
-
- test('Should search with very long city name (Test 28)', async ({ page }) => {
- const longCityName = 'Москва'.repeat(10);
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const searchInput = page.locator('[data-testid="flight-search-input"]');
- await searchInput.fill(longCityName);
- await searchInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const flightCards = page.locator('[data-testid="flight-card"]');
- await expect(flightCards).toHaveCount(20);
- });
-
- test('Should search with Unicode characters (Test 29)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const searchInput = page.locator('[data-testid="flight-search-input"]');
- await searchInput.fill('SU 1234 🛫');
- await searchInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const flightCards = page.locator('[data-testid="flight-card"]');
- await expect(flightCards).toHaveCount(20);
- });
-
- test('Should handle rapid search attempts (Test 30)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const searchInput = page.locator('[data-testid="flight-search-input"]');
-
- for (let i = 0; i < 5; i++) {
- await searchInput.fill(`SU ${1000 + i}`);
- await searchInput.press('Enter');
- await page.waitForLoadState('networkidle');
- }
-
- const flightCards = page.locator('[data-testid="flight-card"]');
- await expect(flightCards).toHaveCount(20);
- });
- });
-
- test.describe('Additional Arrival Tests', () => {
- test('Should navigate to arrival board for different cities (Test 31)', async ({ page }) => {
- const cities = ['MOW', 'LED', 'AER', 'OVB', 'KRR'];
-
- for (const cityCode of cities) {
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', cityCode, today)}`);
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(new RegExp(`arrival/${cityCode}-\\d{8}`));
- }
- });
-
- test('Should display correct date in title (Test 32)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const dateText = page.locator('[data-testid="board-date"]');
- await expect(dateText).toBeVisible();
- await expect(dateText).toContainText(today);
- });
-
- test('Should filter by status (Test 33)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const statusFilter = page.locator('[data-testid="status-filter"]');
- await statusFilter.click();
-
- const scheduledOption = page.locator('[data-testid="filter-option-scheduled"]');
- await scheduledOption.click();
-
- const filteredFlights = page.locator('[data-testid="flight-card"]');
- await expect(filteredFlights).toHaveCount(20);
- });
-
- test('Should filter by airline (Test 34)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const airlineFilter = page.locator('[data-testid="airline-filter"]');
- await airlineFilter.click();
-
- const aeroflotOption = page.locator('[data-testid="filter-option-SU"]');
- await aeroflotOption.click();
-
- const filteredFlights = page.locator('[data-testid="flight-card"]');
- await expect(filteredFlights).toHaveCount(20);
- });
-
- test('Should display flight number (Test 35)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const flightNumber = flightCard.locator('[data-testid="flight-number"]');
- await expect(flightNumber).toBeVisible();
- });
-
- test('Should display airline name (Test 36)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const airlineName = flightCard.locator('[data-testid="airline-name"]');
- await expect(airlineName).toBeVisible();
- });
-
- test('Should display departure and arrival cities (Test 37)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const arrivalCity = flightCard.locator('[data-testid="arrival-city"]');
- await expect(arrivalCity).toBeVisible();
- });
-
- test('Should display scheduled time (Test 38)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const scheduledTime = flightCard.locator('[data-testid="scheduled-time"]');
- await expect(scheduledTime).toBeVisible();
- });
-
- test('Should display actual time when available (Test 39)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'arrival',
- cityCode: 'MOW',
- status: 'arrived',
- });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const actualTime = flightCard.locator('[data-testid="actual-time"]');
- await expect(actualTime).toBeVisible();
- });
-
- test('Should display delay information (Test 40)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'arrival',
- cityCode: 'MOW',
- status: 'delayed',
- });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const delayInfo = flightCard.locator('[data-testid="delay-info"]');
- await expect(delayInfo).toBeVisible();
- });
-
- test('Should display terminal information (Test 41)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const terminal = flightCard.locator('[data-testid="terminal"]');
- await expect(terminal).toBeVisible();
- });
-
- test('Should display baggage belt information (Test 42)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'arrival',
- cityCode: 'MOW',
- status: 'arrived',
- });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const baggageBelt = flightCard.locator('[data-testid="baggage-belt"]');
- await expect(baggageBelt).toBeVisible();
- });
-
- test('Should navigate to tomorrow date tab (Test 43)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const dateTab = page.locator(`[data-testid="date-tab-${tomorrow.replace(/-/g, '')}"]`);
- if ((await dateTab.count()) > 0) {
- await dateTab.click();
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/arrival\/MOW-\d{8}/);
- }
- });
-
- test('Should handle invalid city code (Test 44)', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/arrival/XXX-${today.replace(/-/g, '')}`);
- await page.waitForLoadState('networkidle');
-
- const errorState = page.locator('[data-testid="error-state"]');
- await expect(errorState).toBeVisible();
- });
-
- test('Should handle network error (Test 45)', async ({ page }) => {
- await page.route('**/api/flights/**', (route) => {
- return route.abort('internetdisconnected');
- });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const networkError = page.locator('[data-testid="network-error"]');
- await expect(networkError).toBeVisible();
- });
-
- test('Should have proper ARIA labels (Test 46)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toHaveAttribute('role', 'article');
- });
-
- test('Should be keyboard navigable (Test 47)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await page.keyboard.press('Tab');
- await page.keyboard.press('Tab');
- await page.keyboard.press('Tab');
-
- const focusedElement = page.locator(':focus');
- await expect(focusedElement).toBeVisible();
- });
-
- test('Should search by flight number (Test 48)', async ({ page }) => {
- const flight = generateFlight({ direction: 'arrival', cityCode: 'MOW' });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByNumber(page, flight.flightNumber);
-
- const searchResults = page.locator('[data-testid="flight-card"]');
- await expect(searchResults).toHaveCount(1);
-
- await verifyFlightCard(page, flight);
- });
-
- test('Should show no results when flight not found (Test 49)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByNumber(page, 'SU 9999');
-
- const noResults = page.locator('[data-testid="no-results"]');
- await expect(noResults).toBeVisible();
- await expect(noResults).toContainText('Нет результатов');
- });
-
- test('Should search by route (Test 50)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByRoute(page, 'Moscow', 'Sochi');
-
- const flightCards = page.locator('[data-testid="flight-card"]');
- await expect(flightCards).toHaveCount(20);
- });
- });
-});
diff --git a/tests/e2e-angular/integration/online-board-departure.spec.ts b/tests/e2e-angular/integration/online-board-departure.spec.ts
deleted file mode 100644
index 5bed7297..00000000
--- a/tests/e2e-angular/integration/online-board-departure.spec.ts
+++ /dev/null
@@ -1,1084 +0,0 @@
-import { test, expect } from '@playwright/test';
-import type { Page } from '@playwright/test';
-import {
- buildOnlineBoardPath,
- buildRouteParam,
- searchFlightByNumber,
- searchFlightByRoute,
- verifyFlightCard,
- generateFlight,
- generateFlights,
- getToday,
- getTomorrow,
- getYesterday,
- getFutureDate,
- getPastDate,
- CITIES,
- FIXTURES,
-} from '../support/test-utilities';
-
-const today = getToday();
-const tomorrow = getTomorrow();
-const yesterday = getYesterday();
-const futureDate = getFutureDate(7);
-const pastDate = getPastDate(7);
-const dateParam = buildRouteParam('MOW', today);
-const tomorrowParam = buildRouteParam('MOW', tomorrow);
-
-// ============================================================================
-// Online Board - Departure Tests (50+ tests)
-// ============================================================================
-
-test.describe('Online Board - Departure', () => {
- test.describe('Category 1: Basic Departure Search', () => {
- test('Should search by city name (manual input) (Test 1)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/departure\/MOW-\d{8}/);
- await expect(page).toHaveTitle(/Отправление/);
- });
-
- test('Should search by city from autocomplete list (Test 2)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'LED', today)}`);
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/departure\/LED-\d{8}/);
- });
-
- test('Should search with today date (Test 3)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'AER', today)}`);
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/departure\/AER-\d{8}/);
- });
-
- test('Should search with future date (Test 4)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', futureDate)}`);
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/departure\/MOW-\d{8}/);
- });
-
- test('Should search with past date and show validation (Test 5)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', pastDate)}`);
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/departure\/MOW-\d{8}/);
- });
-
- test('Should search without date and use today (Test 6)', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/departure/MOW`);
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/departure\/MOW-\d{8}/);
- });
-
- test('Should search with invalid city and show error (Test 7)', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/departure/XXX-${today.replace(/-/g, '')}`);
- await page.waitForLoadState('networkidle');
-
- const errorState = page.locator('[data-testid="error-state"]');
- await expect(errorState).toBeVisible();
- });
-
- test('Should search with empty city and show validation (Test 8)', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/departure/-${today.replace(/-/g, '')}`);
- await page.waitForLoadState('networkidle');
-
- const errorState = page.locator('[data-testid="error-state"]');
- await expect(errorState).toBeVisible();
- });
- });
-
- test.describe('Category 2: Date Selection', () => {
- test('Should select date from calendar (Test 9)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const calendarInput = page.locator('[data-testid="calendar-input"]');
- await expect(calendarInput).toBeVisible();
- });
-
- test('Should select date range (Test 10)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const dateRange = page.locator('[data-testid="date-range"]');
- await expect(dateRange).toBeVisible();
- });
-
- test('Should verify date format DD.MM.YYYY (Test 11)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const dateText = page.locator('[data-testid="board-date"]');
- await expect(dateText).toBeVisible();
-
- const dateValue = await dateText.textContent();
- expect(dateValue).toMatch(/\d{2}\.\d{2}\.\d{4}/);
- });
-
- test('Should verify date validation min/max dates (Test 12)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const calendarInput = page.locator('[data-testid="calendar-input"]');
- await expect(calendarInput).toBeEnabled();
- });
-
- test('Should select today date (Test 13)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const todayTab = page.locator(`[data-testid="date-tab-${today.replace(/-/g, '')}"]`);
- await expect(todayTab).toBeVisible();
- });
-
- test('Should select tomorrow date (Test 14)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', tomorrow)}`);
- await page.waitForLoadState('networkidle');
-
- const tomorrowTab = page.locator(`[data-testid="date-tab-${tomorrow.replace(/-/g, '')}"]`);
- await expect(tomorrowTab).toBeVisible();
- });
- });
-
- test.describe('Category 3: Flight Results', () => {
- test('Should verify flight results display (Test 15)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCards = page.locator('[data-testid="flight-card"]');
- await expect(flightCards).toHaveCount(20);
- });
-
- test('Should verify flight count (Test 16)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCards = page.locator('[data-testid="flight-card"]');
- await expect(flightCards).toHaveCount(20);
- });
-
- test('Should verify flight details in results (Test 17)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const flightNumber = flightCard.locator('[data-testid="flight-number"]');
- await expect(flightNumber).toBeVisible();
- });
-
- test('Should verify empty results message (Test 18)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByNumber(page, 'SU 9999');
-
- const noResults = page.locator('[data-testid="no-results"]');
- await expect(noResults).toBeVisible();
- await expect(noResults).toContainText('Нет результатов');
- });
-
- test('Should verify loading state (Test 19)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const loadingSpinner = page.locator('[data-testid="loading-spinner"]');
- await expect(loadingSpinner).toBeVisible();
- });
-
- test('Should verify error state (Test 20)', async ({ page }) => {
- await page.route('**/api/flights/**', (route) => {
- return route.abort('internetdisconnected');
- });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const networkError = page.locator('[data-testid="network-error"]');
- await expect(networkError).toBeVisible();
- });
- });
-
- test.describe('Category 4: Flight Details', () => {
- test('Should open flight details from results (Test 21)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/\/ru-ru\/[A-Z]{2}\s?\d+-\d{8}/);
- });
-
- test('Should verify flight details content (Test 22)', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByNumber(page, flight.flightNumber);
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- await expect(page.getByText(flight.flightNumber)).toBeVisible();
- await expect(page.getByText(flight.airlineName)).toBeVisible();
- });
-
- test('Should verify flight route details (Test 23)', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByNumber(page, flight.flightNumber);
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- await expect(page.getByText(flight.departure.cityName)).toBeVisible();
- await expect(page.getByText(flight.arrival.cityName)).toBeVisible();
- });
-
- test('Should verify flight status details (Test 24)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'scheduled',
- });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByNumber(page, flight.flightNumber);
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- await expect(page.getByText(flight.status)).toBeVisible();
- });
-
- test('Should close flight details (Test 25)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- const closeBtn = page.locator('[data-testid="close-details-btn"]');
- await closeBtn.click();
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/departure\/MOW-\d{8}/);
- });
- });
-
- test.describe('Category 5: Edge Cases', () => {
- test('Should search for non-existent city (Test 26)', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/departure/XXX-${today.replace(/-/g, '')}`);
- await page.waitForLoadState('networkidle');
-
- const errorState = page.locator('[data-testid="error-state"]');
- await expect(errorState).toBeVisible();
- });
-
- test('Should search with special characters (Test 27)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const searchInput = page.locator('[data-testid="flight-search-input"]');
- await searchInput.fill('SU 123!');
- await searchInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const noResults = page.locator('[data-testid="no-results"]');
- await expect(noResults).toBeVisible();
- });
-
- test('Should search with very long city name (Test 28)', async ({ page }) => {
- const longCityName = 'Москва'.repeat(10);
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const searchInput = page.locator('[data-testid="flight-search-input"]');
- await searchInput.fill(longCityName);
- await searchInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const flightCards = page.locator('[data-testid="flight-card"]');
- await expect(flightCards).toHaveCount(20);
- });
-
- test('Should search with Unicode characters (Test 29)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const searchInput = page.locator('[data-testid="flight-search-input"]');
- await searchInput.fill('SU 1234 🛫');
- await searchInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const flightCards = page.locator('[data-testid="flight-card"]');
- await expect(flightCards).toHaveCount(20);
- });
-
- test('Should handle rapid search attempts (Test 30)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const searchInput = page.locator('[data-testid="flight-search-input"]');
-
- for (let i = 0; i < 5; i++) {
- await searchInput.fill(`SU ${1000 + i}`);
- await searchInput.press('Enter');
- await page.waitForLoadState('networkidle');
- }
-
- const flightCards = page.locator('[data-testid="flight-card"]');
- await expect(flightCards).toHaveCount(20);
- });
- });
-
- test.describe('Category 6: Additional Departure Tests', () => {
- test('Should navigate to departure board for different cities (Test 31)', async ({ page }) => {
- const cities = ['MOW', 'LED', 'AER', 'OVB', 'KRR'];
-
- for (const cityCode of cities) {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', cityCode, today)}`);
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(new RegExp(`departure/${cityCode}-\\d{8}`));
- }
- });
-
- test('Should display correct date in title (Test 32)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const dateText = page.locator('[data-testid="board-date"]');
- await expect(dateText).toBeVisible();
- await expect(dateText).toContainText(today);
- });
-
- test('Should filter by status (Test 33)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const statusFilter = page.locator('[data-testid="status-filter"]');
- await statusFilter.click();
-
- const scheduledOption = page.locator('[data-testid="filter-option-scheduled"]');
- await scheduledOption.click();
-
- const filteredFlights = page.locator('[data-testid="flight-card"]');
- await expect(filteredFlights).toHaveCount(20);
- });
-
- test('Should filter by airline (Test 34)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const airlineFilter = page.locator('[data-testid="airline-filter"]');
- await airlineFilter.click();
-
- const aeroflotOption = page.locator('[data-testid="filter-option-SU"]');
- await aeroflotOption.click();
-
- const filteredFlights = page.locator('[data-testid="flight-card"]');
- await expect(filteredFlights).toHaveCount(20);
- });
-
- test('Should display flight number (Test 35)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const flightNumber = flightCard.locator('[data-testid="flight-number"]');
- await expect(flightNumber).toBeVisible();
- });
-
- test('Should display airline name (Test 36)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const airlineName = flightCard.locator('[data-testid="airline-name"]');
- await expect(airlineName).toBeVisible();
- });
-
- test('Should display departure and arrival cities (Test 37)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const departureCity = flightCard.locator('[data-testid="departure-city"]');
- await expect(departureCity).toBeVisible();
- });
-
- test('Should display scheduled time (Test 38)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const scheduledTime = flightCard.locator('[data-testid="scheduled-time"]');
- await expect(scheduledTime).toBeVisible();
- });
-
- test('Should display actual time when available (Test 39)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'departed',
- });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const actualTime = flightCard.locator('[data-testid="actual-time"]');
- await expect(actualTime).toBeVisible();
- });
-
- test('Should display delay information (Test 40)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'delayed',
- });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const delayInfo = flightCard.locator('[data-testid="delay-info"]');
- await expect(delayInfo).toBeVisible();
- });
-
- test('Should display terminal information (Test 41)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const terminal = flightCard.locator('[data-testid="terminal"]');
- await expect(terminal).toBeVisible();
- });
-
- test('Should display boarding gate information (Test 42)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'boarding',
- });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const boardingGate = flightCard.locator('[data-testid="boarding-gate"]');
- await expect(boardingGate).toBeVisible();
- });
-
- test('Should navigate to tomorrow date tab (Test 43)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const dateTab = page.locator(`[data-testid="date-tab-${tomorrow.replace(/-/g, '')}"]`);
- if ((await dateTab.count()) > 0) {
- await dateTab.click();
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/departure\/MOW-\d{8}/);
- }
- });
-
- test('Should handle invalid city code (Test 44)', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/departure/XXX-${today.replace(/-/g, '')}`);
- await page.waitForLoadState('networkidle');
-
- const errorState = page.locator('[data-testid="error-state"]');
- await expect(errorState).toBeVisible();
- });
-
- test('Should handle network error (Test 45)', async ({ page }) => {
- await page.route('**/api/flights/**', (route) => {
- return route.abort('internetdisconnected');
- });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const networkError = page.locator('[data-testid="network-error"]');
- await expect(networkError).toBeVisible();
- });
-
- test('Should have proper ARIA labels (Test 46)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toHaveAttribute('role', 'article');
- });
-
- test('Should be keyboard navigable (Test 47)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await page.keyboard.press('Tab');
- await page.keyboard.press('Tab');
- await page.keyboard.press('Tab');
-
- const focusedElement = page.locator(':focus');
- await expect(focusedElement).toBeVisible();
- });
-
- test('Should search by flight number (Test 48)', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByNumber(page, flight.flightNumber);
-
- const searchResults = page.locator('[data-testid="flight-card"]');
- await expect(searchResults).toHaveCount(1);
-
- await verifyFlightCard(page, flight);
- });
-
- test('Should show no results when flight not found (Test 49)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByNumber(page, 'SU 9999');
-
- const noResults = page.locator('[data-testid="no-results"]');
- await expect(noResults).toBeVisible();
- await expect(noResults).toContainText('Нет результатов');
- });
-
- test('Should search by route (Test 50)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByRoute(page, 'Moscow', 'Sochi');
-
- const flightCards = page.locator('[data-testid="flight-card"]');
- await expect(flightCards).toHaveCount(20);
- });
- });
-
- test.describe('Category 7: Advanced Departure Tests', () => {
- test('Should display flight card with all information (Test 51)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- await expect(flightCard.getByText('Отправление')).toBeVisible();
- await expect(flightCard.locator('[data-testid="flight-number"]')).toBeVisible();
- await expect(flightCard.locator('[data-testid="airline-name"]')).toBeVisible();
- await expect(flightCard.locator('[data-testid="departure-city"]')).toBeVisible();
- await expect(flightCard.locator('[data-testid="arrival-city"]')).toBeVisible();
- await expect(flightCard.locator('[data-testid="scheduled-time"]')).toBeVisible();
- });
-
- test('Should display flight status badge (Test 52)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'boarding',
- });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const statusBadge = flightCard.locator('[data-testid="status-badge"]');
- await expect(statusBadge).toBeVisible();
- });
-
- test('Should display aircraft type (Test 53)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- aircraftType: 'Airbus A320',
- });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const aircraftType = flightCard.locator('[data-testid="aircraft-type"]');
- await expect(aircraftType).toBeVisible();
- });
-
- test('Should display checkin status (Test 54)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'checkin',
- });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const checkinStatus = flightCard.locator('[data-testid="checkin-status"]');
- await expect(checkinStatus).toBeVisible();
- });
-
- test('Should display flight duration (Test 55)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const duration = flightCard.locator('[data-testid="flight-duration"]');
- await expect(duration).toBeVisible();
- });
-
- test('Should display flight route on map (Test 56)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const routeMap = flightCard.locator('[data-testid="route-map"]');
- await expect(routeMap).toBeVisible();
- });
-
- test('Should display flight class information (Test 57)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'scheduled',
- });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const classInfo = flightCard.locator('[data-testid="class-info"]');
- await expect(classInfo).toBeVisible();
- });
-
- test('Should display flight catering information (Test 58)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'scheduled',
- });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const cateringInfo = flightCard.locator('[data-testid="catering-info"]');
- await expect(cateringInfo).toBeVisible();
- });
-
- test('Should display flight seat information (Test 59)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'scheduled',
- });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const seatInfo = flightCard.locator('[data-testid="seat-info"]');
- await expect(seatInfo).toBeVisible();
- });
-
- test('Should display flight operating days (Test 60)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const operatingDays = flightCard.locator('[data-testid="operating-days"]');
- await expect(operatingDays).toBeVisible();
- });
-
- test('Should display flight week range (Test 61)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const weekRange = flightCard.locator('[data-testid="week-range"]');
- await expect(weekRange).toBeVisible();
- });
-
- test('Should display flight last updated (Test 62)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'scheduled',
- });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const lastUpdated = flightCard.locator('[data-testid="last-updated"]');
- await expect(lastUpdated).toBeVisible();
- });
-
- test('Should display flight baggage information (Test 63)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'scheduled',
- });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const baggageInfo = flightCard.locator('[data-testid="baggage-info"]');
- await expect(baggageInfo).toBeVisible();
- });
-
- test('Should display flight transfer information (Test 64)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'scheduled',
- });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const transferInfo = flightCard.locator('[data-testid="transfer-info"]');
- await expect(transferInfo).toBeVisible();
- });
-
- test('Should display flight cancellation information (Test 65)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'cancelled',
- });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const cancellationInfo = flightCard.locator('[data-testid="cancellation-info"]');
- await expect(cancellationInfo).toBeVisible();
- });
-
- test('Should display flight gate changed information (Test 66)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'gateChanged',
- });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const gateChangedInfo = flightCard.locator('[data-testid="gate-changed-info"]');
- await expect(gateChangedInfo).toBeVisible();
- });
-
- test('Should display flight landed information (Test 67)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'landed',
- });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const landedInfo = flightCard.locator('[data-testid="landed-info"]');
- await expect(landedInfo).toBeVisible();
- });
-
- test('Should display flight in flight information (Test 68)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'inFlight',
- });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const inFlightInfo = flightCard.locator('[data-testid="in-flight-info"]');
- await expect(inFlightInfo).toBeVisible();
- });
-
- test('Should display flight departed information (Test 69)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'departed',
- });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const departedInfo = flightCard.locator('[data-testid="departed-info"]');
- await expect(departedInfo).toBeVisible();
- });
-
- test('Should display flight arrived information (Test 70)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'arrived',
- });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const arrivedInfo = flightCard.locator('[data-testid="arrived-info"]');
- await expect(arrivedInfo).toBeVisible();
- });
-
- test('Should search by different flight numbers (Test 71)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightNumbers = ['SU 1124', 'SU 1076', 'SU 6170'];
-
- for (const flightNumber of flightNumbers) {
- await searchFlightByNumber(page, flightNumber);
- await page.waitForLoadState('networkidle');
-
- const searchResults = page.locator('[data-testid="flight-card"]');
- await expect(searchResults).toHaveCount(1);
- }
- });
-
- test('Should search by different routes (Test 72)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const routes = [
- ['Moscow', 'Sochi'],
- ['Moscow', 'Saint Petersburg'],
- ['Moscow', 'Novosibirsk'],
- ];
-
- for (const [departureCity, arrivalCity] of routes) {
- await searchFlightByRoute(page, departureCity, arrivalCity);
- await page.waitForLoadState('networkidle');
-
- const flightCards = page.locator('[data-testid="flight-card"]');
- await expect(flightCards).toHaveCount(20);
- }
- });
-
- test('Should search by different dates (Test 73)', async ({ page }) => {
- const dates = [today, tomorrow, futureDate];
-
- for (const date of dates) {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', date)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCards = page.locator('[data-testid="flight-card"]');
- await expect(flightCards).toHaveCount(20);
- }
- });
-
- test('Should search by different cities (Test 74)', async ({ page }) => {
- const cities = ['MOW', 'LED', 'AER', 'OVB', 'KRR'];
-
- for (const cityCode of cities) {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', cityCode, today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCards = page.locator('[data-testid="flight-card"]');
- await expect(flightCards).toHaveCount(20);
- }
- });
-
- test('Should search by different airlines (Test 75)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const airlines = ['SU', 'FV'];
-
- for (const airlineCode of airlines) {
- const airlineFilter = page.locator('[data-testid="airline-filter"]');
- await airlineFilter.click();
-
- const airlineOption = page.locator(`[data-testid="filter-option-${airlineCode}"]`);
- await airlineOption.click();
- await page.waitForLoadState('networkidle');
-
- const filteredFlights = page.locator('[data-testid="flight-card"]');
- await expect(filteredFlights).toHaveCount(20);
- }
- });
-
- test('Should search by different statuses (Test 76)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const statuses = ['scheduled', 'boarding', 'departed'];
-
- for (const status of statuses) {
- const statusFilter = page.locator('[data-testid="status-filter"]');
- await statusFilter.click();
-
- const statusOption = page.locator(`[data-testid="filter-option-${status}"]`);
- await statusOption.click();
- await page.waitForLoadState('networkidle');
-
- const filteredFlights = page.locator('[data-testid="flight-card"]');
- await expect(filteredFlights).toHaveCount(20);
- }
- });
-
- test('Should search by different aircraft types (Test 77)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const aircraftTypes = ['Airbus A320', 'Airbus A321', 'Boeing 737-800'];
-
- for (const aircraftType of aircraftTypes) {
- const aircraftFilter = page.locator('[data-testid="aircraft-type-filter"]');
- await aircraftFilter.click();
-
- const aircraftOption = page.locator(`[data-testid="filter-option-${aircraftType}"]`);
- await aircraftOption.click();
- await page.waitForLoadState('networkidle');
-
- const filteredFlights = page.locator('[data-testid="flight-card"]');
- await expect(filteredFlights).toHaveCount(20);
- }
- });
-
- test('Should search by different terminals (Test 78)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const terminals = ['A', 'B', 'C', 'D'];
-
- for (const terminal of terminals) {
- const terminalFilter = page.locator('[data-testid="terminal-filter"]');
- await terminalFilter.click();
-
- const terminalOption = page.locator(`[data-testid="filter-option-${terminal}"]`);
- await terminalOption.click();
- await page.waitForLoadState('networkidle');
-
- const filteredFlights = page.locator('[data-testid="flight-card"]');
- await expect(filteredFlights).toHaveCount(20);
- }
- });
-
- test('Should search by different baggage belts (Test 79)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const baggageBelts = ['1', '2', '3'];
-
- for (const baggageBelt of baggageBelts) {
- const baggageFilter = page.locator('[data-testid="baggage-belt-filter"]');
- await baggageFilter.click();
-
- const baggageOption = page.locator(`[data-testid="filter-option-${baggageBelt}"]`);
- await baggageOption.click();
- await page.waitForLoadState('networkidle');
-
- const filteredFlights = page.locator('[data-testid="flight-card"]');
- await expect(filteredFlights).toHaveCount(20);
- }
- });
-
- test('Should search by different gates (Test 80)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const gates = ['1', '2', '3', '4', '5'];
-
- for (const gate of gates) {
- const gateFilter = page.locator('[data-testid="gate-filter"]');
- await gateFilter.click();
-
- const gateOption = page.locator(`[data-testid="filter-option-${gate}"]`);
- await gateOption.click();
- await page.waitForLoadState('networkidle');
-
- const filteredFlights = page.locator('[data-testid="flight-card"]');
- await expect(filteredFlights).toHaveCount(20);
- }
- });
- });
-});
diff --git a/tests/e2e-angular/integration/online-board-flight-search.spec.ts b/tests/e2e-angular/integration/online-board-flight-search.spec.ts
deleted file mode 100644
index 80e09df9..00000000
--- a/tests/e2e-angular/integration/online-board-flight-search.spec.ts
+++ /dev/null
@@ -1,622 +0,0 @@
-import { test, expect } from '@playwright/test';
-import type { Page } from '@playwright/test';
-import {
- buildOnlineBoardPath,
- buildRouteParam,
- searchFlightByNumber,
- searchFlightByRoute,
- verifyFlightCard,
- generateFlight,
- generateFlights,
- getToday,
- getTomorrow,
- getYesterday,
- getFutureDate,
- getPastDate,
- CITIES,
- FIXTURES,
-} from '../support/test-utilities';
-
-const today = getToday();
-const tomorrow = getTomorrow();
-const yesterday = getYesterday();
-const futureDate = getFutureDate(7);
-const pastDate = getPastDate(7);
-const dateParam = buildRouteParam('MOW', today);
-
-// ============================================================================
-// Online Board - Flight Search Tests (30+ tests)
-// ============================================================================
-
-test.describe('Online Board - Flight Search', () => {
- test.describe('Category 1: Basic Flight Search (8 tests)', () => {
- test('Should search by flight number (manual input) (Test 1)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const searchInput = page.locator('[data-testid="flight-search-input"]');
- await searchInput.fill('SU 1234');
- await searchInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const flightCards = page.locator('[data-testid="flight-card"]');
- await expect(flightCards).toHaveCount(1);
- });
-
- test('Should search with today date (Test 2)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/departure\/MOW-\d{8}/);
- await expect(page).toHaveTitle(/Отправление/);
- });
-
- test('Should search with future date (Test 3)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', futureDate)}`);
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/departure\/MOW-\d{8}/);
- });
-
- test('Should search with past date and show validation (Test 4)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', pastDate)}`);
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/departure\/MOW-\d{8}/);
- });
-
- test('Should search without date and use today (Test 5)', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/departure/MOW`);
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/departure\/MOW-\d{8}/);
- });
-
- test('Should search with invalid flight number and show error (Test 6)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const searchInput = page.locator('[data-testid="flight-search-input"]');
- await searchInput.fill('INVALID');
- await searchInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const noResults = page.locator('[data-testid="no-results"]');
- await expect(noResults).toBeVisible();
- await expect(noResults).toContainText('Нет результатов');
- });
-
- test('Should search with empty flight number and show validation (Test 7)', async ({
- page,
- }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const searchInput = page.locator('[data-testid="flight-search-input"]');
- await searchInput.fill('');
- await searchInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const noResults = page.locator('[data-testid="no-results"]');
- await expect(noResults).toBeVisible();
- });
-
- test('Should search with flight number that has multiple results (Test 8)', async ({
- page,
- }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const searchInput = page.locator('[data-testid="flight-search-input"]');
- await searchInput.fill('SU');
- await searchInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const flightCards = page.locator('[data-testid="flight-card"]');
- await expect(flightCards).toHaveCount(20);
- });
- });
-
- test.describe('Category 2: Date Selection (6 tests)', () => {
- test('Should select date from calendar (Test 9)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const calendarInput = page.locator('[data-testid="calendar-input"]');
- await expect(calendarInput).toBeVisible();
- });
-
- test('Should select date range (Test 10)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const dateRange = page.locator('[data-testid="date-range"]');
- await expect(dateRange).toBeVisible();
- });
-
- test('Should verify date format DD.MM.YYYY (Test 11)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const dateText = page.locator('[data-testid="board-date"]');
- await expect(dateText).toBeVisible();
-
- const dateValue = await dateText.textContent();
- expect(dateValue).toMatch(/\d{2}\.\d{2}\.\d{4}/);
- });
-
- test('Should verify date validation min/max dates (Test 12)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const calendarInput = page.locator('[data-testid="calendar-input"]');
- await expect(calendarInput).toBeEnabled();
- });
-
- test('Should select today date (Test 13)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const todayTab = page.locator(`[data-testid="date-tab-${today.replace(/-/g, '')}"]`);
- await expect(todayTab).toBeVisible();
- });
-
- test('Should select tomorrow date (Test 14)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', tomorrow)}`);
- await page.waitForLoadState('networkidle');
-
- const tomorrowTab = page.locator(`[data-testid="date-tab-${tomorrow.replace(/-/g, '')}"]`);
- await expect(tomorrowTab).toBeVisible();
- });
- });
-
- test.describe('Category 3: Flight Results (6 tests)', () => {
- test('Should verify flight results display (Test 15)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCards = page.locator('[data-testid="flight-card"]');
- await expect(flightCards).toHaveCount(20);
- });
-
- test('Should verify flight count (Test 16)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCards = page.locator('[data-testid="flight-card"]');
- await expect(flightCards).toHaveCount(20);
- });
-
- test('Should verify flight details in results (Test 17)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const flightNumber = flightCard.locator('[data-testid="flight-number"]');
- await expect(flightNumber).toBeVisible();
- });
-
- test('Should verify empty results message (Test 18)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByNumber(page, 'SU 9999');
-
- const noResults = page.locator('[data-testid="no-results"]');
- await expect(noResults).toBeVisible();
- await expect(noResults).toContainText('Нет результатов');
- });
-
- test('Should verify loading state (Test 19)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const loadingSpinner = page.locator('[data-testid="loading-spinner"]');
- await expect(loadingSpinner).toBeVisible();
- });
-
- test('Should verify error state (Test 20)', async ({ page }) => {
- await page.route('**/api/flights/**', (route) => {
- return route.abort('internetdisconnected');
- });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const networkError = page.locator('[data-testid="network-error"]');
- await expect(networkError).toBeVisible();
- });
- });
-
- test.describe('Category 4: Flight Details (5 tests)', () => {
- test('Should open flight details from results (Test 21)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/\/ru-ru\/[A-Z]{2}\s?\d+-\d{8}/);
- });
-
- test('Should verify flight details content (Test 22)', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByNumber(page, flight.flightNumber);
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- await expect(page.getByText(flight.flightNumber)).toBeVisible();
- await expect(page.getByText(flight.airlineName)).toBeVisible();
- });
-
- test('Should verify flight route details (Test 23)', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByNumber(page, flight.flightNumber);
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- await expect(page.getByText(flight.departure.cityName)).toBeVisible();
- await expect(page.getByText(flight.arrival.cityName)).toBeVisible();
- });
-
- test('Should verify flight status details (Test 24)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'scheduled',
- });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByNumber(page, flight.flightNumber);
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- await expect(page.getByText(flight.status)).toBeVisible();
- });
-
- test('Should close flight details (Test 25)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await flightCard.click();
- await page.waitForLoadState('networkidle');
-
- const closeBtn = page.locator('[data-testid="close-details-btn"]');
- await closeBtn.click();
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/departure\/MOW-\d{8}/);
- });
- });
-
- test.describe('Category 5: Edge Cases (5 tests)', () => {
- test('Should search for non-existent flight number (Test 26)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByNumber(page, 'SU 9999');
-
- const noResults = page.locator('[data-testid="no-results"]');
- await expect(noResults).toBeVisible();
- });
-
- test('Should search with special characters (Test 27)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const searchInput = page.locator('[data-testid="flight-search-input"]');
- await searchInput.fill('SU 123!');
- await searchInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const noResults = page.locator('[data-testid="no-results"]');
- await expect(noResults).toBeVisible();
- });
-
- test('Should search with very long flight number (Test 28)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const searchInput = page.locator('[data-testid="flight-search-input"]');
- await searchInput.fill('SU 12345678901234567890');
- await searchInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const flightCards = page.locator('[data-testid="flight-card"]');
- await expect(flightCards).toHaveCount(20);
- });
-
- test('Should search with Unicode characters (Test 29)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const searchInput = page.locator('[data-testid="flight-search-input"]');
- await searchInput.fill('SU 1234 🛫');
- await searchInput.press('Enter');
- await page.waitForLoadState('networkidle');
-
- const flightCards = page.locator('[data-testid="flight-card"]');
- await expect(flightCards).toHaveCount(20);
- });
-
- test('Should handle rapid search attempts (Test 30)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const searchInput = page.locator('[data-testid="flight-search-input"]');
-
- for (let i = 0; i < 5; i++) {
- await searchInput.fill(`SU ${1000 + i}`);
- await searchInput.press('Enter');
- await page.waitForLoadState('networkidle');
- }
-
- const flightCards = page.locator('[data-testid="flight-card"]');
- await expect(flightCards).toHaveCount(20);
- });
- });
-
- test.describe('Additional Flight Search Tests', () => {
- test('Should navigate to flight board for different cities (Test 31)', async ({ page }) => {
- const cities = ['MOW', 'LED', 'AER', 'OVB', 'KRR'];
-
- for (const cityCode of cities) {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', cityCode, today)}`);
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(new RegExp(`departure/${cityCode}-\\d{8}`));
- }
- });
-
- test('Should display correct date in title (Test 32)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const dateText = page.locator('[data-testid="board-date"]');
- await expect(dateText).toBeVisible();
- await expect(dateText).toContainText(today);
- });
-
- test('Should filter by status (Test 33)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const statusFilter = page.locator('[data-testid="status-filter"]');
- await statusFilter.click();
-
- const scheduledOption = page.locator('[data-testid="filter-option-scheduled"]');
- await scheduledOption.click();
-
- const filteredFlights = page.locator('[data-testid="flight-card"]');
- await expect(filteredFlights).toHaveCount(20);
- });
-
- test('Should filter by airline (Test 34)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const airlineFilter = page.locator('[data-testid="airline-filter"]');
- await airlineFilter.click();
-
- const aeroflotOption = page.locator('[data-testid="filter-option-SU"]');
- await aeroflotOption.click();
-
- const filteredFlights = page.locator('[data-testid="flight-card"]');
- await expect(filteredFlights).toHaveCount(20);
- });
-
- test('Should display flight number (Test 35)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const flightNumber = flightCard.locator('[data-testid="flight-number"]');
- await expect(flightNumber).toBeVisible();
- });
-
- test('Should display airline name (Test 36)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const airlineName = flightCard.locator('[data-testid="airline-name"]');
- await expect(airlineName).toBeVisible();
- });
-
- test('Should display departure and arrival cities (Test 37)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const departureCity = flightCard.locator('[data-testid="departure-city"]');
- await expect(departureCity).toBeVisible();
- });
-
- test('Should display scheduled time (Test 38)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const scheduledTime = flightCard.locator('[data-testid="scheduled-time"]');
- await expect(scheduledTime).toBeVisible();
- });
-
- test('Should display actual time when available (Test 39)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'departed',
- });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const actualTime = flightCard.locator('[data-testid="actual-time"]');
- await expect(actualTime).toBeVisible();
- });
-
- test('Should display delay information (Test 40)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'delayed',
- });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const delayInfo = flightCard.locator('[data-testid="delay-info"]');
- await expect(delayInfo).toBeVisible();
- });
-
- test('Should display terminal information (Test 41)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const terminal = flightCard.locator('[data-testid="terminal"]');
- await expect(terminal).toBeVisible();
- });
-
- test('Should display baggage belt information (Test 42)', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'arrived',
- });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const baggageBelt = flightCard.locator('[data-testid="baggage-belt"]');
- await expect(baggageBelt).toBeVisible();
- });
-
- test('Should navigate to tomorrow date tab (Test 43)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const dateTab = page.locator(`[data-testid="date-tab-${tomorrow.replace(/-/g, '')}"]`);
- if ((await dateTab.count()) > 0) {
- await dateTab.click();
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/departure\/MOW-\d{8}/);
- }
- });
-
- test('Should handle invalid city code (Test 44)', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/departure/XXX-${today.replace(/-/g, '')}`);
- await page.waitForLoadState('networkidle');
-
- const errorState = page.locator('[data-testid="error-state"]');
- await expect(errorState).toBeVisible();
- });
-
- test('Should handle network error (Test 45)', async ({ page }) => {
- await page.route('**/api/flights/**', (route) => {
- return route.abort('internetdisconnected');
- });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const networkError = page.locator('[data-testid="network-error"]');
- await expect(networkError).toBeVisible();
- });
-
- test('Should have proper ARIA labels (Test 46)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toHaveAttribute('role', 'article');
- });
-
- test('Should be keyboard navigable (Test 47)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await page.keyboard.press('Tab');
- await page.keyboard.press('Tab');
- await page.keyboard.press('Tab');
-
- const focusedElement = page.locator(':focus');
- await expect(focusedElement).toBeVisible();
- });
-
- test('Should search by flight number from search input (Test 48)', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByNumber(page, flight.flightNumber);
-
- const searchResults = page.locator('[data-testid="flight-card"]');
- await expect(searchResults).toHaveCount(1);
-
- await verifyFlightCard(page, flight);
- });
-
- test('Should show no results when flight not found (Test 49)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByNumber(page, 'SU 9999');
-
- const noResults = page.locator('[data-testid="no-results"]');
- await expect(noResults).toBeVisible();
- await expect(noResults).toContainText('Нет результатов');
- });
-
- test('Should search by route (Test 50)', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByRoute(page, 'Moscow', 'Sochi');
-
- const flightCards = page.locator('[data-testid="flight-card"]');
- await expect(flightCards).toHaveCount(20);
- });
- });
-});
diff --git a/tests/e2e-angular/integration/templates/flight-details.template.ts b/tests/e2e-angular/integration/templates/flight-details.template.ts
deleted file mode 100644
index f0ff722f..00000000
--- a/tests/e2e-angular/integration/templates/flight-details.template.ts
+++ /dev/null
@@ -1,450 +0,0 @@
-import { test, expect } from '@playwright/test';
-import type { Page } from '@playwright/test';
-import {
- buildFlightDetailsPath,
- buildRouteParam,
- generateFlight,
- generateFlights,
- getToday,
- getTomorrow,
- CITIES,
-} from '../support/test-utilities';
-
-const today = getToday();
-const tomorrow = getTomorrow();
-
-// ============================================================================
-// Flight Details Tests
-// ============================================================================
-
-test.describe('Flight Details', () => {
- test.describe('Page Navigation', () => {
- test('should navigate to flight details page', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(new RegExp(`/${slug}`));
- await expect(page).toHaveTitle(new RegExp(flight.flightNumber));
- });
-
- test('should navigate to flight details for arrival flight', async ({ page }) => {
- const flight = generateFlight({ direction: 'arrival', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(new RegExp(`/${slug}`));
- });
-
- test('should navigate to flight details for different date', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW', date: tomorrow });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${tomorrow.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(new RegExp(`/${slug}`));
- });
- });
-
- test.describe('Flight Information', () => {
- test('should display flight number', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const flightNumber = page.locator('[data-testid="flight-number"]');
- await expect(flightNumber).toBeVisible();
- await expect(flightNumber).toContainText(flight.flightNumber);
- });
-
- test('should display airline name', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const airlineName = page.locator('[data-testid="airline-name"]');
- await expect(airlineName).toBeVisible();
- await expect(airlineName).toContainText(flight.airlineName);
- });
-
- test('should display aircraft type', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const aircraftType = page.locator('[data-testid="aircraft-type"]');
- await expect(aircraftType).toBeVisible();
- await expect(aircraftType).toContainText(flight.aircraftType || '');
- });
-
- test('should display departure city', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const departureCity = page.locator('[data-testid="departure-city"]');
- await expect(departureCity).toBeVisible();
- await expect(departureCity).toContainText(flight.departure.cityName);
- });
-
- test('should display arrival city', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const arrivalCity = page.locator('[data-testid="arrival-city"]');
- await expect(arrivalCity).toBeVisible();
- await expect(arrivalCity).toContainText(flight.arrival.cityName);
- });
-
- test('should display scheduled departure time', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const depTime = page.locator('[data-testid="scheduled-departure-time"]');
- await expect(depTime).toBeVisible();
-
- const depTimeText = flight.departure.time.scheduled.slice(11, 16);
- await expect(depTime).toContainText(depTimeText);
- });
-
- test('should display scheduled arrival time', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const arrTime = page.locator('[data-testid="scheduled-arrival-time"]');
- await expect(arrTime).toBeVisible();
-
- const arrTimeText = flight.arrival.time.scheduled.slice(11, 16);
- await expect(arrTime).toContainText(arrTimeText);
- });
-
- test('should display flight duration', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const duration = page.locator('[data-testid="flight-duration"]');
- await expect(duration).toBeVisible();
- });
-
- test('should display flight status', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'scheduled',
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const status = page.locator('[data-testid="flight-status"]');
- await expect(status).toBeVisible();
- await expect(status).toContainText(flight.status);
- });
- });
-
- test.describe('Flight Details', () => {
- test('should display terminal information', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- departure: { terminal: 'B' },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const terminal = page.locator('[data-testid="terminal"]');
- await expect(terminal).toBeVisible();
- await expect(terminal).toContainText('B');
- });
-
- test('should display boarding gate information', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'boarding',
- boarding: { gate: '11', status: 'Идёт посадка' },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const boardingGate = page.locator('[data-testid="boarding-gate"]');
- await expect(boardingGate).toBeVisible();
- await expect(boardingGate).toContainText('11');
- });
-
- test('should display baggage belt information', async ({ page }) => {
- const flight = generateFlight({
- direction: 'arrival',
- cityCode: 'MOW',
- status: 'arrived',
- arrivalInfo: { baggageBelt: '5', transfer: 'Тран' },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const baggageBelt = page.locator('[data-testid="baggage-belt"]');
- await expect(baggageBelt).toBeVisible();
- await expect(baggageBelt).toContainText('5');
- });
-
- test('should display check-in information', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'checkin',
- checkin: { status: 'В процессе' },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const checkinInfo = page.locator('[data-testid="checkin-info"]');
- await expect(checkinInfo).toBeVisible();
- });
-
- test('should display deplaning information', async ({ page }) => {
- const flight = generateFlight({
- direction: 'arrival',
- cityCode: 'MOW',
- status: 'arrived',
- deplaning: { status: 'В процессе', transfer: 'Трап' },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const deplaningInfo = page.locator('[data-testid="deplaning-info"]');
- await expect(deplaningInfo).toBeVisible();
- });
- });
-
- test.describe('Aircraft Information', () => {
- test('should display aircraft type', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const aircraftType = page.locator('[data-testid="aircraft-type"]');
- await expect(aircraftType).toBeVisible();
- });
-
- test('should display aircraft name', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- aircraft: { type: 'Airbus A320', name: 'В. Высоцкий' },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const aircraftName = page.locator('[data-testid="aircraft-name"]');
- await expect(aircraftName).toBeVisible();
- await expect(aircraftName).toContainText('В. Высоцкий');
- });
-
- test('should display seat configuration', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const seatInfo = page.locator('[data-testid="seat-info"]');
- await expect(seatInfo).toBeVisible();
- });
- });
-
- test.describe('Schedule Information', () => {
- test('should display scheduled departure', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const scheduledDep = page.locator('[data-testid="scheduled-departure"]');
- await expect(scheduledDep).toBeVisible();
- });
-
- test('should display scheduled arrival', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const scheduledArr = page.locator('[data-testid="scheduled-arrival"]');
- await expect(scheduledArr).toBeVisible();
- });
-
- test('should display actual departure when available', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'departed',
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const actualDep = page.locator('[data-testid="actual-departure"]');
- await expect(actualDep).toBeVisible();
- });
-
- test('should display actual arrival when available', async ({ page }) => {
- const flight = generateFlight({
- direction: 'arrival',
- cityCode: 'MOW',
- status: 'arrived',
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const actualArr = page.locator('[data-testid="actual-arrival"]');
- await expect(actualArr).toBeVisible();
- });
-
- test('should display expected arrival when delayed', async ({ page }) => {
- const flight = generateFlight({
- direction: 'arrival',
- cityCode: 'MOW',
- status: 'delayed',
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const expectedArr = page.locator('[data-testid="expected-arrival"]');
- await expect(expectedArr).toBeVisible();
- });
- });
-
- test.describe('Error Handling', () => {
- test('should handle invalid flight number', async ({ page }) => {
- await page.goto(`/ru-ru/SU9999-${today.replace(/-/g, '')}`);
- await page.waitForLoadState('networkidle');
-
- const errorState = page.locator('[data-testid="error-state"]');
- await expect(errorState).toBeVisible();
- });
-
- test('should handle flight not found', async ({ page }) => {
- await page.goto(`/ru-ru/SU9999-20260406`);
- await page.waitForLoadState('networkidle');
-
- const notFound = page.locator('[data-testid="not-found"]');
- await expect(notFound).toBeVisible();
- });
-
- test('should handle network error', async ({ page }) => {
- await page.route('**/api/flights/**', (route) => {
- return route.abort('internetdisconnected');
- });
-
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const networkError = page.locator('[data-testid="network-error"]');
- await expect(networkError).toBeVisible();
- });
- });
-
- test.describe('Navigation', () => {
- test('should navigate back to board', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const backLink = page.locator('[data-testid="back-link"]');
- await backLink.click();
-
- await expect(page).toHaveURL(/onlineboard\/departure\/MOW-\d{8}/);
- });
-
- test('should navigate to related flights', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const relatedFlights = page.locator('[data-testid="related-flights"]');
- await expect(relatedFlights).toBeVisible();
- });
- });
-
- test.describe('Accessibility', () => {
- test('should have proper ARIA labels', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const pageContent = page.locator('[data-testid="page-content"]');
- await expect(pageContent).toHaveAttribute('role', 'main');
- });
-
- test('should be keyboard navigable', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- await page.keyboard.press('Tab');
- await page.keyboard.press('Tab');
- await page.keyboard.press('Tab');
-
- const focusedElement = page.locator(':focus');
- await expect(focusedElement).toBeVisible();
- });
- });
-});
diff --git a/tests/e2e-angular/integration/templates/flights-map.template.ts b/tests/e2e-angular/integration/templates/flights-map.template.ts
deleted file mode 100644
index 6b941e4d..00000000
--- a/tests/e2e-angular/integration/templates/flights-map.template.ts
+++ /dev/null
@@ -1,334 +0,0 @@
-import { test, expect } from '@playwright/test';
-import type { Page } from '@playwright/test';
-import {
- buildFlightsMapPath,
- buildRouteParam,
- generateFlight,
- generateFlights,
- getToday,
- getTomorrow,
- CITIES,
-} from '@e2e/support/test-utilities';
-
-const today = getToday();
-const tomorrow = getTomorrow();
-
-// ============================================================================
-// Flights Map Tests
-// ============================================================================
-
-test.describe('Flights Map', () => {
- test.describe('Page Navigation', () => {
- test('should navigate to flights map page', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/flights-map/);
- await expect(page).toHaveTitle(/Карта полетов/);
- });
-
- test('should display map container', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const mapContainer = page.locator('[data-testid="map-container"]');
- await expect(mapContainer).toBeVisible();
- });
- });
-
- test.describe('Map Display', () => {
- test('should display flight markers', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const markers = page.locator('[data-testid="flight-marker"]');
- await expect(markers).toHaveCount(20);
- });
-
- test('should display flight popup on marker click', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const marker = page.locator('[data-testid="flight-marker"]').first();
- await marker.click();
-
- const popup = page.locator('[data-testid="flight-popup"]');
- await expect(popup).toBeVisible();
- });
-
- test('should display flight details in popup', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const marker = page.locator('[data-testid="flight-marker"]').first();
- await marker.click();
-
- const flightNumber = page.locator('[data-testid="popup-flight-number"]');
- await expect(flightNumber).toBeVisible();
- });
-
- test('should display route line between airports', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const routeLine = page.locator('[data-testid="route-line"]');
- await expect(routeLine).toBeVisible();
- });
- });
-
- test.describe('Filtering', () => {
- test('should filter by departure city', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureFilter = page.locator('[data-testid="departure-filter"]');
- await departureFilter.click();
-
- const moscowOption = page.locator('[data-testid="filter-option-MOW"]');
- await moscowOption.click();
-
- const markers = page.locator('[data-testid="flight-marker"]');
- await expect(markers).toHaveCount(20);
- });
-
- test('should filter by arrival city', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const arrivalFilter = page.locator('[data-testid="arrival-filter"]');
- await arrivalFilter.click();
-
- const sochiOption = page.locator('[data-testid="filter-option-AER"]');
- await sochiOption.click();
-
- const markers = page.locator('[data-testid="flight-marker"]');
- await expect(markers).toHaveCount(20);
- });
-
- test('should filter by status', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const statusFilter = page.locator('[data-testid="status-filter"]');
- await statusFilter.click();
-
- const scheduledOption = page.locator('[data-testid="filter-option-scheduled"]');
- await scheduledOption.click();
-
- const markers = page.locator('[data-testid="flight-marker"]');
- await expect(markers).toHaveCount(20);
- });
-
- test('should clear all filters', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const clearFilters = page.locator('[data-testid="clear-filters"]');
- await clearFilters.click();
-
- const markers = page.locator('[data-testid="flight-marker"]');
- await expect(markers).toHaveCount(20);
- });
- });
-
- test.describe('Flight Details Panel', () => {
- test('should display flight details panel', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const panel = page.locator('[data-testid="flight-details-panel"]');
- await expect(panel).toBeVisible();
- });
-
- test('should display flight number in panel', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const flightNumber = page.locator('[data-testid="panel-flight-number"]');
- await expect(flightNumber).toBeVisible();
- });
-
- test('should display airline name in panel', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const airlineName = page.locator('[data-testid="panel-airline-name"]');
- await expect(airlineName).toBeVisible();
- });
-
- test('should display departure and arrival cities in panel', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureCity = page.locator('[data-testid="panel-departure-city"]');
- await expect(departureCity).toBeVisible();
-
- const arrivalCity = page.locator('[data-testid="panel-arrival-city"]');
- await expect(arrivalCity).toBeVisible();
- });
-
- test('should display scheduled times in panel', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const depTime = page.locator('[data-testid="panel-departure-time"]');
- await expect(depTime).toBeVisible();
-
- const arrTime = page.locator('[data-testid="panel-arrival-time"]');
- await expect(arrTime).toBeVisible();
- });
-
- test('should display aircraft type in panel', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const aircraftType = page.locator('[data-testid="panel-aircraft-type"]');
- await expect(aircraftType).toBeVisible();
- });
-
- test('should display flight status in panel', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const status = page.locator('[data-testid="panel-status"]');
- await expect(status).toBeVisible();
- });
- });
-
- test.describe('Map Controls', () => {
- test('should have zoom in button', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const zoomIn = page.locator('[data-testid="zoom-in"]');
- await expect(zoomIn).toBeVisible();
- });
-
- test('should have zoom out button', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const zoomOut = page.locator('[data-testid="zoom-out"]');
- await expect(zoomOut).toBeVisible();
- });
-
- test('should have full screen button', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const fullScreen = page.locator('[data-testid="full-screen"]');
- await expect(fullScreen).toBeVisible();
- });
-
- test('should have layer toggle', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const layerToggle = page.locator('[data-testid="layer-toggle"]');
- await expect(layerToggle).toBeVisible();
- });
- });
-
- test.describe('Cluster Markers', () => {
- test('should display cluster markers for multiple flights', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const cluster = page.locator('[data-testid="cluster-marker"]');
- if ((await cluster.count()) > 0) {
- await expect(cluster).toBeVisible();
- }
- });
-
- test('should expand cluster on click', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const cluster = page.locator('[data-testid="cluster-marker"]').first();
- if ((await cluster.count()) > 0) {
- await cluster.click();
- await page.waitForTimeout(500);
-
- const markers = page.locator('[data-testid="flight-marker"]');
- await expect(markers).toHaveCount(20);
- }
- });
- });
-
- test.describe('Error Handling', () => {
- test('should handle network error', async ({ page }) => {
- await page.route('**/api/flights-map/**', (route) => {
- return route.abort('internetdisconnected');
- });
-
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const networkError = page.locator('[data-testid="network-error"]');
- await expect(networkError).toBeVisible();
- });
-
- test('should handle map loading error', async ({ page }) => {
- await page.route('**/api/maps/**', (route) => {
- return route.abort('internetdisconnected');
- });
-
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const mapError = page.locator('[data-testid="map-error"]');
- await expect(mapError).toBeVisible();
- });
- });
-
- test.describe('Accessibility', () => {
- test('should have proper ARIA labels', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const mapContainer = page.locator('[data-testid="map-container"]');
- await expect(mapContainer).toHaveAttribute('role', 'application');
- });
-
- test('should be keyboard navigable', async ({ page }) => {
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- await page.keyboard.press('Tab');
- await page.keyboard.press('Tab');
- await page.keyboard.press('Tab');
-
- const focusedElement = page.locator(':focus');
- await expect(focusedElement).toBeVisible();
- });
- });
-
- test.describe('Responsive Design', () => {
- test('should be responsive on mobile', async ({ page }) => {
- await page.setViewportSize({ width: 375, height: 667 });
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const mapContainer = page.locator('[data-testid="map-container"]');
- await expect(mapContainer).toBeVisible();
- });
-
- test('should be responsive on tablet', async ({ page }) => {
- await page.setViewportSize({ width: 768, height: 1024 });
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const mapContainer = page.locator('[data-testid="map-container"]');
- await expect(mapContainer).toBeVisible();
- });
-
- test('should be responsive on desktop', async ({ page }) => {
- await page.setViewportSize({ width: 1920, height: 1080 });
- await page.goto(`/ru-ru${buildFlightsMapPath()}`);
- await page.waitForLoadState('networkidle');
-
- const mapContainer = page.locator('[data-testid="map-container"]');
- await expect(mapContainer).toBeVisible();
- });
- });
-});
diff --git a/tests/e2e-angular/integration/templates/online-board-arrival.template.ts b/tests/e2e-angular/integration/templates/online-board-arrival.template.ts
deleted file mode 100644
index 9fe7a381..00000000
--- a/tests/e2e-angular/integration/templates/online-board-arrival.template.ts
+++ /dev/null
@@ -1,301 +0,0 @@
-import { test, expect } from '@playwright/test';
-import type { Page } from '@playwright/test';
-import {
- buildOnlineBoardPath,
- buildRouteParam,
- searchFlightByNumber,
- searchFlightByRoute,
- verifyFlightCard,
- generateFlight,
- generateFlights,
- getToday,
- getTomorrow,
- CITIES,
-} from '../support/test-utilities';
-
-const today = getToday();
-const tomorrow = getTomorrow();
-const dateParam = buildRouteParam('MOW', today);
-const tomorrowParam = buildRouteParam('MOW', tomorrow);
-
-// ============================================================================
-// Online Board - Arrival Tests
-// ============================================================================
-
-test.describe('Online Board - Arrival', () => {
- test.describe('Page Navigation', () => {
- test('should navigate to arrival board for Moscow', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/arrival\/MOW-\d{8}/);
- await expect(page).toHaveTitle(/Прибытие/);
- });
-
- test('should navigate to arrival board for Saint Petersburg', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'LED', today)}`);
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/arrival\/LED-\d{8}/);
- });
-
- test('should navigate to arrival board for Sochi', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'AER', today)}`);
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/arrival\/AER-\d{8}/);
- });
- });
-
- test.describe('Flight Display', () => {
- test('should display arrival flights for Moscow', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCards = page.locator('[data-testid="flight-card"]');
- await expect(flightCards).toHaveCount(20);
- });
-
- test('should display flight details correctly', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- await expect(flightCard.getByText('Прибытие')).toBeVisible();
- });
- });
-
- test.describe('Flight Search', () => {
- test('should search flight by flight number', async ({ page }) => {
- const flight = generateFlight({ direction: 'arrival', cityCode: 'MOW' });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByNumber(page, flight.flightNumber);
-
- const searchResults = page.locator('[data-testid="flight-card"]');
- await expect(searchResults).toHaveCount(1);
-
- await verifyFlightCard(page, flight);
- });
-
- test('should show no results when flight not found', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByNumber(page, 'SU 9999');
-
- const noResults = page.locator('[data-testid="no-results"]');
- await expect(noResults).toBeVisible();
- await expect(noResults).toContainText('Нет результатов');
- });
- });
-
- test.describe('Date Navigation', () => {
- test('should navigate to tomorrow', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const dateTab = page.locator(`[data-testid="date-tab-${tomorrow.replace(/-/g, '')}"]`);
- if ((await dateTab.count()) > 0) {
- await dateTab.click();
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/arrival\/MOW-\d{8}/);
- }
- });
-
- test('should display correct date in title', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const dateText = page.locator('[data-testid="board-date"]');
- await expect(dateText).toBeVisible();
- });
- });
-
- test.describe('Filtering', () => {
- test('should filter by status', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const statusFilter = page.locator('[data-testid="status-filter"]');
- await statusFilter.click();
-
- const scheduledOption = page.locator('[data-testid="filter-option-scheduled"]');
- await scheduledOption.click();
-
- const filteredFlights = page.locator('[data-testid="flight-card"]');
- await expect(filteredFlights).toHaveCount(20);
- });
-
- test('should filter by airline', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const airlineFilter = page.locator('[data-testid="airline-filter"]');
- await airlineFilter.click();
-
- const aeroflotOption = page.locator('[data-testid="filter-option-SU"]');
- await aeroflotOption.click();
-
- const filteredFlights = page.locator('[data-testid="flight-card"]');
- await expect(filteredFlights).toHaveCount(20);
- });
- });
-
- test.describe('Flight Card', () => {
- test('should display flight number', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const flightNumber = flightCard.locator('[data-testid="flight-number"]');
- await expect(flightNumber).toBeVisible();
- });
-
- test('should display airline name', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const airlineName = flightCard.locator('[data-testid="airline-name"]');
- await expect(airlineName).toBeVisible();
- });
-
- test('should display departure and arrival cities', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const arrivalCity = flightCard.locator('[data-testid="arrival-city"]');
- await expect(arrivalCity).toBeVisible();
- });
-
- test('should display scheduled time', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const scheduledTime = flightCard.locator('[data-testid="scheduled-time"]');
- await expect(scheduledTime).toBeVisible();
- });
-
- test('should display actual time when available', async ({ page }) => {
- const flight = generateFlight({
- direction: 'arrival',
- cityCode: 'MOW',
- status: 'arrived',
- });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const actualTime = flightCard.locator('[data-testid="actual-time"]');
- await expect(actualTime).toBeVisible();
- });
-
- test('should display delay information', async ({ page }) => {
- const flight = generateFlight({
- direction: 'arrival',
- cityCode: 'MOW',
- status: 'delayed',
- });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const delayInfo = flightCard.locator('[data-testid="delay-info"]');
- await expect(delayInfo).toBeVisible();
- });
-
- test('should display terminal information', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const terminal = flightCard.locator('[data-testid="terminal"]');
- await expect(terminal).toBeVisible();
- });
-
- test('should display baggage belt information', async ({ page }) => {
- const flight = generateFlight({
- direction: 'arrival',
- cityCode: 'MOW',
- status: 'arrived',
- });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const baggageBelt = flightCard.locator('[data-testid="baggage-belt"]');
- await expect(baggageBelt).toBeVisible();
- });
- });
-
- test.describe('Error Handling', () => {
- test('should handle invalid city code', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/arrival/XXX-${today.replace(/-/g, '')}`);
- await page.waitForLoadState('networkidle');
-
- const errorState = page.locator('[data-testid="error-state"]');
- await expect(errorState).toBeVisible();
- });
-
- test('should handle network error', async ({ page }) => {
- await page.route('**/api/flights/**', (route) => {
- return route.abort('internetdisconnected');
- });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const networkError = page.locator('[data-testid="network-error"]');
- await expect(networkError).toBeVisible();
- });
- });
-
- test.describe('Accessibility', () => {
- test('should have proper ARIA labels', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toHaveAttribute('role', 'article');
- });
-
- test('should be keyboard navigable', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await page.keyboard.press('Tab');
- await page.keyboard.press('Tab');
- await page.keyboard.press('Tab');
-
- const focusedElement = page.locator(':focus');
- await expect(focusedElement).toBeVisible();
- });
- });
-});
diff --git a/tests/e2e-angular/integration/templates/online-board-departure.template.ts b/tests/e2e-angular/integration/templates/online-board-departure.template.ts
deleted file mode 100644
index 81a81747..00000000
--- a/tests/e2e-angular/integration/templates/online-board-departure.template.ts
+++ /dev/null
@@ -1,301 +0,0 @@
-import { test, expect } from '@playwright/test';
-import type { Page } from '@playwright/test';
-import {
- buildOnlineBoardPath,
- buildRouteParam,
- searchFlightByNumber,
- searchFlightByRoute,
- verifyFlightCard,
- generateFlight,
- generateFlights,
- getToday,
- getTomorrow,
- CITIES,
-} from '../support/test-utilities';
-
-const today = getToday();
-const tomorrow = getTomorrow();
-const dateParam = buildRouteParam('MOW', today);
-const tomorrowParam = buildRouteParam('MOW', tomorrow);
-
-// ============================================================================
-// Online Board - Departure Tests
-// ============================================================================
-
-test.describe('Online Board - Departure', () => {
- test.describe('Page Navigation', () => {
- test('should navigate to departure board for Moscow', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/departure\/MOW-\d{8}/);
- await expect(page).toHaveTitle(/Отправление/);
- });
-
- test('should navigate to departure board for Saint Petersburg', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'LED', today)}`);
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/departure\/LED-\d{8}/);
- });
-
- test('should navigate to departure board for Sochi', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'AER', today)}`);
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/departure\/AER-\d{8}/);
- });
- });
-
- test.describe('Flight Display', () => {
- test('should display departure flights for Moscow', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCards = page.locator('[data-testid="flight-card"]');
- await expect(flightCards).toHaveCount(20);
- });
-
- test('should display flight details correctly', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- await expect(flightCard.getByText('Отправление')).toBeVisible();
- });
- });
-
- test.describe('Flight Search', () => {
- test('should search flight by flight number', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByNumber(page, flight.flightNumber);
-
- const searchResults = page.locator('[data-testid="flight-card"]');
- await expect(searchResults).toHaveCount(1);
-
- await verifyFlightCard(page, flight);
- });
-
- test('should show no results when flight not found', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByNumber(page, 'SU 9999');
-
- const noResults = page.locator('[data-testid="no-results"]');
- await expect(noResults).toBeVisible();
- await expect(noResults).toContainText('Нет результатов');
- });
- });
-
- test.describe('Date Navigation', () => {
- test('should navigate to tomorrow', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const dateTab = page.locator(`[data-testid="date-tab-${tomorrow.replace(/-/g, '')}"]`);
- if ((await dateTab.count()) > 0) {
- await dateTab.click();
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/departure\/MOW-\d{8}/);
- }
- });
-
- test('should display correct date in title', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const dateText = page.locator('[data-testid="board-date"]');
- await expect(dateText).toBeVisible();
- });
- });
-
- test.describe('Filtering', () => {
- test('should filter by status', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const statusFilter = page.locator('[data-testid="status-filter"]');
- await statusFilter.click();
-
- const scheduledOption = page.locator('[data-testid="filter-option-scheduled"]');
- await scheduledOption.click();
-
- const filteredFlights = page.locator('[data-testid="flight-card"]');
- await expect(filteredFlights).toHaveCount(20);
- });
-
- test('should filter by airline', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const airlineFilter = page.locator('[data-testid="airline-filter"]');
- await airlineFilter.click();
-
- const aeroflotOption = page.locator('[data-testid="filter-option-SU"]');
- await aeroflotOption.click();
-
- const filteredFlights = page.locator('[data-testid="flight-card"]');
- await expect(filteredFlights).toHaveCount(20);
- });
- });
-
- test.describe('Flight Card', () => {
- test('should display flight number', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const flightNumber = flightCard.locator('[data-testid="flight-number"]');
- await expect(flightNumber).toBeVisible();
- });
-
- test('should display airline name', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const airlineName = flightCard.locator('[data-testid="airline-name"]');
- await expect(airlineName).toBeVisible();
- });
-
- test('should display departure and arrival cities', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const departureCity = flightCard.locator('[data-testid="departure-city"]');
- await expect(departureCity).toBeVisible();
- });
-
- test('should display scheduled time', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const scheduledTime = flightCard.locator('[data-testid="scheduled-time"]');
- await expect(scheduledTime).toBeVisible();
- });
-
- test('should display actual time when available', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'departed',
- });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const actualTime = flightCard.locator('[data-testid="actual-time"]');
- await expect(actualTime).toBeVisible();
- });
-
- test('should display delay information', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'delayed',
- });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const delayInfo = flightCard.locator('[data-testid="delay-info"]');
- await expect(delayInfo).toBeVisible();
- });
-
- test('should display terminal information', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const terminal = flightCard.locator('[data-testid="terminal"]');
- await expect(terminal).toBeVisible();
- });
-
- test('should display boarding gate information', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'boarding',
- });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const boardingGate = flightCard.locator('[data-testid="boarding-gate"]');
- await expect(boardingGate).toBeVisible();
- });
- });
-
- test.describe('Error Handling', () => {
- test('should handle invalid city code', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/departure/XXX-${today.replace(/-/g, '')}`);
- await page.waitForLoadState('networkidle');
-
- const errorState = page.locator('[data-testid="error-state"]');
- await expect(errorState).toBeVisible();
- });
-
- test('should handle network error', async ({ page }) => {
- await page.route('**/api/flights/**', (route) => {
- return route.abort('internetdisconnected');
- });
-
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const networkError = page.locator('[data-testid="network-error"]');
- await expect(networkError).toBeVisible();
- });
- });
-
- test.describe('Accessibility', () => {
- test('should have proper ARIA labels', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toHaveAttribute('role', 'article');
- });
-
- test('should be keyboard navigable', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await page.keyboard.press('Tab');
- await page.keyboard.press('Tab');
- await page.keyboard.press('Tab');
-
- const focusedElement = page.locator(':focus');
- await expect(focusedElement).toBeVisible();
- });
- });
-});
diff --git a/tests/e2e-angular/integration/templates/online-board-flight.template.ts b/tests/e2e-angular/integration/templates/online-board-flight.template.ts
deleted file mode 100644
index ad9c7ba1..00000000
--- a/tests/e2e-angular/integration/templates/online-board-flight.template.ts
+++ /dev/null
@@ -1,454 +0,0 @@
-import { test, expect } from '@playwright/test';
-import type { Page } from '@playwright/test';
-import {
- buildOnlineBoardPath,
- buildRouteParam,
- searchFlightByNumber,
- verifyFlightCard,
- generateFlight,
- generateFlights,
- getToday,
- getTomorrow,
- CITIES,
-} from '../support/test-utilities';
-
-const today = getToday();
-const tomorrow = getTomorrow();
-const dateParam = buildRouteParam('MOW', today);
-const tomorrowParam = buildRouteParam('MOW', tomorrow);
-
-// ============================================================================
-// Online Board - Flight Tests
-// ============================================================================
-
-test.describe('Online Board - Flight', () => {
- test.describe('Page Navigation', () => {
- test('should navigate to flight details page', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(new RegExp(`/${slug}`));
- await expect(page).toHaveTitle(new RegExp(flight.flightNumber));
- });
-
- test('should navigate to flight details for arrival flight', async ({ page }) => {
- const flight = generateFlight({ direction: 'arrival', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(new RegExp(`/${slug}`));
- });
-
- test('should navigate to flight details for different date', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW', date: tomorrow });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${tomorrow.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(new RegExp(`/${slug}`));
- });
- });
-
- test.describe('Flight Information', () => {
- test('should display flight number', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const flightNumber = page.locator('[data-testid="flight-number"]');
- await expect(flightNumber).toBeVisible();
- await expect(flightNumber).toContainText(flight.flightNumber);
- });
-
- test('should display airline name', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const airlineName = page.locator('[data-testid="airline-name"]');
- await expect(airlineName).toBeVisible();
- await expect(airlineName).toContainText(flight.airlineName);
- });
-
- test('should display aircraft type', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const aircraftType = page.locator('[data-testid="aircraft-type"]');
- await expect(aircraftType).toBeVisible();
- await expect(aircraftType).toContainText(flight.aircraftType || '');
- });
-
- test('should display departure city', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const departureCity = page.locator('[data-testid="departure-city"]');
- await expect(departureCity).toBeVisible();
- await expect(departureCity).toContainText(flight.departure.cityName);
- });
-
- test('should display arrival city', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const arrivalCity = page.locator('[data-testid="arrival-city"]');
- await expect(arrivalCity).toBeVisible();
- await expect(arrivalCity).toContainText(flight.arrival.cityName);
- });
-
- test('should display scheduled departure time', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const depTime = page.locator('[data-testid="scheduled-departure-time"]');
- await expect(depTime).toBeVisible();
-
- const depTimeText = flight.departure.time.scheduled.slice(11, 16);
- await expect(depTime).toContainText(depTimeText);
- });
-
- test('should display scheduled arrival time', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const arrTime = page.locator('[data-testid="scheduled-arrival-time"]');
- await expect(arrTime).toBeVisible();
-
- const arrTimeText = flight.arrival.time.scheduled.slice(11, 16);
- await expect(arrTime).toContainText(arrTimeText);
- });
-
- test('should display flight duration', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const duration = page.locator('[data-testid="flight-duration"]');
- await expect(duration).toBeVisible();
- });
-
- test('should display flight status', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'scheduled',
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const status = page.locator('[data-testid="flight-status"]');
- await expect(status).toBeVisible();
- await expect(status).toContainText(flight.status);
- });
- });
-
- test.describe('Flight Details', () => {
- test('should display terminal information', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- departure: { terminal: 'B' },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const terminal = page.locator('[data-testid="terminal"]');
- await expect(terminal).toBeVisible();
- await expect(terminal).toContainText('B');
- });
-
- test('should display boarding gate information', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'boarding',
- boarding: { gate: '11', status: 'Идёт посадка' },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const boardingGate = page.locator('[data-testid="boarding-gate"]');
- await expect(boardingGate).toBeVisible();
- await expect(boardingGate).toContainText('11');
- });
-
- test('should display baggage belt information', async ({ page }) => {
- const flight = generateFlight({
- direction: 'arrival',
- cityCode: 'MOW',
- status: 'arrived',
- arrivalInfo: { baggageBelt: '5', transfer: 'Тран' },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const baggageBelt = page.locator('[data-testid="baggage-belt"]');
- await expect(baggageBelt).toBeVisible();
- await expect(baggageBelt).toContainText('5');
- });
-
- test('should display check-in information', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'checkin',
- checkin: { status: 'В процессе' },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const checkinInfo = page.locator('[data-testid="checkin-info"]');
- await expect(checkinInfo).toBeVisible();
- });
-
- test('should display deplaning information', async ({ page }) => {
- const flight = generateFlight({
- direction: 'arrival',
- cityCode: 'MOW',
- status: 'arrived',
- deplaning: { status: 'В процессе', transfer: 'Трап' },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const deplaningInfo = page.locator('[data-testid="deplaning-info"]');
- await expect(deplaningInfo).toBeVisible();
- });
- });
-
- test.describe('Aircraft Information', () => {
- test('should display aircraft type', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const aircraftType = page.locator('[data-testid="aircraft-type"]');
- await expect(aircraftType).toBeVisible();
- });
-
- test('should display aircraft name', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- aircraft: { type: 'Airbus A320', name: 'В. Высоцкий' },
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const aircraftName = page.locator('[data-testid="aircraft-name"]');
- await expect(aircraftName).toBeVisible();
- await expect(aircraftName).toContainText('В. Высоцкий');
- });
-
- test('should display seat configuration', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const seatInfo = page.locator('[data-testid="seat-info"]');
- await expect(seatInfo).toBeVisible();
- });
- });
-
- test.describe('Schedule Information', () => {
- test('should display scheduled departure', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const scheduledDep = page.locator('[data-testid="scheduled-departure"]');
- await expect(scheduledDep).toBeVisible();
- });
-
- test('should display scheduled arrival', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const scheduledArr = page.locator('[data-testid="scheduled-arrival"]');
- await expect(scheduledArr).toBeVisible();
- });
-
- test('should display actual departure when available', async ({ page }) => {
- const flight = generateFlight({
- direction: 'departure',
- cityCode: 'MOW',
- status: 'departed',
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const actualDep = page.locator('[data-testid="actual-departure"]');
- await expect(actualDep).toBeVisible();
- });
-
- test('should display actual arrival when available', async ({ page }) => {
- const flight = generateFlight({
- direction: 'arrival',
- cityCode: 'MOW',
- status: 'arrived',
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const actualArr = page.locator('[data-testid="actual-arrival"]');
- await expect(actualArr).toBeVisible();
- });
-
- test('should display expected arrival when delayed', async ({ page }) => {
- const flight = generateFlight({
- direction: 'arrival',
- cityCode: 'MOW',
- status: 'delayed',
- });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const expectedArr = page.locator('[data-testid="expected-arrival"]');
- await expect(expectedArr).toBeVisible();
- });
- });
-
- test.describe('Error Handling', () => {
- test('should handle invalid flight number', async ({ page }) => {
- await page.goto(`/ru-ru/SU9999-${today.replace(/-/g, '')}`);
- await page.waitForLoadState('networkidle');
-
- const errorState = page.locator('[data-testid="error-state"]');
- await expect(errorState).toBeVisible();
- });
-
- test('should handle flight not found', async ({ page }) => {
- await page.goto(`/ru-ru/SU9999-20260406`);
- await page.waitForLoadState('networkidle');
-
- const notFound = page.locator('[data-testid="not-found"]');
- await expect(notFound).toBeVisible();
- });
-
- test('should handle network error', async ({ page }) => {
- await page.route('**/api/flights/**', (route) => {
- return route.abort('internetdisconnected');
- });
-
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const networkError = page.locator('[data-testid="network-error"]');
- await expect(networkError).toBeVisible();
- });
- });
-
- test.describe('Navigation', () => {
- test('should navigate back to board', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const backLink = page.locator('[data-testid="back-link"]');
- await backLink.click();
-
- await expect(page).toHaveURL(/onlineboard\/departure\/MOW-\d{8}/);
- });
-
- test('should navigate to related flights', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const relatedFlights = page.locator('[data-testid="related-flights"]');
- await expect(relatedFlights).toBeVisible();
- });
- });
-
- test.describe('Accessibility', () => {
- test('should have proper ARIA labels', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- const pageContent = page.locator('[data-testid="page-content"]');
- await expect(pageContent).toHaveAttribute('role', 'main');
- });
-
- test('should be keyboard navigable', async ({ page }) => {
- const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
- const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
-
- await page.goto(`/ru-ru/${slug}`);
- await page.waitForLoadState('networkidle');
-
- await page.keyboard.press('Tab');
- await page.keyboard.press('Tab');
- await page.keyboard.press('Tab');
-
- const focusedElement = page.locator(':focus');
- await expect(focusedElement).toBeVisible();
- });
- });
-});
diff --git a/tests/e2e-angular/integration/templates/online-board-route.template.ts b/tests/e2e-angular/integration/templates/online-board-route.template.ts
deleted file mode 100644
index 8d9b7cff..00000000
--- a/tests/e2e-angular/integration/templates/online-board-route.template.ts
+++ /dev/null
@@ -1,310 +0,0 @@
-import { test, expect } from '@playwright/test';
-import type { Page } from '@playwright/test';
-import {
- buildOnlineBoardPath,
- buildRouteParam,
- searchFlightByRoute,
- verifyFlightCard,
- generateFlight,
- generateFlights,
- getToday,
- getTomorrow,
- CITIES,
-} from '../support/test-utilities';
-
-const today = getToday();
-const tomorrow = getTomorrow();
-const dateParam = buildRouteParam('MOW', today);
-const tomorrowParam = buildRouteParam('MOW', tomorrow);
-
-// ============================================================================
-// Online Board - Route Tests
-// ============================================================================
-
-test.describe('Online Board - Route', () => {
- test.describe('Page Navigation', () => {
- test('should navigate to route board for Moscow to Sochi', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/route/MOW-AER-${today.replace(/-/g, '')}`);
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/route\/MOW-AER-\d{8}/);
- await expect(page).toHaveTitle(/Москва - Сочи/);
- });
-
- test('should navigate to route board for Saint Petersburg to Moscow', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/route/LED-MOW-${today.replace(/-/g, '')}`);
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/route\/LED-MOW-\d{8}/);
- });
-
- test('should navigate to route board for Novosibirsk to Moscow', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/route/OVB-MOW-${today.replace(/-/g, '')}`);
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/route\/OVB-MOW-\d{8}/);
- });
- });
-
- test.describe('Route Search', () => {
- test('should search route by departure and arrival cities', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByRoute(page, 'Moscow', 'Sochi');
-
- const searchResults = page.locator('[data-testid="flight-card"]');
- await expect(searchResults).toHaveCount(20);
- });
-
- test('should show no results when route not found', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByRoute(page, 'Moscow', 'Unknown City');
-
- const noResults = page.locator('[data-testid="no-results"]');
- await expect(noResults).toBeVisible();
- await expect(noResults).toContainText('Нет результатов');
- });
-
- test('should validate departure city', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByRoute(page, '', 'Sochi');
-
- const error = page.locator('[data-testid="validation-error"]');
- await expect(error).toBeVisible();
- });
-
- test('should validate arrival city', async ({ page }) => {
- await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await searchFlightByRoute(page, 'Moscow', '');
-
- const error = page.locator('[data-testid="validation-error"]');
- await expect(error).toBeVisible();
- });
- });
-
- test.describe('Flight Display', () => {
- test('should display flights for selected route', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/route/MOW-AER-${today.replace(/-/g, '')}`);
- await page.waitForLoadState('networkidle');
-
- const flightCards = page.locator('[data-testid="flight-card"]');
- await expect(flightCards).toHaveCount(20);
- });
-
- test('should display route information', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/route/MOW-AER-${today.replace(/-/g, '')}`);
- await page.waitForLoadState('networkidle');
-
- const routeInfo = page.locator('[data-testid="route-info"]');
- await expect(routeInfo).toBeVisible();
-
- await expect(routeInfo).toContainText('Москва');
- await expect(routeInfo).toContainText('Сочи');
- });
-
- test('should display flight count', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/route/MOW-AER-${today.replace(/-/g, '')}`);
- await page.waitForLoadState('networkidle');
-
- const flightCount = page.locator('[data-testid="flight-count"]');
- await expect(flightCount).toBeVisible();
- });
- });
-
- test.describe('Date Navigation', () => {
- test('should navigate to tomorrow', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/route/MOW-AER-${today.replace(/-/g, '')}`);
- await page.waitForLoadState('networkidle');
-
- const dateTab = page.locator(`[data-testid="date-tab-${tomorrow.replace(/-/g, '')}"]`);
- if ((await dateTab.count()) > 0) {
- await dateTab.click();
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/route\/MOW-AER-\d{8}/);
- }
- });
-
- test('should display correct date in title', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/route/MOW-AER-${today.replace(/-/g, '')}`);
- await page.waitForLoadState('networkidle');
-
- const dateText = page.locator('[data-testid="board-date"]');
- await expect(dateText).toBeVisible();
- });
- });
-
- test.describe('Filtering', () => {
- test('should filter by status', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/route/MOW-AER-${today.replace(/-/g, '')}`);
- await page.waitForLoadState('networkidle');
-
- const statusFilter = page.locator('[data-testid="status-filter"]');
- await statusFilter.click();
-
- const scheduledOption = page.locator('[data-testid="filter-option-scheduled"]');
- await scheduledOption.click();
-
- const filteredFlights = page.locator('[data-testid="flight-card"]');
- await expect(filteredFlights).toHaveCount(20);
- });
-
- test('should filter by airline', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/route/MOW-AER-${today.replace(/-/g, '')}`);
- await page.waitForLoadState('networkidle');
-
- const airlineFilter = page.locator('[data-testid="airline-filter"]');
- await airlineFilter.click();
-
- const aeroflotOption = page.locator('[data-testid="filter-option-SU"]');
- await aeroflotOption.click();
-
- const filteredFlights = page.locator('[data-testid="flight-card"]');
- await expect(filteredFlights).toHaveCount(20);
- });
-
- test('should filter by time range', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/route/MOW-AER-${today.replace(/-/g, '')}`);
- await page.waitForLoadState('networkidle');
-
- const timeFilter = page.locator('[data-testid="time-filter"]');
- await timeFilter.click();
-
- const timeOption = page.locator('[data-testid="filter-option-morning"]');
- await timeOption.click();
-
- const filteredFlights = page.locator('[data-testid="flight-card"]');
- await expect(filteredFlights).toHaveCount(20);
- });
- });
-
- test.describe('Flight Card', () => {
- test('should display flight number', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/route/MOW-AER-${today.replace(/-/g, '')}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const flightNumber = flightCard.locator('[data-testid="flight-number"]');
- await expect(flightNumber).toBeVisible();
- });
-
- test('should display airline name', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/route/MOW-AER-${today.replace(/-/g, '')}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const airlineName = flightCard.locator('[data-testid="airline-name"]');
- await expect(airlineName).toBeVisible();
- });
-
- test('should display departure city', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/route/MOW-AER-${today.replace(/-/g, '')}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const departureCity = flightCard.locator('[data-testid="departure-city"]');
- await expect(departureCity).toBeVisible();
- });
-
- test('should display arrival city', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/route/MOW-AER-${today.replace(/-/g, '')}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const arrivalCity = flightCard.locator('[data-testid="arrival-city"]');
- await expect(arrivalCity).toBeVisible();
- });
-
- test('should display scheduled departure time', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/route/MOW-AER-${today.replace(/-/g, '')}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const depTime = flightCard.locator('[data-testid="scheduled-departure-time"]');
- await expect(depTime).toBeVisible();
- });
-
- test('should display scheduled arrival time', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/route/MOW-AER-${today.replace(/-/g, '')}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const arrTime = flightCard.locator('[data-testid="scheduled-arrival-time"]');
- await expect(arrTime).toBeVisible();
- });
-
- test('should display flight duration', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/route/MOW-AER-${today.replace(/-/g, '')}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toBeVisible();
-
- const duration = flightCard.locator('[data-testid="flight-duration"]');
- await expect(duration).toBeVisible();
- });
- });
-
- test.describe('Error Handling', () => {
- test('should handle invalid route parameters', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/route/XXX-YYY-${today.replace(/-/g, '')}`);
- await page.waitForLoadState('networkidle');
-
- const errorState = page.locator('[data-testid="error-state"]');
- await expect(errorState).toBeVisible();
- });
-
- test('should handle network error', async ({ page }) => {
- await page.route('**/api/flights/**', (route) => {
- return route.abort('internetdisconnected');
- });
-
- await page.goto(`/ru-ru/onlineboard/route/MOW-AER-${today.replace(/-/g, '')}`);
- await page.waitForLoadState('networkidle');
-
- const networkError = page.locator('[data-testid="network-error"]');
- await expect(networkError).toBeVisible();
- });
- });
-
- test.describe('Accessibility', () => {
- test('should have proper ARIA labels', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/route/MOW-AER-${today.replace(/-/g, '')}`);
- await page.waitForLoadState('networkidle');
-
- const flightCard = page.locator('[data-testid="flight-card"]').first();
- await expect(flightCard).toHaveAttribute('role', 'article');
- });
-
- test('should be keyboard navigable', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/route/MOW-AER-${today.replace(/-/g, '')}`);
- await page.waitForLoadState('networkidle');
-
- await page.keyboard.press('Tab');
- await page.keyboard.press('Tab');
- await page.keyboard.press('Tab');
-
- const focusedElement = page.locator(':focus');
- await expect(focusedElement).toBeVisible();
- });
- });
-});
diff --git a/tests/e2e-angular/integration/templates/popular-requests.template.ts b/tests/e2e-angular/integration/templates/popular-requests.template.ts
deleted file mode 100644
index dfbdbc1b..00000000
--- a/tests/e2e-angular/integration/templates/popular-requests.template.ts
+++ /dev/null
@@ -1,301 +0,0 @@
-import { test, expect } from '@playwright/test';
-import type { Page } from '@playwright/test';
-import {
- buildRouteParam,
- generateDestination,
- generateDestinations,
- getToday,
- getTomorrow,
- CITIES,
-} from '@e2e/support/test-utilities';
-
-const today = getToday();
-const tomorrow = getTomorrow();
-
-// ============================================================================
-// Popular Requests Tests
-// ============================================================================
-
-test.describe('Popular Requests', () => {
- test.describe('Page Navigation', () => {
- test('should display popular requests section', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/departure/MOW-${buildRouteParam('MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const popularRequests = page.locator('[data-testid="popular-requests"]');
- await expect(popularRequests).toBeVisible();
- });
-
- test('should display popular requests title', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/departure/MOW-${buildRouteParam('MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const title = page.locator('[data-testid="popular-requests-title"]');
- await expect(title).toBeVisible();
- await expect(title).toContainText('Популярные направления');
- });
- });
-
- test.describe('Request Display', () => {
- test('should display departure city', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/departure/MOW-${buildRouteParam('MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const request = page.locator('[data-testid="popular-request"]').first();
- await expect(request).toBeVisible();
-
- const departureCity = request.locator('[data-testid="request-departure-city"]');
- await expect(departureCity).toBeVisible();
- });
-
- test('should display arrival city', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/departure/MOW-${buildRouteParam('MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const request = page.locator('[data-testid="popular-request"]').first();
- await expect(request).toBeVisible();
-
- const arrivalCity = request.locator('[data-testid="request-arrival-city"]');
- await expect(arrivalCity).toBeVisible();
- });
-
- test('should display flight count', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/departure/MOW-${buildRouteParam('MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const request = page.locator('[data-testid="popular-request"]').first();
- await expect(request).toBeVisible();
-
- const flightCount = request.locator('[data-testid="request-flight-count"]');
- await expect(flightCount).toBeVisible();
- });
-
- test('should display date range', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/departure/MOW-${buildRouteParam('MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const request = page.locator('[data-testid="popular-request"]').first();
- await expect(request).toBeVisible();
-
- const dateRange = request.locator('[data-testid="request-date-range"]');
- await expect(dateRange).toBeVisible();
- });
- });
-
- test.describe('Request Interaction', () => {
- test('should navigate to flight board on click', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/departure/MOW-${buildRouteParam('MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const request = page.locator('[data-testid="popular-request"]').first();
- await request.click();
-
- await expect(page).toHaveURL(/onlineboard\/departure\/MOW-\d{8}/);
- });
-
- test('should navigate to flight board with correct city', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/departure/MOW-${buildRouteParam('MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const request = page.locator('[data-testid="popular-request"]').first();
- await request.click();
-
- await expect(page).toHaveURL(/onlineboard\/departure\/[A-Z]{3}-\d{8}/);
- });
-
- test('should open flight board for arrival direction', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/departure/MOW-${buildRouteParam('MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const request = page.locator('[data-testid="popular-request"]').first();
- await request.click();
-
- await expect(page).toHaveURL(/onlineboard\/arrival\/[A-Z]{3}-\d{8}/);
- });
- });
-
- test.describe('Request Sorting', () => {
- test('should sort by flight count', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/departure/MOW-${buildRouteParam('MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const sortButton = page.locator('[data-testid="sort-button"]');
- await sortButton.click();
-
- const sortOption = page.locator('[data-testid="sort-option-flight-count"]');
- await sortOption.click();
-
- const requests = page.locator('[data-testid="popular-request"]');
- const count1 = await requests
- .nth(0)
- .locator('[data-testid="request-flight-count"]')
- .textContent();
- const count2 = await requests
- .nth(1)
- .locator('[data-testid="request-flight-count"]')
- .textContent();
-
- if (count1 && count2) {
- expect(parseInt(count1.replace(/\D/g, ''))).toBeGreaterThanOrEqual(
- parseInt(count2.replace(/\D/g, '')),
- );
- }
- });
-
- test('should sort by date', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/departure/MOW-${buildRouteParam('MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const sortButton = page.locator('[data-testid="sort-button"]');
- await sortButton.click();
-
- const sortOption = page.locator('[data-testid="sort-option-date"]');
- await sortOption.click();
-
- const requests = page.locator('[data-testid="popular-request"]');
- await expect(requests).toHaveCount(20);
- });
- });
-
- test.describe('Request Filtering', () => {
- test('should filter by departure city', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/departure/MOW-${buildRouteParam('MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const filterInput = page.locator('[data-testid="filter-input"]');
- await filterInput.fill('Moscow');
-
- const requests = page.locator('[data-testid="popular-request"]');
- await expect(requests).toHaveCount(20);
- });
-
- test('should clear filter', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/departure/MOW-${buildRouteParam('MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const filterInput = page.locator('[data-testid="filter-input"]');
- await filterInput.fill('Moscow');
-
- const clearButton = page.locator('[data-testid="clear-filter"]');
- await clearButton.click();
-
- const requests = page.locator('[data-testid="popular-request"]');
- await expect(requests).toHaveCount(20);
- });
- });
-
- test.describe('Request Pagination', () => {
- test('should display pagination controls', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/departure/MOW-${buildRouteParam('MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const pagination = page.locator('[data-testid="pagination"]');
- await expect(pagination).toBeVisible();
- });
-
- test('should navigate to next page', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/departure/MOW-${buildRouteParam('MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const nextButton = page.locator('[data-testid="pagination-next"]');
- await nextButton.click();
-
- const requests = page.locator('[data-testid="popular-request"]');
- await expect(requests).toHaveCount(20);
- });
-
- test('should navigate to previous page', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/departure/MOW-${buildRouteParam('MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const nextButton = page.locator('[data-testid="pagination-next"]');
- await nextButton.click();
-
- const prevButton = page.locator('[data-testid="pagination-prev"]');
- await prevButton.click();
-
- const requests = page.locator('[data-testid="popular-request"]');
- await expect(requests).toHaveCount(20);
- });
- });
-
- test.describe('Error Handling', () => {
- test('should handle network error', async ({ page }) => {
- await page.route('**/api/popular-requests/**', (route) => {
- return route.abort('internetdisconnected');
- });
-
- await page.goto(`/ru-ru/onlineboard/departure/MOW-${buildRouteParam('MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const networkError = page.locator('[data-testid="network-error"]');
- await expect(networkError).toBeVisible();
- });
-
- test('should handle empty results', async ({ page }) => {
- await page.route('**/api/popular-requests/**', (route) => {
- return route.fulfill({
- status: 200,
- json: { requests: [], total: 0 },
- });
- });
-
- await page.goto(`/ru-ru/onlineboard/departure/MOW-${buildRouteParam('MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const noResults = page.locator('[data-testid="no-results"]');
- await expect(noResults).toBeVisible();
- });
- });
-
- test.describe('Accessibility', () => {
- test('should have proper ARIA labels', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/departure/MOW-${buildRouteParam('MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const popularRequests = page.locator('[data-testid="popular-requests"]');
- await expect(popularRequests).toHaveAttribute('role', 'region');
- });
-
- test('should be keyboard navigable', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/departure/MOW-${buildRouteParam('MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- await page.keyboard.press('Tab');
- await page.keyboard.press('Tab');
- await page.keyboard.press('Tab');
-
- const focusedElement = page.locator(':focus');
- await expect(focusedElement).toBeVisible();
- });
- });
-
- test.describe('Responsive Design', () => {
- test('should be responsive on mobile', async ({ page }) => {
- await page.setViewportSize({ width: 375, height: 667 });
- await page.goto(`/ru-ru/onlineboard/departure/MOW-${buildRouteParam('MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const popularRequests = page.locator('[data-testid="popular-requests"]');
- await expect(popularRequests).toBeVisible();
- });
-
- test('should be responsive on tablet', async ({ page }) => {
- await page.setViewportSize({ width: 768, height: 1024 });
- await page.goto(`/ru-ru/onlineboard/departure/MOW-${buildRouteParam('MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const popularRequests = page.locator('[data-testid="popular-requests"]');
- await expect(popularRequests).toBeVisible();
- });
-
- test('should be responsive on desktop', async ({ page }) => {
- await page.setViewportSize({ width: 1920, height: 1080 });
- await page.goto(`/ru-ru/onlineboard/departure/MOW-${buildRouteParam('MOW', today)}`);
- await page.waitForLoadState('networkidle');
-
- const popularRequests = page.locator('[data-testid="popular-requests"]');
- await expect(popularRequests).toBeVisible();
- });
- });
-});
diff --git a/tests/e2e-angular/integration/templates/schedule-search.template.ts b/tests/e2e-angular/integration/templates/schedule-search.template.ts
deleted file mode 100644
index e2864843..00000000
--- a/tests/e2e-angular/integration/templates/schedule-search.template.ts
+++ /dev/null
@@ -1,427 +0,0 @@
-import { test, expect } from '@playwright/test';
-import type { Page } from '@playwright/test';
-import {
- buildSchedulePath,
- buildRouteParam,
- generateScheduleEntry,
- generateScheduleEntries,
- getToday,
- getTomorrow,
- CITIES,
-} from '../support/test-utilities';
-
-const today = getToday();
-const tomorrow = getTomorrow();
-const dateFrom = today;
-const dateTo = tomorrow;
-
-// ============================================================================
-// Schedule Search Tests
-// ============================================================================
-
-test.describe('Schedule Search', () => {
- test.describe('Page Navigation', () => {
- test('should navigate to schedule page', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/schedule/);
- await expect(page).toHaveTitle(/Расписание/);
- });
-
- test('should navigate to schedule with pre-filled search', async ({ page }) => {
- await page.goto(`/ru-ru/schedule?from=MOW&to=AER&dateFrom=${dateFrom}&dateTo=${dateTo}`);
- await page.waitForLoadState('networkidle');
-
- await expect(page).toHaveURL(/schedule/);
- });
- });
-
- test.describe('Search Form', () => {
- test('should display search form', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const form = page.locator('[data-testid="schedule-search-form"]');
- await expect(form).toBeVisible();
- });
-
- test('should display departure city input', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="departure-city-input"]');
- await expect(departureInput).toBeVisible();
- });
-
- test('should display arrival city input', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const arrivalInput = page.locator('[data-testid="arrival-city-input"]');
- await expect(arrivalInput).toBeVisible();
- });
-
- test('should display date range inputs', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const dateFromInput = page.locator('[data-testid="date-from-input"]');
- await expect(dateFromInput).toBeVisible();
-
- const dateToInput = page.locator('[data-testid="date-to-input"]');
- await expect(dateToInput).toBeVisible();
- });
-
- test('should display search button', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const searchButton = page.locator('[data-testid="search-button"]');
- await expect(searchButton).toBeVisible();
- });
- });
-
- test.describe('Search Functionality', () => {
- test('should search by departure and arrival cities', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const searchButton = page.locator('[data-testid="search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const results = page.locator('[data-testid="schedule-entry"]');
- await expect(results).toHaveCount(50);
- });
-
- test('should search with date range', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const dateFromInput = page.locator('[data-testid="date-from-input"]');
- await dateFromInput.fill(dateFrom);
-
- const dateToInput = page.locator('[data-testid="date-to-input"]');
- await dateToInput.fill(dateTo);
-
- const searchButton = page.locator('[data-testid="search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const results = page.locator('[data-testid="schedule-entry"]');
- await expect(results).toHaveCount(50);
- });
-
- test('should show validation error for missing departure city', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const arrivalInput = page.locator('[data-testid="arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const searchButton = page.locator('[data-testid="search-button"]');
- await searchButton.click();
-
- const error = page.locator('[data-testid="validation-error"]');
- await expect(error).toBeVisible();
- });
-
- test('should show validation error for missing arrival city', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const searchButton = page.locator('[data-testid="search-button"]');
- await searchButton.click();
-
- const error = page.locator('[data-testid="validation-error"]');
- await expect(error).toBeVisible();
- });
-
- test('should show no results when no schedules found', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="departure-city-input"]');
- await departureInput.fill('Unknown City');
-
- const arrivalInput = page.locator('[data-testid="arrival-city-input"]');
- await arrivalInput.fill('Unknown City');
-
- const searchButton = page.locator('[data-testid="search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const noResults = page.locator('[data-testid="no-results"]');
- await expect(noResults).toBeVisible();
- await expect(noResults).toContainText('Нет результатов');
- });
- });
-
- test.describe('Schedule Entry Display', () => {
- test('should display flight number', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const searchButton = page.locator('[data-testid="search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const entry = page.locator('[data-testid="schedule-entry"]').first();
- await expect(entry).toBeVisible();
-
- const flightNumber = entry.locator('[data-testid="flight-number"]');
- await expect(flightNumber).toBeVisible();
- });
-
- test('should display airline name', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const searchButton = page.locator('[data-testid="search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const entry = page.locator('[data-testid="schedule-entry"]').first();
- await expect(entry).toBeVisible();
-
- const airlineName = entry.locator('[data-testid="airline-name"]');
- await expect(airlineName).toBeVisible();
- });
-
- test('should display aircraft type', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const searchButton = page.locator('[data-testid="search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const entry = page.locator('[data-testid="schedule-entry"]').first();
- await expect(entry).toBeVisible();
-
- const aircraftType = entry.locator('[data-testid="aircraft-type"]');
- await expect(aircraftType).toBeVisible();
- });
-
- test('should display departure time', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const searchButton = page.locator('[data-testid="search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const entry = page.locator('[data-testid="schedule-entry"]').first();
- await expect(entry).toBeVisible();
-
- const departureTime = entry.locator('[data-testid="departure-time"]');
- await expect(departureTime).toBeVisible();
- });
-
- test('should display arrival time', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const searchButton = page.locator('[data-testid="search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const entry = page.locator('[data-testid="schedule-entry"]').first();
- await expect(entry).toBeVisible();
-
- const arrivalTime = entry.locator('[data-testid="arrival-time"]');
- await expect(arrivalTime).toBeVisible();
- });
-
- test('should display days of week', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const searchButton = page.locator('[data-testid="search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const entry = page.locator('[data-testid="schedule-entry"]').first();
- await expect(entry).toBeVisible();
-
- const daysOfWeek = entry.locator('[data-testid="days-of-week"]');
- await expect(daysOfWeek).toBeVisible();
- });
-
- test('should display effective date range', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const searchButton = page.locator('[data-testid="search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const entry = page.locator('[data-testid="schedule-entry"]').first();
- await expect(entry).toBeVisible();
-
- const dateRange = entry.locator('[data-testid="date-range"]');
- await expect(dateRange).toBeVisible();
- });
- });
-
- test.describe('Filtering', () => {
- test('should filter by direct flights only', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const directFilter = page.locator('[data-testid="direct-filter"]');
- await directFilter.click();
-
- const departureInput = page.locator('[data-testid="departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const searchButton = page.locator('[data-testid="search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const entries = page.locator('[data-testid="schedule-entry"]');
- await expect(entries).toHaveCount(50);
- });
-
- test('should filter by airline', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const airlineFilter = page.locator('[data-testid="airline-filter"]');
- await airlineFilter.click();
-
- const aeroflotOption = page.locator('[data-testid="filter-option-SU"]');
- await aeroflotOption.click();
-
- const departureInput = page.locator('[data-testid="departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const searchButton = page.locator('[data-testid="search-button"]');
- await searchButton.click();
- await page.waitForLoadState('networkidle');
-
- const entries = page.locator('[data-testid="schedule-entry"]');
- await expect(entries).toHaveCount(50);
- });
- });
-
- test.describe('Error Handling', () => {
- test('should handle invalid date format', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="departure-city-input"]');
- await departureInput.fill('Moscow');
-
- const arrivalInput = page.locator('[data-testid="arrival-city-input"]');
- await arrivalInput.fill('Sochi');
-
- const dateFromInput = page.locator('[data-testid="date-from-input"]');
- await dateFromInput.fill('invalid-date');
-
- const searchButton = page.locator('[data-testid="search-button"]');
- await searchButton.click();
-
- const error = page.locator('[data-testid="validation-error"]');
- await expect(error).toBeVisible();
- });
-
- test('should handle network error', async ({ page }) => {
- await page.route('**/api/schedule/**', (route) => {
- return route.abort('internetdisconnected');
- });
-
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const networkError = page.locator('[data-testid="network-error"]');
- await expect(networkError).toBeVisible();
- });
- });
-
- test.describe('Accessibility', () => {
- test('should have proper ARIA labels', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- const form = page.locator('[data-testid="schedule-search-form"]');
- await expect(form).toHaveAttribute('role', 'form');
- });
-
- test('should be keyboard navigable', async ({ page }) => {
- await page.goto(`/ru-ru${buildSchedulePath()}`);
- await page.waitForLoadState('networkidle');
-
- await page.keyboard.press('Tab');
- await page.keyboard.press('Tab');
- await page.keyboard.press('Tab');
-
- const focusedElement = page.locator(':focus');
- await expect(focusedElement).toBeVisible();
- });
- });
-});
diff --git a/tests/e2e-angular/navigation.spec.ts b/tests/e2e-angular/navigation.spec.ts
deleted file mode 100644
index 9ea2218b..00000000
--- a/tests/e2e-angular/navigation.spec.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-import { test, expect } from '@playwright/test';
-
-test.describe('Navigation & Language (US-1, US-2) - React ru-ru', () => {
- test.beforeEach(async ({ page }) => {
- await page.goto('http://localhost:3002/ru-ru/onlineboard');
- });
-
- test('US-1: Tab navigation - switch between all tabs', async ({ page }) => {
- // Verify Online Board tab is active
- const onlineTab = page.locator('[data-testid="nav-onlineboard-tab"]');
- await expect(onlineTab).toHaveClass(/active/);
-
- // Click Schedule tab
- const scheduleTab = page.locator('[data-testid="nav-schedule-tab"]');
- await scheduleTab.click();
- await expect(page).toHaveURL(/\/ru-ru\/schedule/);
- await expect(scheduleTab).toHaveClass(/active/);
-
- // Click Flights Map tab
- const mapTab = page.locator('[data-testid="nav-flights-map-tab"]');
- if (await mapTab.isVisible()) {
- await mapTab.click();
- await expect(page).toHaveURL(/\/ru-ru\/flights-map/);
- await expect(mapTab).toHaveClass(/active/);
- }
-
- // Navigate back to Online Board
- await onlineTab.click();
- await expect(page).toHaveURL(/\/ru-ru\/onlineboard/);
- await expect(onlineTab).toHaveClass(/active/);
- });
-
- test('US-2: Language switching - ru-ru to en-us to ru-ru', async ({ page }) => {
- // Verify current language is Russian
- await expect(page.locator('[data-testid="nav-onlineboard-tab"]')).toContainText('Онлайн-Табло');
-
- // Click locale switcher dropdown
- const switcher = page.locator('[data-testid="layout-locale-switcher"]');
- await switcher.click();
-
- // Select English (en-us)
- await page.locator('text=English').first().click();
- await expect(page).toHaveURL(/\/en-us\/onlineboard/);
-
- // Verify UI is now in English
- await expect(page.locator('[data-testid="nav-onlineboard-tab"]')).toContainText('Online Board');
-
- // Click locale switcher dropdown again
- await switcher.click();
-
- // Switch back to Russian
- await page.locator('text=Русский').first().click();
- await expect(page).toHaveURL(/\/ru-ru\/onlineboard/);
- await expect(page.locator('[data-testid="nav-onlineboard-tab"]')).toContainText('Онлайн-Табло');
- });
-
- test('US-2: Language switching - preserve page context', async ({ page }) => {
- // Navigate to Schedule
- await page.locator('[data-testid="nav-schedule-tab"]').click();
- await expect(page).toHaveURL(/\/ru-ru\/schedule/);
-
- // Switch to English via dropdown
- const switcher = page.locator('[data-testid="layout-locale-switcher"]');
- await switcher.click();
- await page.locator('text=English').first().click();
-
- // Should still be on Schedule page (not Online Board)
- await expect(page).toHaveURL(/\/en-us\/schedule/);
- await expect(page.locator('[data-testid="nav-schedule-tab"]')).toContainText('Schedule');
- });
-});
diff --git a/tests/e2e-angular/popular-requests.spec.ts b/tests/e2e-angular/popular-requests.spec.ts
deleted file mode 100644
index f24125b3..00000000
--- a/tests/e2e-angular/popular-requests.spec.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-import { test, expect } from '@playwright/test';
-
-test.describe('Popular Requests (US-7)', () => {
- test.beforeEach(async ({ page }) => {
- await page.goto('http://localhost:3000/ru-ru/onlineboard');
- await page.waitForLoadState('networkidle');
- });
-
- test('should display popular requests section', async ({ page }) => {
- const section = page.locator('[data-testid="popular-requests"]');
- const exists = await section.isVisible().catch(() => false);
-
- // Section may not always be visible depending on data availability
- expect(typeof exists).toBe('boolean');
- });
-
- test('should show popular route cards', async ({ page }) => {
- const routes = page.locator('[data-testid="popular-request-card"]');
- const count = await routes.count();
-
- // Component may have 0 or more cards
- expect(count >= 0).toBe(true);
- });
-
- test('should handle click on popular route', async ({ page }) => {
- const firstRoute = page.locator('[data-testid="popular-request-card"]').first();
- const exists = await firstRoute.isVisible().catch(() => false);
-
- if (exists) {
- await firstRoute.click();
- // Should trigger search or navigation
- await page.waitForLoadState('networkidle');
- }
-
- // Test completes successfully if no errors occur
- expect(true).toBe(true);
- });
-
- test('should display route information', async ({ page }) => {
- const routes = page.locator('[data-testid="popular-request-card"]');
- const count = await routes.count();
-
- expect(count >= 0).toBe(true);
- });
-
- test('should be responsive on mobile', async ({ page }) => {
- await page.setViewportSize({ width: 375, height: 667 });
- await page.goto('http://localhost:3000/ru-ru/onlineboard');
- await page.waitForLoadState('networkidle');
-
- const section = page.locator('[data-testid="popular-requests"]');
- const exists = await section.isVisible().catch(() => false);
-
- expect(typeof exists).toBe('boolean');
- });
-
- test('should be responsive on tablet', async ({ page }) => {
- await page.setViewportSize({ width: 768, height: 1024 });
- await page.goto('http://localhost:3000/ru-ru/onlineboard');
- await page.waitForLoadState('networkidle');
-
- const routes = page.locator('[data-testid="popular-request-card"]');
- const count = await routes.count();
-
- expect(count >= 0).toBe(true);
- });
-
- test('should render without errors', async ({ page }) => {
- // Check for JavaScript errors
- const errors: string[] = [];
- page.on('console', (msg) => {
- if (msg.type() === 'error') {
- errors.push(msg.text());
- }
- });
-
- await page.goto('http://localhost:3000/ru-ru/onlineboard');
- await page.waitForLoadState('networkidle');
-
- // Component should render without throwing errors
- expect(errors.length).toBe(0);
- });
-});
diff --git a/tests/e2e-angular/responsive.spec.ts b/tests/e2e-angular/responsive.spec.ts
deleted file mode 100644
index b8678c74..00000000
--- a/tests/e2e-angular/responsive.spec.ts
+++ /dev/null
@@ -1,146 +0,0 @@
-import { test, expect } from '@playwright/test';
-
-const VIEWPORTS = [
- { name: 'mobile', width: 375, height: 667 },
- { name: 'tablet', width: 768, height: 1024 },
- { name: 'desktop', width: 1920, height: 1080 },
-];
-
-const PAGES = ['/ru-ru/onlineboard', '/ru-ru/schedule', '/ru-ru/flights-map'];
-
-test.describe('Responsive Design (US-10)', () => {
- // Test all pages at all breakpoints
- PAGES.forEach((page) => {
- VIEWPORTS.forEach(({ name, width, height }) => {
- test(`${page} should be responsive on ${name} (${width}x${height})`, async ({
- page: browserPage,
- baseURL,
- }) => {
- await browserPage.setViewportSize({ width, height });
- await browserPage.goto(`${baseURL}${page}`);
-
- // Wait for page to load
- await browserPage.waitForLoadState('networkidle');
-
- // Check for layout shift or overflow
- const bodyWidth = await browserPage.evaluate(() => document.body.scrollWidth);
- const viewportWidth = width;
-
- // Body should not be wider than viewport (allowing 1px tolerance)
- expect(bodyWidth).toBeLessThanOrEqual(viewportWidth + 1);
- });
- });
- });
-
- test('should display navigation bar on mobile', async ({ page, baseURL }) => {
- await page.setViewportSize({ width: 375, height: 667 });
- await page.goto(`${baseURL}/ru-ru/onlineboard`);
-
- const tabNavigation = page.locator('[data-testid="nav"]');
- await expect(tabNavigation).toBeVisible();
- });
-
- test('should display hamburger menu on mobile if needed', async ({ page, baseURL }) => {
- await page.setViewportSize({ width: 375, height: 667 });
- await page.goto(`${baseURL}/ru-ru/onlineboard`);
-
- const menu = page.locator('[data-testid="mobile-menu"]');
- // Menu may or may not exist, but if it exists, should be visible
- if ((await menu.count()) > 0) {
- await expect(menu).toBeVisible();
- }
- });
-
- test('should stack layout vertically on mobile', async ({ page, baseURL }) => {
- await page.setViewportSize({ width: 375, height: 667 });
- await page.goto(`${baseURL}/ru-ru/onlineboard`);
-
- const layout = page.locator('[data-testid="dashboard-layout"]');
- const style = await layout.evaluate((el) => window.getComputedStyle(el).display);
- expect(['block', 'flex']).toContain(style);
- });
-
- test('should use two-column layout on tablet', async ({ page, baseURL }) => {
- await page.setViewportSize({ width: 768, height: 1024 });
- await page.goto(`${baseURL}/ru-ru/onlineboard`);
-
- const layout = page.locator('[data-testid="dashboard-layout"]');
- await expect(layout).toBeVisible();
- });
-
- test('should use full-width layout on desktop', async ({ page, baseURL }) => {
- await page.setViewportSize({ width: 1920, height: 1080 });
- await page.goto(`${baseURL}/ru-ru/onlineboard`);
-
- const layout = page.locator('[data-testid="dashboard-layout"]');
- await expect(layout).toBeVisible();
- });
-
- test('should handle text wrapping on mobile', async ({ page, baseURL }) => {
- await page.setViewportSize({ width: 375, height: 667 });
- await page.goto(`${baseURL}/ru-ru/onlineboard`);
-
- // Check that text elements don't overflow
- const textElements = await page.locator('h1, h2, p').all();
- for (const element of textElements) {
- const scrollWidth = await element.evaluate((el) => el.scrollWidth);
- const clientWidth = await element.evaluate((el) => el.clientWidth);
- expect(scrollWidth).toBeLessThanOrEqual(clientWidth + 1);
- }
- });
-
- test('should scale images properly on mobile', async ({ page, baseURL }) => {
- await page.setViewportSize({ width: 375, height: 667 });
- await page.goto(`${baseURL}/ru-ru/onlineboard`);
-
- const images = await page.locator('img').all();
- for (const img of images) {
- const scrollWidth = await img.evaluate((el) => el.scrollWidth);
- const clientWidth = await img.evaluate((el) => el.clientWidth);
- expect(scrollWidth).toBeLessThanOrEqual(clientWidth + 1);
- }
- });
-
- test('should maintain usability at all viewport sizes', async ({ page, baseURL }) => {
- for (const { width, height } of VIEWPORTS) {
- await page.setViewportSize({ width, height });
- await page.goto(`${baseURL}/ru-ru/onlineboard`);
-
- // Check buttons are clickable
- const buttons = page.locator('button');
- const count = await buttons.count();
-
- for (let i = 0; i < Math.min(count, 3); i++) {
- const button = buttons.nth(i);
- const box = await button.boundingBox();
-
- if (box) {
- // Button should be at least 44x44 for mobile usability
- expect(Math.min(box.width, box.height)).toBeGreaterThanOrEqual(32);
- }
- }
- }
- });
-
- test('should handle long content on mobile', async ({ page, baseURL }) => {
- await page.setViewportSize({ width: 375, height: 667 });
- await page.goto(`${baseURL}/ru-ru/schedule`);
-
- const content = page.locator('[data-testid="main-content"]');
- if ((await content.count()) > 0) {
- await expect(content).toBeVisible();
- }
- });
-
- test('should display search panel properly on all sizes', async ({ page, baseURL }) => {
- for (const { width, height } of VIEWPORTS) {
- await page.setViewportSize({ width, height });
- await page.goto(`${baseURL}/ru-ru/onlineboard`);
-
- const searchPanel = page.locator('[data-testid="filter-accordion"]');
- if ((await searchPanel.count()) > 0) {
- await expect(searchPanel).toBeVisible();
- }
- }
- });
-});
diff --git a/tests/e2e-angular/ru-ru/aria-labels.spec.ts b/tests/e2e-angular/ru-ru/aria-labels.spec.ts
deleted file mode 100644
index 737419a2..00000000
--- a/tests/e2e-angular/ru-ru/aria-labels.spec.ts
+++ /dev/null
@@ -1,84 +0,0 @@
-import { test, expect } from '@playwright/test';
-
-const BASE_URL = process.env.BASE_URL || 'http://localhost:3002';
-
-test.describe('US-96: ARIA Labels & Semantic HTML', () => {
- test.beforeEach(async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/schedule`, { waitUntil: 'networkidle' });
- await page.waitForLoadState('networkidle');
- });
-
- test('should have search section with proper aria-label', async ({ page }) => {
- // Check for search section with role="search"
- const searchSection = page.locator('[role="search"]').first();
- if (await searchSection.isVisible()) {
- const ariaLabel = await searchSection.getAttribute('aria-label');
- expect(ariaLabel).toBeTruthy();
- expect(ariaLabel?.length).toBeGreaterThan(0);
- }
- });
-
- test('should have form fields with aria-label or associated labels', async ({ page }) => {
- // Look for search inputs in the form
- const inputs = await page.locator('input[type="text"]').all();
- // At least some inputs should have aria attributes or be associated with labels
- let validatedCount = 0;
- for (const input of inputs) {
- const ariaLabel = await input.getAttribute('aria-label');
- const ariaLabelledby = await input.getAttribute('aria-labelledby');
-
- if (ariaLabel || ariaLabelledby) {
- validatedCount++;
- }
- }
- expect(validatedCount).toBeGreaterThan(0);
- });
-
- test('should have buttons with aria-label or visible text', async ({ page }) => {
- const buttons = await page.locator('button').all();
- let validatedCount = 0;
- for (const button of buttons) {
- const ariaLabel = await button.getAttribute('aria-label');
- const text = (await button.textContent())?.trim();
-
- if (ariaLabel || (text && text.length > 0)) {
- validatedCount++;
- }
- }
- expect(validatedCount).toBeGreaterThan(0);
- });
-
- test('should have semantic HTML structure', async ({ page }) => {
- // Check that page has proper semantic structure
- const headings = page.locator('h1, h2, h3, h4, h5, h6');
- const headingCount = await headings.count();
- // Should have at least one heading for proper semantic structure
- expect(headingCount).toBeGreaterThanOrEqual(0);
- });
-
- test('should have error messages with aria-live role="alert"', async ({ page }) => {
- // Check if alert role exists (might not if no validation error)
- const alerts = page.locator('[role="alert"]');
- const alertCount = await alerts.count();
- if (alertCount > 0) {
- for (const alert of await alerts.all()) {
- const ariaLive = await alert.getAttribute('aria-live');
- expect(ariaLive).toBeTruthy();
- }
- }
- });
-
- test('should have zero console errors', async ({ page }) => {
- const errors: string[] = [];
- page.on('console', (msg) => {
- if (msg.type() === 'error') {
- errors.push(msg.text());
- }
- });
-
- await page.goto(`${BASE_URL}/ru-ru/schedule`, { waitUntil: 'networkidle' });
- await page.waitForLoadState('networkidle');
-
- expect(errors).toHaveLength(0);
- });
-});
diff --git a/tests/e2e-angular/ru-ru/caching-refresh.spec.ts b/tests/e2e-angular/ru-ru/caching-refresh.spec.ts
deleted file mode 100644
index 48d38f47..00000000
--- a/tests/e2e-angular/ru-ru/caching-refresh.spec.ts
+++ /dev/null
@@ -1,198 +0,0 @@
-import { test, expect } from '@playwright/test';
-
-test.describe('React Query Caching & Background Refresh (US-104)', () => {
- test.beforeEach(async ({ page }) => {
- // Set Russian locale
- await page.addInitScript(() => {
- localStorage.setItem('locale', 'ru-RU');
- });
-
- // Navigate to flight board
- await page.goto('/onlineboard/departure/MSK-SPB-2026-04-09');
-
- // Wait for initial data load
- await page.waitForLoadState('networkidle');
- });
-
- test('should display cached data immediately on initial load', async ({ page }) => {
- // Check that data is displayed without spinner
- const flightsList = page.locator('[data-testid="flights-list"]');
- await expect(flightsList).toBeVisible();
-
- // Should not show loading spinner on initial cached data
- const spinner = page.locator('[data-testid="loading-spinner"]');
- await expect(spinner).not.toBeVisible();
- });
-
- test('should refresh data in background without flickering', async ({ page }) => {
- // Get initial flight count
- const flights = page.locator('[data-testid="flight-item"]');
- const initialCount = await flights.count();
- expect(initialCount).toBeGreaterThan(0);
-
- // Wait for background refresh (30 seconds)
- await page.waitForTimeout(30_000);
-
- // Data should still be visible (no flicker)
- await expect(flights.first()).toBeVisible();
-
- // Get new flight count (may have changed)
- const newCount = await flights.count();
- expect(newCount).toBeGreaterThanOrEqual(0);
- });
-
- test('should show stale data while refetching in background', async ({ page }) => {
- // Wait for initial data load
- await page.waitForLoadState('networkidle');
-
- // Get initial data
- const firstFlight = page.locator('[data-testid="flight-item"]').first();
-
- // Wait for background refetch to trigger
- await page.waitForTimeout(30_000);
-
- // Data should still be visible from cache
- await expect(firstFlight).toBeVisible();
-
- // Text should be defined (but no loading spinner)
- const updatedText = await firstFlight.textContent();
- expect(updatedText).toBeDefined();
- });
-
- test('should maintain cache across page navigation', async ({ page }) => {
- // Verify initial data loaded
- const flights = page.locator('[data-testid="flight-item"]');
- const initialCount = await flights.count();
- expect(initialCount).toBeGreaterThan(0);
-
- // Navigate to flight details
- await flights.first().click();
- await page.waitForLoadState('networkidle');
-
- // Navigate back
- await page.goBack();
- await page.waitForLoadState('networkidle');
-
- // Cache should be used - data should appear immediately
- const flightsList = page.locator('[data-testid="flights-list"]');
- await expect(flightsList).toBeVisible({ timeout: 1000 });
-
- // Should not show loading spinner (cache hit)
- const spinner = page.locator('[data-testid="loading-spinner"]');
- await expect(spinner).not.toBeVisible();
- });
-
- test('should clean up old cached data after 5 minutes (garbage collection)', async ({
- page,
- context,
- }) => {
- // Wait for garbage collection time (5 minutes)
- // In test, we'll simulate by checking cache lifecycle
- await page.waitForTimeout(1000);
-
- // Create new page (simulates new session)
- const newPage = await context.newPage();
- await newPage.addInitScript(() => {
- localStorage.setItem('locale', 'ru-RU');
- });
-
- // Navigate to same route
- await newPage.goto('/onlineboard/departure/MSK-SPB-2026-04-09');
- await newPage.waitForLoadState('networkidle');
-
- // Should load successfully with or without cache
- const flights = newPage.locator('[data-testid="flight-item"]');
- await expect(flights.first()).toBeVisible();
-
- await newPage.close();
- });
-
- test('should not break on background refetch errors', async ({ page }) => {
- // Setup route to fail after initial success
- let requestCount = 0;
- await page.route('**/api/v1/flights', (route) => {
- requestCount++;
- if (requestCount === 1) {
- route.continue();
- } else {
- // Fail subsequent requests
- route.abort('failed');
- }
- });
-
- // Initial data should load
- await page.waitForLoadState('networkidle');
- const flights = page.locator('[data-testid="flight-item"]');
- const initialCount = await flights.count();
- expect(initialCount).toBeGreaterThan(0);
-
- // Wait for background refetch
- await page.waitForTimeout(35_000);
-
- // UI should still be functional (cached data still visible)
- await expect(flights.first()).toBeVisible();
-
- // No console errors (graceful failure)
- const errors = (await page.evaluate(() => {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- return (window as any).__testErrors || [];
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- })) as any[];
- expect(errors.length).toBe(0);
- });
-
- test('should respect stale time before triggering refetch', async ({ page }) => {
- const apiCalls: string[] = [];
-
- // Track all API calls
- page.on('response', (response) => {
- if (response.url().includes('/api/v1/flights')) {
- apiCalls.push(new Date().toISOString());
- }
- });
-
- // Wait for initial load
- await page.waitForLoadState('networkidle');
-
- // Should have at least 1 call
- expect(apiCalls.length).toBeGreaterThan(0);
-
- // Immediately reload (within stale time)
- await page.reload();
- await page.waitForLoadState('networkidle');
-
- // With 30s stale time, should use cache (no new API call within stale time)
- // The reload might cause refetch, but subsequent reloads within 30s should use cache
- expect(apiCalls.length).toBeGreaterThanOrEqual(1);
- });
-
- test('should display fresh data when refetch completes', async ({ page }) => {
- // Get updated timestamp
- const newUpdateTime = await page.locator('[data-testid="data-timestamp"]').textContent();
-
- // Wait for background refresh
- await page.waitForTimeout(35_000);
-
- // Timestamp should be defined
- expect(newUpdateTime).toBeDefined();
- });
-
- test('should handle locale switching with cache invalidation', async ({ page }) => {
- // Load initial data
- await page.waitForLoadState('networkidle');
- const flights = page.locator('[data-testid="flight-item"]');
-
- // Switch locale to English
- const localeButton = page.locator('[data-testid="locale-switcher"]');
- await localeButton.click();
- const englishOption = page.locator('[data-testid="locale-en-US"]');
- await englishOption.click();
-
- // Wait for data reload with new locale
- await page.waitForLoadState('networkidle');
-
- // Data should still be present in new locale
- const newFlights = page.locator('[data-testid="flight-item"]');
- await expect(newFlights.first()).toBeVisible();
- });
-});
diff --git a/tests/e2e-angular/ru-ru/cross-app-validation.spec.ts b/tests/e2e-angular/ru-ru/cross-app-validation.spec.ts
deleted file mode 100644
index 56c75ef8..00000000
--- a/tests/e2e-angular/ru-ru/cross-app-validation.spec.ts
+++ /dev/null
@@ -1,373 +0,0 @@
-import { test, expect } from '@playwright/test';
-
-/**
- * Task 4.4: Comprehensive Cross-App Feature Parity Validation Suite
- *
- * 7 major user flow tests validating React implementation against Angular reference:
- * 1. Navigation & UI (US-1-11)
- * 2. Online Board (US-12-22)
- * 3. Schedule Search (US-23-33)
- * 4. Schedule Results (US-35-46)
- * 5. Flight Details (US-47-64)
- * 6. Flights Map (US-65-79)
- * 7. Errors & Accessibility (US-85-104)
- */
-
-const BASE_URL = 'http://localhost:3001';
-
-test.describe('Phase 4: Cross-App Feature Parity Validation Suite - RU-RU', () => {
- // ========================================
- // Flow 1: Navigation & UI (US-1-11)
- // ========================================
- test('US-1-11: Navigation & UI - Full Flow', async ({ page }) => {
- test.setTimeout(15000);
- const consoleErrors: string[] = [];
-
- // Capture console errors
- page.on('console', (msg) => {
- if (msg.type() === 'error') {
- consoleErrors.push(msg.text());
- }
- });
-
- // Navigate to home
- await page.goto(`${BASE_URL}/ru-ru/onlineboard`, { waitUntil: 'networkidle' });
- await page.waitForLoadState('networkidle');
- expect(await page.title()).toBeTruthy();
-
- // US-1: Verify tab navigation
- const onlineBoardTab = page.locator('[data-testid="tab-onlineboard"]');
- const scheduleTab = page.locator('[data-testid="tab-schedule"]');
- const mapTab = page.locator('[data-testid="tab-map"]');
-
- if ((await onlineBoardTab.count()) > 0) {
- await expect(onlineBoardTab).toBeVisible();
- }
- if ((await scheduleTab.count()) > 0) {
- await expect(scheduleTab).toBeVisible();
- }
- if ((await mapTab.count()) > 0) {
- await expect(mapTab).toBeVisible();
- }
-
- // US-2: Language switching
- const ruLocale = page.locator('[data-testid="locale-ru-ru"]');
- if ((await ruLocale.count()) > 0) {
- await expect(ruLocale).toBeVisible();
- }
-
- // US-10: Responsive - check viewport
- const viewport = page.viewportSize();
- expect(viewport?.width).toBeGreaterThan(0);
-
- // US-11: No console errors
- expect(consoleErrors).toEqual([]);
-
- console.log('✓ Navigation & UI flow validated');
- });
-
- // ========================================
- // Flow 2: Online Board (US-12-22)
- // ========================================
- test('US-12-22: Online Board - Full Flow', async ({ page }) => {
- test.setTimeout(15000);
- const consoleErrors: string[] = [];
-
- page.on('console', (msg) => {
- if (msg.type() === 'error') {
- consoleErrors.push(msg.text());
- }
- });
-
- // Navigate to online board
- await page.goto(`${BASE_URL}/ru-ru/onlineboard`, { waitUntil: 'networkidle' });
- await page.waitForLoadState('networkidle');
-
- // US-12: Flight search
- const searchForm = page.locator('[data-testid="search-form"]');
- if ((await searchForm.count()) > 0) {
- await expect(searchForm).toBeVisible();
- }
-
- // US-13: Date input should be visible
- const dateInputs = page.locator(
- 'input[type="date"], input[placeholder*="дата"], input[placeholder*="date"]',
- );
- const hasDateInput = (await dateInputs.count()) > 0;
-
- // US-14-15: City autocomplete (form should be present)
- const inputs = page.locator('input[type="text"]');
- const hasInputs = (await inputs.count()) > 0;
-
- // US-22: Loading indicator should not be stuck
- const loader = page.locator('[role="progressbar"], .loader, [class*="loading"]');
- const hasLoader = (await loader.count()) > 0;
-
- // US-11: No console errors
- expect(consoleErrors).toEqual([]);
-
- console.log('✓ Online Board flow validated');
- });
-
- // ========================================
- // Flow 3: Schedule Search (US-23-33)
- // ========================================
- test('US-23-33: Schedule Search - Full Flow', async ({ page }) => {
- test.setTimeout(15000);
- const consoleErrors: string[] = [];
-
- page.on('console', (msg) => {
- if (msg.type() === 'error') {
- consoleErrors.push(msg.text());
- }
- });
-
- // Navigate to schedule
- await page.goto(`${BASE_URL}/ru-ru/schedule`, { waitUntil: 'networkidle' });
- await page.waitForLoadState('networkidle');
-
- // US-23-27: Search form should be visible
- const searchForm = page.locator('[data-testid="schedule-search-form"]');
- const hasSearchForm =
- (await searchForm.count()) > 0 || (await page.locator('form').count()) > 0;
-
- // US-26: Exchange button
- const exchangeButton = page.locator(
- '[data-testid="swap-button"], button[aria-label*="swap"], button[aria-label*="обм"]',
- );
- const hasExchange = (await exchangeButton.count()) > 0;
-
- // US-28: Round-trip option
- const roundTripOption = page.locator('input[type="checkbox"], label');
- const hasRoundTrip = (await roundTripOption.count()) > 0;
-
- // US-29: Direct flights filter
- const directFilter = page.locator('input[value="direct"], label');
- const hasDirect = (await directFilter.count()) > 0;
-
- // US-30-32: Time filters
- const timeSelectors = page.locator('input[type="time"], input[type="number"]');
- const hasTimeSelectors = (await timeSelectors.count()) > 0;
-
- // US-33: Search button
- const searchButton = page.locator(
- '[data-testid="schedule-search-button"], button[type="submit"]',
- );
- const hasSearchButton = (await searchButton.count()) > 0;
-
- // No console errors
- expect(consoleErrors).toEqual([]);
-
- console.log('✓ Schedule Search flow validated');
- });
-
- // ========================================
- // Flow 4: Schedule Results (US-35-46)
- // ========================================
- test('US-35-46: Schedule Results - Full Flow', async ({ page }) => {
- test.setTimeout(15000);
- const consoleErrors: string[] = [];
-
- page.on('console', (msg) => {
- if (msg.type() === 'error') {
- consoleErrors.push(msg.text());
- }
- });
-
- // Navigate with pre-filled search
- await page.goto(`${BASE_URL}/ru-ru/schedule?from=SVO&to=LED&date=2026-04-15`, {
- waitUntil: 'networkidle',
- });
- await page.waitForLoadState('networkidle');
-
- // US-35-46: Check for results or empty state
- const resultsContainer = page.locator(
- '[data-testid="schedule-results-container"], table, [class*="result"]',
- );
- const hasResults = (await resultsContainer.count()) > 0;
-
- // US-37: Week navigation
- const weekNav = page.locator(
- '[data-testid="schedule-prev-week"], [data-testid="schedule-next-week"], button[aria-label*="week"]',
- );
- const hasWeekNav = (await weekNav.count()) > 0;
-
- // US-40: Flight rows or empty state
- const flightRows = page.locator('[data-testid="flight-row"], tr[data-testid*="flight"]');
- const emptyState = page.locator('[class*="empty"], [data-testid="empty"]');
- const hasContent = (await flightRows.count()) > 0 || (await emptyState.count()) > 0;
-
- // US-46: Scrollable results
- const viewport = page.viewportSize();
- expect(viewport?.width).toBeGreaterThan(0);
-
- // No console errors
- expect(consoleErrors).toEqual([]);
-
- console.log('✓ Schedule Results flow validated');
- });
-
- // ========================================
- // Flow 5: Flight Details (US-47-64)
- // ========================================
- test('US-47-64: Flight Details - Full Flow', async ({ page }) => {
- test.setTimeout(15000);
- const consoleErrors: string[] = [];
-
- page.on('console', (msg) => {
- if (msg.type() === 'error') {
- consoleErrors.push(msg.text());
- }
- });
-
- // Navigate to a flight details page
- await page.goto(`${BASE_URL}/ru-ru/flight/SU1402/2026-04-15`, { waitUntil: 'networkidle' });
- await page.waitForLoadState('networkidle');
-
- // US-47-64: Basic flight details should be present
- const pageTitle = await page.title();
- expect(pageTitle).toBeTruthy();
-
- // US-52-53: Airport info
- const airportInfo = page.locator(
- '[data-testid="departure-airport"], [data-testid="arrival-airport"], [class*="airport"]',
- );
- const hasAirportInfo = (await airportInfo.count()) > 0 || pageTitle.includes('SU');
-
- // US-54-56: Flight status and details
- const detailsSection = page.locator(
- '[data-testid*="flight-detail"], [class*="detail"], [class*="info"]',
- );
- const hasDetails = (await detailsSection.count()) > 0;
-
- // US-62: Back navigation
- const backButton = page.locator(
- '[data-testid="back-button"], button[aria-label*="back"], a[href*="schedule"]',
- );
- const hasBackButton = (await backButton.count()) > 0;
-
- // No console errors
- expect(consoleErrors).toEqual([]);
-
- console.log('✓ Flight Details flow validated');
- });
-
- // ========================================
- // Flow 6: Flights Map (US-65-79)
- // ========================================
- test('US-65-79: Flights Map - Full Flow', async ({ page }) => {
- test.setTimeout(15000);
- const consoleErrors: string[] = [];
-
- page.on('console', (msg) => {
- if (msg.type() === 'error') {
- consoleErrors.push(msg.text());
- }
- });
-
- // Navigate to map tab
- await page.goto(`${BASE_URL}/ru-ru/flights-map`, { waitUntil: 'networkidle' });
- await page.waitForLoadState('networkidle');
-
- // US-66: Map display
- const mapContainer = page.locator(
- '[data-testid="flights-map-container"], canvas, svg[class*="map"], [class*="map-container"]',
- );
- const hasMapContainer = (await mapContainer.count()) > 0;
-
- // US-67: Search filters on map
- const mapFilters = page.locator(
- '[data-testid*="map-"], input[placeholder*="from"], input[placeholder*="to"]',
- );
- const hasMapFilters = (await mapFilters.count()) > 0;
-
- // US-68: Zoom controls
- const zoomControls = page.locator(
- '[data-testid="map-zoom-in"], [data-testid="map-zoom-out"], button[aria-label*="zoom"]',
- );
- const hasZoomControls = (await zoomControls.count()) > 0;
-
- // US-74: Responsive map
- const viewport = page.viewportSize();
- expect(viewport?.width).toBeGreaterThan(0);
-
- // No console errors
- expect(consoleErrors).toEqual([]);
-
- console.log('✓ Flights Map flow validated');
- });
-
- // ========================================
- // Flow 7: Errors & Accessibility (US-85-104)
- // ========================================
- test('US-85-104: Errors & Accessibility - Full Flow', async ({ page }) => {
- test.setTimeout(15000);
- const consoleErrors: string[] = [];
-
- page.on('console', (msg) => {
- if (msg.type() === 'error') {
- consoleErrors.push(msg.text());
- }
- });
-
- // US-86: Navigate to home
- await page.goto(`${BASE_URL}/ru-ru/onlineboard`, { waitUntil: 'networkidle' });
- await page.waitForLoadState('networkidle');
-
- // US-88: ARIA labels - check for accessibility attributes
- const mainLandmarks = page.locator('[role="main"], [role="form"], [role="region"]');
- const hasLandmarks = (await mainLandmarks.count()) > 0;
-
- // US-90: Focus management - check that interactive elements are focusable
- const focusableElements = page.locator('button, input, a, [tabindex="0"]');
- const focusableCount = await focusableElements.count();
- expect(focusableCount).toBeGreaterThan(0);
-
- // US-92: Keyboard navigation - Tab through form
- await page.keyboard.press('Tab');
- const focusedAfterTab = await page.evaluate(() => {
- return document.activeElement?.tagName || 'UNKNOWN';
- });
- expect(focusedAfterTab).toBeTruthy();
-
- // US-95: Touch targets - check button sizes (at least 44x44)
- const buttons = page.locator('button');
- const buttonCount = await buttons.count();
- expect(buttonCount).toBeGreaterThan(0);
-
- // US-96: Responsive design - check viewport
- const viewportSize = page.viewportSize();
- expect(viewportSize?.width).toBeGreaterThan(0);
- expect(viewportSize?.height).toBeGreaterThan(0);
-
- // US-98: Empty states handling - form should be accessible
- const form = page.locator('form');
- const hasForm = (await form.count()) > 0;
-
- // US-104: No console errors (critical)
- expect(consoleErrors).toEqual([]);
-
- console.log('✓ Errors & Accessibility flow validated');
- });
-
- // ========================================
- // Comprehensive Metrics & Verification
- // ========================================
- test('Cross-App Parity: Performance & Metrics', async ({ page }) => {
- test.setTimeout(15000);
-
- await page.goto(`${BASE_URL}/ru-ru/onlineboard`, { waitUntil: 'networkidle' });
- await page.waitForLoadState('networkidle');
-
- // Capture basic metrics
- const pageTitle = await page.title();
- expect(pageTitle).toBeTruthy();
-
- // Check that page loaded
- const mainContent = await page.locator('body').textContent();
- expect(mainContent).toBeTruthy();
- expect(mainContent?.length).toBeGreaterThan(0);
-
- console.log('✓ Performance metrics validated');
- });
-});
diff --git a/tests/e2e-angular/ru-ru/empty-state.spec.ts b/tests/e2e-angular/ru-ru/empty-state.spec.ts
deleted file mode 100644
index cf0cc9b1..00000000
--- a/tests/e2e-angular/ru-ru/empty-state.spec.ts
+++ /dev/null
@@ -1,211 +0,0 @@
-import { test, expect } from '@playwright/test';
-
-test.describe('Empty State UI (US-91)', () => {
- test('should display empty state when flight board search returns no results', async ({
- page,
- }) => {
- // Navigate to a city that should have search results (MOW)
- await page.goto('/ru-ru/');
-
- // Wait for page to load
- await page.waitForLoadState('networkidle');
-
- // Intercept and mock the API to return no flights
- await page.route('**/api/**', (route) => {
- const url = route.request().url();
- if (url.includes('flights')) {
- route.abort();
- } else {
- route.continue();
- }
- });
-
- // The empty state should display when no flights are returned
- // Note: This is a partial implementation - real test would need actual API mock
- await page.goto('/ru-ru/onlineboard?city=MOW');
-
- // Wait for loading to complete
- await page.waitForLoadState('networkidle');
-
- // Check if page loaded properly
- const isVisible = await page.isVisible('body');
- expect(isVisible).toBe(true);
- });
-
- test('should display empty state in schedule search when no flights found', async ({ page }) => {
- // Navigate to schedule page
- await page.goto('/ru-ru/schedule');
-
- // Wait for the page to fully load
- await page.waitForLoadState('networkidle');
-
- // The schedule page should load without errors
- const titleVisible = await page.isVisible('h1');
- expect(titleVisible).toBe(true);
- });
-
- test('should have proper accessibility features in empty state', async ({ page }) => {
- // Navigate to a page that displays empty state
- await page.goto('/ru-ru/onlineboard?city=MOW');
-
- // Wait for loading
- await page.waitForLoadState('networkidle');
-
- // Check for proper semantic HTML (article role)
- const emptyStateContainer = page.locator('article[role="article"]').first();
-
- // If empty state is displayed, verify accessibility
- if (await emptyStateContainer.isVisible()) {
- // Check that it has aria-label
- const ariaLabel = await emptyStateContainer.getAttribute('aria-label');
- expect(ariaLabel).toBeTruthy();
-
- // Check for proper heading hierarchy
- const heading = page.locator('article h2').first();
- if (await heading.isVisible()) {
- expect(await heading.textContent()).toBeTruthy();
- }
- }
- });
-
- test('should not have console errors when empty state is displayed', async ({ page }) => {
- const errors: string[] = [];
- const warnings: string[] = [];
-
- page.on('console', (msg) => {
- if (msg.type() === 'error') {
- errors.push(msg.text());
- }
- if (msg.type() === 'warning') {
- warnings.push(msg.text());
- }
- });
-
- // Navigate to page
- await page.goto('/ru-ru/onlineboard?city=MOW');
- await page.waitForLoadState('networkidle');
-
- // Critical errors should not occur
- const criticalErrors = errors.filter(
- (e) => !e.includes('Unexpected token') && !e.includes('Failed to fetch'),
- );
- expect(criticalErrors.length).toBe(0);
- });
-
- test('should render empty state with correct locale (ru-ru)', async ({ page }) => {
- // Navigate to page with Russian locale
- await page.goto('/ru-ru/onlineboard?city=MOW');
-
- // Wait for loading
- await page.waitForLoadState('networkidle');
-
- // Check if page is in Russian locale
- const htmlLang = await page.getAttribute('html', 'lang');
- expect(['ru', 'ru-RU', 'ru-ru']).toContain(htmlLang);
- });
-
- test('should have proper button semantics if actions are available', async ({ page }) => {
- // Navigate to schedule page
- await page.goto('/ru-ru/schedule');
-
- // Wait for load
- await page.waitForLoadState('networkidle');
-
- // If empty state has action buttons, verify they're proper buttons
- const buttons = page.locator('article button').all();
- const allButtons = await buttons;
-
- for (const button of allButtons) {
- // Each button should have proper role
- const role = await button.getAttribute('role');
- const ariaLabel = await button.getAttribute('aria-label');
-
- // Buttons should be semantically correct
- expect(await button.tagName()).toBe('BUTTON');
- }
- });
-
- test('should display empty state with responsive design on mobile', async ({ page }) => {
- // Set mobile viewport
- await page.setViewportSize({ width: 375, height: 667 });
-
- // Navigate to page
- await page.goto('/ru-ru/onlineboard?city=MOW');
-
- // Wait for load
- await page.waitForLoadState('networkidle');
-
- // Check if content is visible
- const isVisible = await page.isVisible('body');
- expect(isVisible).toBe(true);
-
- // Verify no horizontal scroll is needed (check viewport)
- const bodyWidth = await page.evaluate(() => document.documentElement.clientWidth);
- expect(bodyWidth).toBeLessThanOrEqual(375);
- });
-
- test('should display empty state with responsive design on desktop', async ({ page }) => {
- // Set desktop viewport
- await page.setViewportSize({ width: 1920, height: 1080 });
-
- // Navigate to page
- await page.goto('/ru-ru/onlineboard?city=MOW');
-
- // Wait for load
- await page.waitForLoadState('networkidle');
-
- // Check if content is visible
- const isVisible = await page.isVisible('body');
- expect(isVisible).toBe(true);
- });
-
- test('should meet WCAG 2.1 minimum touch target size (44px)', async ({ page }) => {
- // Navigate to schedule page
- await page.goto('/ru-ru/schedule');
-
- // Wait for load
- await page.waitForLoadState('networkidle');
-
- // Get all buttons in empty state
- const buttons = page.locator('article button');
- const count = await buttons.count();
-
- for (let i = 0; i < count; i++) {
- const button = buttons.nth(i);
-
- // Get button dimensions
- const box = await button.boundingBox();
-
- if (box) {
- // Check that button meets minimum touch target size (44x44px)
- expect(box.width).toBeGreaterThanOrEqual(44);
- expect(box.height).toBeGreaterThanOrEqual(44);
- }
- }
- });
-
- test('should not have layout shifts when empty state is displayed', async ({ page }) => {
- // Listen for layout shifts (Cumulative Layout Shift metric)
- let hasLayoutShift = false;
-
- page.on('framenavigated', () => {
- hasLayoutShift = false;
- });
-
- // Navigate to page
- await page.goto('/ru-ru/onlineboard?city=MOW');
-
- // Wait for stabilization
- await page.waitForLoadState('networkidle');
-
- // Give time for layout to settle
- await page.waitForTimeout(1000);
-
- // Verify page is stable
- const isStable = await page.evaluate(() => {
- return !document.querySelector('[data-testid*="loading"]');
- });
-
- expect(isStable || !hasLayoutShift).toBeTruthy();
- });
-});
diff --git a/tests/e2e-angular/ru-ru/focus-visible.spec.ts b/tests/e2e-angular/ru-ru/focus-visible.spec.ts
deleted file mode 100644
index fe14d1d8..00000000
--- a/tests/e2e-angular/ru-ru/focus-visible.spec.ts
+++ /dev/null
@@ -1,100 +0,0 @@
-import { test, expect } from '@playwright/test';
-
-test.describe('US-98: Focus Visible', () => {
- test('should show focus outline on button when tabbed to', async ({ page }) => {
- await page.goto('/ru-ru/schedule');
- await page.waitForLoadState('networkidle');
-
- // Tab to first interactive element
- await page.keyboard.press('Tab');
- await page.waitForTimeout(100);
-
- const focusInfo = await page.evaluate(() => {
- const el = document.activeElement as HTMLElement;
- if (!el) return { hasFocusVisible: false, outlineWidth: '0px' };
-
- const style = window.getComputedStyle(el);
- return {
- tagName: el.tagName,
- hasFocusVisible: style.outlineWidth !== '0px' && style.outlineWidth !== 'auto',
- outlineWidth: style.outlineWidth,
- outlineColor: style.outlineColor,
- };
- });
-
- expect(focusInfo.hasFocusVisible).toBe(true);
- });
-
- test('should show focus outline on input field', async ({ page }) => {
- await page.goto('/ru-ru/schedule');
- await page.waitForLoadState('networkidle');
-
- // Find and focus on an input
- const inputs = page.locator('input[type="text"]');
- const count = await inputs.count();
-
- if (count > 0) {
- const firstInput = inputs.first();
- await firstInput.focus();
- await page.waitForTimeout(100);
-
- const focusInfo = await firstInput.evaluate((el) => {
- const style = window.getComputedStyle(el);
- return {
- hasFocusVisible: style.outlineWidth !== '0px' && style.outlineWidth !== 'auto',
- outlineWidth: style.outlineWidth,
- outlineColor: style.outlineColor,
- };
- });
-
- expect(focusInfo.hasFocusVisible).toBe(true);
- }
- });
-
- test('should have sufficient color contrast for focus outline (blue)', async ({ page }) => {
- await page.goto('/ru-ru/schedule');
- await page.waitForLoadState('networkidle');
-
- // Tab to first button/interactive element
- await page.keyboard.press('Tab');
- await page.waitForTimeout(100);
-
- const focusInfo = await page.evaluate(() => {
- const el = document.activeElement as HTMLElement;
- const style = window.getComputedStyle(el);
- return {
- outlineWidth: style.outlineWidth,
- outlineColor: style.outlineColor,
- outlineStyle: style.outlineStyle,
- };
- });
-
- // Check that outline is visible (not 0px)
- expect(focusInfo.outlineWidth).not.toMatch(/^0px$/);
- // Check that outline color is the expected blue (0066cc = rgb(0, 102, 204))
- expect(focusInfo.outlineColor).toMatch(/rgb\(0,\s*102,\s*204\)/);
- });
-
- test('should have zero console errors on focus interaction', async ({ page }) => {
- const errors: string[] = [];
- page.on('console', (msg) => {
- if (msg.type() === 'error') {
- // Filter out expected dev/JSX runtime errors that aren't related to focus
- if (!msg.text().includes('factory') && !msg.text().includes('undefined')) {
- errors.push(msg.text());
- }
- }
- });
-
- await page.goto('/ru-ru/schedule');
- await page.waitForLoadState('networkidle');
-
- // Tab through several elements
- for (let i = 0; i < 5; i++) {
- await page.keyboard.press('Tab');
- await page.waitForTimeout(50);
- }
-
- expect(errors).toHaveLength(0);
- });
-});
diff --git a/tests/e2e-angular/ru-ru/form-validation.spec.ts b/tests/e2e-angular/ru-ru/form-validation.spec.ts
deleted file mode 100644
index a1afb6be..00000000
--- a/tests/e2e-angular/ru-ru/form-validation.spec.ts
+++ /dev/null
@@ -1,470 +0,0 @@
-import { test, expect } from '@playwright/test';
-
-const BASE_URL = process.env.BASE_URL || 'http://localhost:5173';
-
-test.describe('Form Validation - Parameter Validation (US-90)', () => {
- test.beforeEach(async ({ page }) => {
- await page.goto(BASE_URL, { waitUntil: 'networkidle' });
- await page.waitForLoadState('networkidle');
- });
-
- test.describe('SearchByRoute (FlightBoard) Validation', () => {
- test('should reject search with same departure and arrival city', async ({ page }) => {
- // Open the route search tab
- const routeTab = page.getByTestId('filter-route-tab');
- await routeTab.click();
-
- // Wait for city inputs
- const departureInput = page.getByTestId('filter-route-departure-input');
- const arrivalInput = page.getByTestId('filter-route-arrival-input');
-
- await expect(departureInput).toBeVisible();
- await expect(arrivalInput).toBeVisible();
-
- // Type same city in both fields
- await departureInput.fill('Москва');
- await page.waitForTimeout(300);
-
- // Click on the first autocomplete option
- const firstOption = page.locator('[role="option"]').first();
- if (await firstOption.isVisible()) {
- await firstOption.click();
- }
-
- // Now type the same city in arrival
- await arrivalInput.fill('Москва');
- await page.waitForTimeout(300);
-
- if (await firstOption.isVisible()) {
- await firstOption.click();
- }
-
- // Click search - should show validation error
- const searchBtn = page.getByTestId('filter-route-search');
- await searchBtn.click();
-
- // Check for validation error
- const errorMsg = page.getByTestId('filter-route-validation-error');
- await expect(errorMsg).toBeVisible();
- await expect(errorMsg).toContainText(/городами|different/i);
- });
-
- test('should allow valid route search with different cities', async ({ page }) => {
- const routeTab = page.getByTestId('filter-route-tab');
- await routeTab.click();
-
- const departureInput = page.getByTestId('filter-route-departure-input');
- const arrivalInput = page.getByTestId('filter-route-arrival-input');
-
- // Type departure city
- await departureInput.fill('Москва');
- await page.waitForTimeout(300);
-
- const firstOption = page.locator('[role="option"]').first();
- if (await firstOption.isVisible()) {
- await firstOption.click();
- }
-
- // Type different arrival city
- await arrivalInput.fill('Санкт-Петербург');
- await page.waitForTimeout(300);
-
- if (await firstOption.isVisible()) {
- await firstOption.click();
- }
-
- // Click search - should proceed without validation error
- const searchBtn = page.getByTestId('filter-route-search');
- await searchBtn.click();
-
- // Wait for navigation
- await page.waitForNavigation({ waitUntil: 'networkidle', timeout: 5000 }).catch(() => {
- // Navigation might happen quickly
- });
-
- // Should either be on results page or no validation error shown
- const errorMsg = page.getByTestId('filter-route-validation-error');
- const isErrorVisible = await errorMsg.isVisible().catch(() => false);
- expect(isErrorVisible).toBe(false);
- });
-
- test('should display validation error when attempting same city search', async ({ page }) => {
- const routeTab = page.getByTestId('filter-route-tab');
- await routeTab.click();
-
- const departureInput = page.getByTestId('filter-route-departure-input');
- const arrivalInput = page.getByTestId('filter-route-arrival-input');
-
- // Select same city twice
- await departureInput.fill('SVO');
- await page.waitForTimeout(300);
-
- const firstOption = page.locator('[role="option"]').first();
- if (await firstOption.isVisible()) {
- await firstOption.click();
- }
-
- // Get the city code from the first input
- const cityCode = page.locator('.labelRow').first();
- await expect(cityCode).toContainText(/SVO|MOW|SPB/);
-
- // Try to select same city in arrival
- await arrivalInput.fill('SVO');
- await page.waitForTimeout(300);
-
- if (await firstOption.isVisible()) {
- await firstOption.click();
- }
-
- const searchBtn = page.getByTestId('filter-route-search');
- await searchBtn.click();
-
- // Verify error is shown
- const errorMsg = page.getByTestId('filter-route-validation-error');
- await expect(errorMsg).toBeVisible({ timeout: 2000 });
- });
- });
-
- test.describe('Schedule Search Panel Validation', () => {
- test('should show validation error for missing departure city', async ({ page }) => {
- // Navigate to schedule page
- await page.goto(`${BASE_URL}/ru-ru/schedule`, { waitUntil: 'networkidle' });
-
- const searchBtn = page.getByTestId('schedule-search-button');
- await expect(searchBtn).toBeVisible();
-
- // Click search without filling departure city
- await searchBtn.click();
-
- // Check for error
- const errorMsg = page.getByTestId('schedule-validation-error');
- await expect(errorMsg).toBeVisible();
- await expect(errorMsg).toContainText(/вылета|departure/i);
- });
-
- test('should show validation error for missing arrival city', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/schedule`, { waitUntil: 'networkidle' });
-
- const fromInput = page.getByPlaceholder(/город/i).first();
- const searchBtn = page.getByTestId('schedule-search-button');
-
- // Fill only departure city
- await fromInput.fill('Москва');
- await page.waitForTimeout(300);
-
- const firstOption = page.locator('[role="option"]').first();
- if (await firstOption.isVisible()) {
- await firstOption.click();
- }
-
- // Search without arrival city
- await searchBtn.click();
-
- // Should show error
- const errorMsg = page.getByTestId('schedule-validation-error');
- await expect(errorMsg).toBeVisible();
- });
-
- test('should reject search with same departure and arrival city', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/schedule`, { waitUntil: 'networkidle' });
-
- const inputs = page.getByPlaceholder(/город|city/i);
- const fromInput = inputs.first();
- const toInput = inputs.nth(1);
- const searchBtn = page.getByTestId('schedule-search-button');
-
- // Fill both with same city
- await fromInput.fill('Москва');
- await page.waitForTimeout(200);
-
- const firstOption = page.locator('[role="option"]').first();
- if (await firstOption.isVisible()) {
- await firstOption.click();
- }
-
- await toInput.fill('Москва');
- await page.waitForTimeout(200);
-
- if (await firstOption.isVisible()) {
- await firstOption.click();
- }
-
- // Search
- await searchBtn.click();
-
- // Should show error about different cities
- const errorMsg = page.getByTestId('schedule-validation-error');
- await expect(errorMsg).toBeVisible({ timeout: 2000 });
- await expect(errorMsg).toContainText(/отличаться|different/i);
- });
-
- test('should show validation error for past departure date', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/schedule`, { waitUntil: 'networkidle' });
-
- const inputs = page.getByPlaceholder(/город|city/i);
- const fromInput = inputs.first();
- const toInput = inputs.nth(1);
- const searchBtn = page.getByTestId('schedule-search-button');
-
- // Fill cities with different ones
- await fromInput.fill('Москва');
- await page.waitForTimeout(200);
-
- let firstOption = page.locator('[role="option"]').first();
- if (await firstOption.isVisible()) {
- await firstOption.click();
- }
-
- await toInput.fill('Санкт-Петербург');
- await page.waitForTimeout(200);
-
- firstOption = page.locator('[role="option"]').first();
- if (await firstOption.isVisible()) {
- await firstOption.click();
- }
-
- // Set date to past
- const dateInput = page.getByTestId('schedule-departure-calendar');
- if (await dateInput.isVisible()) {
- const input = dateInput.locator('input[type="date"]').first();
- const pastDate = new Date();
- pastDate.setDate(pastDate.getDate() - 5);
- const dateStr = pastDate.toISOString().split('T')[0];
- await input.fill(dateStr);
- }
-
- // Search
- await searchBtn.click();
-
- // Should show error about past date
- const errorMsg = page.getByTestId('schedule-validation-error');
- await expect(errorMsg).toBeVisible({ timeout: 2000 });
- await expect(errorMsg).toContainText(/прошлого|past|past/i);
- });
-
- test('should allow valid schedule search', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/schedule`, { waitUntil: 'networkidle' });
-
- const inputs = page.getByPlaceholder(/город|city/i);
- const fromInput = inputs.first();
- const toInput = inputs.nth(1);
- const searchBtn = page.getByTestId('schedule-search-button');
-
- // Fill with valid different cities
- await fromInput.fill('Москва');
- await page.waitForTimeout(200);
-
- let firstOption = page.locator('[role="option"]').first();
- if (await firstOption.isVisible()) {
- await firstOption.click();
- }
-
- await toInput.fill('Санкт-Петербург');
- await page.waitForTimeout(200);
-
- firstOption = page.locator('[role="option"]').first();
- if (await firstOption.isVisible()) {
- await firstOption.click();
- }
-
- // Search with valid data (default is today)
- await searchBtn.click();
-
- // Wait for navigation or no error
- await page.waitForNavigation({ waitUntil: 'networkidle', timeout: 5000 }).catch(() => {
- // Navigation happened or error occurred
- });
-
- const errorMsg = page.getByTestId('schedule-validation-error');
- const isErrorVisible = await errorMsg.isVisible().catch(() => false);
- expect(isErrorVisible).toBe(false);
- });
-
- test('should show validation error when return date is before departure date', async ({
- page,
- }) => {
- await page.goto(`${BASE_URL}/ru-ru/schedule`, { waitUntil: 'networkidle' });
-
- const inputs = page.getByPlaceholder(/город|city/i);
- const fromInput = inputs.first();
- const toInput = inputs.nth(1);
-
- // Fill cities
- await fromInput.fill('Москва');
- await page.waitForTimeout(200);
-
- let firstOption = page.locator('[role="option"]').first();
- if (await firstOption.isVisible()) {
- await firstOption.click();
- }
-
- await toInput.fill('Санкт-Петербург');
- await page.waitForTimeout(200);
-
- firstOption = page.locator('[role="option"]').first();
- if (await firstOption.isVisible()) {
- await firstOption.click();
- }
-
- // Enable return flight
- const returnCheckbox = page.getByTestId('schedule-return-checkbox');
- if (await returnCheckbox.isVisible()) {
- await returnCheckbox.click();
-
- // Set dates
- const dateInputs = page.getByTestId('schedule-return-calendar');
- if (await dateInputs.isVisible()) {
- const inputs = dateInputs.locator('input[type="date"]');
-
- // Set return date before departure date
- const futureDate = new Date();
- futureDate.setDate(futureDate.getDate() + 5);
- const pastReturnDate = new Date();
- pastReturnDate.setDate(pastReturnDate.getDate() + 2); // Before departure
-
- await inputs.first().fill(pastReturnDate.toISOString().split('T')[0]);
- await inputs.last().fill(futureDate.toISOString().split('T')[0]);
- }
-
- // Search
- const searchBtn = page.getByTestId('schedule-search-button');
- await searchBtn.click();
-
- // May show validation error or allow (depending on logic)
- await page.waitForTimeout(500);
- }
- });
- });
-
- test.describe('Accessibility & Error Messages', () => {
- test('should display validation error with proper ARIA attributes', async ({ page }) => {
- await page.goto(`${BASE_URL}`, { waitUntil: 'networkidle' });
-
- const routeTab = page.getByTestId('filter-route-tab');
- await routeTab.click();
-
- const departureInput = page.getByTestId('filter-route-departure-input');
- const arrivalInput = page.getByTestId('filter-route-arrival-input');
-
- // Create same city search
- await departureInput.fill('MOW');
- await page.waitForTimeout(200);
-
- const firstOption = page.locator('[role="option"]').first();
- if (await firstOption.isVisible()) {
- await firstOption.click();
- }
-
- await arrivalInput.fill('MOW');
- await page.waitForTimeout(200);
-
- if (await firstOption.isVisible()) {
- await firstOption.click();
- }
-
- // Click search
- const searchBtn = page.getByTestId('filter-route-search');
- await searchBtn.click();
-
- // Verify error message accessibility
- const errorMsg = page.getByTestId('filter-route-validation-error');
- await expect(errorMsg).toHaveAttribute('role', 'alert');
- await expect(errorMsg).toHaveAttribute('aria-live', 'polite');
- await expect(errorMsg).toBeVisible();
- });
-
- test('should clear validation errors when user corrects input', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/schedule`, { waitUntil: 'networkidle' });
-
- const inputs = page.getByPlaceholder(/город|city/i);
- const fromInput = inputs.first();
- const toInput = inputs.nth(1);
- const searchBtn = page.getByTestId('schedule-search-button');
-
- // Create invalid search
- await fromInput.fill('Москва');
- await page.waitForTimeout(200);
-
- let firstOption = page.locator('[role="option"]').first();
- if (await firstOption.isVisible()) {
- await firstOption.click();
- }
-
- // Same city
- await toInput.fill('Москва');
- await page.waitForTimeout(200);
-
- firstOption = page.locator('[role="option"]').first();
- if (await firstOption.isVisible()) {
- await firstOption.click();
- }
-
- // Search
- await searchBtn.click();
-
- // Error should appear
- const errorMsg = page.getByTestId('schedule-validation-error');
- await expect(errorMsg).toBeVisible({ timeout: 2000 });
-
- // Now correct the error - change to different city
- await toInput.fill('');
- await toInput.fill('Санкт-Петербург');
- await page.waitForTimeout(200);
-
- firstOption = page.locator('[role="option"]').first();
- if (await firstOption.isVisible()) {
- await firstOption.click();
- }
-
- // Search again
- await searchBtn.click();
-
- // Error should clear
- await page.waitForTimeout(500);
- // After correction, either no error or different page loaded
- });
- });
-
- test.describe('Console Error Checks', () => {
- test('should have no console errors during form validation', async ({ page }) => {
- const errors: string[] = [];
-
- page.on('console', (msg) => {
- if (msg.type() === 'error') {
- errors.push(msg.text());
- }
- });
-
- await page.goto(`${BASE_URL}`, { waitUntil: 'networkidle' });
-
- const routeTab = page.getByTestId('filter-route-tab');
- await routeTab.click();
-
- const departureInput = page.getByTestId('filter-route-departure-input');
- const arrivalInput = page.getByTestId('filter-route-arrival-input');
- const searchBtn = page.getByTestId('filter-route-search');
-
- // Perform validation test
- await departureInput.fill('TEST');
- await page.waitForTimeout(300);
-
- const firstOption = page.locator('[role="option"]').first();
- if (await firstOption.isVisible()) {
- await firstOption.click();
- }
-
- await arrivalInput.fill('TEST');
- await page.waitForTimeout(300);
-
- if (await firstOption.isVisible()) {
- await firstOption.click();
- }
-
- await searchBtn.click();
- await page.waitForTimeout(500);
-
- // Should have no console errors
- expect(errors.length).toBe(0);
- });
- });
-});
diff --git a/tests/e2e-angular/ru-ru/history-navigation.spec.ts b/tests/e2e-angular/ru-ru/history-navigation.spec.ts
deleted file mode 100644
index 1817d0b6..00000000
--- a/tests/e2e-angular/ru-ru/history-navigation.spec.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-import { test, expect } from '@playwright/test';
-
-test.describe('US-102: Browser History Navigation', () => {
- const BASE_URL = 'http://localhost:3001';
-
- test('should update URL when search form changes', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/schedule`);
- await page.waitForLoadState('networkidle');
-
- // Fill departure city input
- const inputs = page.locator('input');
- await inputs.first().fill('Moscow');
-
- // Check URL contains parameter
- const url = page.url();
- expect(url).toContain('city=Moscow');
- });
-
- test('should preserve state on back button', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/schedule`);
-
- // Set initial state
- const inputs = page.locator('input');
- await inputs.first().fill('Moscow');
- await page.waitForLoadState('networkidle');
-
- // Navigate away and back
- await page.goto(`${BASE_URL}/ru-ru/flights`);
- await page.goBack();
- await page.waitForLoadState('networkidle');
-
- // State should be preserved in URL
- const url = page.url();
- expect(url).toContain('city=Moscow');
- });
-
- test('should support forward navigation', async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/schedule`);
-
- const inputs = page.locator('input');
- await inputs.first().fill('St Petersburg');
- await page.waitForLoadState('networkidle');
-
- // Go back then forward
- await page.goBack();
- await page.goForward();
- await page.waitForLoadState('networkidle');
-
- // URL should match forward state
- const url = page.url();
- expect(url).toContain('St');
- });
-
- test('should console have zero errors', async ({ page }) => {
- const errors: string[] = [];
- page.on('console', (msg) => {
- if (msg.type() === 'error') errors.push(msg.text());
- });
-
- await page.goto(`${BASE_URL}/ru-ru/schedule`);
- const inputs = page.locator('input');
- await inputs.first().fill('Moscow');
- await page.waitForLoadState('networkidle');
-
- await page.goBack();
- await page.waitForLoadState('networkidle');
- await page.goForward();
-
- expect(errors).toHaveLength(0);
- });
-});
diff --git a/tests/e2e-angular/ru-ru/input-validation.spec.ts b/tests/e2e-angular/ru-ru/input-validation.spec.ts
deleted file mode 100644
index d8402e23..00000000
--- a/tests/e2e-angular/ru-ru/input-validation.spec.ts
+++ /dev/null
@@ -1,171 +0,0 @@
-import { test, expect } from '@playwright/test';
-
-test.describe('Input Validation and XSS Prevention (US-89)', () => {
- test.beforeEach(async ({ page }) => {
- await page.goto('/');
- // Accept any cookie consent if present
- const cookieButton = page.locator('button:has-text("Accept")').first();
- if (await cookieButton.isVisible({ timeout: 1000 }).catch(() => false)) {
- await cookieButton.click();
- }
- });
-
- test.describe('Flight Search - Valid Input Handling', () => {
- test('should accept valid Cyrillic city names', async ({ page }) => {
- const departureContainer = page.locator('[data-testid="filter-route-departure-input"]');
- const departureInput = departureContainer.locator('input').first();
-
- // Enter valid city name
- await departureInput.fill('Москва');
-
- const inputValue = await departureInput.inputValue();
- expect(inputValue).toBe('Москва');
- });
-
- test('should accept valid Latin city names', async ({ page }) => {
- const arrivalContainer = page.locator('[data-testid="filter-route-arrival-input"]');
- const arrivalInput = arrivalContainer.locator('input').first();
-
- // Enter valid city name
- await arrivalInput.fill('Paris');
-
- const inputValue = await arrivalInput.inputValue();
- expect(inputValue).toBe('Paris');
- });
-
- test('should trim whitespace from city input', async ({ page }) => {
- const departureContainer = page.locator('[data-testid="filter-route-departure-input"]');
- const departureInput = departureContainer.locator('input').first();
-
- // Enter city with extra whitespace
- await departureInput.fill(' Москва ');
- await departureInput.blur();
-
- const inputValue = await departureInput.inputValue();
- // After sanitization, whitespace should be trimmed
- expect(inputValue.trim()).toBe('Москва');
- });
- });
-
- test.describe('No Console Errors on Valid Input', () => {
- test('should not produce XSS-related console errors when handling valid input', async ({
- page,
- }) => {
- // Listen for console errors that indicate XSS attempts
- const xssErrors: string[] = [];
- page.on('console', (msg) => {
- const text = msg.text();
- if (
- msg.type() === 'error' &&
- (text.includes('script') || text.includes('xss') || text.includes('alert'))
- ) {
- xssErrors.push(text);
- }
- });
-
- const departureContainer = page.locator('[data-testid="filter-route-departure-input"]');
- const departureInput = departureContainer.locator('input').first();
-
- // Enter valid city names
- const testInputs = ['Москва', 'Paris', 'London'];
-
- for (const input of testInputs) {
- await departureInput.clear();
- await departureInput.fill(input);
- await departureInput.blur();
- await page.waitForTimeout(50);
- }
-
- // Should not have any XSS-related console errors
- expect(xssErrors).toHaveLength(0);
- });
- });
-
- test.describe('XSS Attack Prevention', () => {
- test('should not execute injected script tags', async ({ page }) => {
- let scriptExecuted = false;
- page.on('dialog', () => {
- scriptExecuted = true;
- });
-
- const departureContainer = page.locator('[data-testid="filter-route-departure-input"]');
- const departureInput = departureContainer.locator('input').first();
-
- // Try to inject script
- await departureInput.fill('');
- await departureInput.blur();
- await page.waitForTimeout(200);
-
- // Alert dialog should not appear
- expect(scriptExecuted).toBe(false);
- });
-
- test('should not execute event handler injections', async ({ page }) => {
- let eventTriggered = false;
- page.on('dialog', () => {
- eventTriggered = true;
- });
-
- const departureContainer = page.locator('[data-testid="filter-route-departure-input"]');
- const departureInput = departureContainer.locator('input').first();
-
- // Try event handler injection via attribute
- await departureInput.fill('City" onload="alert(1)');
- await departureInput.blur();
- await page.waitForTimeout(200);
-
- // Event should not fire
- expect(eventTriggered).toBe(false);
- });
-
- test('should not execute onerror handlers in IMG tags', async ({ page }) => {
- let errorTriggered = false;
- page.on('dialog', () => {
- errorTriggered = true;
- });
-
- const departureContainer = page.locator('[data-testid="filter-route-departure-input"]');
- const departureInput = departureContainer.locator('input').first();
-
- // Try to inject img with onerror
- await departureInput.fill('
');
- await departureInput.blur();
- await page.waitForTimeout(200);
-
- // Alert should not fire
- expect(errorTriggered).toBe(false);
- });
- });
-
- test.describe('Search Functionality Preserved', () => {
- test('should preserve functionality with valid city input', async ({ page }) => {
- const departureContainer = page.locator('[data-testid="filter-route-departure-input"]');
- const departureInput = departureContainer.locator('input').first();
- const arrivalContainer = page.locator('[data-testid="filter-route-arrival-input"]');
- const arrivalInput = arrivalContainer.locator('input').first();
-
- // Enter valid cities
- await departureInput.fill('Москва');
- await arrivalInput.fill('Paris');
-
- expect(await departureInput.inputValue()).toBe('Москва');
- expect(await arrivalInput.inputValue()).toBe('Paris');
- });
-
- test('should allow search button to be clicked after valid input', async ({ page }) => {
- const departureContainer = page.locator('[data-testid="filter-route-departure-input"]');
- const departureInput = departureContainer.locator('input').first();
- const arrivalContainer = page.locator('[data-testid="filter-route-arrival-input"]');
- const arrivalInput = arrivalContainer.locator('input').first();
- const searchBtn = page.locator('[data-testid="filter-route-search"]');
-
- // Enter valid cities
- await departureInput.fill('Москва');
- await arrivalInput.fill('Paris');
-
- // Verify search button is visible and clickable
- expect(await searchBtn.isVisible()).toBe(true);
- expect(await searchBtn.isEnabled()).toBe(true);
- });
- });
-});
diff --git a/tests/e2e-angular/ru-ru/keyboard-navigation.spec.ts b/tests/e2e-angular/ru-ru/keyboard-navigation.spec.ts
deleted file mode 100644
index 7d421628..00000000
--- a/tests/e2e-angular/ru-ru/keyboard-navigation.spec.ts
+++ /dev/null
@@ -1,153 +0,0 @@
-import { test, expect } from '@playwright/test';
-
-const BASE_URL = process.env.BASE_URL || 'http://localhost:3002';
-
-test.describe('US-95: Keyboard Navigation', () => {
- test.beforeEach(async ({ page }) => {
- await page.goto(`${BASE_URL}/ru-ru/schedule`, { waitUntil: 'networkidle' });
- await page.waitForLoadState('networkidle');
- });
-
- test('should navigate search form with Tab key', async ({ page }) => {
- // Get the search form
- const searchForm = page.getByTestId('schedule-search-form');
- await expect(searchForm).toBeVisible();
-
- // Start with Tab from body - should focus first interactive element
- await page.keyboard.press('Tab');
- const focusedElement1 = await page.evaluate(() => {
- const el = document.activeElement as HTMLElement;
- return el?.id || el?.getAttribute('data-testid') || el?.tagName;
- });
-
- expect(focusedElement1).toBeTruthy();
-
- // Continue tabbing through form
- for (let i = 0; i < 5; i++) {
- await page.keyboard.press('Tab');
- }
-
- const focusedElement2 = await page.evaluate(() => {
- const el = document.activeElement as HTMLElement;
- return el?.id || el?.getAttribute('data-testid') || el?.tagName;
- });
-
- // Should have moved to a different element
- expect(focusedElement2).toBeTruthy();
- expect(focusedElement2).not.toBe(focusedElement1);
- });
-
- test('should support Tab navigation to date inputs', async ({ page }) => {
- // Tab to the date input
- for (let i = 0; i < 3; i++) {
- await page.keyboard.press('Tab');
- }
-
- // Current focus should be on a form element
- const focusedEl = await page.evaluate(() => {
- const el = document.activeElement as HTMLElement;
- return el?.getAttribute('id') || el?.tagName;
- });
-
- expect(['date-from', 'INPUT']).toContain(focusedEl);
- });
-
- test('should have proper tabIndex on form elements', async ({ page }) => {
- // Check that key form elements have proper tabIndex attributes
- const departureInput = page.locator('[data-testid="schedule-departure-input"] input').first();
- const arrivalInput = page.locator('[data-testid="schedule-arrival-input"] input').first();
- const dateFromInput = page.locator('#date-from');
- const dateToInput = page.locator('#date-to');
- const searchButton = page.getByTestId('schedule-search-button');
-
- // All should be visible
- await expect(departureInput).toBeVisible();
- await expect(arrivalInput).toBeVisible();
- await expect(dateFromInput).toBeVisible();
- await expect(dateToInput).toBeVisible();
- await expect(searchButton).toBeVisible();
-
- // Check tabIndex attributes exist
- const depTabIndex = await departureInput.getAttribute('tabindex');
- const arrTabIndex = await arrivalInput.getAttribute('tabindex');
- const dateFromTabIndex = await dateFromInput.getAttribute('tabindex');
- const dateToTabIndex = await dateToInput.getAttribute('tabindex');
- const btnTabIndex = await searchButton.getAttribute('tabindex');
-
- // Either tabIndex is set or it's a native form element (which is keyboard accessible by default)
- expect([depTabIndex, '0']).toContain(depTabIndex || '0');
- expect([arrTabIndex, '1']).toContain(arrTabIndex || '1');
- expect([dateFromTabIndex, '2']).toContain(dateFromTabIndex || '2');
- expect([dateToTabIndex, '3']).toContain(dateToTabIndex || '3');
- expect([btnTabIndex, '7']).toContain(btnTabIndex || '7');
- });
-
- test('should have no keyboard traps in form', async ({ page }) => {
- const searchForm = page.getByTestId('schedule-search-form');
- await expect(searchForm).toBeVisible();
-
- // Tab through the form multiple times
- for (let i = 0; i < 20; i++) {
- await page.keyboard.press('Tab');
- }
-
- // Should be able to reach some interactive element (not stuck in a trap)
- const activeElement = await page.evaluate(() => {
- const el = document.activeElement;
- return el?.tagName;
- });
-
- expect(['BUTTON', 'A', 'INPUT', 'SELECT', 'TEXTAREA', 'DIV']).toContain(activeElement);
- });
-
- test('should have zero console errors during keyboard navigation', async ({ page }) => {
- const errors: string[] = [];
-
- page.on('console', (msg) => {
- if (msg.type() === 'error') {
- errors.push(msg.text());
- }
- });
-
- // Perform keyboard navigation
- await page.keyboard.press('Tab');
- await page.keyboard.press('Tab');
- await page.keyboard.press('Tab');
- await page.keyboard.press('Tab');
- await page.keyboard.press('Tab');
-
- // Should have no console errors
- expect(errors).toHaveLength(0);
- });
-
- test('should support Tab navigation through all interactive elements in order', async ({
- page,
- }) => {
- // Get initial focus
- await page.keyboard.press('Tab');
- const firstFocused = await page.evaluate(() => {
- const el = document.activeElement as HTMLElement;
- return el?.getAttribute('data-testid') || el?.id;
- });
-
- // The first element should be the departure input
- expect(firstFocused).toBeTruthy();
-
- // Tab several more times
- const focusedElements = [firstFocused];
- for (let i = 0; i < 10; i++) {
- await page.keyboard.press('Tab');
- const focused = await page.evaluate(() => {
- const el = document.activeElement as HTMLElement;
- return el?.getAttribute('data-testid') || el?.id || el?.getAttribute('type');
- });
- if (focused) {
- focusedElements.push(focused);
- }
- }
-
- // Should have visited multiple different elements
- const uniqueElements = new Set(focusedElements);
- expect(uniqueElements.size).toBeGreaterThan(1);
- });
-});
diff --git a/tests/e2e-angular/ru-ru/large-datasets.spec.ts b/tests/e2e-angular/ru-ru/large-datasets.spec.ts
deleted file mode 100644
index 01886f8d..00000000
--- a/tests/e2e-angular/ru-ru/large-datasets.spec.ts
+++ /dev/null
@@ -1,239 +0,0 @@
-import { test, expect } from '@playwright/test';
-
-const BASE_URL = process.env.BASE_URL || 'http://localhost:3001';
-
-test.describe('US-103: Large Dataset Handling', () => {
- test.beforeEach(async ({ page }) => {
- // Navigate to a page that uses VirtualizedList (schedule page with large datasets)
- await page.goto(`${BASE_URL}/ru-ru/schedule`, { waitUntil: 'networkidle' });
- await page.waitForLoadState('networkidle');
- });
-
- test('should render large list efficiently without freezing', async ({ page }) => {
- // Check for virtualized list presence
- const listContainer = page.getByRole('list');
- await expect(listContainer).toBeVisible();
-
- // Measure initial rendering performance
- const performanceTiming = await page.evaluate(() => {
- const timing = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
- return {
- loadEventEnd: timing.loadEventEnd,
- loadEventStart: timing.loadEventStart,
- domInteractive: timing.domInteractive,
- domContentLoadedEventEnd: timing.domContentLoadedEventEnd,
- };
- });
-
- // Initial page load should complete
- expect(performanceTiming.loadEventEnd).toBeGreaterThan(0);
- });
-
- test('should maintain smooth scrolling without console errors', async ({ page }) => {
- // Capture console messages
- const consoleMessages: { type: string; message: string }[] = [];
- page.on('console', (msg) => {
- if (msg.type() === 'error' || msg.type() === 'warning') {
- consoleMessages.push({ type: msg.type(), message: msg.text() });
- }
- });
-
- const listContainer = page.getByRole('list');
- await expect(listContainer).toBeVisible();
-
- // Scroll down multiple times to simulate large dataset navigation
- for (let i = 0; i < 5; i++) {
- await page.evaluate(() => {
- const scrollable = document.querySelector('[role="list"]');
- if (scrollable) {
- scrollable.scrollTop += 200;
- }
- });
-
- // Allow time for scroll events to process
- await page.waitForTimeout(100);
- }
-
- // Verify no console errors were logged
- const errors = consoleMessages.filter((m) => m.type === 'error');
- expect(errors).toHaveLength(0);
- });
-
- test('should support keyboard navigation Home key', async ({ page }) => {
- const listContainer = page.getByRole('list');
- await expect(listContainer).toBeVisible();
-
- // Focus the list
- await listContainer.focus();
-
- // Press Home key
- await page.keyboard.press('Home');
-
- // Verify list is still visible and in focus
- await expect(listContainer).toBeFocused();
- });
-
- test('should support keyboard navigation End key', async ({ page }) => {
- const listContainer = page.getByRole('list');
- await expect(listContainer).toBeVisible();
-
- // Focus the list
- await listContainer.focus();
-
- // Press End key
- await page.keyboard.press('End');
-
- // Verify list is still visible and in focus
- await expect(listContainer).toBeFocused();
- });
-
- test('should maintain >60 FPS during scroll', async ({ page }) => {
- // Enable performance monitoring
- // frameMetrics tracked during scroll
-
- const listContainer = page.getByRole('list');
- await expect(listContainer).toBeVisible();
-
- // Measure frame times during scroll
- const metricsPromise = page.evaluateHandle(() => {
- return new Promise((resolve) => {
- let frameCount = 0;
- let startTime = performance.now();
- const frameTimes: number[] = [];
-
- function measureFrame() {
- frameCount++;
- const now = performance.now();
- const frameDuration = now - startTime;
- frameTimes.push(frameDuration);
-
- if (frameCount < 30) {
- // Measure 30 frames
- requestAnimationFrame(measureFrame);
- } else {
- const avgFrameTime = frameTimes.reduce((a, b) => a + b, 0) / frameTimes.length;
- resolve(avgFrameTime);
- }
-
- startTime = now;
- }
-
- requestAnimationFrame(measureFrame);
- });
- });
-
- // Perform scrolling
- await page.evaluate(() => {
- const scrollable = document.querySelector('[role="list"]');
- if (scrollable) {
- let scrollAmount = 0;
- const scrollInterval = setInterval(() => {
- scrollable.scrollTop += 50;
- scrollAmount += 50;
- if (scrollAmount > 1000) {
- clearInterval(scrollInterval);
- }
- }, 16); // ~60 FPS
- }
- });
-
- const avgFrameTime = await metricsPromise;
-
- // Frame time should be < 16ms for 60 FPS, allow some tolerance
- expect(avgFrameTime).toBeLessThan(17);
- });
-
- test('should be accessible with proper ARIA attributes', async ({ page }) => {
- const listContainer = page.getByRole('list');
- await expect(listContainer).toBeVisible();
-
- // Verify ARIA label exists
- const ariaLabel = await listContainer.getAttribute('aria-label');
- expect(ariaLabel).toBeTruthy();
-
- // List items should be keyboard accessible
- const listItems = page.locator('[role="button"]').first();
- await expect(listItems).toBeVisible();
- });
-
- test('should handle rapid scroll events', async ({ page }) => {
- const consoleErrors: string[] = [];
- page.on('console', (msg) => {
- if (msg.type() === 'error') {
- consoleErrors.push(msg.text());
- }
- });
-
- const listContainer = page.getByRole('list');
- await expect(listContainer).toBeVisible();
-
- // Perform rapid scrolling
- await page.evaluate(async () => {
- const scrollable = document.querySelector('[role="list"]');
- if (scrollable) {
- for (let i = 0; i < 20; i++) {
- scrollable.scrollTop += 100;
- await new Promise((resolve) => setTimeout(resolve, 10));
- }
- }
- });
-
- // No errors should occur during rapid scrolling
- expect(consoleErrors).toHaveLength(0);
- });
-
- test('should render visible items dynamically', async ({ page }) => {
- const listContainer = page.getByRole('list');
- await expect(listContainer).toBeVisible();
-
- // Get initial visible item count
- const initialVisibleItems = await page
- .locator('[role="button"][aria-selected="false"]')
- .count();
- expect(initialVisibleItems).toBeGreaterThan(0);
-
- // Scroll down
- await page.evaluate(() => {
- const scrollable = document.querySelector('[role="list"]');
- if (scrollable) {
- scrollable.scrollTop += 500;
- }
- });
-
- // Wait for re-render
- await page.waitForTimeout(200);
-
- // Visible items should still exist (virtualization working)
- const visibleItemsAfterScroll = await page.locator('[role="button"]').count();
- expect(visibleItemsAfterScroll).toBeGreaterThan(0);
- });
-
- test('should not have memory leaks during extended scrolling', async ({ page }) => {
- const consoleErrors: string[] = [];
- page.on('console', (msg) => {
- if (msg.type() === 'error') {
- consoleErrors.push(msg.text());
- }
- });
-
- const listContainer = page.getByRole('list');
- await expect(listContainer).toBeVisible();
-
- // Perform extended scrolling simulation
- await page.evaluate(async () => {
- const scrollable = document.querySelector('[role="list"]');
- if (scrollable) {
- for (let i = 0; i < 50; i++) {
- scrollable.scrollTop += 50;
- if (i % 10 === 0) {
- await new Promise((resolve) => setTimeout(resolve, 50));
- }
- }
- }
- });
-
- // No memory-related errors should occur
- expect(consoleErrors).toHaveLength(0);
- await expect(listContainer).toBeVisible();
- });
-});
diff --git a/tests/e2e-angular/ru-ru/persistent-state.spec.ts b/tests/e2e-angular/ru-ru/persistent-state.spec.ts
deleted file mode 100644
index 9260b2e5..00000000
--- a/tests/e2e-angular/ru-ru/persistent-state.spec.ts
+++ /dev/null
@@ -1,117 +0,0 @@
-import { test, expect } from '@playwright/test';
-
-test.describe('US-101: Persistent State Management', () => {
- test('should persist search form state across page reload', async ({ page, context }) => {
- await page.goto('http://localhost:3002/ru-ru/schedule');
- await page.waitForLoadState('networkidle');
-
- // Fill search form
- await page
- .locator('input[placeholder*="city"], [aria-label*="город отправления"]')
- .first()
- .fill('Moscow');
- await page
- .locator('input[placeholder*="city"], [aria-label*="город прибытия"]')
- .nth(1)
- .fill('St Petersburg');
-
- // Reload page
- await page.reload();
- await page.waitForLoadState('networkidle');
-
- // Check that form values are still there
- const fromInput = await page.locator('input').first().inputValue();
- const toInput = await page.locator('input').nth(1).inputValue();
-
- expect(fromInput).toBe('Moscow');
- expect(toInput).toBe('St Petersburg');
- });
-
- test('should respect 30-day expiration for persisted state', async ({ page }) => {
- await page.goto('http://localhost:3002/ru-ru/schedule');
-
- // Store data with old timestamp (31 days ago)
- await page.evaluate(() => {
- const thirtyOneDaysAgo = Date.now() - 31 * 24 * 60 * 60 * 1000;
- const data = JSON.stringify({
- value: { test: 'data' },
- timestamp: thirtyOneDaysAgo,
- });
- localStorage.setItem('aeroflot_expiredTest', data);
- });
-
- // Reload and check if expired data is gone
- await page.reload();
- await page.waitForLoadState('networkidle');
-
- const expiredData = await page.evaluate(() => {
- return localStorage.getItem('aeroflot_expiredTest');
- });
-
- expect(expiredData).toBeNull();
- });
-
- test('should handle localStorage quota gracefully', async ({ page }) => {
- await page.goto('http://localhost:3002/ru-ru/schedule');
-
- // Try to fill with large data (may trigger quota exceeded)
- const largeData = 'x'.repeat(10000);
-
- // Should not crash, should cleanup or fail gracefully
- const result = await page.evaluate(async (data) => {
- try {
- localStorage.setItem('aeroflot_largeData', data);
- return { success: true };
- } catch (error) {
- // Quota exceeded is acceptable
- return { success: false, quotaExceeded: true };
- }
- }, largeData);
-
- // Either succeeds or fails gracefully with quota exceeded
- expect(result.success || result.quotaExceeded).toBe(true);
- });
-
- test('should clear state when requested', async ({ page }) => {
- await page.goto('http://localhost:3002/ru-ru/schedule');
-
- // Store data
- await page.evaluate(() => {
- localStorage.setItem(
- 'aeroflot_clearTest',
- JSON.stringify({
- value: { test: 'data' },
- timestamp: Date.now(),
- }),
- );
- });
-
- // Clear it
- await page.evaluate(() => {
- localStorage.removeItem('aeroflot_clearTest');
- });
-
- // Verify it's gone
- const cleared = await page.evaluate(() => {
- return localStorage.getItem('aeroflot_clearTest');
- });
-
- expect(cleared).toBeNull();
- });
-
- test('should console have zero errors', async ({ page }) => {
- const errors: string[] = [];
- page.on('console', (msg) => {
- if (msg.type() === 'error') errors.push(msg.text());
- });
-
- await page.goto('http://localhost:3002/ru-ru/schedule');
- await page.waitForLoadState('networkidle');
-
- // Interact with persistent state
- await page.locator('input').first().fill('Moscow');
- await page.reload();
-
- expect(errors).toHaveLength(0);
- });
-});
diff --git a/tests/e2e-angular/ru-ru/text-ellipsis.spec.ts b/tests/e2e-angular/ru-ru/text-ellipsis.spec.ts
deleted file mode 100644
index 32606b9c..00000000
--- a/tests/e2e-angular/ru-ru/text-ellipsis.spec.ts
+++ /dev/null
@@ -1,251 +0,0 @@
-import { test, expect } from '@playwright/test';
-
-test.describe('TextEllipsis Component - Text Truncation & Tooltips', () => {
- test.beforeEach(async ({ page }) => {
- // Navigate to the app
- await page.goto('/');
- });
-
- test('should display long Russian city names with ellipsis', async ({ page }) => {
- // Fill departure city with a long Russian name
- const citySelectorDeparture = page.locator('input[placeholder*="Откуда"]').first();
- await citySelectorDeparture.click();
- await citySelectorDeparture.fill('Санкт-Петербург');
-
- // Wait for autocomplete to appear
- await page.waitForTimeout(300);
-
- // Select first option
- const firstOption = page.locator('[role="option"]').first();
- await firstOption.click();
-
- // Verify city name is displayed (may be truncated depending on viewport width)
- const cityDisplay = page.locator('input[placeholder*="Откуда"]').first();
- const value = await cityDisplay.inputValue();
- expect(value).toContain('Санкт-Петербург');
- });
-
- test('should display long airport names with ellipsis', async ({ page }) => {
- // Navigate to flight details where airport names are displayed
- const departureCityInput = page.locator('input[placeholder*="Откуда"]').first();
- await departureCityInput.click();
- await departureCityInput.fill('Москва');
-
- // Wait for suggestions
- await page.waitForTimeout(300);
- const firstSuggestion = page.locator('[role="option"]').first();
- await firstSuggestion.click();
-
- // Set arrival city
- const arrivalCityInput = page.locator('input[placeholder*="Куда"]').first();
- await arrivalCityInput.click();
- await arrivalCityInput.fill('Санкт-Петербург');
-
- await page.waitForTimeout(300);
- const arrivalSuggestion = page.locator('[role="option"]').first();
- await arrivalSuggestion.click();
-
- // Submit search
- await page.locator('button:has-text("Найти")').click();
-
- // Wait for results to load
- await page.waitForLoadState('networkidle');
-
- // Verify no console errors
- const consoleMessages: string[] = [];
- page.on('console', (msg) => {
- if (msg.type() === 'error' || msg.type() === 'warning') {
- consoleMessages.push(`${msg.type()}: ${msg.text()}`);
- }
- });
-
- // Check for flight results
- const flightResults = page.locator('[data-testid*="flight"]').first();
- if (await flightResults.isVisible()) {
- // Verify results are displayed without truncation issues
- expect(consoleMessages).not.toContain(jasmine.stringMatching(/error/i));
- }
- });
-
- test('should show tooltip on hover for long names (desktop)', async ({ page }) => {
- // Set viewport to desktop size
- await page.setViewportSize({ width: 1024, height: 768 });
-
- // Navigate to a page with flight information
- const departureCityInput = page.locator('input[placeholder*="Откуда"]').first();
- await departureCityInput.click();
- await departureCityInput.fill('Москва');
-
- await page.waitForTimeout(300);
- const firstSuggestion = page.locator('[role="option"]').first();
- await firstSuggestion.click();
-
- const arrivalCityInput = page.locator('input[placeholder*="Куда"]').first();
- await arrivalCityInput.click();
- await arrivalCityInput.fill('Екатеринбург');
-
- await page.waitForTimeout(300);
- const arrivalSuggestion = page.locator('[role="option"]').first();
- await arrivalSuggestion.click();
-
- // Search for flights
- await page.locator('button:has-text("Найти")').click();
- await page.waitForLoadState('networkidle');
-
- // Look for elements with title attributes (tooltip indicators)
- const elementsWithTitle = await page.locator('[title]').count();
- expect(elementsWithTitle).toBeGreaterThan(0);
- });
-
- test('should handle text truncation on mobile layout', async ({ page }) => {
- // Set mobile viewport
- await page.setViewportSize({ width: 375, height: 667 });
-
- // Navigate and search
- const departureCityInput = page.locator('input[placeholder*="Откуда"]').first();
- await departureCityInput.click();
- await departureCityInput.fill('Санкт-Петербург');
-
- await page.waitForTimeout(300);
- const firstSuggestion = page.locator('[role="option"]').first();
- await firstSuggestion.click();
-
- const arrivalCityInput = page.locator('input[placeholder*="Куда"]').first();
- await arrivalCityInput.click();
- await arrivalCityInput.fill('Новосибирск');
-
- await page.waitForTimeout(300);
- const arrivalSuggestion = page.locator('[role="option"]').first();
- await arrivalSuggestion.click();
-
- // Submit search
- await page.locator('button:has-text("Найти")').click();
- await page.waitForLoadState('networkidle');
-
- // Verify page is usable and no layout shifts
- const mainContent = page.locator('main').first();
- expect(await mainContent.isVisible()).toBe(true);
-
- // Check for console errors
- let hasErrors = false;
- page.on('console', (msg) => {
- if (msg.type() === 'error') {
- hasErrors = true;
- }
- });
-
- await page.waitForTimeout(500);
- expect(hasErrors).toBe(false);
- });
-
- test('should not cause layout issues with very long names', async ({ page }) => {
- await page.setViewportSize({ width: 768, height: 1024 });
-
- // Navigate to schedule or flight board page
- const scheduleButton = page.locator('a:has-text("Расписание")');
- if (await scheduleButton.isVisible()) {
- await scheduleButton.click();
- await page.waitForLoadState('networkidle');
-
- // Verify layout is not broken
- const mainContent = page.locator('main');
- const boundingBox = await mainContent.boundingBox();
- expect(boundingBox).not.toBeNull();
- expect(boundingBox?.width).toBeGreaterThan(0);
- }
- });
-
- test('should support Unicode characters in truncated text', async ({ page }) => {
- // Test with various Unicode scripts
- const testCities = [
- 'Санкт-Петербург', // Russian Cyrillic
- 'Москва', // Russian Cyrillic
- 'Екатеринбург', // Russian Cyrillic
- ];
-
- for (const city of testCities) {
- const citySelectorInput = page.locator('input[placeholder*="Откуда"]').first();
- await citySelectorInput.click();
- await citySelectorInput.clear();
- await citySelectorInput.fill(city);
-
- // Wait for autocomplete
- await page.waitForTimeout(300);
-
- const firstOption = page.locator('[role="option"]').first();
- if (await firstOption.isVisible()) {
- await firstOption.click();
-
- // Verify text is entered correctly
- const value = await citySelectorInput.inputValue();
- expect(value).toContain(city.charAt(0)); // At least first character present
- }
- }
- });
-
- test('should maintain text accessibility with ellipsis', async ({ page }) => {
- // Verify that full text is still available for screen readers via title attribute
- const departureCityInput = page.locator('input[placeholder*="Откуда"]').first();
- await departureCityInput.click();
- await departureCityInput.fill('Санкт-Петербург');
-
- await page.waitForTimeout(300);
- const firstSuggestion = page.locator('[role="option"]').first();
- await firstSuggestion.click();
-
- // Check that page is accessible
- // Simple check: verify page has proper semantic elements
- const mainElement = page.locator('main').first();
- expect(await mainElement.isVisible()).toBe(true);
-
- // Verify inputs have labels or aria-labels
- const inputElements = page.locator('input').first();
- const ariaLabel = await inputElements.getAttribute('aria-label');
- const placeholder = await inputElements.getAttribute('placeholder');
- expect(ariaLabel || placeholder).toBeTruthy();
- });
-
- test('should handle rapid viewport resizing without layout breaks', async ({ page }) => {
- const departureCityInput = page.locator('input[placeholder*="Откуда"]').first();
- await departureCityInput.click();
- await departureCityInput.fill('Москва');
-
- await page.waitForTimeout(300);
- const firstSuggestion = page.locator('[role="option"]').first();
- await firstSuggestion.click();
-
- // Rapidly change viewport sizes
- const sizes = [
- { width: 375, height: 667 }, // Mobile
- { width: 768, height: 1024 }, // Tablet
- { width: 1920, height: 1080 }, // Desktop
- { width: 375, height: 667 }, // Back to mobile
- ];
-
- for (const size of sizes) {
- await page.setViewportSize(size);
- await page.waitForTimeout(200);
-
- // Verify page is still visible
- const mainContent = page.locator('main').first();
- expect(await mainContent.isVisible()).toBe(true);
- }
- });
-
- test('should not create horizontal scroll with long names', async ({ page }) => {
- await page.setViewportSize({ width: 375, height: 667 });
-
- // Navigate through app with narrow viewport
- const departureCityInput = page.locator('input[placeholder*="Откуда"]').first();
- await departureCityInput.click();
- await departureCityInput.fill('Санкт-Петербург Пулково Международный');
-
- await page.waitForTimeout(300);
-
- // Check for horizontal scroll
- const scrollWidth = await page.evaluate(() => document.documentElement.scrollWidth);
- const clientWidth = await page.evaluate(() => document.documentElement.clientWidth);
-
- expect(scrollWidth).toBeLessThanOrEqual(clientWidth + 1); // Allow 1px tolerance
- });
-});
diff --git a/tests/e2e-angular/ru-ru/text-scaling.spec.ts b/tests/e2e-angular/ru-ru/text-scaling.spec.ts
deleted file mode 100644
index 570886e5..00000000
--- a/tests/e2e-angular/ru-ru/text-scaling.spec.ts
+++ /dev/null
@@ -1,122 +0,0 @@
-import { test, expect } from '@playwright/test';
-
-test.describe('US-99: Text Scaling & Responsive Typography', () => {
- test('should support 150% zoom without horizontal scroll', async ({ page }) => {
- await page.goto('/ru-ru/schedule');
- await page.waitForLoadState('networkidle');
-
- // Set zoom to 150%
- await page.evaluate(() => {
- document.body.style.zoom = '150%';
- });
-
- await page.waitForTimeout(500);
-
- // Check if horizontal scroll is not excessive
- const scrollWidth = await page.evaluate(() => document.documentElement.scrollWidth);
- const clientWidth = await page.evaluate(() => window.innerWidth);
-
- // At 150% zoom, we allow some overflow but content should be mostly accessible
- // Allow up to 10% overflow for UI controls
- expect(scrollWidth).toBeLessThanOrEqual(clientWidth * 1.15);
-
- // Reset zoom
- await page.evaluate(() => {
- document.body.style.zoom = '100%';
- });
- });
-
- test('should support 200% zoom with graceful reflow', async ({ page }) => {
- await page.goto('/ru-ru/schedule');
- await page.waitForLoadState('networkidle');
-
- // Set zoom to 200%
- await page.evaluate(() => {
- document.body.style.zoom = '200%';
- });
-
- await page.waitForTimeout(500);
-
- // Content should be readable and not hidden
- const mainHeading = page.locator('h1, h2, [role="heading"]');
- const headingCount = await mainHeading.count();
-
- if (headingCount > 0) {
- // At least one heading should be visible
- const firstVisible = await mainHeading.first().isVisible();
- expect(firstVisible).toBe(true);
- }
-
- // Check that text content exists (not cut off or hidden)
- const bodyText = await page.locator('body').textContent();
- expect(bodyText?.length).toBeGreaterThan(0);
-
- // Reset zoom
- await page.evaluate(() => {
- document.body.style.zoom = '100%';
- });
- });
-
- test('should use rem-based font sizes that respect browser settings', async ({ page }) => {
- await page.goto('/ru-ru/schedule');
- await page.waitForLoadState('networkidle');
-
- // Check that body uses rem/computed sizes (respects browser font size)
- const computedSize = await page.evaluate(() => {
- const style = window.getComputedStyle(document.body);
- return style.fontSize;
- });
-
- // Should be around 16px or 15px on mobile (base size)
- // Allow 12-20px range to account for responsive breakpoints
- expect(computedSize).toMatch(/^1[2-9]px|20px$/);
- });
-
- test('should have zero console errors during zoom operations', async ({ page }) => {
- const errors: string[] = [];
- const warnings: string[] = [];
-
- page.on('console', (msg) => {
- if (msg.type() === 'error') {
- errors.push(msg.text());
- }
- if (msg.type() === 'warning') {
- warnings.push(msg.text());
- }
- });
-
- // Test at 100%
- await page.goto('/ru-ru/schedule');
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(300);
-
- // Test at 150%
- await page.evaluate(() => {
- document.body.style.zoom = '150%';
- });
- await page.waitForTimeout(500);
-
- // Verify no critical errors
- const criticalErrors = errors.filter(
- (e) => !e.includes('ResizeObserver') && !e.includes('non-Error promise rejection'),
- );
- expect(criticalErrors).toHaveLength(0);
-
- // Test at 200%
- await page.evaluate(() => {
- document.body.style.zoom = '200%';
- });
- await page.waitForTimeout(500);
-
- // Verify still no critical errors
- const criticalErrors2 = errors.filter(
- (e) => !e.includes('ResizeObserver') && !e.includes('non-Error promise rejection'),
- );
- expect(criticalErrors2).toHaveLength(0);
-
- // Reset zoom
- await page.evaluate(() => {
- document.body.style.zoom = '100%';
- });
- });
-});
diff --git a/tests/e2e-angular/ru-ru/touch-navigation.spec.ts b/tests/e2e-angular/ru-ru/touch-navigation.spec.ts
deleted file mode 100644
index 8c4135fc..00000000
--- a/tests/e2e-angular/ru-ru/touch-navigation.spec.ts
+++ /dev/null
@@ -1,149 +0,0 @@
-import { test, expect } from '@playwright/test';
-
-test.describe('US-100: Touch Navigation & Mobile Accessibility', () => {
- test('should have 44px minimum touch targets on buttons', async ({ page, context }) => {
- // Create a mobile context for touch support
- const mobileContext = await context.browser()?.newContext({
- viewport: { width: 375, height: 667 },
- hasTouch: true,
- isMobile: true,
- });
-
- if (!mobileContext) return;
-
- const mobilePage = await mobileContext.newPage();
- await mobilePage.goto('/ru-ru/schedule');
- await mobilePage.waitForLoadState('networkidle');
-
- const buttons = await mobilePage.locator('button').all();
-
- for (const button of buttons) {
- const box = await button.boundingBox();
- if (box) {
- expect(box.width).toBeGreaterThanOrEqual(44);
- expect(box.height).toBeGreaterThanOrEqual(44);
- }
- }
-
- await mobileContext.close();
- });
-
- test('should have proper spacing between touch targets', async ({ page, context }) => {
- // Create a mobile context
- const mobileContext = await context.browser()?.newContext({
- viewport: { width: 375, height: 667 },
- hasTouch: true,
- isMobile: true,
- });
-
- if (!mobileContext) return;
-
- const mobilePage = await mobileContext.newPage();
- await mobilePage.goto('/ru-ru/schedule');
-
- const buttons = await mobilePage.locator('button').all();
-
- // Check if any two adjacent buttons on the same row have sufficient spacing
- let foundValidSpacing = false;
- for (let i = 0; i < buttons.length - 1; i++) {
- const box1 = await buttons[i].boundingBox();
- const box2 = await buttons[i + 1].boundingBox();
-
- if (box1 && box2) {
- // Check if buttons are roughly on the same vertical line (within 10px)
- const onSameRow = Math.abs(box1.y - box2.y) < 10;
-
- if (onSameRow) {
- // Minimum 8px spacing between targets
- const horizontalSpacing = box2.x - (box1.x + box1.width);
- if (horizontalSpacing >= 8) {
- foundValidSpacing = true;
- break;
- }
- }
- }
- }
-
- // If no horizontal spacing found, buttons are likely stacked vertically which is fine
- // Just verify that vertically stacked buttons have minimum size
- if (buttons.length >= 2) {
- const box1 = await buttons[0].boundingBox();
- const box2 = await buttons[1].boundingBox();
-
- if (box1 && box2) {
- // Either they have horizontal spacing >= 8px or they're on different rows (which is valid)
- const onSameRow = Math.abs(box1.y - box2.y) < 10;
- const horizontalSpacing = box2.x - (box1.x + box1.width);
-
- if (onSameRow) {
- expect(horizontalSpacing).toBeGreaterThanOrEqual(8);
- } else {
- // Vertical stacking is valid, just ensure minimum height
- expect(box1.height).toBeGreaterThanOrEqual(44);
- expect(box2.height).toBeGreaterThanOrEqual(44);
- }
- }
- }
-
- await mobileContext.close();
- });
-
- test('should not zoom on input focus', async ({ page, context }) => {
- // Create a mobile context
- const mobileContext = await context.browser()?.newContext({
- viewport: { width: 375, height: 667 },
- hasTouch: true,
- isMobile: true,
- });
-
- if (!mobileContext) return;
-
- const mobilePage = await mobileContext.newPage();
- await mobilePage.goto('/ru-ru/schedule');
-
- // Get initial zoom level
- const initialZoom = await mobilePage.evaluate(() => window.devicePixelRatio);
-
- // Focus an input
- const input = mobilePage.locator('input').first();
- await input.focus();
-
- // Check zoom level hasn't changed
- const zoomAfterFocus = await mobilePage.evaluate(() => window.devicePixelRatio);
-
- expect(zoomAfterFocus).toBe(initialZoom);
-
- await mobileContext.close();
- });
-
- test('should console have zero errors on mobile', async ({ page, context }) => {
- // Create a mobile context with touch support
- const mobileContext = await context.browser()?.newContext({
- viewport: { width: 375, height: 667 },
- hasTouch: true,
- isMobile: true,
- });
-
- if (!mobileContext) return;
-
- const mobilePage = await mobileContext.newPage();
-
- const errors: string[] = [];
- mobilePage.on('console', (msg) => {
- if (msg.type() === 'error') errors.push(msg.text());
- });
-
- await mobilePage.goto('/ru-ru/schedule');
- await mobilePage.waitForLoadState('networkidle');
-
- // Simulate touch interactions
- const buttons = await mobilePage.locator('button').all();
- if (buttons.length > 0) {
- await buttons[0].tap();
- }
-
- expect(errors).toHaveLength(0);
-
- await mobileContext.close();
- });
-});
diff --git a/tests/e2e-angular/ru-ru/unicode-support.spec.ts b/tests/e2e-angular/ru-ru/unicode-support.spec.ts
deleted file mode 100644
index 9dab4650..00000000
--- a/tests/e2e-angular/ru-ru/unicode-support.spec.ts
+++ /dev/null
@@ -1,278 +0,0 @@
-import { test, expect } from '@playwright/test';
-
-test.describe('Unicode Character Support (US-92)', () => {
- test.beforeEach(async ({ page }) => {
- await page.goto('/');
- // Accept any cookie consent if present
- const cookieButton = page.locator('button:has-text("Accept")').first();
- if (await cookieButton.isVisible({ timeout: 1000 }).catch(() => false)) {
- await cookieButton.click();
- }
- });
-
- test.describe('Unicode City Names - CJK Languages', () => {
- test('should accept Chinese city names (北京)', async ({ page }) => {
- const departureContainer = page.locator('[data-testid="filter-route-departure-input"]');
- const departureInput = departureContainer.locator('input').first();
-
- // Enter Chinese city name
- await departureInput.fill('北京');
-
- const inputValue = await departureInput.inputValue();
- expect(inputValue).toBe('北京');
- });
-
- test('should accept Japanese city names (東京)', async ({ page }) => {
- const departureContainer = page.locator('[data-testid="filter-route-departure-input"]');
- const departureInput = departureContainer.locator('input').first();
-
- // Enter Japanese city name
- await departureInput.fill('東京');
-
- const inputValue = await departureInput.inputValue();
- expect(inputValue).toBe('東京');
- });
-
- test('should accept Korean city names (서울)', async ({ page }) => {
- const departureContainer = page.locator('[data-testid="filter-route-departure-input"]');
- const departureInput = departureContainer.locator('input').first();
-
- // Enter Korean city name
- await departureInput.fill('서울');
-
- const inputValue = await departureInput.inputValue();
- expect(inputValue).toBe('서울');
- });
- });
-
- test.describe('Unicode City Names - Arabic', () => {
- test('should accept Arabic city names (مصر)', async ({ page }) => {
- const departureContainer = page.locator('[data-testid="filter-route-departure-input"]');
- const departureInput = departureContainer.locator('input').first();
-
- // Enter Arabic city name
- await departureInput.fill('مصر');
-
- const inputValue = await departureInput.inputValue();
- expect(inputValue).toBe('مصر');
- });
-
- test('should accept Arabic city names (القاهرة)', async ({ page }) => {
- const departureContainer = page.locator('[data-testid="filter-route-departure-input"]');
- const departureInput = departureContainer.locator('input').first();
-
- // Enter Arabic city name
- await departureInput.fill('القاهرة');
-
- const inputValue = await departureInput.inputValue();
- expect(inputValue).toBe('القاهرة');
- });
-
- test('should accept Arabic city names (دبي)', async ({ page }) => {
- const departureContainer = page.locator('[data-testid="filter-route-departure-input"]');
- const departureInput = departureContainer.locator('input').first();
-
- // Enter Arabic city name
- await departureInput.fill('دبي');
-
- const inputValue = await departureInput.inputValue();
- expect(inputValue).toBe('دبي');
- });
- });
-
- test.describe('Unicode City Names - Thai', () => {
- test('should accept Thai city names (กรุงเทพ)', async ({ page }) => {
- const departureContainer = page.locator('[data-testid="filter-route-departure-input"]');
- const departureInput = departureContainer.locator('input').first();
-
- // Enter Thai city name
- await departureInput.fill('กรุงเทพ');
-
- const inputValue = await departureInput.inputValue();
- expect(inputValue).toBe('กรุงเทพ');
- });
-
- test('should accept Thai city names (เชียงใหม่)', async ({ page }) => {
- const departureContainer = page.locator('[data-testid="filter-route-departure-input"]');
- const departureInput = departureContainer.locator('input').first();
-
- // Enter Thai city name
- await departureInput.fill('เชียงใหม่');
-
- const inputValue = await departureInput.inputValue();
- expect(inputValue).toBe('เชียงใหม่');
- });
- });
-
- test.describe('Unicode with Punctuation and Spaces', () => {
- test('should accept Cyrillic with hyphens (Санкт-Петербург)', async ({ page }) => {
- const departureContainer = page.locator('[data-testid="filter-route-departure-input"]');
- const departureInput = departureContainer.locator('input').first();
-
- // Enter Cyrillic city with hyphen
- await departureInput.fill('Санкт-Петербург');
-
- const inputValue = await departureInput.inputValue();
- expect(inputValue).toBe('Санкт-Петербург');
- });
-
- test('should accept Latin with spaces (New York)', async ({ page }) => {
- const departureContainer = page.locator('[data-testid="filter-route-departure-input"]');
- const departureInput = departureContainer.locator('input').first();
-
- // Enter Latin city with space
- await departureInput.fill('New York');
-
- const inputValue = await departureInput.inputValue();
- expect(inputValue).toBe('New York');
- });
-
- test("should accept Latin with apostrophes (L'Aquila)", async ({ page }) => {
- const departureContainer = page.locator('[data-testid="filter-route-departure-input"]');
- const departureInput = departureContainer.locator('input').first();
-
- // Enter Latin city with apostrophe
- await departureInput.fill("L'Aquila");
-
- const inputValue = await departureInput.inputValue();
- expect(inputValue).toBe("L'Aquila");
- });
- });
-
- test.describe('Unicode Rejection - Invalid Characters', () => {
- test('should reject emoji characters', async ({ page }) => {
- const consoleErrors: string[] = [];
- page.on('console', (msg) => {
- if (msg.type() === 'error') {
- consoleErrors.push(msg.text());
- }
- });
-
- const departureContainer = page.locator('[data-testid="filter-route-departure-input"]');
- const departureInput = departureContainer.locator('input').first();
-
- // Try to enter emoji with text
- await departureInput.fill('Москва 🎉');
- await departureInput.blur();
- await page.waitForTimeout(100);
-
- // Input should not accept or display emoji
- const inputValue = await departureInput.inputValue();
- expect(inputValue).not.toContain('🎉');
- });
-
- test('should reject numeric characters', async ({ page }) => {
- const departureContainer = page.locator('[data-testid="filter-route-departure-input"]');
- const departureInput = departureContainer.locator('input').first();
-
- // Try to enter text with numbers
- await departureInput.fill('City123');
- await departureInput.blur();
- await page.waitForTimeout(100);
-
- // Input should not accept numbers
- const inputValue = await departureInput.inputValue();
- expect(inputValue).not.toContain('123');
- });
- });
-
- test.describe('Unicode Preservation in UI', () => {
- test('should preserve Unicode characters when clearing and re-entering', async ({ page }) => {
- const departureContainer = page.locator('[data-testid="filter-route-departure-input"]');
- const departureInput = departureContainer.locator('input').first();
-
- // First entry
- await departureInput.fill('北京');
- let inputValue = await departureInput.inputValue();
- expect(inputValue).toBe('北京');
-
- // Clear and re-enter
- await departureInput.clear();
- await departureInput.fill('北京');
- inputValue = await departureInput.inputValue();
- expect(inputValue).toBe('北京');
- });
-
- test('should handle multiple Unicode scripts in sequence', async ({ page }) => {
- const departureContainer = page.locator('[data-testid="filter-route-departure-input"]');
- const departureInput = departureContainer.locator('input').first();
- const arrivalContainer = page.locator('[data-testid="filter-route-arrival-input"]');
- const arrivalInput = arrivalContainer.locator('input').first();
-
- // Enter different Unicode scripts
- await departureInput.fill('北京');
- await arrivalInput.fill('مصر');
-
- expect(await departureInput.inputValue()).toBe('北京');
- expect(await arrivalInput.inputValue()).toBe('مصر');
- });
-
- test('should trim whitespace from Unicode city names', async ({ page }) => {
- const departureContainer = page.locator('[data-testid="filter-route-departure-input"]');
- const departureInput = departureContainer.locator('input').first();
-
- // Enter Unicode with whitespace
- await departureInput.fill(' 東京 ');
- await departureInput.blur();
-
- const inputValue = await departureInput.inputValue();
- // After sanitization, whitespace should be trimmed
- expect(inputValue.trim()).toBe('東京');
- });
- });
-
- test.describe('Console - No Errors on Unicode Input', () => {
- test('should not produce errors when handling Unicode characters', async ({ page }) => {
- const consoleErrors: string[] = [];
- page.on('console', (msg) => {
- if (msg.type() === 'error') {
- consoleErrors.push(msg.text());
- }
- });
-
- const departureContainer = page.locator('[data-testid="filter-route-departure-input"]');
- const departureInput = departureContainer.locator('input').first();
-
- // Test various Unicode scripts
- const testInputs = ['北京', 'مصر', 'กรุงเทพ', 'Москва', 'Paris'];
-
- for (const input of testInputs) {
- await departureInput.clear();
- await departureInput.fill(input);
- await departureInput.blur();
- await page.waitForTimeout(50);
- }
-
- // Should not have any console errors related to Unicode handling
- expect(consoleErrors).toHaveLength(0);
- });
-
- test('should not throw errors on search with Unicode input', async ({ page }) => {
- const consoleErrors: string[] = [];
- page.on('console', (msg) => {
- if (msg.type() === 'error') {
- consoleErrors.push(msg.text());
- }
- });
-
- const departureContainer = page.locator('[data-testid="filter-route-departure-input"]');
- const departureInput = departureContainer.locator('input').first();
- const arrivalContainer = page.locator('[data-testid="filter-route-arrival-input"]');
- const arrivalInput = arrivalContainer.locator('input').first();
- const searchBtn = page.locator('[data-testid="filter-route-search"]');
-
- // Enter Unicode cities and attempt search
- await departureInput.fill('北京');
- await arrivalInput.fill('مصر');
-
- // Try to click search (if enabled)
- if (await searchBtn.isEnabled()) {
- await searchBtn.click();
- await page.waitForTimeout(200);
- }
-
- // Should not have console errors
- expect(consoleErrors).toHaveLength(0);
- });
- });
-});
diff --git a/tests/e2e-angular/schedule-details.spec.ts b/tests/e2e-angular/schedule-details.spec.ts
deleted file mode 100644
index 819b7189..00000000
--- a/tests/e2e-angular/schedule-details.spec.ts
+++ /dev/null
@@ -1,564 +0,0 @@
-import { test, expect } from '@playwright/test';
-
-test.describe('Schedule Details - Document 4 Phase 2 (US-42, US-46)', () => {
- test.beforeEach(async ({ page }) => {
- // Navigate to schedule page
- await page.goto('http://localhost:3005/schedule');
- await page.waitForLoadState('networkidle');
- });
-
- test.describe('US-42: Multi-leg Flights Display', () => {
- test('should display multi-leg flight badge for connecting flights', async ({ page }) => {
- // Perform search for flights
- const departureInput = page.locator('[data-testid="schedule-departure-input"] input');
- const arrivalInput = page.locator('[data-testid="schedule-arrival-input"] input');
-
- await departureInput.fill('SVO');
- await arrivalInput.fill('AER');
-
- // Set date and submit
- const dateInputs = page.locator('[data-testid="schedule-date-input"]');
- await dateInputs.first().click();
- await page.waitForTimeout(500);
-
- // Look for search button
- const searchButton = page.locator('button:has-text("Search")');
- if (await searchButton.isVisible()) {
- await searchButton.click();
- }
-
- // Wait for results
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- // Click on a flight to see details
- const flightItems = page.locator('[data-testid="schedule-flight-item"]');
- if ((await flightItems.count()) > 0) {
- await flightItems.first().click();
- await page.waitForTimeout(300);
-
- // Check for multi-leg display (badge might appear for connecting flights)
- const multiLegBadge = page.locator('text=Connecting Flight');
- const isVisible = await multiLegBadge.isVisible().catch(() => false);
- // Badge may or may not be visible depending on test data
- expect(isVisible || true).toBeTruthy();
- }
- });
-
- test('should display segment count for multi-leg flights', async ({ page }) => {
- // Perform search
- const departureInput = page.locator('[data-testid="schedule-departure-input"] input');
- const arrivalInput = page.locator('[data-testid="schedule-arrival-input"] input');
-
- await departureInput.fill('SVO');
- await arrivalInput.fill('VKO');
-
- const dateInputs = page.locator('[data-testid="schedule-date-input"]');
- await dateInputs.first().click();
- await page.waitForTimeout(500);
-
- const searchButton = page.locator('button:has-text("Search")');
- if (await searchButton.isVisible()) {
- await searchButton.click();
- }
-
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- // Check if segments are displayed
- const segmentInfo = page.locator('text=segments');
- const isVisible = await segmentInfo.isVisible().catch(() => false);
- expect(isVisible || true).toBeTruthy();
- });
-
- test('should display flight legs with individual details', async ({ page }) => {
- const departureInput = page.locator('[data-testid="schedule-departure-input"] input');
- const arrivalInput = page.locator('[data-testid="schedule-arrival-input"] input');
-
- await departureInput.fill('SVO');
- await arrivalInput.fill('AER');
-
- const dateInputs = page.locator('[data-testid="schedule-date-input"]');
- await dateInputs.first().click();
- await page.waitForTimeout(500);
-
- const searchButton = page.locator('button:has-text("Search")');
- if (await searchButton.isVisible()) {
- await searchButton.click();
- }
-
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- const flightItems = page.locator('[data-testid="schedule-flight-item"]');
- if ((await flightItems.count()) > 0) {
- await flightItems.first().click();
- await page.waitForTimeout(300);
-
- // Look for leg indicators
- const legLabels = page.locator('text=/Leg \\d+/');
- const legCount = await legLabels.count();
- // May or may not have multi-leg flights in test data
- expect(legCount >= 0).toBeTruthy();
- }
- });
-
- test('should display stopover information between legs', async ({ page }) => {
- const departureInput = page.locator('[data-testid="schedule-departure-input"] input');
- const arrivalInput = page.locator('[data-testid="schedule-arrival-input"] input');
-
- await departureInput.fill('SVO');
- await arrivalInput.fill('LED');
-
- const dateInputs = page.locator('[data-testid="schedule-date-input"]');
- await dateInputs.first().click();
- await page.waitForTimeout(500);
-
- const searchButton = page.locator('button:has-text("Search")');
- if (await searchButton.isVisible()) {
- await searchButton.click();
- }
-
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- const flightItems = page.locator('[data-testid="schedule-flight-item"]');
- if ((await flightItems.count()) > 0) {
- await flightItems.first().click();
- await page.waitForTimeout(300);
-
- // Look for stopover/ground time info
- const groundTimeInfo = page.locator('text=Ground time');
- const isVisible = await groundTimeInfo.isVisible().catch(() => false);
- expect(isVisible || true).toBeTruthy();
- }
- });
-
- test('should mark tight connections with warning', async ({ page }) => {
- const departureInput = page.locator('[data-testid="schedule-departure-input"] input');
- const arrivalInput = page.locator('[data-testid="schedule-arrival-input"] input');
-
- await departureInput.fill('SVO');
- await arrivalInput.fill('AER');
-
- const dateInputs = page.locator('[data-testid="schedule-date-input"]');
- await dateInputs.first().click();
- await page.waitForTimeout(500);
-
- const searchButton = page.locator('button:has-text("Search")');
- if (await searchButton.isVisible()) {
- await searchButton.click();
- }
-
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- const flightItems = page.locator('[data-testid="schedule-flight-item"]');
- if ((await flightItems.count()) > 0) {
- await flightItems.first().click();
- await page.waitForTimeout(300);
-
- // Look for tight connection warning
- const tightConnectionWarning = page.locator('text=/Tight connection|⚠️/');
- const isVisible = await tightConnectionWarning.isVisible().catch(() => false);
- expect(isVisible || true).toBeTruthy();
- }
- });
-
- test('should display aircraft equipment for each leg', async ({ page }) => {
- const departureInput = page.locator('[data-testid="schedule-departure-input"] input');
- const arrivalInput = page.locator('[data-testid="schedule-arrival-input"] input');
-
- await departureInput.fill('SVO');
- await arrivalInput.fill('VKO');
-
- const dateInputs = page.locator('[data-testid="schedule-date-input"]');
- await dateInputs.first().click();
- await page.waitForTimeout(500);
-
- const searchButton = page.locator('button:has-text("Search")');
- if (await searchButton.isVisible()) {
- await searchButton.click();
- }
-
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- const flightItems = page.locator('[data-testid="schedule-flight-item"]');
- if ((await flightItems.count()) > 0) {
- await flightItems.first().click();
- await page.waitForTimeout(300);
-
- // Look for equipment info (✈ symbol)
- const equipmentInfo = page.locator('text=/✈|equipment/i');
- const isVisible = await equipmentInfo.isVisible().catch(() => false);
- expect(isVisible || true).toBeTruthy();
- }
- });
-
- test('should handle three-leg or longer routes correctly', async ({ page }) => {
- const departureInput = page.locator('[data-testid="schedule-departure-input"] input');
- const arrivalInput = page.locator('[data-testid="schedule-arrival-input"] input');
-
- await departureInput.fill('SVO');
- await arrivalInput.fill('AER');
-
- const dateInputs = page.locator('[data-testid="schedule-date-input"]');
- await dateInputs.first().click();
- await page.waitForTimeout(500);
-
- const searchButton = page.locator('button:has-text("Search")');
- if (await searchButton.isVisible()) {
- await searchButton.click();
- }
-
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- const flightItems = page.locator('[data-testid="schedule-flight-item"]');
- if ((await flightItems.count()) > 0) {
- await flightItems.first().click();
- await page.waitForTimeout(300);
-
- // Look for Leg 3 or higher
- const leg3Label = page.locator('text=Leg 3');
- const isVisible = await leg3Label.isVisible().catch(() => false);
- expect(isVisible || true).toBeTruthy();
- }
- });
- });
-
- test.describe('US-46: Back Button on Flight Details', () => {
- test('should display back button on flight details page', async ({ page }) => {
- const departureInput = page.locator('[data-testid="schedule-departure-input"] input');
- const arrivalInput = page.locator('[data-testid="schedule-arrival-input"] input');
-
- await departureInput.fill('SVO');
- await arrivalInput.fill('AER');
-
- const dateInputs = page.locator('[data-testid="schedule-date-input"]');
- await dateInputs.first().click();
- await page.waitForTimeout(500);
-
- const searchButton = page.locator('button:has-text("Search")');
- if (await searchButton.isVisible()) {
- await searchButton.click();
- }
-
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- // Open flight details
- const flightItems = page.locator('[data-testid="schedule-flight-item"]');
- if ((await flightItems.count()) > 0) {
- await flightItems.first().click();
- await page.waitForTimeout(300);
-
- // Check for back button
- const backButton = page.locator('[data-testid="flight-details-back-btn"]');
- await expect(backButton).toBeVisible();
- }
- });
-
- test('back button should navigate back to results list', async ({ page }) => {
- const departureInput = page.locator('[data-testid="schedule-departure-input"] input');
- const arrivalInput = page.locator('[data-testid="schedule-arrival-input"] input');
-
- await departureInput.fill('SVO');
- await arrivalInput.fill('AER');
-
- const dateInputs = page.locator('[data-testid="schedule-date-input"]');
- await dateInputs.first().click();
- await page.waitForTimeout(500);
-
- const searchButton = page.locator('button:has-text("Search")');
- if (await searchButton.isVisible()) {
- await searchButton.click();
- }
-
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- // Get URL before clicking flight
- const urlBeforeClick = page.url();
-
- // Open flight details
- const flightItems = page.locator('[data-testid="schedule-flight-item"]');
- if ((await flightItems.count()) > 0) {
- await flightItems.first().click();
- await page.waitForTimeout(300);
-
- // Verify we're on details page
- const detailsUrl = page.url();
- expect(detailsUrl).not.toEqual(urlBeforeClick);
-
- // Click back button
- const backButton = page.locator('[data-testid="flight-details-back-btn"]');
- if (await backButton.isVisible()) {
- await backButton.click();
- await page.waitForTimeout(300);
-
- // Should return to results
- const finalUrl = page.url();
- expect(finalUrl).toContain('schedule');
- }
- }
- });
-
- test('back button should be keyboard accessible', async ({ page }) => {
- const departureInput = page.locator('[data-testid="schedule-departure-input"] input');
- const arrivalInput = page.locator('[data-testid="schedule-arrival-input"] input');
-
- await departureInput.fill('SVO');
- await arrivalInput.fill('AER');
-
- const dateInputs = page.locator('[data-testid="schedule-date-input"]');
- await dateInputs.first().click();
- await page.waitForTimeout(500);
-
- const searchButton = page.locator('button:has-text("Search")');
- if (await searchButton.isVisible()) {
- await searchButton.click();
- }
-
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- const flightItems = page.locator('[data-testid="schedule-flight-item"]');
- if ((await flightItems.count()) > 0) {
- await flightItems.first().click();
- await page.waitForTimeout(300);
-
- // Focus on back button using Tab
- const backButton = page.locator('[data-testid="flight-details-back-btn"]');
- if (await backButton.isVisible()) {
- await backButton.focus();
-
- // Verify it's focused
- const isFocused = await page.evaluate(() => {
- const el = document.activeElement;
- return (
- (el as HTMLElement)?.hasAttribute('data-testid') &&
- (el as HTMLElement)?.getAttribute('data-testid') === 'flight-details-back-btn'
- );
- });
-
- expect(isFocused || true).toBeTruthy(); // May vary based on focus management
- }
- }
- });
-
- test('back button should have accessible aria-label', async ({ page }) => {
- const departureInput = page.locator('[data-testid="schedule-departure-input"] input');
- const arrivalInput = page.locator('[data-testid="schedule-arrival-input"] input');
-
- await departureInput.fill('SVO');
- await arrivalInput.fill('AER');
-
- const dateInputs = page.locator('[data-testid="schedule-date-input"]');
- await dateInputs.first().click();
- await page.waitForTimeout(500);
-
- const searchButton = page.locator('button:has-text("Search")');
- if (await searchButton.isVisible()) {
- await searchButton.click();
- }
-
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- const flightItems = page.locator('[data-testid="schedule-flight-item"]');
- if ((await flightItems.count()) > 0) {
- await flightItems.first().click();
- await page.waitForTimeout(300);
-
- const backButton = page.locator('[data-testid="flight-details-back-btn"]');
- if (await backButton.isVisible()) {
- const ariaLabel = await backButton.getAttribute('aria-label');
- expect(ariaLabel).toBeTruthy();
- expect(ariaLabel).toMatch(/back|назад|Back/i);
- }
- }
- });
-
- test('back button should be mobile-friendly (appropriate size)', async ({ page }) => {
- // Set mobile viewport
- await page.setViewportSize({ width: 375, height: 667 });
-
- const departureInput = page.locator('[data-testid="schedule-departure-input"] input');
- const arrivalInput = page.locator('[data-testid="schedule-arrival-input"] input');
-
- await departureInput.fill('SVO');
- await arrivalInput.fill('AER');
-
- const dateInputs = page.locator('[data-testid="schedule-date-input"]');
- await dateInputs.first().click();
- await page.waitForTimeout(500);
-
- const searchButton = page.locator('button:has-text("Search")');
- if (await searchButton.isVisible()) {
- await searchButton.click();
- }
-
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- const flightItems = page.locator('[data-testid="schedule-flight-item"]');
- if ((await flightItems.count()) > 0) {
- await flightItems.first().click();
- await page.waitForTimeout(300);
-
- const backButton = page.locator('[data-testid="flight-details-back-btn"]');
- if (await backButton.isVisible()) {
- // Check that button is visible and accessible on mobile
- const boundingBox = await backButton.boundingBox();
- expect(boundingBox).toBeTruthy();
- if (boundingBox) {
- // Button should have reasonable size (at least 36x36 for touch targets)
- expect(boundingBox.width).toBeGreaterThanOrEqual(24);
- expect(boundingBox.height).toBeGreaterThanOrEqual(24);
- }
- }
- }
- });
-
- test('back button should preserve search context', async ({ page }) => {
- const departureInput = page.locator('[data-testid="schedule-departure-input"] input');
- const arrivalInput = page.locator('[data-testid="schedule-arrival-input"] input');
-
- await departureInput.fill('SVO');
- await arrivalInput.fill('AER');
-
- const dateInputs = page.locator('[data-testid="schedule-date-input"]');
- await dateInputs.first().click();
- await page.waitForTimeout(500);
-
- const searchButton = page.locator('button:has-text("Search")');
- if (await searchButton.isVisible()) {
- await searchButton.click();
- }
-
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- const flightItems = page.locator('[data-testid="schedule-flight-item"]');
- if ((await flightItems.count()) > 0) {
- // Get initial flight count
- const initialFlightCount = await flightItems.count();
-
- await flightItems.first().click();
- await page.waitForTimeout(300);
-
- // Click back
- const backButton = page.locator('[data-testid="flight-details-back-btn"]');
- if (await backButton.isVisible()) {
- await backButton.click();
- await page.waitForTimeout(300);
-
- // Check that results are still there with same flight count
- const finalFlightCount = await flightItems.count();
- expect(finalFlightCount).toBeGreaterThan(0);
- expect(finalFlightCount).toEqual(initialFlightCount);
- }
- }
- });
- });
-
- test.describe('Console Audit - Multi-leg & Back Button', () => {
- test('should have no console errors with multi-leg flights', async ({ page }) => {
- const errors: string[] = [];
-
- page.on('console', (message) => {
- if (message.type() === 'error') {
- errors.push(message.text());
- }
- });
-
- const departureInput = page.locator('[data-testid="schedule-departure-input"] input');
- const arrivalInput = page.locator('[data-testid="schedule-arrival-input"] input');
-
- await departureInput.fill('SVO');
- await arrivalInput.fill('AER');
-
- const dateInputs = page.locator('[data-testid="schedule-date-input"]');
- await dateInputs.first().click();
- await page.waitForTimeout(500);
-
- const searchButton = page.locator('button:has-text("Search")');
- if (await searchButton.isVisible()) {
- await searchButton.click();
- }
-
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- const flightItems = page.locator('[data-testid="schedule-flight-item"]');
- if ((await flightItems.count()) > 0) {
- await flightItems.first().click();
- await page.waitForTimeout(300);
- }
-
- const criticalErrors = errors.filter(
- (e) =>
- !e.includes('hydration') &&
- !e.includes('useLayoutEffect') &&
- !e.includes('act()') &&
- !e.includes('warning') &&
- e.length > 0,
- );
-
- expect(criticalErrors).toHaveLength(0);
- });
-
- test('should have no console errors when clicking back button', async ({ page }) => {
- const errors: string[] = [];
-
- page.on('console', (message) => {
- if (message.type() === 'error') {
- errors.push(message.text());
- }
- });
-
- const departureInput = page.locator('[data-testid="schedule-departure-input"] input');
- const arrivalInput = page.locator('[data-testid="schedule-arrival-input"] input');
-
- await departureInput.fill('SVO');
- await arrivalInput.fill('AER');
-
- const dateInputs = page.locator('[data-testid="schedule-date-input"]');
- await dateInputs.first().click();
- await page.waitForTimeout(500);
-
- const searchButton = page.locator('button:has-text("Search")');
- if (await searchButton.isVisible()) {
- await searchButton.click();
- }
-
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- const flightItems = page.locator('[data-testid="schedule-flight-item"]');
- if ((await flightItems.count()) > 0) {
- await flightItems.first().click();
- await page.waitForTimeout(300);
-
- const backButton = page.locator('[data-testid="flight-details-back-btn"]');
- if (await backButton.isVisible()) {
- await backButton.click();
- await page.waitForTimeout(300);
- }
- }
-
- const criticalErrors = errors.filter(
- (e) =>
- !e.includes('hydration') &&
- !e.includes('useLayoutEffect') &&
- !e.includes('act()') &&
- !e.includes('warning') &&
- e.length > 0,
- );
-
- expect(criticalErrors).toHaveLength(0);
- });
- });
-});
diff --git a/tests/e2e-angular/schedule-filters.spec.ts b/tests/e2e-angular/schedule-filters.spec.ts
deleted file mode 100644
index 9c046670..00000000
--- a/tests/e2e-angular/schedule-filters.spec.ts
+++ /dev/null
@@ -1,530 +0,0 @@
-import { test, expect, Page } from '@playwright/test';
-
-const BASE_URL = process.env.BASE_URL || 'http://localhost:5173';
-
-test.describe('Schedule Filters - US-28 to US-33', () => {
- test.beforeEach(async ({ page }) => {
- // Navigate to schedule search page
- await page.goto(`${BASE_URL}/schedule`);
- // Wait for page to be loaded
- await page.waitForSelector('[data-testid="schedule-search-form"]', { timeout: 5000 });
- });
-
- test.describe('US-28: Round Trip Search Toggle', () => {
- test('should render round-trip checkbox', async ({ page }) => {
- const returnCheckbox = page.getByTestId('schedule-return-checkbox');
- await expect(returnCheckbox).toBeVisible();
- await expect(returnCheckbox).not.toBeChecked();
- });
-
- test('should show return date inputs when round-trip is enabled', async ({ page }) => {
- // Initially, return calendar should not be visible
- let returnCalendar = page.getByTestId('schedule-return-calendar');
- await expect(returnCalendar).not.toBeVisible();
-
- // Click to enable return flight
- const returnCheckbox = page.getByTestId('schedule-return-checkbox');
- await returnCheckbox.click();
-
- // Return calendar should now be visible
- returnCalendar = page.getByTestId('schedule-return-calendar');
- await expect(returnCalendar).toBeVisible();
- });
-
- test('should hide return date inputs when round-trip is disabled', async ({ page }) => {
- const returnCheckbox = page.getByTestId('schedule-return-checkbox');
-
- // Enable round-trip
- await returnCheckbox.click();
- let returnCalendar = page.getByTestId('schedule-return-calendar');
- await expect(returnCalendar).toBeVisible();
-
- // Disable round-trip
- await returnCheckbox.click();
- returnCalendar = page.getByTestId('schedule-return-calendar');
- await expect(returnCalendar).not.toBeVisible();
- });
-
- test('should toggle return flight multiple times', async ({ page }) => {
- const returnCheckbox = page.getByTestId('schedule-return-checkbox');
- const returnCalendar = page.getByTestId('schedule-return-calendar');
-
- // Toggle on-off-on
- for (let i = 0; i < 2; i++) {
- await returnCheckbox.click();
- await expect(returnCalendar).toBeVisible();
-
- await returnCheckbox.click();
- await expect(returnCalendar).not.toBeVisible();
- }
-
- // Final state: on
- await returnCheckbox.click();
- await expect(returnCalendar).toBeVisible();
- });
- });
-
- test.describe('US-29: Direct Flights Only Filter', () => {
- test('should render direct flights checkbox', async ({ page }) => {
- const directCheckbox = page.getByTestId('schedule-direct-only-checkbox');
- await expect(directCheckbox).toBeVisible();
- await expect(directCheckbox).not.toBeChecked();
- });
-
- test('should toggle direct flights filter', async ({ page }) => {
- const directCheckbox = page.getByTestId('schedule-direct-only-checkbox');
-
- // Check
- await directCheckbox.click();
- await expect(directCheckbox).toBeChecked();
-
- // Uncheck
- await directCheckbox.click();
- await expect(directCheckbox).not.toBeChecked();
- });
-
- test('should maintain direct filter state while interacting with other form elements', async ({
- page,
- }) => {
- const directCheckbox = page.getByTestId('schedule-direct-only-checkbox');
- const departureInput = page.getByTestId('schedule-departure-input');
-
- // Enable direct filter
- await directCheckbox.click();
- await expect(directCheckbox).toBeChecked();
-
- // Interact with departure input
- const input = departureInput.locator('input').first();
- await input.focus();
-
- // Direct filter should still be checked
- await expect(directCheckbox).toBeChecked();
- });
-
- test('should allow toggling direct filter multiple times', async ({ page }) => {
- const directCheckbox = page.getByTestId('schedule-direct-only-checkbox');
-
- for (let i = 0; i < 3; i++) {
- await directCheckbox.click();
- await expect(directCheckbox).toBeChecked();
-
- await directCheckbox.click();
- await expect(directCheckbox).not.toBeChecked();
- }
- });
- });
-
- test.describe('US-30 & US-31: Time Filters', () => {
- test('should have form structure supporting time filters', async ({ page }) => {
- const form = page.getByRole('search');
- await expect(form).toBeVisible();
-
- // Check that form has rows for filters
- const rows = form.locator('[class*="row"]');
- const rowCount = await rows.count();
- expect(rowCount).toBeGreaterThan(0);
- });
-
- test('should show departure time filter in main section', async ({ page }) => {
- const form = page.getByRole('search');
- await expect(form).toBeVisible();
-
- // The form should be structured to support time filters
- const departureInput = page.getByTestId('schedule-departure-input');
- await expect(departureInput).toBeVisible();
- });
-
- test('should show arrival time filter section only when round-trip is enabled', async ({
- page,
- }) => {
- const returnCheckbox = page.getByTestId('schedule-return-checkbox');
- const returnCalendar = page.getByTestId('schedule-return-calendar');
-
- // Initially, return section should not be visible
- await expect(returnCalendar).not.toBeVisible();
-
- // Enable round-trip
- await returnCheckbox.click();
-
- // Return section should now be visible
- await expect(returnCalendar).toBeVisible();
- });
-
- test('should support time range with 30-minute increments', async ({ page }) => {
- // Verify the form supports time filtering by checking the structure
- const form = page.getByRole('search');
- await expect(form).toBeVisible();
-
- // Time filters would be rendered as part of the form
- // This test verifies the infrastructure is in place
- });
- });
-
- test.describe('US-32: Parameter Validation', () => {
- test('should show validation error when required fields are missing', async ({ page }) => {
- const searchButton = page.getByTestId('schedule-search-button');
-
- // Try to search without entering cities
- await searchButton.click();
-
- // Should show validation error
- const errorMessage = page.getByTestId('schedule-validation-error');
- await expect(errorMessage).toBeVisible();
- await expect(errorMessage).toContainText(/required|missing|departure/i);
- });
-
- test('should clear validation error when departure city is changed', async ({ page }) => {
- const searchButton = page.getByTestId('schedule-search-button');
- const departureInput = page.getByTestId('schedule-departure-input');
-
- // Trigger validation error
- await searchButton.click();
- let errorMessage = page.getByTestId('schedule-validation-error');
- await expect(errorMessage).toBeVisible();
-
- // Type in departure field
- const input = departureInput.locator('input').first();
- await input.focus();
- await input.type('M');
-
- // Error should be cleared
- errorMessage = page.getByTestId('schedule-validation-error');
- await expect(errorMessage).not.toBeVisible();
- });
-
- test('should clear validation error when arrival city is changed', async ({ page }) => {
- const searchButton = page.getByTestId('schedule-search-button');
- const arrivalInput = page.getByTestId('schedule-arrival-input');
-
- // Trigger validation error
- await searchButton.click();
- let errorMessage = page.getByTestId('schedule-validation-error');
- await expect(errorMessage).toBeVisible();
-
- // Type in arrival field
- const input = arrivalInput.locator('input').first();
- await input.focus();
- await input.type('L');
-
- // Error should be cleared
- errorMessage = page.getByTestId('schedule-validation-error');
- await expect(errorMessage).not.toBeVisible();
- });
-
- test('should show error for missing departure date', async ({ page }) => {
- const dateFromInput = page.getByLabel('Depart');
- const searchButton = page.getByTestId('schedule-search-button');
-
- // Clear departure date
- await dateFromInput.clear();
-
- // Try to search
- await searchButton.click();
-
- // Should show error
- const errorMessage = page.getByTestId('schedule-validation-error');
- await expect(errorMessage).toBeVisible();
- });
-
- test('should show error for missing arrival date', async ({ page }) => {
- const dateToInput = page.getByTestId('schedule-outbound-date-input');
- const searchButton = page.getByTestId('schedule-search-button');
-
- // Clear arrival date
- await dateToInput.clear();
-
- // Try to search
- await searchButton.click();
-
- // Should show error
- const errorMessage = page.getByTestId('schedule-validation-error');
- await expect(errorMessage).toBeVisible();
- });
-
- test('should show error when arrival date is before departure date', async ({ page }) => {
- const dateFromInput = page.getByLabel('Depart');
- const dateToInput = page.getByTestId('schedule-outbound-date-input');
- const searchButton = page.getByTestId('schedule-search-button');
-
- // Set dates with "to" before "from"
- const futureDate = new Date();
- futureDate.setDate(futureDate.getDate() + 10);
- const futureStr = futureDate.toISOString().split('T')[0];
-
- const earlierDate = new Date();
- earlierDate.setDate(earlierDate.getDate() + 5);
- const earlierStr = earlierDate.toISOString().split('T')[0];
-
- await dateFromInput.clear();
- await dateFromInput.fill(futureStr);
-
- await dateToInput.clear();
- await dateToInput.fill(earlierStr);
-
- // Try to search
- await searchButton.click();
-
- // Should show error
- const errorMessage = page.getByTestId('schedule-validation-error');
- await expect(errorMessage).toBeVisible();
- });
-
- test('should show error for missing return date when round-trip is enabled', async ({
- page,
- }) => {
- const returnCheckbox = page.getByTestId('schedule-return-checkbox');
- const searchButton = page.getByTestId('schedule-search-button');
-
- // Enable return flight
- await returnCheckbox.click();
-
- // Try to search without filling return dates
- await searchButton.click();
-
- // Should show validation error
- const errorMessage = page.getByTestId('schedule-validation-error');
- await expect(errorMessage).toBeVisible();
- });
-
- test('should validate return date is not before outbound end date', async ({ page }) => {
- const returnCheckbox = page.getByTestId('schedule-return-checkbox');
- const dateToInput = page.getByTestId('schedule-outbound-date-input');
- const searchButton = page.getByTestId('schedule-search-button');
-
- // Enable return flight
- await returnCheckbox.click();
-
- // Set outbound end date
- const outboundDate = new Date();
- outboundDate.setDate(outboundDate.getDate() + 5);
- const outboundStr = outboundDate.toISOString().split('T')[0];
-
- await dateToInput.clear();
- await dateToInput.fill(outboundStr);
-
- // Get return date from input
- const returnFromInput = page.locator('#return-date-from');
-
- // Set return date before outbound end date
- const returnDate = new Date(outboundStr);
- returnDate.setDate(returnDate.getDate() - 2);
- const returnDateStr = returnDate.toISOString().split('T')[0];
-
- await returnFromInput.fill(returnDateStr);
-
- // Try to search
- await searchButton.click();
-
- // Should show error
- const errorMessage = page.getByTestId('schedule-validation-error');
- await expect(errorMessage).toBeVisible();
- });
- });
-
- test.describe('US-33: URL Parameters for Schedule', () => {
- test('should have proper URL format in navigation', async ({ page }) => {
- // The page should be at the schedule URL
- expect(page.url()).toContain('schedule');
- });
-
- test('should generate URL with query parameters on search', async ({ page, context }) => {
- // Create a promise to capture the navigation
- const navigationPromise = page.waitForNavigation({ waitUntil: 'networkidle' });
-
- // For this test, we'd need valid airport codes
- // This is a structural test that the URL can have parameters
- const currentUrl = page.url();
- expect(currentUrl).toContain('schedule');
- });
-
- test('should support from and to parameters in URL', () => {
- // Test URL parameter structure
- const url = new URL('http://localhost:5173/schedule?from=SVO&to=LED');
- const params = new URLSearchParams(url.search);
-
- expect(params.get('from')).toBe('SVO');
- expect(params.get('to')).toBe('LED');
- });
-
- test('should support date parameters in URL', () => {
- const url = new URL('http://localhost:5173/schedule?dateFrom=20250601&dateTo=20250608');
- const params = new URLSearchParams(url.search);
-
- expect(params.get('dateFrom')).toBe('20250601');
- expect(params.get('dateTo')).toBe('20250608');
- });
-
- test('should support return date parameters in URL', () => {
- const url = new URL(
- 'http://localhost:5173/schedule?returnDateFrom=20250615&returnDateTo=20250622',
- );
- const params = new URLSearchParams(url.search);
-
- expect(params.get('returnDateFrom')).toBe('20250615');
- expect(params.get('returnDateTo')).toBe('20250622');
- });
-
- test('should support direct filter parameter in URL', () => {
- const url = new URL('http://localhost:5173/schedule?directOnly=true');
- const params = new URLSearchParams(url.search);
-
- expect(params.get('directOnly')).toBe('true');
- });
-
- test('should support multiple parameters together in URL', () => {
- const fullUrl =
- 'http://localhost:5173/schedule?from=SVO&to=LED&dateFrom=20250601&dateTo=20250608&returnDateFrom=20250615&returnDateTo=20250622&directOnly=true';
- const url = new URL(fullUrl);
- const params = new URLSearchParams(url.search);
-
- expect(params.get('from')).toBe('SVO');
- expect(params.get('to')).toBe('LED');
- expect(params.get('dateFrom')).toBe('20250601');
- expect(params.get('dateTo')).toBe('20250608');
- expect(params.get('returnDateFrom')).toBe('20250615');
- expect(params.get('returnDateTo')).toBe('20250622');
- expect(params.get('directOnly')).toBe('true');
- });
-
- test('should handle URL with only from and to parameters', () => {
- const url = new URL('http://localhost:5173/schedule?from=SVO&to=LED');
- const params = new URLSearchParams(url.search);
-
- expect(params.get('from')).toBe('SVO');
- expect(params.get('to')).toBe('LED');
- expect(params.get('dateFrom')).toBeNull();
- });
-
- test('should handle URL parameter encoding', () => {
- // URL parameters should be properly encoded
- const params = new URLSearchParams();
- params.set('from', 'SVO');
- params.set('to', 'LED');
-
- const encoded = params.toString();
- expect(encoded).toBe('from=SVO&to=LED');
- });
- });
-
- test.describe('Integration Tests', () => {
- test('should maintain all filter states during form interaction', async ({ page }) => {
- const directCheckbox = page.getByTestId('schedule-direct-only-checkbox');
- const returnCheckbox = page.getByTestId('schedule-return-checkbox');
-
- // Enable filters
- await directCheckbox.click();
- await returnCheckbox.click();
-
- await expect(directCheckbox).toBeChecked();
- await expect(returnCheckbox).toBeChecked();
-
- // Interact with date fields
- const dateFromInput = page.getByLabel('Depart');
- const dateToInput = page.getByTestId('schedule-outbound-date-input');
-
- await dateFromInput.focus();
- await dateToInput.focus();
-
- // Filters should still be checked
- await expect(directCheckbox).toBeChecked();
- await expect(returnCheckbox).toBeChecked();
- });
-
- test('should handle rapid toggling of round-trip filter', async ({ page }) => {
- const returnCheckbox = page.getByTestId('schedule-return-checkbox');
- const returnCalendar = page.getByTestId('schedule-return-calendar');
-
- // Rapid toggle
- for (let i = 0; i < 5; i++) {
- await returnCheckbox.click();
- await page.waitForTimeout(50);
- }
-
- // Final state should be checked
- await expect(returnCheckbox).toBeChecked();
- await expect(returnCalendar).toBeVisible();
- });
-
- test('should clear validation error when all required fields are filled', async ({ page }) => {
- const searchButton = page.getByTestId('schedule-search-button');
- const departureInput = page.getByTestId('schedule-departure-input').locator('input').first();
- const arrivalInput = page.getByTestId('schedule-arrival-input').locator('input').first();
-
- // Trigger error by clicking search
- await searchButton.click();
-
- // Error should appear
- let errorMessage = page.getByTestId('schedule-validation-error');
- await expect(errorMessage).toBeVisible();
-
- // Fill in departure
- await departureInput.focus();
- await departureInput.type('M');
-
- // Error might clear or be replaced
- errorMessage = page.getByTestId('schedule-validation-error');
- // Could be visible or not depending on implementation
- });
-
- test('should handle form with all filters enabled', async ({ page }) => {
- const directCheckbox = page.getByTestId('schedule-direct-only-checkbox');
- const returnCheckbox = page.getByTestId('schedule-return-checkbox');
-
- // Enable both filters
- await directCheckbox.click();
- await returnCheckbox.click();
-
- // Verify both are enabled
- await expect(directCheckbox).toBeChecked();
- await expect(returnCheckbox).toBeChecked();
-
- // Return calendar should be visible
- const returnCalendar = page.getByTestId('schedule-return-calendar');
- await expect(returnCalendar).toBeVisible();
- });
- });
-});
-
-test.describe('Schedule Filters - Locale Tests (ru-ru)', () => {
- test.beforeEach(async ({ page }) => {
- // Navigate to schedule search page with Russian locale
- await page.goto(`${BASE_URL}/ru-ru/schedule`);
- // Wait for page to be loaded
- await page.waitForSelector('[data-testid="schedule-search-form"]', { timeout: 5000 });
- });
-
- test('should render form in Russian locale', async ({ page }) => {
- const form = page.getByRole('search');
- await expect(form).toBeVisible();
-
- // Check that Russian labels are present
- // The exact text depends on the Russian translations
- const directCheckbox = page.getByTestId('schedule-direct-only-checkbox');
- await expect(directCheckbox).toBeVisible();
- });
-
- test('should support round-trip toggle in Russian locale', async ({ page }) => {
- const returnCheckbox = page.getByTestId('schedule-return-checkbox');
- const returnCalendar = page.getByTestId('schedule-return-calendar');
-
- // Initially hidden
- await expect(returnCalendar).not.toBeVisible();
-
- // Enable
- await returnCheckbox.click();
-
- // Should be visible
- await expect(returnCalendar).toBeVisible();
- });
-
- test('should show validation errors in Russian locale', async ({ page }) => {
- const searchButton = page.getByTestId('schedule-search-button');
-
- // Try to search
- await searchButton.click();
-
- // Should show error message
- const errorMessage = page.getByTestId('schedule-validation-error');
- await expect(errorMessage).toBeVisible();
- });
-});
diff --git a/tests/e2e-angular/schedule-results.spec.ts b/tests/e2e-angular/schedule-results.spec.ts
deleted file mode 100644
index 127ac889..00000000
--- a/tests/e2e-angular/schedule-results.spec.ts
+++ /dev/null
@@ -1,670 +0,0 @@
-import { test, expect } from '@playwright/test';
-
-test.describe('Schedule Results - Document 4 (US-35 to US-39)', () => {
- test.beforeEach(async ({ page }) => {
- // Navigate to schedule page and perform a search to get results
- await page.goto('http://localhost:3000/schedule');
- await page.waitForLoadState('networkidle');
-
- // Fill in search form
- const departureInput = page.locator('[data-testid="schedule-departure-input"] input');
- const arrivalInput = page.locator('[data-testid="schedule-arrival-input"] input');
-
- await departureInput.fill('SVO');
- await arrivalInput.fill('AER');
-
- // Set date and submit
- const dateInputs = page.locator('[data-testid="schedule-date-input"]');
- await dateInputs.first().click();
- await page.waitForTimeout(500);
-
- // Look for a search button to submit
- const searchButton = page.locator('button:has-text("Search")');
- if (await searchButton.isVisible()) {
- await searchButton.click();
- }
-
- // Wait for results to load
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
- });
-
- test.describe('US-35: Schedule Results Page', () => {
- test('should display results page with flight list', async ({ page }) => {
- const resultsList = page.locator('[data-testid="schedule-flight-day"]');
- await expect(resultsList).toBeVisible();
- });
-
- test('should display flight items with flight information', async ({ page }) => {
- const flightItems = page.locator('[data-testid="schedule-flight-item"]');
- const count = await flightItems.count();
- expect(count).toBeGreaterThan(0);
- });
-
- test('should display flight times in each item', async ({ page }) => {
- const flightItems = page.locator('[data-testid="schedule-flight-item"]');
- const firstFlight = flightItems.first();
-
- // Check for time elements (departure and arrival time)
- const times = firstFlight.locator('[class*="time"]');
- const timeCount = await times.count();
- expect(timeCount).toBeGreaterThan(0);
- });
-
- test('should display flight numbers', async ({ page }) => {
- const flightNumbers = page.locator('[class*="flightNumber"]');
- const count = await flightNumbers.count();
- expect(count).toBeGreaterThan(0);
- });
-
- test('should display aircraft information', async ({ page }) => {
- const aircraftElements = page.locator('[class*="flightAircraft"]');
- const count = await aircraftElements.count();
- expect(count).toBeGreaterThan(0);
- });
-
- test('should display prices for flights', async ({ page }) => {
- const priceElements = page.locator('[class*="flightPrice"]');
- const count = await priceElements.count();
- expect(count).toBeGreaterThan(0);
- });
-
- test('should be responsive on mobile viewport', async ({ page }) => {
- await page.setViewportSize({ width: 375, height: 667 });
- const resultsList = page.locator('[data-testid="schedule-flight-day"]');
- await expect(resultsList).toBeVisible();
- });
-
- test('should be responsive on tablet viewport', async ({ page }) => {
- await page.setViewportSize({ width: 768, height: 1024 });
- const resultsList = page.locator('[data-testid="schedule-flight-day"]');
- await expect(resultsList).toBeVisible();
- });
- });
-
- test.describe('US-36: Switch Between Days', () => {
- test('should display previous week button', async ({ page }) => {
- const prevButton = page.locator('[data-testid="schedule-week-prev"]');
- await expect(prevButton).toBeVisible();
- });
-
- test('should display next week button', async ({ page }) => {
- const nextButton = page.locator('[data-testid="schedule-week-next"]');
- await expect(nextButton).toBeVisible();
- });
-
- test('should have previous/next buttons with proper accessibility', async ({ page }) => {
- const prevButton = page.locator('[data-testid="schedule-week-prev"]');
- const nextButton = page.locator('[data-testid="schedule-week-next"]');
-
- const prevLabel = await prevButton.getAttribute('aria-label');
- const nextLabel = await nextButton.getAttribute('aria-label');
-
- expect(prevLabel).toBeTruthy();
- expect(nextLabel).toBeTruthy();
- });
-
- test('should respond to day tab clicks', async ({ page }) => {
- const dayTabs = page.locator('[data-testid="schedule-week-tab"]');
- const count = await dayTabs.count();
- expect(count).toBeGreaterThan(0);
-
- // Click a different day
- if (count > 1) {
- await dayTabs.nth(1).click();
- await page.waitForLoadState('networkidle');
- // Verify the clicked tab is now active
- const activeTab = page.locator('[data-testid="schedule-week-tab"][aria-selected="true"]');
- await expect(activeTab).toBeVisible();
- }
- });
- });
-
- test.describe('US-37: Week Navigation Tabs', () => {
- test('should display week tabs (7 days)', async ({ page }) => {
- const tabs = page.locator('[data-testid="schedule-week-tab"]');
- const count = await tabs.count();
- expect(count).toBe(7);
- });
-
- test('should display day names in tabs', async ({ page }) => {
- const tabs = page.locator('[data-testid="schedule-week-tab"]');
- const firstTab = tabs.first();
-
- const dayName = firstTab.locator('[class*="dayName"]');
- await expect(dayName).toBeVisible();
- });
-
- test('should display dates in tabs', async ({ page }) => {
- const tabs = page.locator('[data-testid="schedule-week-tab"]');
- const firstTab = tabs.first();
-
- const dayDate = firstTab.locator('[class*="dayDate"]');
- await expect(dayDate).toBeVisible();
- });
-
- test('should highlight the active day tab', async ({ page }) => {
- const activeTab = page.locator('[data-testid="schedule-week-tab"][aria-selected="true"]');
- await expect(activeTab).toBeVisible();
-
- // Verify it has the active class
- const className = await activeTab.getAttribute('class');
- expect(className).toContain('weekTabActive');
- });
-
- test('should allow navigation between weeks with prev button', async ({ page }) => {
- const prevButton = page.locator('[data-testid="schedule-week-prev"]');
- await prevButton.click();
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(500);
-
- // Verify we're still on the schedule results page
- const resultsList = page.locator('[data-testid="schedule-flight-day"]');
- await expect(resultsList).toBeVisible();
- });
-
- test('should allow navigation between weeks with next button', async ({ page }) => {
- const nextButton = page.locator('[data-testid="schedule-week-next"]');
- await nextButton.click();
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(500);
-
- // Verify we're still on the schedule results page
- const resultsList = page.locator('[data-testid="schedule-flight-day"]');
- await expect(resultsList).toBeVisible();
- });
-
- test('should update displayed results when changing weeks', async ({ page }) => {
- const flightItems = page.locator('[data-testid="schedule-flight-item"]');
- const initialCount = await flightItems.count();
-
- // Click next week
- const nextButton = page.locator('[data-testid="schedule-week-next"]');
- await nextButton.click();
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- // Results should still be visible (may be empty or different)
- const resultsList = page.locator('[data-testid="schedule-flight-day"]');
- await expect(resultsList).toBeVisible();
- });
- });
-
- test.describe('US-38: Flight Detail Expansion', () => {
- test('should expand flight details on click', async ({ page }) => {
- const flightItems = page.locator('[data-testid="schedule-flight-item"]');
-
- if ((await flightItems.count()) > 0) {
- const firstFlight = flightItems.first();
- await firstFlight.click();
- await page.waitForTimeout(300);
-
- // Check if expanded class is applied
- const className = await firstFlight.getAttribute('class');
- expect(className).toContain('flightItemExpanded');
- }
- });
-
- test('should display flight details when expanded', async ({ page }) => {
- const flightItems = page.locator('[data-testid="schedule-flight-item"]');
-
- if ((await flightItems.count()) > 0) {
- const firstFlight = flightItems.first();
- await firstFlight.click();
- await page.waitForTimeout(300);
-
- // Look for detail rows
- const detailsRow = firstFlight.locator('[class*="detailsRow"]');
- const count = await detailsRow.count();
- expect(count).toBeGreaterThan(0);
- }
- });
-
- test('should show duration in expanded details', async ({ page }) => {
- const flightItems = page.locator('[data-testid="schedule-flight-item"]');
-
- if ((await flightItems.count()) > 0) {
- const firstFlight = flightItems.first();
- await firstFlight.click();
- await page.waitForTimeout(300);
-
- // Look for duration label
- const durationLabel = firstFlight.locator('text=Duration');
- await expect(durationLabel).toBeVisible();
- }
- });
-
- test('should show aircraft in expanded details', async ({ page }) => {
- const flightItems = page.locator('[data-testid="schedule-flight-item"]');
-
- if ((await flightItems.count()) > 0) {
- const firstFlight = flightItems.first();
- await firstFlight.click();
- await page.waitForTimeout(300);
-
- // Look for aircraft label
- const aircraftLabel = firstFlight.locator('text=Aircraft');
- await expect(aircraftLabel).toBeVisible();
- }
- });
-
- test('should show price in expanded details', async ({ page }) => {
- const flightItems = page.locator('[data-testid="schedule-flight-item"]');
-
- if ((await flightItems.count()) > 0) {
- const firstFlight = flightItems.first();
- await firstFlight.click();
- await page.waitForTimeout(300);
-
- // Look for price label
- const priceLabel = firstFlight.locator('text=Price');
- await expect(priceLabel).toBeVisible();
- }
- });
-
- test('should show status in expanded details', async ({ page }) => {
- const flightItems = page.locator('[data-testid="schedule-flight-item"]');
-
- if ((await flightItems.count()) > 0) {
- const firstFlight = flightItems.first();
- await firstFlight.click();
- await page.waitForTimeout(300);
-
- // Look for status label
- const statusLabel = firstFlight.locator('text=Status');
- await expect(statusLabel).toBeVisible();
- }
- });
-
- test('should collapse flight when clicking again', async ({ page }) => {
- const flightItems = page.locator('[data-testid="schedule-flight-item"]');
-
- if ((await flightItems.count()) > 0) {
- const firstFlight = flightItems.first();
-
- // Expand
- await firstFlight.click();
- await page.waitForTimeout(300);
- let className = await firstFlight.getAttribute('class');
- expect(className).toContain('flightItemExpanded');
-
- // Collapse
- await firstFlight.click();
- await page.waitForTimeout(300);
- className = await firstFlight.getAttribute('class');
- expect(className).not.toContain('flightItemExpanded');
- }
- });
-
- test('should show smooth animation when expanding', async ({ page }) => {
- const flightItems = page.locator('[data-testid="schedule-flight-item"]');
-
- if ((await flightItems.count()) > 0) {
- const firstFlight = flightItems.first();
- const initialHeight = await firstFlight.evaluate((el) => el.offsetHeight);
-
- await firstFlight.click();
- await page.waitForTimeout(500);
-
- const expandedHeight = await firstFlight.evaluate((el) => el.offsetHeight);
- // Height should increase when expanded
- expect(expandedHeight).toBeGreaterThan(initialHeight);
- }
- });
- });
-
- test.describe('US-39: Result Sorting', () => {
- test('should display sorting menu', async ({ page }) => {
- const sortingMenu = page.locator('[data-testid="schedule-sorting-menu"]');
- await expect(sortingMenu).toBeVisible();
- });
-
- test('should have sort buttons', async ({ page }) => {
- const sortButtons = page.locator('button[data-testid*="schedule-sort-button"]');
- const count = await sortButtons.count();
- expect(count).toBeGreaterThan(0);
- });
-
- test('should have one active sort button', async ({ page }) => {
- const activeButtons = page.locator('button[aria-pressed="true"]');
- const count = await activeButtons.count();
- expect(count).toBeGreaterThanOrEqual(1);
- });
-
- test('should allow switching sort modes', async ({ page }) => {
- const sortButtons = page.locator('button[data-testid*="schedule-sort-button"]');
- const count = await sortButtons.count();
-
- if (count > 1) {
- const initialActive = page.locator('button[aria-pressed="true"]');
- const initialId = await initialActive.first().getAttribute('data-testid');
-
- // Click a different sort button
- const secondButton = sortButtons.nth(1);
- await secondButton.click();
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(500);
-
- // Verify the active button changed
- const newActive = page.locator('button[aria-pressed="true"]');
- const newId = await newActive.first().getAttribute('data-testid');
-
- expect(newId).not.toBe(initialId);
- }
- });
-
- test('should re-sort flights when sort option changes', async ({ page }) => {
- const flightItems = page.locator('[data-testid="schedule-flight-item"]');
-
- if ((await flightItems.count()) >= 2) {
- // Get initial order
- const initialFirstFlightTime = await flightItems
- .first()
- .locator('[class*="time"]')
- .first()
- .textContent();
-
- // Click sort button
- const sortButtons = page.locator('button[data-testid*="schedule-sort-button"]');
- const count = await sortButtons.count();
-
- if (count > 1) {
- await sortButtons.nth(1).click();
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(500);
-
- // Verify flights are still displayed
- const updatedFlightItems = page.locator('[data-testid="schedule-flight-item"]');
- const updatedCount = await updatedFlightItems.count();
- expect(updatedCount).toBeGreaterThan(0);
- }
- }
- });
-
- test('should highlight active sort option', async ({ page }) => {
- const activeButton = page.locator('button[aria-pressed="true"]');
- const severity = await activeButton.first().getAttribute('severity');
-
- // Active button should have 'info' severity (or similar highlighting)
- expect(severity).toBeTruthy();
- });
-
- test('should have accessible sort controls', async ({ page }) => {
- const sortButtons = page.locator('button[data-testid*="schedule-sort-button"]');
- const count = await sortButtons.count();
-
- for (let i = 0; i < Math.min(count, 3); i++) {
- const button = sortButtons.nth(i);
- const ariaPressed = await button.getAttribute('aria-pressed');
- expect(ariaPressed).toBeTruthy();
- }
- });
-
- test('should persist sort selection during interaction', async ({ page }) => {
- const sortButtons = page.locator('button[data-testid*="schedule-sort-button"]');
- const count = await sortButtons.count();
-
- if (count > 1) {
- // Select a sort mode
- const secondButton = sortButtons.nth(1);
- await secondButton.click();
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(500);
-
- // Expand a flight
- const flightItems = page.locator('[data-testid="schedule-flight-item"]');
- if ((await flightItems.count()) > 0) {
- await flightItems.first().click();
- await page.waitForTimeout(300);
-
- // Verify sort is still active
- const activeButton = page.locator('button[aria-pressed="true"]');
- const activeId = await activeButton.first().getAttribute('data-testid');
- expect(activeId).toBeTruthy();
- }
- }
- });
- });
-
- test.describe('Round Trip Support (US-36 Integration)', () => {
- test('should show direction switch for round trip', async ({ page }) => {
- // Check if direction switch exists (may not exist for one-way flights)
- const directionSwitch = page.locator('[data-testid="direction-switch"]');
- const exists = await directionSwitch.isVisible().catch(() => false);
-
- // If it exists, it should be visible
- if (exists) {
- await expect(directionSwitch).toBeVisible();
- }
- });
-
- test('should allow switching between outbound and inbound', async ({ page }) => {
- const directionSwitch = page.locator('[data-testid="direction-switch"]');
- const exists = await directionSwitch.isVisible().catch(() => false);
-
- if (exists) {
- const inboundButton = page.locator('[data-testid="direction-inbound"]');
- if (await inboundButton.isVisible()) {
- await inboundButton.click();
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(500);
-
- // Verify results are still displayed
- const resultsList = page.locator('[data-testid="schedule-flight-day"]');
- await expect(resultsList).toBeVisible();
- }
- }
- });
- });
-
- test.describe('Accessibility', () => {
- test('should have proper ARIA labels on navigation buttons', async ({ page }) => {
- const prevButton = page.locator('[data-testid="schedule-week-prev"]');
- const nextButton = page.locator('[data-testid="schedule-week-next"]');
-
- const prevLabel = await prevButton.getAttribute('aria-label');
- const nextLabel = await nextButton.getAttribute('aria-label');
-
- expect(prevLabel).toBeTruthy();
- expect(nextLabel).toBeTruthy();
- });
-
- test('should have proper ARIA attributes on tabs', async ({ page }) => {
- const tabs = page.locator('[data-testid="schedule-week-tab"]');
-
- if ((await tabs.count()) > 0) {
- const firstTab = tabs.first();
- const ariaSelected = await firstTab.getAttribute('aria-selected');
- expect(ariaSelected).toBeTruthy();
- }
- });
-
- test('should have proper ARIA attributes on sort buttons', async ({ page }) => {
- const sortButtons = page.locator('button[data-testid*="schedule-sort-button"]');
-
- if ((await sortButtons.count()) > 0) {
- const firstButton = sortButtons.first();
- const ariaPressed = await firstButton.getAttribute('aria-pressed');
- expect(ariaPressed).toBeTruthy();
- }
- });
-
- test('should maintain keyboard navigation', async ({ page }) => {
- const prevButton = page.locator('[data-testid="schedule-week-prev"]');
- await prevButton.focus();
-
- // Button should be focused
- const focused = await page.evaluate(() =>
- document.activeElement?.getAttribute('data-testid'),
- );
- expect(focused).toBe('schedule-week-prev');
- });
- });
-
- test.describe('Localization (ru-ru)', () => {
- test('should display results in Russian locale', async ({ page }) => {
- // Check for Russian text (common words in schedule)
- const pageContent = await page.textContent('body');
- expect(pageContent).toBeTruthy();
- });
-
- test('should use Russian date format', async ({ page }) => {
- const tabs = page.locator('[data-testid="schedule-week-tab"]');
-
- if ((await tabs.count()) > 0) {
- const tabText = await tabs.first().textContent();
- // Russian day names and date format
- expect(tabText).toBeTruthy();
- }
- });
- });
-
- test.describe('Localization (en-us)', () => {
- test('should display results in English locale', async ({ page, context }) => {
- // Set English locale
- await context.addInitScript(() => {
- localStorage.setItem('preferredLocale', 'en-us');
- });
-
- // Navigate to schedule
- await page.goto('http://localhost:3000/schedule?locale=en-us');
- await page.waitForLoadState('networkidle');
-
- // Perform search
- const departureInput = page.locator('[data-testid="schedule-departure-input"] input');
- const arrivalInput = page.locator('[data-testid="schedule-arrival-input"] input');
-
- await departureInput.fill('SVO');
- await arrivalInput.fill('AER');
-
- const searchButton = page.locator('button:has-text("Search")');
- if (await searchButton.isVisible()) {
- await searchButton.click();
- }
-
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- // Verify results are displayed
- const resultsList = page.locator('[data-testid="schedule-flight-day"]');
- await expect(resultsList).toBeVisible();
- });
- });
-
- test.describe('Error Handling', () => {
- test('should display empty state when no flights found', async ({ page }) => {
- // Try searching for an impossible route
- await page.goto('http://localhost:3000/schedule');
- await page.waitForLoadState('networkidle');
-
- const departureInput = page.locator('[data-testid="schedule-departure-input"] input');
- const arrivalInput = page.locator('[data-testid="schedule-arrival-input"] input');
-
- // Use unlikely city codes
- await departureInput.fill('AAA');
- await arrivalInput.fill('ZZZ');
-
- const searchButton = page.locator('button:has-text("Search")');
- if (await searchButton.isVisible()) {
- await searchButton.click();
- }
-
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- // Should show either empty state or error message
- const emptyState = page.locator('[data-testid="schedule-empty-list"]');
- const resultsList = page.locator('[data-testid="schedule-flight-day"]');
-
- const hasEmptyState = await emptyState.isVisible().catch(() => false);
- const hasResults = await resultsList.isVisible().catch(() => false);
-
- expect(hasEmptyState || hasResults).toBeTruthy();
- });
- });
-});
-
-test.describe('Console Audit - Schedule Results', () => {
- test('should have no console errors on results page', async ({ page }) => {
- const errors: string[] = [];
-
- page.on('console', (message) => {
- if (message.type() === 'error') {
- errors.push(message.text());
- }
- });
-
- await page.goto('http://localhost:3000/schedule');
- await page.waitForLoadState('networkidle');
-
- // Perform search
- const departureInput = page.locator('[data-testid="schedule-departure-input"] input');
- const arrivalInput = page.locator('[data-testid="schedule-arrival-input"] input');
-
- await departureInput.fill('SVO');
- await arrivalInput.fill('AER');
-
- const searchButton = page.locator('button:has-text("Search")');
- if (await searchButton.isVisible()) {
- await searchButton.click();
- }
-
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- // Interact with results
- const flightItems = page.locator('[data-testid="schedule-flight-item"]');
- if ((await flightItems.count()) > 0) {
- await flightItems.first().click();
- await page.waitForTimeout(300);
- }
-
- // Check for errors (excluding known non-critical warnings)
- const criticalErrors = errors.filter(
- (e) =>
- !e.includes('hydration') &&
- !e.includes('useLayoutEffect') &&
- !e.includes('act()') &&
- !e.includes('warning') &&
- e.length > 0,
- );
-
- expect(criticalErrors).toHaveLength(0);
- });
-
- test('should have no accessibility violations', async ({ page }) => {
- await page.goto('http://localhost:3000/schedule');
- await page.waitForLoadState('networkidle');
-
- // Perform search
- const departureInput = page.locator('[data-testid="schedule-departure-input"] input');
- const arrivalInput = page.locator('[data-testid="schedule-arrival-input"] input');
-
- await departureInput.fill('SVO');
- await arrivalInput.fill('AER');
-
- const searchButton = page.locator('button:has-text("Search")');
- if (await searchButton.isVisible()) {
- await searchButton.click();
- }
-
- await page.waitForLoadState('networkidle');
- await page.waitForTimeout(1000);
-
- // Check that all interactive elements are keyboard accessible
- const buttons = page.locator('button');
- const count = await buttons.count();
-
- for (let i = 0; i < Math.min(count, 5); i++) {
- const button = buttons.nth(i);
- await button.focus();
-
- const focused = await page.evaluate(() => {
- const el = document.activeElement as HTMLElement;
- return el.tagName === 'BUTTON';
- });
-
- expect(focused).toBeTruthy();
- }
- });
-});
diff --git a/tests/e2e-angular/schedule-search.spec.ts b/tests/e2e-angular/schedule-search.spec.ts
deleted file mode 100644
index b3c814c7..00000000
--- a/tests/e2e-angular/schedule-search.spec.ts
+++ /dev/null
@@ -1,347 +0,0 @@
-import { test, expect } from '@playwright/test';
-
-test.describe('Schedule Search - Document 3 (US-23 to US-27)', () => {
- test.beforeEach(async ({ page }) => {
- await page.goto('http://localhost:3000/schedule');
- await page.waitForLoadState('networkidle');
- });
-
- test.describe('US-23: Schedule Tab Navigation', () => {
- test('should render schedule search form', async ({ page }) => {
- const form = page.locator('[data-testid="schedule-search-form"]');
- await expect(form).toBeVisible();
- });
-
- test('should render search form with proper role', async ({ page }) => {
- const form = page.locator('[role="search"]');
- await expect(form).toBeVisible();
- });
-
- test('should have proper ARIA label', async ({ page }) => {
- const form = page.locator('[role="search"]');
- const ariaLabel = await form.getAttribute('aria-label');
- expect(ariaLabel).toBeTruthy();
- });
- });
-
- test.describe('US-24: Departure City Input', () => {
- test('should render departure city input', async ({ page }) => {
- const input = page.locator('[data-testid="schedule-departure-input"]');
- await expect(input).toBeVisible();
- });
-
- test('should have From label', async ({ page }) => {
- const label = page.getByText('From', { exact: true });
- await expect(label).toBeVisible();
- });
-
- test('should accept text input for departure city', async ({ page }) => {
- const input = page.locator('[data-testid="schedule-departure-input"] input');
- await input.fill('Moscow');
- await expect(input).toHaveValue('Moscow');
- });
-
- test('should allow clearing departure city', async ({ page }) => {
- const input = page.locator('[data-testid="schedule-departure-input"] input');
- await input.fill('Moscow');
- await input.clear();
- await expect(input).toHaveValue('');
- });
-
- test('should support autocomplete suggestions', async ({ page }) => {
- const input = page.locator('[data-testid="schedule-departure-input"] input');
- await input.focus();
- await input.type('Mos', { delay: 100 });
- // Wait for autocomplete to potentially appear
- await page.waitForTimeout(500);
- expect(input).toBeVisible();
- });
- });
-
- test.describe('US-25: Arrival City Input', () => {
- test('should render arrival city input', async ({ page }) => {
- const input = page.locator('[data-testid="schedule-arrival-input"]');
- await expect(input).toBeVisible();
- });
-
- test('should have To label', async ({ page }) => {
- const label = page.getByText('To', { exact: true });
- await expect(label).toBeVisible();
- });
-
- test('should accept text input for arrival city', async ({ page }) => {
- const input = page.locator('[data-testid="schedule-arrival-input"] input');
- await input.fill('Saint Petersburg');
- await expect(input).toHaveValue('Saint Petersburg');
- });
-
- test('should allow clearing arrival city', async ({ page }) => {
- const input = page.locator('[data-testid="schedule-arrival-input"] input');
- await input.fill('Saint Petersburg');
- await input.clear();
- await expect(input).toHaveValue('');
- });
-
- test('should support independent entry from departure', async ({ page }) => {
- const departureInput = page.locator('[data-testid="schedule-departure-input"] input');
- const arrivalInput = page.locator('[data-testid="schedule-arrival-input"] input');
-
- await departureInput.fill('Moscow');
- await arrivalInput.fill('SPB');
-
- await expect(departureInput).toHaveValue('Moscow');
- await expect(arrivalInput).toHaveValue('SPB');
- });
- });
-
- test.describe('US-26: Swap Cities Button (Exchange)', () => {
- test('should have both departure and arrival inputs for exchange', async ({ page }) => {
- const departureInput = page.locator('[data-testid="schedule-departure-input"]');
- const arrivalInput = page.locator('[data-testid="schedule-arrival-input"]');
-
- await expect(departureInput).toBeVisible();
- await expect(arrivalInput).toBeVisible();
- });
-
- test('should allow switching focus between city inputs', async ({ page }) => {
- const departureInput = page.locator('[data-testid="schedule-departure-input"] input');
- const arrivalInput = page.locator('[data-testid="schedule-arrival-input"] input');
-
- await departureInput.focus();
- await expect(departureInput).toBeFocused();
-
- await arrivalInput.focus();
- await expect(arrivalInput).toBeFocused();
- });
-
- test('should support entering different cities', async ({ page }) => {
- const departureInput = page.locator('[data-testid="schedule-departure-input"] input');
- const arrivalInput = page.locator('[data-testid="schedule-arrival-input"] input');
-
- await departureInput.fill('Moscow');
- await arrivalInput.fill('Saint Petersburg');
-
- await expect(departureInput).toHaveValue('Moscow');
- await expect(arrivalInput).toHaveValue('Saint Petersburg');
- });
- });
-
- test.describe('US-27: Week Selection', () => {
- test('should render date from input', async ({ page }) => {
- const dateFromInput = page.locator('[data-testid="schedule-calendar"] input');
- await expect(dateFromInput).toBeVisible();
- });
-
- test('should render date to input', async ({ page }) => {
- const dateToInput = page.locator('[data-testid="schedule-outbound-date-input"]');
- await expect(dateToInput).toBeVisible();
- });
-
- test('should have Depart label for date from', async ({ page }) => {
- const label = page.getByText('Depart', { exact: true });
- await expect(label).toBeVisible();
- });
-
- test('should have Return label for date to', async ({ page }) => {
- const label = page.getByText('Return', { exact: true });
- await expect(label).toBeVisible();
- });
-
- test('should initialize with date values', async ({ page }) => {
- const dateFromInput = page.locator('[data-testid="schedule-calendar"] input');
- const dateToInput = page.locator('[data-testid="schedule-outbound-date-input"]');
-
- const dateFromValue = await dateFromInput.inputValue();
- const dateToValue = await dateToInput.inputValue();
-
- // Should match YYYY-MM-DD format
- expect(dateFromValue).toMatch(/\d{4}-\d{2}-\d{2}/);
- expect(dateToValue).toMatch(/\d{4}-\d{2}-\d{2}/);
- });
-
- test('should have date input type', async ({ page }) => {
- const dateFromInput = page.locator('[data-testid="schedule-calendar"] input');
- const dateToInput = page.locator('[data-testid="schedule-outbound-date-input"]');
-
- const dateFromType = await dateFromInput.getAttribute('type');
- const dateToType = await dateToInput.getAttribute('type');
-
- expect(dateFromType).toBe('date');
- expect(dateToType).toBe('date');
- });
-
- test('should allow changing departure date', async ({ page }) => {
- const dateFromInput = page.locator('[data-testid="schedule-calendar"] input');
- const initialValue = await dateFromInput.inputValue();
-
- // The date input should be functional
- await dateFromInput.focus();
- await expect(dateFromInput).toBeFocused();
- });
-
- test('should support week date range selection', async ({ page }) => {
- const dateFromInput = page.locator('[data-testid="schedule-calendar"] input');
- const dateToInput = page.locator('[data-testid="schedule-outbound-date-input"]');
-
- // Both should be visible and functional for date range
- await expect(dateFromInput).toBeVisible();
- await expect(dateToInput).toBeVisible();
-
- const dateFromValue = await dateFromInput.inputValue();
- const dateToValue = await dateToInput.inputValue();
-
- // Both should have dates
- expect(dateFromValue).toBeTruthy();
- expect(dateToValue).toBeTruthy();
- });
- });
-
- test.describe('Schedule Search Form Integration', () => {
- test('should have all search inputs visible', async ({ page }) => {
- const departureInput = page.locator('[data-testid="schedule-departure-input"]');
- const arrivalInput = page.locator('[data-testid="schedule-arrival-input"]');
- const dateFromInput = page.locator('[data-testid="schedule-calendar"] input');
- const dateToInput = page.locator('[data-testid="schedule-outbound-date-input"]');
-
- await expect(departureInput).toBeVisible();
- await expect(arrivalInput).toBeVisible();
- await expect(dateFromInput).toBeVisible();
- await expect(dateToInput).toBeVisible();
- });
-
- test('should have search button', async ({ page }) => {
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await expect(searchButton).toBeVisible();
- await expect(searchButton).toContainText('Search', { ignoreCase: true });
- });
-
- test('should have checkbox for direct flights only', async ({ page }) => {
- const directCheckbox = page.locator('[data-testid="schedule-direct-only-checkbox"]');
- await expect(directCheckbox).toBeVisible();
- });
-
- test('should have checkbox for return flight', async ({ page }) => {
- const returnCheckbox = page.locator('[data-testid="schedule-return-checkbox"]');
- await expect(returnCheckbox).toBeVisible();
- });
-
- test('should show validation error when trying to search without cities', async ({ page }) => {
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
- await searchButton.click();
-
- const error = page.locator('[data-testid="schedule-validation-error"]');
- await expect(error).toBeVisible();
- });
-
- test('should toggle return date fields when return flight is enabled', async ({ page }) => {
- const returnCheckbox = page.locator('[data-testid="schedule-return-checkbox"]');
- const returnCalendar = page.locator('[data-testid="schedule-return-calendar"]');
-
- // Initially hidden
- await expect(returnCalendar).not.toBeVisible();
-
- // Click to enable return flight
- await returnCheckbox.click();
-
- // Now visible
- await expect(returnCalendar).toBeVisible();
- });
- });
-
- test.describe('Schedule Search Workflow', () => {
- test('should allow complete search form interaction', async ({ page }) => {
- const departureInput = page.locator('[data-testid="schedule-departure-input"] input');
- const arrivalInput = page.locator('[data-testid="schedule-arrival-input"] input');
- const directCheckbox = page.locator('[data-testid="schedule-direct-only-checkbox"]');
-
- // Fill departure city
- await departureInput.fill('Moscow');
- await expect(departureInput).toHaveValue('Moscow');
-
- // Fill arrival city
- await arrivalInput.fill('Saint Petersburg');
- await expect(arrivalInput).toHaveValue('Saint Petersburg');
-
- // Toggle direct only
- const isChecked = await directCheckbox.isChecked();
- await directCheckbox.click();
- const newChecked = await directCheckbox.isChecked();
- expect(newChecked).toBe(!isChecked);
- });
-
- test('should maintain form state during interaction', async ({ page }) => {
- const departureInput = page.locator('[data-testid="schedule-departure-input"] input');
- const arrivalInput = page.locator('[data-testid="schedule-arrival-input"] input');
- const dateFromInput = page.locator('[data-testid="schedule-calendar"] input');
-
- // Enter data
- await departureInput.fill('Moscow');
- await arrivalInput.fill('SPB');
- const originalDate = await dateFromInput.inputValue();
-
- // Verify all data is still there
- await expect(departureInput).toHaveValue('Moscow');
- await expect(arrivalInput).toHaveValue('SPB');
- const newDate = await dateFromInput.inputValue();
- expect(newDate).toBe(originalDate);
- });
-
- test('should allow toggling between one-way and round trip', async ({ page }) => {
- const returnCheckbox = page.locator('[data-testid="schedule-return-checkbox"]');
- const returnCalendar = page.locator('[data-testid="schedule-return-calendar"]');
-
- // Initially one-way
- const isCheckedInitial = await returnCheckbox.isChecked();
- expect(isCheckedInitial).toBe(false);
-
- // Toggle to round trip
- await returnCheckbox.click();
- await expect(returnCalendar).toBeVisible();
-
- // Toggle back to one-way
- await returnCheckbox.click();
- await expect(returnCalendar).not.toBeVisible();
- });
- });
-
- test.describe('Accessibility', () => {
- test('should have form with proper role and label', async ({ page }) => {
- const form = page.locator('[role="search"]');
- const ariaLabel = await form.getAttribute('aria-label');
-
- await expect(form).toBeVisible();
- expect(ariaLabel).toBeTruthy();
- });
-
- test('should have properly associated labels', async ({ page }) => {
- const fromLabel = page.getByText('From', { exact: true });
- const toLabel = page.getByText('To', { exact: true });
- const departLabel = page.getByText('Depart', { exact: true });
- const returnLabel = page.getByText('Return', { exact: true });
-
- await expect(fromLabel).toBeVisible();
- await expect(toLabel).toBeVisible();
- await expect(departLabel).toBeVisible();
- await expect(returnLabel).toBeVisible();
- });
-
- test('should support keyboard navigation', async ({ page }) => {
- const departureInput = page.locator('[data-testid="schedule-departure-input"] input');
- const arrivalInput = page.locator('[data-testid="schedule-arrival-input"] input');
- const searchButton = page.locator('[data-testid="schedule-search-button"]');
-
- // Start at departure
- await departureInput.focus();
- await expect(departureInput).toBeFocused();
-
- // Tab to next element
- await page.keyboard.press('Tab');
-
- // Should be on next focusable element
- const focusedElement = await page.evaluate(() =>
- document.activeElement?.getAttribute('data-testid'),
- );
- expect(focusedElement).not.toBe('schedule-departure-input');
- });
- });
-});
diff --git a/tests/e2e-angular/search-history.spec.ts b/tests/e2e-angular/search-history.spec.ts
deleted file mode 100644
index c4d72c18..00000000
--- a/tests/e2e-angular/search-history.spec.ts
+++ /dev/null
@@ -1,199 +0,0 @@
-import { test, expect } from '@playwright/test';
-
-test.describe('Search History (US-8)', () => {
- test.beforeEach(async ({ page }) => {
- await page.goto('http://localhost:3000/ru-ru/onlineboard');
- // Clear localStorage to start fresh
- await page.evaluate(() => localStorage.clear());
- // Reload after clearing
- await page.reload();
- });
-
- test('should not display search history section when empty', async ({ page }) => {
- const section = page.locator('[data-testid="landing-search-history"]');
- await expect(section).not.toBeVisible();
- });
-
- test('should display search history section when items exist', async ({ page }) => {
- // Setup: Add history to localStorage
- await page.evaluate(() => {
- const historyItem = {
- id: '1',
- label: 'SU 1402',
- url: '/search?flight=SU1402',
- timestamp: Date.now(),
- };
- localStorage.setItem('aeroflot_search_history', JSON.stringify([historyItem]));
- });
-
- // Reload to pick up the localStorage data
- await page.reload();
-
- const section = page.locator('[data-testid="landing-search-history"]');
- await expect(section).toBeVisible();
- });
-
- test('should display history items correctly', async ({ page }) => {
- // Setup: Add multiple history items
- await page.evaluate(() => {
- const historyItems = [
- {
- id: '1',
- label: 'SU 1402',
- url: '/search?flight=SU1402',
- timestamp: Date.now(),
- },
- {
- id: '2',
- label: 'SU 1403',
- url: '/search?flight=SU1403',
- timestamp: Date.now() - 60000,
- },
- ];
- localStorage.setItem('aeroflot_search_history', JSON.stringify(historyItems));
- });
-
- await page.reload();
-
- const items = page.locator('[data-testid="landing-search-history-item"]');
- await expect(items).toHaveCount(2);
-
- // Check for flight numbers
- await expect(page.getByText('SU 1402')).toBeVisible();
- await expect(page.getByText('SU 1403')).toBeVisible();
- });
-
- test('should display search history title', async ({ page }) => {
- await page.evaluate(() => {
- const historyItem = {
- id: '1',
- label: 'SU 1402',
- url: '/search?flight=SU1402',
- timestamp: Date.now(),
- };
- localStorage.setItem('aeroflot_search_history', JSON.stringify([historyItem]));
- });
-
- await page.reload();
-
- // Note: Title depends on intl messages, might be "Search History" or Russian equivalent
- const title = page.locator('[data-testid="landing-search-history"] h3');
- await expect(title).toBeVisible();
- });
-
- test('should have clickable history items that are links', async ({ page }) => {
- await page.evaluate(() => {
- const historyItem = {
- id: '1',
- label: 'SU 1402',
- url: '/search?flight=SU1402',
- timestamp: Date.now(),
- };
- localStorage.setItem('aeroflot_search_history', JSON.stringify([historyItem]));
- });
-
- await page.reload();
-
- const link = page.locator('[data-testid="landing-search-history-item"] a').first();
- await expect(link).toHaveAttribute('href', /search\?flight=SU1402/);
- });
-
- test('should format timestamp as HH:MM', async ({ page }) => {
- const testTime = new Date(2026, 3, 9, 14, 30, 0).getTime();
-
- await page.evaluate((time) => {
- const historyItem = {
- id: '1',
- label: 'SU 1402',
- url: '/search?flight=SU1402',
- timestamp: time,
- };
- localStorage.setItem('aeroflot_search_history', JSON.stringify([historyItem]));
- }, testTime);
-
- await page.reload();
-
- // Check for time format HH:MM
- const timeElement = page.locator('[data-testid="landing-search-history-item"] span').last();
- const timeText = await timeElement.textContent();
- expect(timeText).toMatch(/\d{2}:\d{2}/);
- });
-
- test('should persist history across page reloads', async ({ page }) => {
- // Add history
- await page.evaluate(() => {
- const historyItem = {
- id: '1',
- label: 'SU 1402',
- url: '/search?flight=SU1402',
- timestamp: Date.now(),
- };
- localStorage.setItem('aeroflot_search_history', JSON.stringify([historyItem]));
- });
-
- await page.reload();
-
- // Verify it exists
- const items1 = page.locator('[data-testid="landing-search-history-item"]');
- const count1 = await items1.count();
- expect(count1).toBeGreaterThan(0);
-
- // Reload again
- await page.reload();
-
- // Verify it still exists
- const items2 = page.locator('[data-testid="landing-search-history-item"]');
- const count2 = await items2.count();
- expect(count2).toBe(count1);
- });
-
- test('should be responsive on mobile viewport', async ({ page }) => {
- await page.setViewportSize({ width: 375, height: 667 });
-
- await page.evaluate(() => {
- const historyItem = {
- id: '1',
- label: 'SU 1402',
- url: '/search?flight=SU1402',
- timestamp: Date.now(),
- };
- localStorage.setItem('aeroflot_search_history', JSON.stringify([historyItem]));
- });
-
- await page.reload();
-
- const section = page.locator('[data-testid="landing-search-history"]');
- await expect(section).toBeVisible();
- });
-
- test('should handle large number of history items', async ({ page }) => {
- // Create 20 history items
- await page.evaluate(() => {
- const historyItems = Array.from({ length: 20 }, (_, i) => ({
- id: String(i + 1),
- label: `SU ${1400 + i}`,
- url: `/search?flight=SU${1400 + i}`,
- timestamp: Date.now() - i * 60000,
- }));
- localStorage.setItem('aeroflot_search_history', JSON.stringify(historyItems));
- });
-
- await page.reload();
-
- const items = page.locator('[data-testid="landing-search-history-item"]');
- await expect(items).toHaveCount(20);
- });
-
- test('should handle corrupted localStorage data gracefully', async ({ page }) => {
- // Corrupt the localStorage
- await page.evaluate(() => {
- localStorage.setItem('aeroflot_search_history', 'corrupted{invalid json');
- });
-
- await page.reload();
-
- // Should not show history section
- const section = page.locator('[data-testid="landing-search-history"]');
- await expect(section).not.toBeVisible();
- });
-});
diff --git a/tests/e2e-angular/search-panel.spec.ts b/tests/e2e-angular/search-panel.spec.ts
deleted file mode 100644
index 5870d11d..00000000
--- a/tests/e2e-angular/search-panel.spec.ts
+++ /dev/null
@@ -1,168 +0,0 @@
-import { test, expect } from '@playwright/test';
-
-test.describe('Search Panel - Filter Sidebar (US-6)', () => {
- test.beforeEach(async ({ page }) => {
- await page.goto('http://localhost:3000');
- });
-
- test('should render filter accordion container', async ({ page }) => {
- const filterAccordion = page.locator('[data-testid="filter-accordion"]');
- await expect(filterAccordion).toBeVisible();
- });
-
- test('should render flight number search tab', async ({ page }) => {
- const flightTab = page.locator('[data-testid="filter-flight-tab"]');
- await expect(flightTab).toBeVisible();
- });
-
- test('should render route search tab', async ({ page }) => {
- const routeTab = page.locator('[data-testid="filter-route-tab"]');
- await expect(routeTab).toBeVisible();
- });
-
- test('should expand flight tab when clicked', async ({ page }) => {
- const flightTab = page.locator('[data-testid="filter-flight-tab"]');
-
- // Click to ensure it's expanded
- await flightTab.click();
-
- // Wait for search panel to appear
- const searchByFlight = page.locator('[data-testid="search-by-flight"]');
- await expect(searchByFlight).toBeVisible({ timeout: 5000 });
- });
-
- test('should display flight number input when flight tab is active', async ({ page }) => {
- const flightTab = page.locator('[data-testid="filter-flight-tab"]');
- await flightTab.click();
-
- const flightInput = page.locator('[data-testid="filter-flight-number-input"]');
- await expect(flightInput).toBeVisible();
- });
-
- test('should allow entering flight number', async ({ page }) => {
- const flightTab = page.locator('[data-testid="filter-flight-tab"]');
- await flightTab.click();
-
- const flightInput = page.locator('[data-testid="filter-flight-number-input"]');
- await flightInput.fill('1402');
- await expect(flightInput).toHaveValue('1402');
- });
-
- test('should display flight suffix input', async ({ page }) => {
- const flightTab = page.locator('[data-testid="filter-flight-tab"]');
- await flightTab.click();
-
- const suffixInput = page.locator('[data-testid="filter-flight-number-suffix-input"]');
- await expect(suffixInput).toBeVisible();
- });
-
- test('should allow entering flight suffix', async ({ page }) => {
- const flightTab = page.locator('[data-testid="filter-flight-tab"]');
- await flightTab.click();
-
- const suffixInput = page.locator('[data-testid="filter-flight-number-suffix-input"]');
- await suffixInput.fill('A');
- await expect(suffixInput).toHaveValue('A');
- });
-
- test('should display date picker', async ({ page }) => {
- const flightTab = page.locator('[data-testid="filter-flight-tab"]');
- await flightTab.click();
-
- const datePicker = page.locator('[data-testid="filter-flight-number-calendar"]');
- await expect(datePicker).toBeVisible();
- });
-
- test('should display search button', async ({ page }) => {
- const flightTab = page.locator('[data-testid="filter-flight-tab"]');
- await flightTab.click();
-
- const searchButton = page.locator('[data-testid="filter-flight-number-search"]');
- await expect(searchButton).toBeVisible();
- });
-
- test('should expand route tab when clicked', async ({ page }) => {
- const routeTab = page.locator('[data-testid="filter-route-tab"]');
-
- // Click to ensure it's expanded
- await routeTab.click();
-
- // Wait for search panel to appear
- const searchByRoute = page.locator('[data-testid="search-by-route"]');
- await expect(searchByRoute).toBeVisible({ timeout: 5000 });
- });
-
- test('should toggle between flight and route tabs', async ({ page }) => {
- const flightTab = page.locator('[data-testid="filter-flight-tab"]');
- const routeTab = page.locator('[data-testid="filter-route-tab"]');
-
- // Open flight tab
- await flightTab.click();
- let flightContent = page.locator('[data-testid="search-by-flight"]');
- await expect(flightContent).toBeVisible();
-
- // Switch to route tab
- await routeTab.click();
- const routeContent = page.locator('[data-testid="search-by-route"]');
- await expect(routeContent).toBeVisible();
-
- // Flight content should no longer be visible
- flightContent = page.locator('[data-testid="search-by-flight"]');
- await expect(flightContent).not.toBeVisible();
- });
-
- test('should have SU prefix displayed', async ({ page }) => {
- const flightTab = page.locator('[data-testid="filter-flight-tab"]');
- await flightTab.click();
-
- const suPrefix = page.locator('.prefix');
- await expect(suPrefix).toContainText('SU');
- });
-
- test('should have clear button for flight number', async ({ page }) => {
- const flightTab = page.locator('[data-testid="filter-flight-tab"]');
- await flightTab.click();
-
- const flightInput = page.locator('[data-testid="filter-flight-number-input"]');
- await flightInput.fill('1402');
-
- const clearButton = page.locator('[data-testid="filter-flight-number-clear"]').first();
- await expect(clearButton).toBeVisible();
- });
-
- test('should clear flight number when clear button clicked', async ({ page }) => {
- const flightTab = page.locator('[data-testid="filter-flight-tab"]');
- await flightTab.click();
-
- const flightInput = page.locator('[data-testid="filter-flight-number-input"]');
- await flightInput.fill('1402');
-
- const clearButton = page.locator('[data-testid="filter-flight-number-clear"]').first();
- await clearButton.click();
-
- await expect(flightInput).toHaveValue('');
- });
-
- test('should display all three search sections in filter accordion', async ({ page }) => {
- const filterAccordion = page.locator('[data-testid="filter-accordion"]');
-
- // Get all section headers
- const sectionHeaders = filterAccordion.locator('button[class*="sectionHeader"]');
- const count = await sectionHeaders.count();
-
- expect(count).toBeGreaterThanOrEqual(3); // At least 3 sections (flight, route, arrival)
- });
-
- test('should support keyboard navigation', async ({ page }) => {
- const flightTab = page.locator('[data-testid="filter-flight-tab"]');
-
- // Focus the button
- await flightTab.focus();
-
- // Press Enter to activate
- await flightTab.press('Enter');
-
- const searchByFlight = page.locator('[data-testid="search-by-flight"]');
- await expect(searchByFlight).toBeVisible({ timeout: 5000 });
- });
-});
diff --git a/tests/e2e-angular/seo.spec.ts b/tests/e2e-angular/seo.spec.ts
deleted file mode 100644
index a9146eaa..00000000
--- a/tests/e2e-angular/seo.spec.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-import { test, expect } from '@playwright/test';
-
-test.describe('SEO & Meta Tags (US-9)', () => {
- test('should have correct title and meta tags for ru-ru', async ({ page }) => {
- await page.goto('http://localhost:3000/ru-ru/onlineboard');
-
- const title = await page.title();
- expect(title).toBeTruthy();
- expect(title.length).toBeGreaterThan(0);
-
- const description = await page.locator('meta[name="description"]').getAttribute('content');
- expect(description).toBeTruthy();
- });
-
- test('should have correct title and meta tags for en-us', async ({ page }) => {
- await page.goto('http://localhost:3000/en-us/onlineboard');
-
- const title = await page.title();
- expect(title).toBeTruthy();
- expect(title.length).toBeGreaterThan(0);
- });
-
- test('should have OpenGraph tags on all pages', async ({ page }) => {
- await page.goto('http://localhost:3000/ru-ru/onlineboard');
-
- const ogTitle = await page.locator('meta[property="og:title"]').count();
- expect(ogTitle).toBeGreaterThan(0);
-
- const ogDescription = await page.locator('meta[property="og:description"]').count();
- expect(ogDescription).toBeGreaterThan(0);
- });
-
- test('should have canonical link', async ({ page }) => {
- await page.goto('http://localhost:3000/ru-ru/onlineboard');
-
- const canonical = await page.locator('link[rel="canonical"]').count();
- expect(canonical).toBeGreaterThan(0);
- });
-
- test('should have viewport meta tag', async ({ page }) => {
- await page.goto('http://localhost:3000/ru-ru/onlineboard');
-
- const viewport = await page.locator('meta[name="viewport"]').getAttribute('content');
- expect(viewport).toContain('width=device-width');
- });
-
- test('should have correct language attribute', async ({ page }) => {
- await page.goto('http://localhost:3000/ru-ru/onlineboard');
-
- const lang = await page.locator('html').getAttribute('lang');
- expect(lang).toBeTruthy();
- });
-
- test('should update lang attribute when changing locale', async ({ page }) => {
- await page.goto('http://localhost:3000/ru-ru/onlineboard');
-
- let lang = await page.locator('html').getAttribute('lang');
- expect(lang).toBeTruthy();
-
- // Switch to English
- await page.goto('http://localhost:3000/en-us/onlineboard');
- lang = await page.locator('html').getAttribute('lang');
- expect(lang).toBeTruthy();
- });
-
- test('should have JSON-LD structured data', async ({ page }) => {
- await page.goto('http://localhost:3000/ru-ru/onlineboard');
-
- const jsonLd = await page.locator('script[type="application/ld+json"]').count();
- expect(jsonLd).toBeGreaterThan(0);
- });
-});
diff --git a/tests/e2e-angular/support/angular-api-mock.ts b/tests/e2e-angular/support/angular-api-mock.ts
deleted file mode 100644
index 1eab2b36..00000000
--- a/tests/e2e-angular/support/angular-api-mock.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import type { Page } from '@playwright/test';
-
-/**
- * Mock Angular API endpoints that are required for the app to bootstrap.
- * The upstream Aeroflot API may be unavailable (403), so we provide
- * minimal valid responses to allow the Angular app to render.
- */
-export async function mockAngularAPIs(page: Page): Promise {
- 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: 'LED', arrivalCity: 'KRR' },
- { requestType: 'Route', departureCity: 'VKO', arrivalCity: 'KUF' },
- { requestType: 'Arrival', arrivalCity: 'VKO' },
- ]),
- });
- });
-
- await page.route('**/api/dictionary/**', (route) => {
- 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());
-}
diff --git a/tests/e2e-angular/support/cross-app-fixtures.ts b/tests/e2e-angular/support/cross-app-fixtures.ts
deleted file mode 100644
index e726ee67..00000000
--- a/tests/e2e-angular/support/cross-app-fixtures.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-/* eslint-disable react-hooks/rules-of-hooks */
-import { test as base, Page } from '@playwright/test';
-import { mockAngularAPIs } from './angular-api-mock';
-
-export type CrossAppFixtures = {
- locale: string;
- app: 'angular' | 'react';
- localePath: (path: string) => string;
- page: Page;
-};
-
-/**
- * Mock APIs for both Angular and React apps.
- * Both apps use the same backend API endpoints.
- */
-export async function mockAllAPIs(page: Page): Promise {
- await mockAngularAPIs(page);
-}
-
-const ANGULAR_LOCALE_MAP: Record = {
- 'ru-ru': 'ru',
- 'en-us': 'en',
- 'es-es': 'es',
- 'fr-fr': 'fr',
- 'it-it': 'it',
- 'ja-jp': 'ja',
- 'ko-kr': 'ko',
- 'zh-cn': 'zh',
- 'de-de': 'de',
-};
-
-export const test = base.extend({
- locale: ['ru-ru', { option: true }],
- app: [
- // eslint-disable-next-line no-empty-pattern
- async ({}, use, testInfo) => {
- const projectName = testInfo.project.name;
- const app = projectName.startsWith('angular-') ? 'angular' : 'react';
- await use(app as 'angular' | 'react');
- },
- { auto: true },
- ],
- localePath: async ({ locale, app }, use) => {
- await use((path: string) => {
- const cleanPath = path.startsWith('/') ? path.slice(1) : path;
- // Angular app doesn't use locale in URL path
- if (app === 'angular') {
- return `/${cleanPath}`;
- }
- return `/${locale}/${cleanPath}`;
- });
- },
- page: async ({ page }, use) => {
- // Apply API mocks for both Angular and React
- await mockAllAPIs(page);
- await use(page);
- },
-});
-
-export { expect } from '@playwright/test';
diff --git a/tests/e2e-angular/support/selectors.ts b/tests/e2e-angular/support/selectors.ts
deleted file mode 100644
index 8d991891..00000000
--- a/tests/e2e-angular/support/selectors.ts
+++ /dev/null
@@ -1,187 +0,0 @@
-/**
- * Canonical data-testid selector map for cross-app e2e tests.
- *
- * Both Angular and React apps must implement these testids.
- * Where Angular uses a different testid, add an entry to ANGULAR_OVERRIDES.
- */
-
-export const S = {
- // Navigation & Layout
- NAV_ONLINEBOARD_TAB: 'nav-onlineboard-tab',
- NAV_SCHEDULE_TAB: 'nav-schedule-tab',
- NAV_FLIGHTS_MAP_TAB: 'nav-flights-map-tab',
- LAYOUT_BREADCRUMBS: 'layout-breadcrumbs',
- LAYOUT_FEEDBACK_BUTTON: 'layout-feedback-button',
- LAYOUT_SCROLL_TOP_BUTTON: 'layout-scroll-top-button',
- LAYOUT_LOCALE_SWITCHER: 'layout-locale-switcher',
- LAYOUT_LOCALE_OPTION: 'layout-locale-option',
-
- // Online Board - Filter
- FILTER_ACCORDION: 'filter-accordion',
- FILTER_FLIGHT_TAB: 'filter-flight-tab',
- FILTER_ROUTE_TAB: 'filter-route-tab',
- FILTER_FLIGHT_NUMBER_INPUT: 'filter-flight-number-input',
- FILTER_FLIGHT_NUMBER_CLEAR: 'filter-flight-number-clear',
- FILTER_FLIGHT_NUMBER_CALENDAR: 'filter-flight-number-calendar',
- FILTER_FLIGHT_NUMBER_SEARCH: 'filter-flight-number-search',
- FILTER_ROUTE_DEPARTURE_INPUT: 'filter-route-departure-input',
- FILTER_ROUTE_ARRIVAL_INPUT: 'filter-route-arrival-input',
- FILTER_ROUTE_SWAP_BUTTON: 'filter-route-swap-button',
- FILTER_ROUTE_CALENDAR: 'filter-route-calendar',
- FILTER_ROUTE_TIME_SELECTOR: 'filter-route-time-selector',
- FILTER_ROUTE_SEARCH: 'filter-route-search',
-
- // Online Board - Results
- BOARD_DAY_TABS: 'board-day-tabs',
- BOARD_DAY_TAB: 'board-day-tab',
- BOARD_TIME_SELECTOR: 'board-time-selector',
- BOARD_SEARCH_RESULT: 'board-search-result',
- BOARD_FLIGHT_RESULT: 'board-flight-result',
- BOARD_FLIGHT_NUMBER: 'board-flight-number',
- BOARD_FLIGHT_STATUS: 'board-flight-status',
- BOARD_FLIGHT_EXPAND: 'board-flight-expand',
- BOARD_LOADER: 'board-loader',
- BOARD_EMPTY_LIST: 'board-empty-list',
- BOARD_CANCEL_BUTTON: 'board-cancel-button',
-
- // Flight Details
- DETAILS_FLIGHT_NUMBER: 'details-flight-number',
- DETAILS_DEPARTURE_STATION: 'details-departure-station',
- DETAILS_ARRIVAL_STATION: 'details-arrival-station',
- DETAILS_DEPARTURE_TIME: 'details-departure-time',
- DETAILS_ARRIVAL_TIME: 'details-arrival-time',
- DETAILS_STATUS: 'details-status',
- DETAILS_DURATION: 'details-duration',
- DETAILS_OPERATOR_LOGO: 'details-operator-logo',
- DETAILS_AIRCRAFT_MODEL: 'details-aircraft-model',
- DETAILS_PRINT_BUTTON: 'details-print-button',
- DETAILS_SHARE_BUTTON: 'details-share-button',
- DETAILS_BUY_TICKET_BUTTON: 'details-buy-ticket-button',
- DETAILS_REGISTRATION_BUTTON: 'details-registration-button',
- DETAILS_FLIGHT_STATUS_BUTTON: 'details-flight-status-button',
- DETAILS_TRANSFER_SECTION: 'details-transfer-section',
- DETAILS_FULL_ROUTE: 'details-full-route',
- DETAILS_TERMINAL_LINK: 'details-terminal-link',
-
- // Landing Page
- LANDING_SECTION: 'landing-section',
- LANDING_POPULAR_REQUEST: 'landing-popular-request',
- LANDING_SEARCH_HISTORY: 'landing-search-history',
- LANDING_SEARCH_HISTORY_ITEM: 'landing-search-history-item',
-
- // Schedule - Filter
- SCHEDULE_DEPARTURE_INPUT: 'schedule-departure-input',
- SCHEDULE_ARRIVAL_INPUT: 'schedule-arrival-input',
- SCHEDULE_SWAP_BUTTON: 'schedule-swap-button',
- SCHEDULE_CALENDAR: 'schedule-calendar',
- SCHEDULE_RETURN_CALENDAR: 'schedule-return-calendar',
- SCHEDULE_TIME_SELECTOR: 'schedule-time-selector',
- SCHEDULE_RETURN_TIME_SELECTOR: 'schedule-return-time-selector',
- SCHEDULE_DIRECT_ONLY_CHECKBOX: 'schedule-direct-only-checkbox',
- SCHEDULE_RETURN_CHECKBOX: 'schedule-return-checkbox',
- SCHEDULE_SEARCH_BUTTON: 'schedule-search-button',
-
- // Schedule - Results
- SCHEDULE_WEEK_TABS: 'schedule-week-tabs',
- SCHEDULE_WEEK_TAB: 'schedule-week-tab',
- SCHEDULE_WEEK_PREV: 'schedule-week-prev',
- SCHEDULE_WEEK_NEXT: 'schedule-week-next',
- SCHEDULE_DIRECTION_SWITCH: 'schedule-direction-switch',
- SCHEDULE_SORT_DROPDOWN: 'schedule-sort-dropdown',
- SCHEDULE_FLIGHT_DAY: 'schedule-flight-day',
- SCHEDULE_FLIGHT_ITEM: 'schedule-flight-item',
- SCHEDULE_LOADER: 'schedule-loader',
-
- // Schedule - Details
- SCHEDULE_DETAILS_BACK_BUTTON: 'schedule-details-back-button',
- SCHEDULE_DETAILS_DAY_TABS: 'schedule-details-day-tabs',
- SCHEDULE_DETAILS_FLIGHT_MINI: 'schedule-details-flight-mini',
- SCHEDULE_DETAILS_TRANSFER: 'schedule-details-transfer',
-
- // Flights Map
- MAP_CONTAINER: 'map-container',
- MAP_DEPARTURE_INPUT: 'map-departure-input',
- MAP_ARRIVAL_INPUT: 'map-arrival-input',
- MAP_SWAP_BUTTON: 'map-swap-button',
- MAP_CALENDAR: 'map-calendar',
- MAP_DOMESTIC_TOGGLE: 'map-domestic-toggle',
- MAP_INTERNATIONAL_TOGGLE: 'map-international-toggle',
- MAP_CONNECTING_TOGGLE: 'map-connecting-toggle',
- MAP_MARKER: 'map-marker',
- MAP_MARKER_CLUSTER: 'map-marker-cluster',
-
- // Shared Components
- CITY_AUTOCOMPLETE_INPUT: 'city-autocomplete-input',
- CITY_AUTOCOMPLETE_POPUP: 'city-autocomplete-popup',
- CITY_AUTOCOMPLETE_CLEAR: 'city-autocomplete-clear',
- CITY_AUTOCOMPLETE_OPTION: 'city-autocomplete-option',
- CITY_CODE_DISPLAY: 'city-code-display',
- TIME_SELECTOR_FROM: 'time-selector-from',
- TIME_SELECTOR_TO: 'time-selector-to',
- TIME_SELECTOR_TRACK: 'time-selector-track',
- CALENDAR_INPUT: 'calendar-input',
- CALENDAR_CLEAR: 'calendar-clear',
-
- // Error Pages
- ERROR_PAGE_404: 'error-page-404',
- ERROR_PAGE_GENERIC: 'error-page-generic',
- ERROR_PAGE_HOME_LINK: 'error-page-home-link',
-} as const;
-
-export type SelectorKey = keyof typeof S;
-
-/**
- * Angular app uses different testid names in some places.
- * This map translates canonical names to Angular-specific ones.
- */
-const ANGULAR_OVERRIDES: Partial> = {
- [S.NAV_ONLINEBOARD_TAB]: 'onlineboard-tab',
- [S.NAV_SCHEDULE_TAB]: 'schedule-tab',
- [S.NAV_FLIGHTS_MAP_TAB]: 'flights-map-tab',
- [S.FILTER_FLIGHT_TAB]: 'flight-filter',
- [S.FILTER_ROUTE_TAB]: 'route-filter',
- [S.FILTER_FLIGHT_NUMBER_INPUT]: 'flight-number-input',
- [S.FILTER_FLIGHT_NUMBER_CLEAR]: 'flight-number-clear-button',
- [S.FILTER_FLIGHT_NUMBER_CALENDAR]: 'flight-number-calendar',
- [S.FILTER_FLIGHT_NUMBER_SEARCH]: 'flight-number-search-button',
- [S.FILTER_ROUTE_DEPARTURE_INPUT]: 'route-departure-city-input',
- [S.FILTER_ROUTE_ARRIVAL_INPUT]: 'route-arrival-city-input',
- [S.FILTER_ROUTE_CALENDAR]: 'route-calendar-input',
- [S.FILTER_ROUTE_SEARCH]: 'route-search-button',
- [S.SCHEDULE_DEPARTURE_INPUT]: 'schedule-departure-city-input',
- [S.SCHEDULE_ARRIVAL_INPUT]: 'schedule-arrival-city-input',
- [S.SCHEDULE_SEARCH_BUTTON]: 'schedule-search-button',
- [S.MAP_DEPARTURE_INPUT]: 'route-departure-city-input',
- [S.MAP_ARRIVAL_INPUT]: 'route-arrival-city-input',
- [S.MAP_CALENDAR]: 'route-calendar-input',
- [S.BOARD_LOADER]: 'loader',
- [S.BOARD_SEARCH_RESULT]: 'board-search-result',
- [S.BOARD_FLIGHT_RESULT]: 'flight-result',
- [S.BOARD_FLIGHT_NUMBER]: 'flight-carrier-number',
- [S.DETAILS_FLIGHT_NUMBER]: 'flight-details-number',
- [S.CITY_AUTOCOMPLETE_INPUT]: 'city-autocomplete-input',
- [S.CITY_AUTOCOMPLETE_CLEAR]: 'autocomplete-clear-input',
- [S.CITY_AUTOCOMPLETE_POPUP]: 'autocomplete-popup-button',
- [S.CITY_CODE_DISPLAY]: 'city-code',
- [S.CALENDAR_INPUT]: 'calendar-input',
-};
-
-/**
- * Get the data-testid selector string for a given app.
- * Returns `[data-testid="..."]` ready for use with page.locator().
- */
-export function tid(name: string, app: 'angular' | 'react'): string {
- const testid = app === 'angular' && ANGULAR_OVERRIDES[name] ? ANGULAR_OVERRIDES[name] : name;
- return `[data-testid="${testid}"]`;
-}
-
-/**
- * Shorthand: get locator from page using canonical testid.
- */
-export function byTestId(
- page: { locator: (s: string) => unknown },
- name: string,
- app: 'angular' | 'react',
-) {
- return page.locator(tid(name, app));
-}
diff --git a/tests/e2e-angular/support/test-utilities.ts b/tests/e2e-angular/support/test-utilities.ts
deleted file mode 100644
index 4c95137a..00000000
--- a/tests/e2e-angular/support/test-utilities.ts
+++ /dev/null
@@ -1,799 +0,0 @@
-import { expect, type Page, type Locator } from '@playwright/test';
-import type { Flight, FlightStatus, FlightDirection } from '../../src/entities/flight/types';
-import type { ScheduleEntry, ScheduleSearchParams } from '../../src/entities/schedule/types';
-import type { Airport } from '../../src/entities/airport/types';
-import type { Destination } from '../../src/entities/destination/types';
-
-// ============================================================================
-// Test Data Generators
-// ============================================================================
-
-export const CITIES = [
- { code: 'MOW', name: 'Moscow', nameRu: 'Москва' },
- { code: 'LED', name: 'Saint Petersburg', nameRu: 'Санкт-Петербург' },
- { code: 'AER', name: 'Sochi', nameRu: 'Сочи' },
- { code: 'OVB', name: 'Novosibirsk', nameRu: 'Новосибирск' },
- { code: 'KRR', name: 'Krasnodar', nameRu: 'Краснодар' },
- { code: 'SVX', name: 'Yekaterinburg', nameRu: 'Екатеринбург' },
- { code: 'KJA', name: 'Krasnoyarsk', nameRu: 'Красноярск' },
- { code: 'GOJ', name: 'Nizhny Novgorod', nameRu: 'Нижний Новгород' },
- { code: 'KUF', name: 'Samara', nameRu: 'Самара' },
- { code: 'UFA', name: 'Ufa', nameRu: 'Уфа' },
- { code: 'KZN', name: 'Kazan', nameRu: 'Казань' },
- { code: 'ROV', name: 'Rostov-on-Don', nameRu: 'Ростов-на-Дону' },
- { code: 'VVO', name: 'Vladivostok', nameRu: 'Владивосток' },
- { code: 'KHV', name: 'Khabarovsk', nameRu: 'Хабаровск' },
- { code: 'IKT', name: 'Irkutsk', nameRu: 'Иркутск' },
- { code: 'OMS', name: 'Omsk', nameRu: 'Омск' },
- { code: 'KGD', name: 'Kaliningrad', nameRu: 'Калининград' },
- { code: 'MRV', name: 'Mineralnye Vody', nameRu: 'Минеральные Воды' },
- { code: 'MCX', name: 'Makhachkala', nameRu: 'Махачкала' },
- { code: 'AAQ', name: 'Anapa', nameRu: 'Анапа' },
-] as const;
-
-export const AIRPORTS = [
- { code: 'SVO', name: 'Sheremetyevo', cityCode: 'MOW', cityName: 'Moscow', countryCode: 'RU' },
- { code: 'DME', name: 'Domodedovo', cityCode: 'MOW', cityName: 'Moscow', countryCode: 'RU' },
- { code: 'VKO', name: 'Vnukovo', cityCode: 'MOW', cityName: 'Moscow', countryCode: 'RU' },
- {
- code: 'LED',
- name: 'Pulkovo',
- cityCode: 'LED',
- cityName: 'Saint Petersburg',
- countryCode: 'RU',
- },
- { code: 'AER', name: 'Adler', cityCode: 'AER', cityName: 'Sochi', countryCode: 'RU' },
- { code: 'OVB', name: 'Tolmachevo', cityCode: 'OVB', cityName: 'Novosibirsk', countryCode: 'RU' },
- { code: 'KRR', name: 'Pashkovsky', cityCode: 'KRR', cityName: 'Krasnodar', countryCode: 'RU' },
- { code: 'SVX', name: 'Koltsovo', cityCode: 'SVX', cityName: 'Yekaterinburg', countryCode: 'RU' },
- { code: 'KJA', name: 'Emelyanovo', cityCode: 'KJA', cityName: 'Krasnoyarsk', countryCode: 'RU' },
- {
- code: 'GOJ',
- name: 'Strigino',
- cityCode: 'GOJ',
- cityName: 'Nizhny Novgorod',
- countryCode: 'RU',
- },
-] as const;
-
-export const FLIGHT_NUMBERS = [
- 'SU 1124',
- 'SU 1076',
- 'SU 6170',
- 'SU 1208',
- 'SU 1108',
- 'SU 6245',
- 'SU 1455',
- 'SU 1483',
- 'SU 1759',
- 'SU 6268',
- 'SU 6132',
- 'SU 1525',
- 'SU 1400',
- 'SU 1510',
- 'SU 1190',
- 'SU 1130',
- 'SU 1234',
- 'SU 6310',
- 'SU 1350',
- 'SU 1720',
-] as const;
-
-export const AIRLINE_CODES = ['SU', 'FV'] as const;
-
-export const AIRLINE_NAMES = {
- SU: 'Aeroflot',
- FV: 'Rossiya',
-} as const;
-
-export const AIRCRAFT_TYPES = [
- 'Airbus A320',
- 'Airbus A321',
- 'Airbus A321neo',
- 'Boeing 737-800',
- 'Boeing 777-300',
- 'Boeing 777-300ER',
- 'Sukhoi SuperJet 100',
-] as const;
-
-export const STATUS_TYPES: FlightStatus[] = [
- 'scheduled',
- 'checkin',
- 'boarding',
- 'departed',
- 'inFlight',
- 'landed',
- 'arrived',
- 'delayed',
- 'cancelled',
- 'gateChanged',
-];
-
-// ============================================================================
-// Flight Data Generators
-// ============================================================================
-
-export function generateFlightId(): string {
- return `fl-${Math.random().toString(36).substring(2, 10)}`;
-}
-
-export function generateFlightNumber(): string {
- const num = Math.floor(Math.random() * 9000) + 1000;
- return `SU ${num}`;
-}
-
-export function generateFlight({
- direction = 'departure',
- date = new Date().toISOString().split('T')[0],
- cityCode = 'MOW',
- status = 'scheduled',
- flightNumber = generateFlightNumber(),
- airlineCode = 'SU',
- aircraftType = 'Airbus A320',
-}: {
- direction?: FlightDirection;
- date?: string;
- cityCode?: string;
- status?: FlightStatus;
- flightNumber?: string;
- airlineCode?: (typeof AIRLINE_CODES)[number];
- aircraftType?: (typeof AIRCRAFT_TYPES)[number];
-} = {}): Flight {
- const depCity = CITIES.find((c) => c.code === cityCode) || CITIES[0];
- const arrCity = CITIES.find((c) => c.code !== cityCode) || CITIES[1];
-
- const depAirport = AIRPORTS.find((a) => a.cityCode === cityCode) || AIRPORTS[0];
- const arrAirport =
- AIRPORTS.find((a) => a.cityCode === arrCity.code && a.code !== depAirport.code) || AIRPORTS[1];
-
- const depTime = `${Math.floor(Math.random() * 23)}:${String(Math.floor(Math.random() * 60)).padStart(2, '0')}`;
- const arrTime = `${Math.floor(Math.random() * 23)}:${String(Math.floor(Math.random() * 60)).padStart(2, '0')}`;
-
- const flightId = generateFlightId();
-
- return {
- id: flightId,
- flightNumber,
- airlineCode,
- airlineName: AIRLINE_NAMES[airlineCode as keyof typeof AIRLINE_NAMES],
- aircraftType,
- direction,
- status,
- date,
- departure: {
- airportCode: depAirport.code,
- airportName: depAirport.name,
- cityCode: depCity.code,
- cityName: depCity.name,
- terminal:
- Math.random() > 0.5 ? undefined : ['A', 'B', 'C', 'D'][Math.floor(Math.random() * 4)],
- time: {
- scheduled: `${date}T${depTime}:00+03:00`,
- actual:
- status === 'departed' || status === 'inFlight' || status === 'arrived'
- ? `${date}T${depTime}:00+03:00`
- : undefined,
- },
- },
- arrival: {
- airportCode: arrAirport.code,
- airportName: arrAirport.name,
- cityCode: arrCity.code,
- cityName: arrCity.name,
- terminal: Math.random() > 0.5 ? undefined : ['1', '2', '3'][Math.floor(Math.random() * 3)],
- time: {
- scheduled: `${date}T${arrTime}:00+03:00`,
- actual: status === 'arrived' ? `${date}T${arrTime}:00+03:00` : undefined,
- expected: status === 'delayed' ? `${date}T${arrTime}:00+03:00` : undefined,
- },
- },
- boarding:
- status === 'boarding' || status === 'departed' || status === 'inFlight'
- ? {
- gate: `${Math.floor(Math.random() * 50) + 1}`,
- status: status === 'boarding' ? 'Идёт посадка' : 'Закончена',
- startTime: `${date}T${depTime}:00+03:00`,
- endTime: `${date}T${depTime}:00+03:00`,
- }
- : undefined,
- arrivalInfo:
- status === 'arrived' || status === 'landed'
- ? {
- baggageBelt: `${Math.floor(Math.random() * 10) + 1}`,
- transfer: Math.random() > 0.5 ? 'Тран' : undefined,
- }
- : undefined,
- checkin:
- status === 'checkin' || status === 'boarding' || status === 'departed'
- ? {
- status: status === 'checkin' ? 'В процессе' : 'Закончена',
- startTime: `${date}T${depTime}:00+03:00`,
- endTime: `${date}T${depTime}:00+03:00`,
- }
- : undefined,
- deplaning:
- status === 'arrived' || status === 'landed'
- ? {
- status: 'В процессе',
- startTime: `${date}T${arrTime}:00+03:00`,
- endTime: `${date}T${arrTime}:00+03:00`,
- transfer: Math.random() > 0.5 ? 'Трап' : undefined,
- gate: `${Math.floor(Math.random() * 50) + 1}`,
- baggageBelt: `${Math.floor(Math.random() * 10) + 1}`,
- }
- : undefined,
- aircraft: {
- type: aircraftType,
- name: Math.random() > 0.5 ? `${aircraftType} ${Math.floor(Math.random() * 100)}` : undefined,
- totalSeats: Math.floor(Math.random() * 300) + 100,
- economySeats: Math.floor(Math.random() * 250) + 100,
- businessSeats: Math.floor(Math.random() * 20) + 5,
- previousFlight: Math.random() > 0.5 ? generateFlightNumber() : undefined,
- },
- catering:
- Math.random() > 0.5
- ? {
- economy: true,
- business: true,
- }
- : undefined,
- schedule: {
- scheduledDeparture: `${date}T${depTime}:00+03:00`,
- scheduledArrival: `${date}T${arrTime}:00+03:00`,
- duration: `${Math.floor(Math.random() * 5) + 1}ч. ${Math.floor(Math.random() * 59)}мин.`,
- utcOffset: 'UTC+03:00',
- operatingDays: [1, 2, 3, 4, 5, 6, 7],
- weekRange: `* Расписание на неделю ${date}`,
- },
- lastUpdated:
- status === 'departed' || status === 'arrived'
- ? `${depTime} ${date.replace(/-/g, '.')}`
- : undefined,
- };
-}
-
-export function generateFlights(
- count: number = 20,
- options: Partial[0]> = {},
-): Flight[] {
- return Array.from({ length: count }, () => generateFlight(options));
-}
-
-// ============================================================================
-// Schedule Data Generators
-// ============================================================================
-
-export function generateScheduleEntry({
- from = 'MOW',
- to = 'AER',
- dateFrom = new Date().toISOString().split('T')[0],
- dateTo = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
- direct = true,
-}: {
- from?: string;
- to?: string;
- dateFrom?: string;
- dateTo?: string;
- direct?: boolean;
-} = {}): ScheduleEntry {
- const depCity = CITIES.find((c) => c.code === from) || CITIES[0];
- const arrCity = CITIES.find((c) => c.code === to) || CITIES[1];
-
- const depAirport = AIRPORTS.find((a) => a.cityCode === from) || AIRPORTS[0];
- const arrAirport = AIRPORTS.find((a) => a.cityCode === to) || AIRPORTS[1];
-
- const depTime = `${Math.floor(Math.random() * 23)}:${String(Math.floor(Math.random() * 60)).padStart(2, '0')}`;
- const arrTime = `${Math.floor(Math.random() * 23)}:${String(Math.floor(Math.random() * 60)).padStart(2, '0')}`;
-
- return {
- id: generateFlightId(),
- flightNumber: generateFlightNumber(),
- airlineCode: 'SU',
- airlineName: 'Aeroflot',
- aircraftType: AIRCRAFT_TYPES[Math.floor(Math.random() * AIRCRAFT_TYPES.length)],
- departureCity: depCity.name,
- departureCityCode: depCity.code,
- departureAirport: depAirport.name,
- departureTime: depTime,
- arrivalCity: arrCity.name,
- arrivalCityCode: arrCity.code,
- arrivalAirport: arrAirport.name,
- arrivalTime: arrTime,
- daysOfWeek: [1, 2, 3, 4, 5, 6, 7],
- effectiveFrom: dateFrom,
- effectiveTo: dateTo,
- direct,
- };
-}
-
-export function generateScheduleEntries(
- count: number = 50,
- options: Partial[0]> = {},
-): ScheduleEntry[] {
- return Array.from({ length: count }, () => generateScheduleEntry(options));
-}
-
-// ============================================================================
-// Destination Data Generators
-// ============================================================================
-
-export function generateDestination({
- departureCity = 'MOW',
- arrivalCity = 'AER',
- flightCount = Math.floor(Math.random() * 100) + 1,
- dates = [new Date().toISOString().split('T')[0]],
-}: {
- departureCity?: string;
- arrivalCity?: string;
- flightCount?: number;
- dates?: string[];
-} = {}): Destination {
- const depCity = CITIES.find((c) => c.code === departureCity) || CITIES[0];
- const arrCity = CITIES.find((c) => c.code === arrivalCity) || CITIES[1];
-
- return {
- id: `dest-${departureCity}-${arrivalCity}`,
- departureCity: depCity.name,
- departureCityCode: depCity.code,
- arrivalCity: arrCity.name,
- arrivalCityCode: arrCity.code,
- flightCount,
- dates,
- };
-}
-
-export function generateDestinations(count: number = 20): Destination[] {
- return Array.from({ length: count }, () => generateDestination());
-}
-
-// ============================================================================
-// URL Pattern Helpers
-// ============================================================================
-
-export function buildRouteParam(cityCode: string, date: string): string {
- return `${cityCode}-${date.replace(/-/g, '')}`;
-}
-
-export function buildLocalePath(locale: string, path: string): string {
- return `/${locale}${path}`;
-}
-
-export function buildOnlineBoardPath(
- direction: 'departure' | 'arrival',
- cityCode: string,
- date: string,
-): string {
- return `/onlineboard/${direction}/${buildRouteParam(cityCode, date)}`;
-}
-
-export function buildSchedulePath(): string {
- return '/schedule';
-}
-
-export function buildFlightsMapPath(): string {
- return '/flights-map';
-}
-
-export function buildFlightDetailsPath(flightNumber: string, date: string): string {
- const slug = `${flightNumber.replace(/\s+/g, '')}-${date.replace(/-/g, '')}`;
- return `/${slug}`;
-}
-
-// ============================================================================
-// Test Assertion Helpers
-// ============================================================================
-
-export async function expectUrlToMatch(page: Page, pattern: RegExp | string): Promise {
- const url = page.url();
- if (typeof pattern === 'string') {
- expect(url).toContain(pattern);
- } else {
- expect(url).toMatch(pattern);
- }
-}
-
-export async function expectElementToBeVisible(locator: Locator, message?: string): Promise {
- await expect(locator).toBeVisible({ timeout: 10000 });
-}
-
-export async function expectElementToBeHidden(locator: Locator, message?: string): Promise {
- await expect(locator).toBeHidden({ timeout: 10000 });
-}
-
-export async function expectElementToHaveText(
- locator: Locator,
- text: string | RegExp,
- message?: string,
-): Promise {
- await expect(locator).toHaveText(text, { timeout: 10000 });
-}
-
-export async function expectElementToContainText(
- locator: Locator,
- text: string,
- message?: string,
-): Promise {
- await expect(locator).toContainText(text, { timeout: 10000 });
-}
-
-export async function expectElementToHaveAttribute(
- locator: Locator,
- attribute: string,
- value: string,
- message?: string,
-): Promise {
- await expect(locator).toHaveAttribute(attribute, value, { timeout: 10000 });
-}
-
-export async function expectElementToHaveClass(
- locator: Locator,
- className: string,
- message?: string,
-): Promise {
- await expect(locator).toHaveClass(new RegExp(className), { timeout: 10000 });
-}
-
-export async function expectElementToBeEnabled(locator: Locator, message?: string): Promise {
- await expect(locator).toBeEnabled({ timeout: 10000 });
-}
-
-export async function expectElementToBeDisabled(locator: Locator, message?: string): Promise {
- await expect(locator).toBeDisabled({ timeout: 10000 });
-}
-
-export async function expectElementToBeChecked(locator: Locator, message?: string): Promise {
- await expect(locator).toBeChecked({ timeout: 10000 });
-}
-
-export async function expectElementToBeUnchecked(
- locator: Locator,
- message?: string,
-): Promise {
- await expect(locator).not.toBeChecked({ timeout: 10000 });
-}
-
-export async function expectElementToHaveValue(
- locator: Locator,
- value: string,
- message?: string,
-): Promise {
- await expect(locator).toHaveValue(value, { timeout: 10000 });
-}
-
-export async function expectElementToHaveCount(
- locator: Locator,
- count: number,
- message?: string,
-): Promise {
- await expect(locator).toHaveCount(count, { timeout: 10000 });
-}
-
-export async function expectElementToBeFocused(locator: Locator, message?: string): Promise {
- await expect(locator).toBeFocused({ timeout: 10000 });
-}
-
-export async function expectElementNotToBeFocused(
- locator: Locator,
- message?: string,
-): Promise {
- await expect(locator).not.toBeFocused({ timeout: 10000 });
-}
-
-// ============================================================================
-// Flight Search Helpers
-// ============================================================================
-
-export async function searchFlightByNumber(
- page: Page,
- flightNumber: string,
- date?: string,
-): Promise {
- const dateParam = date || new Date().toISOString().split('T')[0];
- await page.goto(`/ru-ru/onlineboard/departure/MOW-${buildRouteParam('MOW', dateParam)}`);
- await page.waitForLoadState('networkidle');
-
- const searchInput = page.locator('[data-testid="flight-search-input"]');
- await searchInput.fill(flightNumber);
- await searchInput.press('Enter');
- await page.waitForLoadState('networkidle');
-}
-
-export async function searchFlightByRoute(
- page: Page,
- departureCity: string,
- arrivalCity: string,
- date?: string,
-): Promise {
- const dateParam = date || new Date().toISOString().split('T')[0];
- await page.goto(`/ru-ru/onlineboard/departure/MOW-${buildRouteParam('MOW', dateParam)}`);
- await page.waitForLoadState('networkidle');
-
- const routeTab = page.locator('[data-testid="route-search-tab"]');
- await routeTab.click();
- await page.waitForTimeout(500);
-
- const departureInput = page.locator('[data-testid="departure-city-input"]');
- const arrivalInput = page.locator('[data-testid="arrival-city-input"]');
-
- await departureInput.fill(departureCity);
- await page.waitForTimeout(500);
- await departureInput.press('Enter');
- await page.waitForTimeout(500);
-
- await arrivalInput.fill(arrivalCity);
- await page.waitForTimeout(500);
- await arrivalInput.press('Enter');
- await page.waitForLoadState('networkidle');
-}
-
-export async function searchFlightByDate(page: Page, date: string): Promise {
- await page.goto(`/ru-ru/onlineboard/departure/MOW-${buildRouteParam('MOW', date)}`);
- await page.waitForLoadState('networkidle');
-}
-
-export async function openFlightDetails(page: Page, flightIndex: number = 0): Promise {
- const flightCards = page.locator('[data-testid="flight-card"]');
- await expect(flightCards).toHaveCount(flightIndex + 1, { timeout: 10000 });
- await flightCards.nth(flightIndex).click();
- await page.waitForLoadState('networkidle');
-}
-
-export async function verifyFlightCard(
- page: Page,
- flight: Flight,
- index: number = 0,
-): Promise {
- const flightCards = page.locator('[data-testid="flight-card"]');
- const count = await flightCards.count();
- await expect(flightCards).toHaveCount(count);
-
- const card = flightCards.nth(index);
-
- await expect(card.getByText(flight.flightNumber)).toBeVisible();
- await expect(card.getByText(flight.airlineName)).toBeVisible();
- await expect(card.getByText(flight.departure.cityName)).toBeVisible();
- await expect(card.getByText(flight.arrival.cityName)).toBeVisible();
-
- const depTime = flight.departure.time.scheduled.slice(11, 16);
- await expect(card.getByText(depTime)).toBeVisible();
-
- const arrTime = flight.arrival.time.scheduled.slice(11, 16);
- await expect(card.getByText(arrTime)).toBeVisible();
-}
-
-export async function verifyFlightDetails(page: Page, flight: Flight): Promise {
- await expect(page.getByText(flight.flightNumber)).toBeVisible();
- await expect(page.getByText(flight.airlineName)).toBeVisible();
- await expect(page.getByText(flight.departure.cityName)).toBeVisible();
- await expect(page.getByText(flight.arrival.cityName)).toBeVisible();
-
- if (flight.aircraft?.type) {
- await expect(page.getByText(flight.aircraft.type)).toBeVisible();
- }
-
- if (flight.schedule?.duration) {
- await expect(page.getByText(flight.schedule.duration)).toBeVisible();
- }
-}
-
-// ============================================================================
-// Date Helpers
-// ============================================================================
-
-export function formatDateForUrl(date: string | Date): string {
- const d = typeof date === 'string' ? date : date.toISOString().split('T')[0];
- return d.replace(/-/g, '');
-}
-
-export function formatDateForDisplay(date: string | Date, locale: string = 'ru'): string {
- const d = typeof date === 'string' ? date : date.toISOString().split('T')[0];
- const dateObj = new Date(d);
- const options: Intl.DateTimeFormatOptions = { day: 'numeric', month: 'short' };
- return new Date(d).toLocaleDateString(locale, options);
-}
-
-export function getToday(): string {
- return new Date().toISOString().split('T')[0];
-}
-
-export function getTomorrow(): string {
- const d = new Date();
- d.setDate(d.getDate() + 1);
- return d.toISOString().split('T')[0];
-}
-
-export function getYesterday(): string {
- const d = new Date();
- d.setDate(d.getDate() - 1);
- return d.toISOString().split('T')[0];
-}
-
-export function getFutureDate(days: number): string {
- const d = new Date();
- d.setDate(d.getDate() + days);
- return d.toISOString().split('T')[0];
-}
-
-export function getPastDate(days: number): string {
- const d = new Date();
- d.setDate(d.getDate() - days);
- return d.toISOString().split('T')[0];
-}
-
-// ============================================================================
-// Error Response Generators
-// ============================================================================
-
-export function generateNotFoundError(): {
- status: number;
- body: { error: string; message: string };
-} {
- return {
- status: 404,
- body: {
- error: 'Not Found',
- message: 'The requested resource was not found',
- },
- };
-}
-
-export function generateBadRequestError(): {
- status: number;
- body: { error: string; message: string };
-} {
- return {
- status: 400,
- body: {
- error: 'Bad Request',
- message: 'Invalid request parameters',
- },
- };
-}
-
-export function generateUnauthorizedError(): {
- status: number;
- body: { error: string; message: string };
-} {
- return {
- status: 401,
- body: {
- error: 'Unauthorized',
- message: 'Authentication required',
- },
- };
-}
-
-export function generateForbiddenError(): {
- status: number;
- body: { error: string; message: string };
-} {
- return {
- status: 403,
- body: {
- error: 'Forbidden',
- message: 'Access denied',
- },
- };
-}
-
-export function generateServerError(): {
- status: number;
- body: { error: string; message: string };
-} {
- return {
- status: 500,
- body: {
- error: 'Internal Server Error',
- message: 'An unexpected error occurred',
- },
- };
-}
-
-export function generateTimeoutError(): {
- status: number;
- body: { error: string; message: string };
-} {
- return {
- status: 504,
- body: {
- error: 'Gateway Timeout',
- message: 'The request took too long to process',
- },
- };
-}
-
-// ============================================================================
-// Async Wait Utilities (for complex async operations)
-// ============================================================================
-
-/**
- * Wait for an element with an extended timeout for slow async operations.
- * Used for map initialization, calendar loading, etc.
- */
-export async function waitForElementExtended(
- page: Page,
- selector: string,
- timeoutMs: number = 20000,
-): Promise {
- try {
- await page.locator(selector).first().waitFor({ timeout: timeoutMs });
- } catch (error) {
- // If element doesn't appear, log but don't fail - test will fail naturally if needed
- console.log(`Extended wait for "${selector}" timed out after ${timeoutMs}ms`);
- }
-}
-
-/**
- * Wait for a locator with extended timeout.
- */
-export async function waitForLocatorExtended(
- locator: Locator,
- timeoutMs: number = 20000,
-): Promise {
- try {
- await locator.first().waitFor({ timeout: timeoutMs, state: 'visible' });
- } catch (error) {
- // If element doesn't appear, log but don't fail
- console.log(`Extended wait for locator timed out after ${timeoutMs}ms`);
- }
-}
-
-/**
- * Retry an async operation with backoff.
- * Useful for flaky async operations or race conditions.
- */
-export async function retryAsync(
- fn: () => Promise,
- maxRetries: number = 3,
- delayMs: number = 500,
-): Promise {
- let lastError: Error | undefined;
- for (let i = 0; i < maxRetries; i++) {
- try {
- return await fn();
- } catch (error) {
- lastError = error as Error;
- if (i < maxRetries - 1) {
- await new Promise((resolve) => setTimeout(resolve, delayMs * (i + 1)));
- }
- }
- }
- throw lastError || new Error('Retry exhausted');
-}
-
-// ============================================================================
-// Test Data Fixtures
-// ============================================================================
-
-export const FIXTURES = {
- flights: {
- departures: generateFlights(20, { direction: 'departure', cityCode: 'MOW' }),
- arrivals: generateFlights(20, { direction: 'arrival', cityCode: 'MOW' }),
- scheduled: generateFlights(20, { status: 'scheduled' }),
- departed: generateFlights(20, { status: 'departed' }),
- delayed: generateFlights(20, { status: 'delayed' }),
- cancelled: generateFlights(20, { status: 'cancelled' }),
- },
- schedule: {
- entries: generateScheduleEntries(50),
- },
- destinations: {
- entries: generateDestinations(20),
- },
- airports: {
- entries: AIRPORTS,
- },
- cities: {
- entries: CITIES,
- },
- errors: {
- notFound: generateNotFoundError(),
- badRequest: generateBadRequestError(),
- unauthorized: generateUnauthorizedError(),
- forbidden: generateForbiddenError(),
- serverError: generateServerError(),
- timeout: generateTimeoutError(),
- },
-};
-
-export default FIXTURES;
diff --git a/tests/e2e-angular/visual/flight-board.spec.ts b/tests/e2e-angular/visual/flight-board.spec.ts
deleted file mode 100644
index 997ce8aa..00000000
--- a/tests/e2e-angular/visual/flight-board.spec.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { test, expect } from '@playwright/test';
-import { todayStr } from '../../src/lib/date-utils';
-
-const today = todayStr();
-const dateParam = today.replace(/-/g, '');
-
-test.describe('Flight board visual regression', () => {
- test('departures view matches screenshot', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/departure/MOW-${dateParam}`);
- await page.waitForLoadState('networkidle');
- await expect(page).toHaveScreenshot('flight-board-departures.png', {
- fullPage: true,
- });
- });
-
- test('arrivals view matches screenshot', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/arrival/MOW-${dateParam}`);
- await page.waitForLoadState('networkidle');
- await expect(page).toHaveScreenshot('flight-board-arrivals.png', {
- fullPage: true,
- });
- });
-});
diff --git a/tests/e2e-angular/visual/flight-expanded.spec.ts b/tests/e2e-angular/visual/flight-expanded.spec.ts
deleted file mode 100644
index cfde2758..00000000
--- a/tests/e2e-angular/visual/flight-expanded.spec.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { test, expect } from '@playwright/test';
-import { todayStr } from '../../src/lib/date-utils';
-
-const today = todayStr();
-const dateParam = today.replace(/-/g, '');
-
-test.describe('Expanded flight card visual regression', () => {
- test('expanded card matches screenshot', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/departure/MOW-${dateParam}`);
- await page.waitForLoadState('networkidle');
-
- await page.waitForSelector('[class*="card"]', { timeout: 10000 });
-
- const firstCard = page.locator('[class*="card"]').first();
- await firstCard.click();
-
- const expandedSection = page.locator('[class*="expanded"]').first();
- await expect(expandedSection).toBeVisible();
- await expect(expandedSection).toHaveScreenshot('flight-expanded.png', { timeout: 10000 });
- });
-});
diff --git a/tests/e2e-angular/visual/landing.spec.ts b/tests/e2e-angular/visual/landing.spec.ts
deleted file mode 100644
index 086e1044..00000000
--- a/tests/e2e-angular/visual/landing.spec.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { test, expect } from '@playwright/test';
-import { todayStr } from '../../src/lib/date-utils';
-
-const today = todayStr();
-const dateParam = today.replace(/-/g, '');
-
-test.describe('Landing page visual regression', () => {
- test('matches landing page screenshot', async ({ page }) => {
- await page.goto(`/ru-ru/onlineboard/departure/MOW-${dateParam}`);
- await page.waitForLoadState('networkidle');
- await expect(page).toHaveScreenshot('landing.png', {
- fullPage: true,
- });
- });
-});