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 210: Locale translation check (no switcher needed) // ───────────────────────────────────────────────────────────────────────── 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')}`; }