diff --git a/tests/e2e-angular/cross-app/19-popular-requests-behavior.spec.ts b/tests/e2e-angular/cross-app/19-popular-requests-behavior.spec.ts new file mode 100644 index 00000000..f30bf30b --- /dev/null +++ b/tests/e2e-angular/cross-app/19-popular-requests-behavior.spec.ts @@ -0,0 +1,169 @@ +import { test, expect } from '../support/cross-app-fixtures'; +import { S, tid } from '../support/selectors'; + +test.describe('Popular Requests Behavior', () => { + test.beforeEach(async ({ page, localePath }) => { + await page.goto(localePath('onlineboard')); + await page.waitForLoadState('networkidle'); + }); + + test('1: Popular requests panel is visible on onlineboard start page', async ({ page, app }) => { + const panel = page.locator(tid(S.POPULAR_REQUESTS_PANEL, app)); + const fallback = page.locator( + '.popular-requests, popular-requests, [data-testid="landing-popular-request"]', + ); + const target = (await panel.count()) > 0 ? panel : fallback.first(); + if ((await target.count()) === 0) { + test.skip(true, 'Popular requests panel not present in this app'); + return; + } + await expect(target).toBeVisible({ timeout: 10000 }); + }); + + test('2: Popular requests panel shows up to 4 items', async ({ page, app }) => { + const items = page.locator(tid(S.POPULAR_REQUEST_ITEM, app)); + const fallback = page.locator( + 'popular-request, .popular-requests__item, [data-testid="landing-popular-request"]', + ); + const target = (await items.count()) > 0 ? items : fallback; + const count = await target.count(); + if (count === 0) { + test.skip(true, 'No popular request items found'); + return; + } + expect(count).toBeGreaterThanOrEqual(1); + expect(count).toBeLessThanOrEqual(4); + }); + + test('3: Clicking a flight number request navigates to flight search', async ({ + page, + app, + locale, + }) => { + const items = page.locator(tid(S.POPULAR_REQUEST_ITEM, app)); + const fallback = page.locator( + 'popular-request, .popular-requests__item, [data-testid="landing-popular-request"]', + ); + const target = (await items.count()) > 0 ? items : fallback; + const count = await target.count(); + if (count === 0) { + test.skip(true, 'No popular request items found'); + return; + } + + // Find a flight-number request item (contains a flight number pattern like SU1234 or SU 1234) + let flightItem = null; + for (let i = 0; i < count; i++) { + const text = await target.nth(i).textContent(); + if (text && /[A-Z]{2}\s*\d{1,4}/i.test(text)) { + flightItem = target.nth(i); + break; + } + } + if (!flightItem) { + test.skip(true, 'No flight-number popular request found'); + return; + } + + const urlBefore = page.url(); + await flightItem.click(); + await page.waitForLoadState('networkidle'); + + const urlAfter = page.url(); + // Should have navigated away from the landing page + expect(urlAfter).not.toBe(urlBefore); + // URL should indicate a flight search (flight number in path or query) + expect(urlAfter).toMatch(/onlineboard\/(departure|arrival|flight)|flight/i); + }); + + test('4: Clicking a route request navigates to route search', async ({ page, app, locale }) => { + const items = page.locator(tid(S.POPULAR_REQUEST_ITEM, app)); + const fallback = page.locator( + 'popular-request, .popular-requests__item, [data-testid="landing-popular-request"]', + ); + const target = (await items.count()) > 0 ? items : fallback; + const count = await target.count(); + if (count === 0) { + test.skip(true, 'No popular request items found'); + return; + } + + // Find a route request item (contains city names or route pattern like MOW-LED, or has a dash/arrow between cities) + let routeItem = null; + for (let i = 0; i < count; i++) { + const text = await target.nth(i).textContent(); + // Route items typically contain an arrow, dash between cities, or two city codes + if (text && (/[A-Z]{3}\s*[-\u2013\u2014\u2192]\s*[A-Z]{3}/.test(text) || /\u2014|\u2192|->/.test(text))) { + routeItem = target.nth(i); + break; + } + } + if (!routeItem) { + // Fallback: just click the last item (routes are often listed after flight numbers) + routeItem = target.last(); + } + + const urlBefore = page.url(); + await routeItem.click(); + await page.waitForLoadState('networkidle'); + + const urlAfter = page.url(); + expect(urlAfter).not.toBe(urlBefore); + // URL should indicate a route/departure search + expect(urlAfter).toMatch(/onlineboard\/(departure|arrival)|schedule/i); + }); + + test('5: Popular requests visible on schedule start page', async ({ page, app, localePath }) => { + await page.goto(localePath('schedule')); + await page.waitForLoadState('networkidle'); + + const panel = page.locator(tid(S.POPULAR_REQUESTS_PANEL, app)); + const fallback = page.locator( + '.popular-requests, popular-requests, [data-testid="landing-popular-request"]', + ); + const target = (await panel.count()) > 0 ? panel : fallback.first(); + if ((await target.count()) === 0) { + test.skip(true, 'Popular requests panel not present on schedule page'); + return; + } + await expect(target).toBeVisible({ timeout: 10000 }); + }); + + test('6: Popular request items are keyboard accessible', async ({ page, app }) => { + const items = page.locator(tid(S.POPULAR_REQUEST_ITEM, app)); + const fallback = page.locator( + 'popular-request, .popular-requests__item, [data-testid="landing-popular-request"]', + ); + const target = (await items.count()) > 0 ? items : fallback; + const count = await target.count(); + if (count === 0) { + test.skip(true, 'No popular request items found'); + return; + } + + const firstItem = target.first(); + await expect(firstItem).toBeVisible(); + + // Check that the item or its child link/button is focusable + const focusable = firstItem.locator('a, button, [tabindex="0"]'); + if ((await focusable.count()) > 0) { + await focusable.first().focus(); + await expect(focusable.first()).toBeFocused(); + + // Press Enter and verify it triggers navigation + const urlBefore = page.url(); + await page.keyboard.press('Enter'); + await page.waitForLoadState('networkidle'); + const urlAfter = page.url(); + expect(urlAfter).not.toBe(urlBefore); + } else { + // The item itself might be focusable + const tabindex = await firstItem.getAttribute('tabindex'); + const role = await firstItem.getAttribute('role'); + const tagName = await firstItem.evaluate((el) => el.tagName.toLowerCase()); + const isFocusable = + tabindex !== null || role === 'link' || role === 'button' || tagName === 'a'; + expect(isFocusable).toBe(true); + } + }); +}); diff --git a/tests/e2e-angular/support/selectors.ts b/tests/e2e-angular/support/selectors.ts index 8d991891..21e00e33 100644 --- a/tests/e2e-angular/support/selectors.ts +++ b/tests/e2e-angular/support/selectors.ts @@ -69,6 +69,10 @@ export const S = { LANDING_SEARCH_HISTORY: 'landing-search-history', LANDING_SEARCH_HISTORY_ITEM: 'landing-search-history-item', + // Popular Requests + POPULAR_REQUESTS_PANEL: 'popular-requests-panel', + POPULAR_REQUEST_ITEM: 'popular-request-item', + // Schedule - Filter SCHEDULE_DEPARTURE_INPUT: 'schedule-departure-input', SCHEDULE_ARRIVAL_INPUT: 'schedule-arrival-input', @@ -154,6 +158,8 @@ const ANGULAR_OVERRIDES: Partial> = { [S.MAP_DEPARTURE_INPUT]: 'route-departure-city-input', [S.MAP_ARRIVAL_INPUT]: 'route-arrival-city-input', [S.MAP_CALENDAR]: 'route-calendar-input', + [S.POPULAR_REQUESTS_PANEL]: 'popular-requests', + [S.POPULAR_REQUEST_ITEM]: 'popular-request', [S.BOARD_LOADER]: 'loader', [S.BOARD_SEARCH_RESULT]: 'board-search-result', [S.BOARD_FLIGHT_RESULT]: 'flight-result',