From de660938ba13142630bb2c44f838d9d3a9e70816 Mon Sep 17 00:00:00 2001 From: gnezim Date: Wed, 15 Apr 2026 23:05:01 +0300 Subject: [PATCH] Remove stray e2e-angular tests and add to gitignore --- .gitignore | 3 + playwright-angular.config.ts | 41 - test-results/angular-e2e-report/index.html | 90 - tests/e2e-angular/QUICK_REFERENCE.md | 217 --- tests/e2e-angular/README.md | 382 ---- tests/e2e-angular/console-audit.spec.ts | 182 -- .../cross-app/01-navigation.spec.ts | 233 --- .../cross-app/02-online-board-landing.spec.ts | 428 ----- .../cross-app/03-flight-search.spec.ts | 636 ------- .../cross-app/04-departure-search.spec.ts | 835 -------- .../cross-app/05-arrival-search.spec.ts | 831 -------- .../cross-app/06-route-search.spec.ts | 1042 ---------- .../cross-app/07-flight-details.spec.ts | 579 ------ .../cross-app/08-schedule-search.spec.ts | 1056 ----------- .../cross-app/09-schedule-results.spec.ts | 691 ------- .../cross-app/10-schedule-details.spec.ts | 661 ------- .../cross-app/11-flights-map.spec.ts | 1376 -------------- .../cross-app/12-error-pages.spec.ts | 515 ----- .../cross-app/13-locale-switching.spec.ts | 640 ------- .../cross-app/14-search-history.spec.ts | 765 -------- .../cross-app/15-ui-elements.spec.ts | 719 ------- .../16-advanced-search-scenarios.spec.ts | 1206 ------------ .../cross-app/17-board-schedule-view.spec.ts | 471 ----- .../cross-app/18-advanced-features.spec.ts | 756 -------- tests/e2e-angular/error-handling.spec.ts | 326 ---- tests/e2e-angular/fixtures/api-responses.json | 409 ---- tests/e2e-angular/fixtures/cities.json | 184 -- tests/e2e-angular/fixtures/errors.json | 168 -- tests/e2e-angular/fixtures/flights.json | 443 ----- tests/e2e-angular/fixtures/routes.json | 252 --- tests/e2e-angular/flight-details.spec.ts | 1081 ----------- tests/e2e-angular/flight-results.spec.ts | 338 ---- tests/e2e-angular/flights-map.spec.ts | 908 --------- .../08 - online board - route.spec.ts | 908 --------- .../10 - schedule - search.spec.ts | 1453 -------------- .../integration/11 - flight details.spec.ts | 1685 ----------------- .../11 - flight details.spec.ts.bak | 1667 ---------------- .../integration/12 - flights map.spec.ts | 743 -------- .../integration/online-board-arrival.spec.ts | 596 ------ .../online-board-departure.spec.ts | 1084 ----------- .../online-board-flight-search.spec.ts | 622 ------ .../templates/flight-details.template.ts | 450 ----- .../templates/flights-map.template.ts | 334 ---- .../online-board-arrival.template.ts | 301 --- .../online-board-departure.template.ts | 301 --- .../templates/online-board-flight.template.ts | 454 ----- .../templates/online-board-route.template.ts | 310 --- .../templates/popular-requests.template.ts | 301 --- .../templates/schedule-search.template.ts | 427 ----- tests/e2e-angular/navigation.spec.ts | 71 - tests/e2e-angular/popular-requests.spec.ts | 83 - tests/e2e-angular/responsive.spec.ts | 146 -- tests/e2e-angular/ru-ru/aria-labels.spec.ts | 84 - .../e2e-angular/ru-ru/caching-refresh.spec.ts | 198 -- .../ru-ru/cross-app-validation.spec.ts | 373 ---- tests/e2e-angular/ru-ru/empty-state.spec.ts | 211 --- tests/e2e-angular/ru-ru/focus-visible.spec.ts | 100 - .../e2e-angular/ru-ru/form-validation.spec.ts | 470 ----- .../ru-ru/history-navigation.spec.ts | 71 - .../ru-ru/input-validation.spec.ts | 171 -- .../ru-ru/keyboard-navigation.spec.ts | 153 -- .../e2e-angular/ru-ru/large-datasets.spec.ts | 239 --- .../ru-ru/persistent-state.spec.ts | 117 -- tests/e2e-angular/ru-ru/text-ellipsis.spec.ts | 251 --- tests/e2e-angular/ru-ru/text-scaling.spec.ts | 122 -- .../ru-ru/touch-navigation.spec.ts | 149 -- .../e2e-angular/ru-ru/unicode-support.spec.ts | 278 --- tests/e2e-angular/schedule-details.spec.ts | 564 ------ tests/e2e-angular/schedule-filters.spec.ts | 530 ------ tests/e2e-angular/schedule-results.spec.ts | 670 ------- tests/e2e-angular/schedule-search.spec.ts | 347 ---- tests/e2e-angular/search-history.spec.ts | 199 -- tests/e2e-angular/search-panel.spec.ts | 168 -- tests/e2e-angular/seo.spec.ts | 72 - tests/e2e-angular/support/angular-api-mock.ts | 56 - .../e2e-angular/support/cross-app-fixtures.ts | 60 - tests/e2e-angular/support/selectors.ts | 187 -- tests/e2e-angular/support/test-utilities.ts | 799 -------- tests/e2e-angular/visual/flight-board.spec.ts | 23 - .../visual/flight-expanded.spec.ts | 21 - tests/e2e-angular/visual/landing.spec.ts | 15 - 81 files changed, 3 insertions(+), 37095 deletions(-) delete mode 100644 playwright-angular.config.ts delete mode 100644 test-results/angular-e2e-report/index.html delete mode 100644 tests/e2e-angular/QUICK_REFERENCE.md delete mode 100644 tests/e2e-angular/README.md delete mode 100644 tests/e2e-angular/console-audit.spec.ts delete mode 100644 tests/e2e-angular/cross-app/01-navigation.spec.ts delete mode 100644 tests/e2e-angular/cross-app/02-online-board-landing.spec.ts delete mode 100644 tests/e2e-angular/cross-app/03-flight-search.spec.ts delete mode 100644 tests/e2e-angular/cross-app/04-departure-search.spec.ts delete mode 100644 tests/e2e-angular/cross-app/05-arrival-search.spec.ts delete mode 100644 tests/e2e-angular/cross-app/06-route-search.spec.ts delete mode 100644 tests/e2e-angular/cross-app/07-flight-details.spec.ts delete mode 100644 tests/e2e-angular/cross-app/08-schedule-search.spec.ts delete mode 100644 tests/e2e-angular/cross-app/09-schedule-results.spec.ts delete mode 100644 tests/e2e-angular/cross-app/10-schedule-details.spec.ts delete mode 100644 tests/e2e-angular/cross-app/11-flights-map.spec.ts delete mode 100644 tests/e2e-angular/cross-app/12-error-pages.spec.ts delete mode 100644 tests/e2e-angular/cross-app/13-locale-switching.spec.ts delete mode 100644 tests/e2e-angular/cross-app/14-search-history.spec.ts delete mode 100644 tests/e2e-angular/cross-app/15-ui-elements.spec.ts delete mode 100644 tests/e2e-angular/cross-app/16-advanced-search-scenarios.spec.ts delete mode 100644 tests/e2e-angular/cross-app/17-board-schedule-view.spec.ts delete mode 100644 tests/e2e-angular/cross-app/18-advanced-features.spec.ts delete mode 100644 tests/e2e-angular/error-handling.spec.ts delete mode 100644 tests/e2e-angular/fixtures/api-responses.json delete mode 100644 tests/e2e-angular/fixtures/cities.json delete mode 100644 tests/e2e-angular/fixtures/errors.json delete mode 100644 tests/e2e-angular/fixtures/flights.json delete mode 100644 tests/e2e-angular/fixtures/routes.json delete mode 100644 tests/e2e-angular/flight-details.spec.ts delete mode 100644 tests/e2e-angular/flight-results.spec.ts delete mode 100644 tests/e2e-angular/flights-map.spec.ts delete mode 100644 tests/e2e-angular/integration/08 - online board - route.spec.ts delete mode 100644 tests/e2e-angular/integration/10 - schedule - search.spec.ts delete mode 100644 tests/e2e-angular/integration/11 - flight details.spec.ts delete mode 100644 tests/e2e-angular/integration/11 - flight details.spec.ts.bak delete mode 100644 tests/e2e-angular/integration/12 - flights map.spec.ts delete mode 100644 tests/e2e-angular/integration/online-board-arrival.spec.ts delete mode 100644 tests/e2e-angular/integration/online-board-departure.spec.ts delete mode 100644 tests/e2e-angular/integration/online-board-flight-search.spec.ts delete mode 100644 tests/e2e-angular/integration/templates/flight-details.template.ts delete mode 100644 tests/e2e-angular/integration/templates/flights-map.template.ts delete mode 100644 tests/e2e-angular/integration/templates/online-board-arrival.template.ts delete mode 100644 tests/e2e-angular/integration/templates/online-board-departure.template.ts delete mode 100644 tests/e2e-angular/integration/templates/online-board-flight.template.ts delete mode 100644 tests/e2e-angular/integration/templates/online-board-route.template.ts delete mode 100644 tests/e2e-angular/integration/templates/popular-requests.template.ts delete mode 100644 tests/e2e-angular/integration/templates/schedule-search.template.ts delete mode 100644 tests/e2e-angular/navigation.spec.ts delete mode 100644 tests/e2e-angular/popular-requests.spec.ts delete mode 100644 tests/e2e-angular/responsive.spec.ts delete mode 100644 tests/e2e-angular/ru-ru/aria-labels.spec.ts delete mode 100644 tests/e2e-angular/ru-ru/caching-refresh.spec.ts delete mode 100644 tests/e2e-angular/ru-ru/cross-app-validation.spec.ts delete mode 100644 tests/e2e-angular/ru-ru/empty-state.spec.ts delete mode 100644 tests/e2e-angular/ru-ru/focus-visible.spec.ts delete mode 100644 tests/e2e-angular/ru-ru/form-validation.spec.ts delete mode 100644 tests/e2e-angular/ru-ru/history-navigation.spec.ts delete mode 100644 tests/e2e-angular/ru-ru/input-validation.spec.ts delete mode 100644 tests/e2e-angular/ru-ru/keyboard-navigation.spec.ts delete mode 100644 tests/e2e-angular/ru-ru/large-datasets.spec.ts delete mode 100644 tests/e2e-angular/ru-ru/persistent-state.spec.ts delete mode 100644 tests/e2e-angular/ru-ru/text-ellipsis.spec.ts delete mode 100644 tests/e2e-angular/ru-ru/text-scaling.spec.ts delete mode 100644 tests/e2e-angular/ru-ru/touch-navigation.spec.ts delete mode 100644 tests/e2e-angular/ru-ru/unicode-support.spec.ts delete mode 100644 tests/e2e-angular/schedule-details.spec.ts delete mode 100644 tests/e2e-angular/schedule-filters.spec.ts delete mode 100644 tests/e2e-angular/schedule-results.spec.ts delete mode 100644 tests/e2e-angular/schedule-search.spec.ts delete mode 100644 tests/e2e-angular/search-history.spec.ts delete mode 100644 tests/e2e-angular/search-panel.spec.ts delete mode 100644 tests/e2e-angular/seo.spec.ts delete mode 100644 tests/e2e-angular/support/angular-api-mock.ts delete mode 100644 tests/e2e-angular/support/cross-app-fixtures.ts delete mode 100644 tests/e2e-angular/support/selectors.ts delete mode 100644 tests/e2e-angular/support/test-utilities.ts delete mode 100644 tests/e2e-angular/visual/flight-board.spec.ts delete mode 100644 tests/e2e-angular/visual/flight-expanded.spec.ts delete mode 100644 tests/e2e-angular/visual/landing.spec.ts 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 - - - - -
- - - \ 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, - }); - }); -});