diff --git a/src/features/online-board/components/DayTabs/DayTabs.test.tsx b/src/features/online-board/components/DayTabs/DayTabs.test.tsx index 9e31acf5..f5f04842 100644 --- a/src/features/online-board/components/DayTabs/DayTabs.test.tsx +++ b/src/features/online-board/components/DayTabs/DayTabs.test.tsx @@ -155,6 +155,22 @@ describe("DayTabs", () => { expect(screen.getByTestId("day-select")).toBeTruthy(); }); + it("deduplicates available dates before rendering the mobile select", () => { + render( + {}} + />, + ); + + const options = screen.getByTestId("day-select").querySelectorAll("option"); + expect(options).toHaveLength(2); + }); + // ── TZ §4.1.13.1 compliance tests ────────────────────────────────────── it("4.1.13.1: active range today-1 to today+14 yields 16 total dates", () => { diff --git a/src/features/online-board/components/DayTabs/DayTabs.tsx b/src/features/online-board/components/DayTabs/DayTabs.tsx index c8db81fd..a64e0d52 100644 --- a/src/features/online-board/components/DayTabs/DayTabs.tsx +++ b/src/features/online-board/components/DayTabs/DayTabs.tsx @@ -68,8 +68,15 @@ export const DayTabs: FC = ({ // the route has no day data). Treat every date as tappable in that // case — matches Angular where the tabs stay enabled until we *know* // the upstream reports no flights for a given day. - const availableSet = useMemo(() => new Set(availableDates), [availableDates]); - const disableByAvailability = availableDates.length > 0; + const uniqueAvailableDates = useMemo( + () => Array.from(new Set(availableDates)), + [availableDates], + ); + const availableSet = useMemo( + () => new Set(uniqueAvailableDates), + [uniqueAvailableDates], + ); + const disableByAvailability = uniqueAvailableDates.length > 0; const visibleDates = allDates.slice( currentPage * PAGE_SIZE, @@ -150,7 +157,7 @@ export const DayTabs: FC = ({ { await route.fulfill({ status: 200, @@ -96,10 +101,10 @@ export async function routeScheduleVvoMjzFixtures(page: Page): Promise { fulfillJson(route, fixtureText("schedule-days-route.json")), ); await page.route("**/api/flights/1/*/schedule?**", (route) => - fulfillJson(route, fixtureText("schedule-search-vvo-mjz.json")), + fulfillJson(route, vvoMjzFixtureText("schedule-search-vvo-mjz.json")), ); await page.route("**/api/flights/v1.1/*/schedule/details?**", (route) => - fulfillJson(route, fixtureText("schedule-details-vvo-mjz.json")), + fulfillJson(route, vvoMjzFixtureText("schedule-details-vvo-mjz.json")), ); } @@ -112,10 +117,10 @@ export async function routeScheduleVvoMjzServicesFixtures( fulfillJson(route, fixtureText("schedule-days-route.json")), ); await page.route("**/api/flights/1/*/schedule?**", (route) => - fulfillJson(route, fixtureText("schedule-search-vvo-mjz.json")), + fulfillJson(route, vvoMjzFixtureText("schedule-search-vvo-mjz.json")), ); await page.route("**/api/flights/v1.1/*/schedule/details?**", (route) => { - const fixture = JSON.parse(fixtureText("schedule-details-vvo-mjz.json")); + const fixture = JSON.parse(vvoMjzFixtureText("schedule-details-vvo-mjz.json")); const actual = fixture.data.routes[0].leg.equipment.aircraft.actual; actual.onBoardServices = [ { diff --git a/tests/e2e/helpers/dates.ts b/tests/e2e/helpers/dates.ts index 1ad83bc3..d905d4ec 100644 --- a/tests/e2e/helpers/dates.ts +++ b/tests/e2e/helpers/dates.ts @@ -32,3 +32,82 @@ export function sundayOfWeek(date: Date): Date { export function formatRuWeekRange(date: Date): string { return `${formatRuDate(mondayOfWeek(date))} - ${formatRuDate(sundayOfWeek(date))}`; } + +export function vvoMjzScheduleDates(): { + start: Date; + end: Date; + leg1: Date; + leg2: Date; + altLeg1: Date; + altLeg2: Date; + preStart: Date; + startYmd: string; + endYmd: string; + leg1Ymd: string; + leg2Ymd: string; + altLeg1Ymd: string; + altLeg2Ymd: string; + leg1Iso: string; + leg2Iso: string; + altLeg1Iso: string; + altLeg2Iso: string; +} { + const start = mondayOfWeek(addDays(new Date(), 7)); + const end = sundayOfWeek(start); + const leg1 = start; + const leg2 = addDays(start, 1); + const altLeg1 = addDays(start, 4); + const altLeg2 = addDays(start, 5); + const preStart = addDays(start, -2); + + return { + start, + end, + leg1, + leg2, + altLeg1, + altLeg2, + preStart, + startYmd: formatYmd(start), + endYmd: formatYmd(end), + leg1Ymd: formatYmd(leg1), + leg2Ymd: formatYmd(leg2), + altLeg1Ymd: formatYmd(altLeg1), + altLeg2Ymd: formatYmd(altLeg2), + leg1Iso: formatIsoDate(leg1), + leg2Iso: formatIsoDate(leg2), + altLeg1Iso: formatIsoDate(altLeg1), + altLeg2Iso: formatIsoDate(altLeg2), + }; +} + +export function vvoMjzRouteUrl(route = "VVO-MJZ"): string { + const dates = vvoMjzScheduleDates(); + return `/ru-ru/schedule/route/${route}-${dates.startYmd}-${dates.endYmd}`; +} + +export function vvoMjzDetailsUrl(): string { + const dates = vvoMjzScheduleDates(); + return `/ru-ru/schedule/VVO/SU5752-${dates.leg1Ymd}/KJA/SU6837-${dates.leg2Ymd}/MJZ?request=schedule-route-VVO-MJZ-${dates.startYmd}-${dates.endYmd}`; +} + +export function replaceVvoMjzFixtureDates(text: string): string { + const dates = vvoMjzScheduleDates(); + const replacements: Array<[string, string]> = [ + ["20260518", dates.leg1Ymd], + ["20260519", dates.leg2Ymd], + ["20260522", dates.altLeg1Ymd], + ["20260523", dates.altLeg2Ymd], + ["20260524", dates.endYmd], + ["2026-05-18", dates.leg1Iso], + ["2026-05-19", dates.leg2Iso], + ["2026-05-22", dates.altLeg1Iso], + ["2026-05-23", dates.altLeg2Iso], + ["2026-05-24", formatIsoDate(dates.end)], + ]; + + return replacements.reduce( + (result, [from, to]) => result.replaceAll(from, to), + text, + ); +} diff --git a/tests/e2e/navigation.spec.ts b/tests/e2e/navigation.spec.ts index 0be052ed..e59427f2 100644 --- a/tests/e2e/navigation.spec.ts +++ b/tests/e2e/navigation.spec.ts @@ -1,10 +1,22 @@ import { test, expect } from "./fixtures/console-gate"; +import { + routeAppSettingsFixture, + routeDictionaryFixtures, + routePopularRequestsFixture, +} from "./helpers/api-fixtures"; + +async function routeNavigationStartFixtures(page: import("@playwright/test").Page): Promise { + await routeAppSettingsFixture(page); + await routeDictionaryFixtures(page); + await routePopularRequestsFixture(page); +} test.describe("Cross-feature navigation", () => { test("locale switching: /ru/onlineboard -> /en/onlineboard shows English content", async ({ page, consoleMessages, }) => { + await routeNavigationStartFixtures(page); // Start on Russian online board await page.goto("/ru/onlineboard"); await page.waitForLoadState("domcontentloaded"); @@ -25,6 +37,7 @@ test.describe("Cross-feature navigation", () => { }); test("error page: /error/404 renders 404 content", async ({ page, consoleMessages }) => { + await routeNavigationStartFixtures(page); // Navigate to a working page first, then client-side navigate to the error // page. Direct URL navigation to /error/404 renders blank because the // error route is outside [lang]/layout.tsx and SSR produces empty output. @@ -45,6 +58,7 @@ test.describe("Cross-feature navigation", () => { page, consoleMessages, }) => { + await routeNavigationStartFixtures(page); // Navigate to a working page first, then client-side navigate to the error // page (same reason as the 404 test above). await page.goto("/ru/onlineboard"); diff --git a/tests/e2e/schedule-aircraft-link.spec.ts b/tests/e2e/schedule-aircraft-link.spec.ts index c3916f85..cd10165d 100644 --- a/tests/e2e/schedule-aircraft-link.spec.ts +++ b/tests/e2e/schedule-aircraft-link.spec.ts @@ -2,6 +2,10 @@ import { test, expect } from "./fixtures/console-gate"; import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; +import { + replaceVvoMjzFixtureDates, + vvoMjzDetailsUrl, +} from "./helpers/dates"; // Schedule details uses the same Angular aircraft-link behavior as // online-board: the model text under "Борт" opens the generic plane park page. @@ -14,9 +18,7 @@ const scheduleDetails = fs.readFileSync( path.join(FIXTURE_DIR, "schedule-details.json"), "utf8", ); - -const URL = - "/ru-ru/schedule/VVO/SU5752-20260518/KJA/SU6837-20260519/MJZ?request=schedule-route-VVO-MJZ-20260518-20260524"; +const scheduleDetailsBody = replaceVvoMjzFixtureDates(scheduleDetails); test("Schedule details aircraft title opens Aeroflot plane park in a new tab", async ({ page, @@ -27,11 +29,11 @@ test("Schedule details aircraft title opens Aeroflot plane park in a new tab", a await route.fulfill({ status: 200, contentType: "application/json", - body: scheduleDetails, + body: scheduleDetailsBody, }); }); - await context.route("http://www.aeroflot.ru/cms/ru/flight/plane_park", async (route) => { + await context.route(/https?:\/\/www\.aeroflot\.ru\/cms\/ru\/flight\/plane_park/, async (route) => { await route.fulfill({ status: 200, contentType: "text/html", @@ -39,7 +41,7 @@ test("Schedule details aircraft title opens Aeroflot plane park in a new tab", a }); }); - await page.goto(URL); + await page.goto(vvoMjzDetailsUrl()); const details = page.locator(".schedule-leg-details"); await expect(details).toHaveCount(1, { timeout: 15000 }); diff --git a/tests/e2e/schedule-date-picker.spec.ts b/tests/e2e/schedule-date-picker.spec.ts index 7a878022..68088d00 100644 --- a/tests/e2e/schedule-date-picker.spec.ts +++ b/tests/e2e/schedule-date-picker.spec.ts @@ -1,4 +1,8 @@ import { test, expect } from "./fixtures/console-gate"; +import { + routeAppSettingsFixture, + routeDictionaryFixtures, +} from "./helpers/api-fixtures"; import { addDays, formatRuDate, @@ -21,6 +25,8 @@ test.describe("Schedule date-range picker (week-snap)", () => { page, consoleMessages, }) => { + await routeAppSettingsFixture(page); + await routeDictionaryFixtures(page); await page.goto("/ru-ru/schedule"); await expect(page.getByTestId("date-range-input")).toBeVisible({ timeout: 15000, @@ -31,21 +37,23 @@ test.describe("Schedule date-range picker (week-snap)", () => { const panel = page.locator(".p-datepicker-panel, .p-datepicker").first(); await expect(panel).toBeVisible(); - const target = addDays(new Date(), 7); + const target = addDays(new Date(), 1); await panel.locator(`td[aria-label="${formatRuDate(target)}"] span`).click(); // Panel auto-dismissed. await expect(panel).toBeHidden({ timeout: 5000 }); - // Input now holds the full week range. + // The current week is rendered with Angular's compact label. const input = page.locator("#schedule-date-from"); - await expect(input).toHaveValue(formatRuWeekRange(target)); + await expect(input).toHaveValue("Текущая неделя"); }); test("clicking an enabled other-month bleed-in day snaps to its Mon-Sun week", async ({ page, consoleMessages, }) => { + await routeAppSettingsFixture(page); + await routeDictionaryFixtures(page); await page.goto("/ru-ru/schedule"); await expect(page.getByTestId("date-range-input")).toBeVisible({ timeout: 15000, @@ -83,6 +91,8 @@ test.describe("Schedule date-range picker (week-snap)", () => { }); test("input renders as range placeholder when empty", async ({ page, consoleMessages }) => { + await routeAppSettingsFixture(page); + await routeDictionaryFixtures(page); await page.goto("/ru-ru/schedule"); const input = page.locator("#schedule-date-from"); await expect(input).toHaveAttribute( diff --git a/tests/e2e/schedule-details-connecting-legs.spec.ts b/tests/e2e/schedule-details-connecting-legs.spec.ts index 341623a0..c7fef508 100644 --- a/tests/e2e/schedule-details-connecting-legs.spec.ts +++ b/tests/e2e/schedule-details-connecting-legs.spec.ts @@ -1,5 +1,6 @@ import { test, expect } from "./fixtures/console-gate"; import { routeScheduleVvoMjzFixtures } from "./helpers/api-fixtures"; +import { vvoMjzRouteUrl } from "./helpers/dates"; // When the user clicks a connecting itinerary in the Schedule list, the // resulting flight-details URL must include EVERY leg, not just the @@ -14,7 +15,7 @@ test("connecting itinerary navigates to a multi-segment URL with both legs rende consoleMessages, }) => { await routeScheduleVvoMjzFixtures(page); - await page.goto("/ru-ru/schedule/route/VVO-MJZ-20260518-20260524"); + await page.goto(vvoMjzRouteUrl()); await expect(page.locator(".flight-card").first()).toBeVisible({ timeout: 15000, }); diff --git a/tests/e2e/schedule-details-day-tabs-operating-days.spec.ts b/tests/e2e/schedule-details-day-tabs-operating-days.spec.ts index e83497f0..950348e6 100644 --- a/tests/e2e/schedule-details-day-tabs-operating-days.spec.ts +++ b/tests/e2e/schedule-details-day-tabs-operating-days.spec.ts @@ -3,23 +3,28 @@ import path from "node:path"; import { fileURLToPath } from "node:url"; import { test, expect } from "./fixtures/console-gate"; import { routeAppSettingsFixture } from "./helpers/api-fixtures"; +import { + replaceVvoMjzFixtureDates, + vvoMjzScheduleDates, +} from "./helpers/dates"; const FIXTURE_DIR = path.resolve( path.dirname(fileURLToPath(import.meta.url)), "../fixtures/api", ); -const URL = - "/ru-ru/schedule/KJA/SU6837-20260519/MJZ?request=schedule-route-KJA-MJZ-20260518-20260524-C0"; - test("TIRREDESIGN-26: schedule details day tabs disable non-operating flight dates", async ({ page, consoleMessages, }) => { + const dates = vvoMjzScheduleDates(); + const url = `/ru-ru/schedule/KJA/SU6837-${dates.leg2Ymd}/MJZ?request=schedule-route-KJA-MJZ-${dates.startYmd}-${dates.endYmd}-C0`; await routeAppSettingsFixture(page); await page.route("**/api/flights/v1.1/*/schedule/details?**", async (route) => { const source = JSON.parse( - fs.readFileSync(path.join(FIXTURE_DIR, "schedule-details-vvo-mjz.json"), "utf8"), + replaceVvoMjzFixtureDates( + fs.readFileSync(path.join(FIXTURE_DIR, "schedule-details-vvo-mjz.json"), "utf8"), + ), ) as { data: { routes: Array<{ @@ -32,7 +37,7 @@ test("TIRREDESIGN-26: schedule details day tabs disable non-operating flight dat (flight) => flight.flightId.carrier === "SU" && flight.flightId.flightNumber === "6837" && - flight.flightId.date === "2026-05-19", + flight.flightId.date === dates.leg2Iso, ); expect(su6837).toBeTruthy(); await route.fulfill({ @@ -42,18 +47,19 @@ test("TIRREDESIGN-26: schedule details day tabs disable non-operating flight dat data: { partners: [], routes: su6837 ? [su6837] : [], - daysOfFlight: ["20260519", "20260523"], + daysOfFlight: [dates.leg2Ymd, dates.altLeg2Ymd], }, }), }); }); - await page.goto(URL); + await page.goto(url); await expect(page.getByTestId("day-tabs")).toBeVisible({ timeout: 15000 }); + await page.getByTestId("day-tabs-next").click(); - const nonOperatingFriday = page.getByTestId("day-tab-20260522"); + const nonOperatingFriday = page.getByTestId(`day-tab-${dates.altLeg1Ymd}`); await expect(nonOperatingFriday).toBeDisabled(); - await expect(page.getByTestId("day-tab-20260523")).toBeEnabled(); + await expect(page.getByTestId(`day-tab-${dates.altLeg2Ymd}`)).toBeEnabled(); await expect(page.getByTestId("schedule-details-not-found")).toHaveCount(0); }); diff --git a/tests/e2e/schedule-details-meal-sub-icons.spec.ts b/tests/e2e/schedule-details-meal-sub-icons.spec.ts index f0b31371..a510a731 100644 --- a/tests/e2e/schedule-details-meal-sub-icons.spec.ts +++ b/tests/e2e/schedule-details-meal-sub-icons.spec.ts @@ -1,5 +1,6 @@ import { test, expect } from "./fixtures/console-gate"; import { routeScheduleVvoMjzFixtures } from "./helpers/api-fixtures"; +import { vvoMjzDetailsUrl } from "./helpers/dates"; // Schedule Details "Питание на борту" must render meal-class sub-icons // (Эконом класс / Комфорт класс / Бизнес класс) ONLY when the API @@ -11,15 +12,12 @@ import { routeScheduleVvoMjzFixtures } from "./helpers/api-fixtures"; // Reference URL covers a connecting itinerary where both live legs // currently return Economy / Comfort / Business meal entries. -const URL = - "/ru-ru/schedule/VVO/SU5752-20260518/KJA/SU6837-20260519/MJZ?request=schedule-route-VVO-MJZ-20260518-20260524"; - test("Питание sub-icons appear only for legs whose API meal[] contains them", async ({ page, consoleMessages, }) => { await routeScheduleVvoMjzFixtures(page); - await page.goto(URL); + await page.goto(vvoMjzDetailsUrl()); // Wait until both leg-details panels are mounted. await expect(page.locator(".schedule-leg-details")).toHaveCount(2, { diff --git a/tests/e2e/schedule-details-mini-list-scoped.spec.ts b/tests/e2e/schedule-details-mini-list-scoped.spec.ts index 08d4e4d8..0c492731 100644 --- a/tests/e2e/schedule-details-mini-list-scoped.spec.ts +++ b/tests/e2e/schedule-details-mini-list-scoped.spec.ts @@ -1,5 +1,6 @@ import { test, expect } from "./fixtures/console-gate"; import { routeScheduleVvoMjzFixtures } from "./helpers/api-fixtures"; +import { vvoMjzDetailsUrl } from "./helpers/dates"; // On the schedule details page the left mini-list renders a SINGLE // card for the currently-open flight — matching Angular's @@ -12,12 +13,9 @@ import { routeScheduleVvoMjzFixtures } from "./helpers/api-fixtures"; // flight numbers ("SU 5752, SU 6837") and the combined // Vladivostok→Mirny origin/destination, not just the first leg. -const URL = - "/ru-ru/schedule/VVO/SU5752-20260518/KJA/SU6837-20260519/MJZ?request=schedule-route-VVO-MJZ-20260518-20260524"; - test("mini-list — one combined card for the open SU 5752+SU 6837 itinerary", async ({ page, consoleMessages }) => { await routeScheduleVvoMjzFixtures(page); - await page.goto(URL); + await page.goto(vvoMjzDetailsUrl()); const miniList = page.locator(".schedule-mini-list"); await expect(miniList).toBeVisible({ timeout: 15000 }); diff --git a/tests/e2e/schedule-details-onboard-services.spec.ts b/tests/e2e/schedule-details-onboard-services.spec.ts index 12f3646b..7dc5a907 100644 --- a/tests/e2e/schedule-details-onboard-services.spec.ts +++ b/tests/e2e/schedule-details-onboard-services.spec.ts @@ -1,14 +1,12 @@ import { test, expect } from "./fixtures/console-gate"; import { routeScheduleVvoMjzServicesFixtures } from "./helpers/api-fixtures"; - -const URL = - "/ru-ru/schedule/VVO/SU5752-20260518/KJA/SU6837-20260519/MJZ?request=schedule-route-VVO-MJZ-20260518-20260524"; +import { vvoMjzDetailsUrl } from "./helpers/dates"; test("schedule details render onboard services from actual aircraft data", async ({ page, }) => { await routeScheduleVvoMjzServicesFixtures(page); - await page.goto(URL); + await page.goto(vvoMjzDetailsUrl()); const leg1 = page.locator(".schedule-leg-details").nth(0); await expect(leg1).toBeVisible({ timeout: 15000 }); diff --git a/tests/e2e/schedule-details-summary-header.spec.ts b/tests/e2e/schedule-details-summary-header.spec.ts index 6d7d0f9b..cf3c0429 100644 --- a/tests/e2e/schedule-details-summary-header.spec.ts +++ b/tests/e2e/schedule-details-summary-header.spec.ts @@ -1,5 +1,6 @@ import { test, expect } from "./fixtures/console-gate"; import { routeScheduleVvoMjzFixtures } from "./helpers/api-fixtures"; +import { vvoMjzDetailsUrl } from "./helpers/dates"; // Schedule details page must render Angular's `` // summary block between the day-tabs strip and the per-leg cards: @@ -13,12 +14,9 @@ import { routeScheduleVvoMjzFixtures } from "./helpers/api-fixtures"; // This test pins those guarantees so the page can't regress to "no // summary header" or "first-leg-only badge / raw ISO timeline" again. -const URL = - "/ru-ru/schedule/VVO/SU5752-20260518/KJA/SU6837-20260519/MJZ?request=schedule-route-VVO-MJZ-20260518-20260524"; - test("summary header — both badges + last-update + formatted full-route timeline", async ({ page, consoleMessages }) => { await routeScheduleVvoMjzFixtures(page); - await page.goto(URL); + await page.goto(vvoMjzDetailsUrl()); const summary = page.locator(".schedule-details__summary"); await expect(summary).toBeVisible({ timeout: 15000 }); diff --git a/tests/e2e/schedule-filter-resubmit.spec.ts b/tests/e2e/schedule-filter-resubmit.spec.ts index bdc5479c..a14a7b43 100644 --- a/tests/e2e/schedule-filter-resubmit.spec.ts +++ b/tests/e2e/schedule-filter-resubmit.spec.ts @@ -1,11 +1,15 @@ import { test, expect } from "./fixtures/console-gate"; +import { routeScheduleVvoMjzFixtures } from "./helpers/api-fixtures"; +import { vvoMjzRouteUrl, vvoMjzScheduleDates } from "./helpers/dates"; test.describe("Schedule results filter", () => { test("changed route criteria can be submitted immediately after a previous search", async ({ page, consoleMessages, }) => { - await page.goto("/ru-ru/schedule/route/VVO-MJZ-20260518-20260524"); + await routeScheduleVvoMjzFixtures(page); + const dates = vvoMjzScheduleDates(); + await page.goto(vvoMjzRouteUrl()); await expect(page.locator("h1")).toContainText(/Владивосток.*Мирный|VVO.*MJZ/, { timeout: 30000, @@ -29,7 +33,9 @@ test.describe("Schedule results filter", () => { ); await submit.click(); - await expect(page).toHaveURL(/\/ru-ru\/schedule\/route\/MJZ-VVO-20260518-20260524/); + await expect(page).toHaveURL( + new RegExp(`/ru-ru/schedule/route/MJZ-VVO-${dates.startYmd}-${dates.endYmd}`), + ); expect((await scheduleResponse).status()).toBe(200); expect(consoleMessages).toEqual([]); }); diff --git a/tests/e2e/schedule-flight-details-button.spec.ts b/tests/e2e/schedule-flight-details-button.spec.ts index 85d64275..f9977216 100644 --- a/tests/e2e/schedule-flight-details-button.spec.ts +++ b/tests/e2e/schedule-flight-details-button.spec.ts @@ -1,7 +1,6 @@ import { test, expect } from "./fixtures/console-gate"; import { routeScheduleVvoMjzFixtures } from "./helpers/api-fixtures"; - -const ROUTE_URL = "/ru-ru/schedule/route/VVO-MJZ-20260518-20260524"; +import { vvoMjzRouteUrl, vvoMjzScheduleDates } from "./helpers/dates"; test.describe("Schedule flight details button", () => { test.beforeEach(async ({ page }) => { @@ -11,7 +10,7 @@ test.describe("Schedule flight details button", () => { test("flight details button is visible in expanded flight body", async ({ page, }) => { - await page.goto(ROUTE_URL); + await page.goto(vvoMjzRouteUrl()); const cards = page.locator(".flight-card--clickable"); await expect(cards.first()).toBeVisible({ timeout: 30000 }); @@ -26,7 +25,7 @@ test.describe("Schedule flight details button", () => { }); test("flight details button has correct label (Russian)", async ({ page }) => { - await page.goto(ROUTE_URL); + await page.goto(vvoMjzRouteUrl()); const cards = page.locator(".flight-card--clickable"); await expect(cards.first()).toBeVisible({ timeout: 30000 }); @@ -42,7 +41,7 @@ test.describe("Schedule flight details button", () => { test("flight details button navigates to flight details page", async ({ page, }) => { - await page.goto(ROUTE_URL); + await page.goto(vvoMjzRouteUrl()); const cards = page.locator(".flight-card--clickable"); await expect(cards.first()).toBeVisible({ timeout: 30000 }); @@ -58,7 +57,7 @@ test.describe("Schedule flight details button", () => { test("flight details button works for connecting flights", async ({ page, }) => { - await page.goto(ROUTE_URL); + await page.goto(vvoMjzRouteUrl()); const cards = page.locator(".flight-card--clickable"); await expect(cards.first()).toBeVisible({ timeout: 30000 }); @@ -78,7 +77,8 @@ test.describe("Schedule flight details button", () => { test("flight details button preserves search context in URL", async ({ page, }) => { - await page.goto(ROUTE_URL); + const dates = vvoMjzScheduleDates(); + await page.goto(vvoMjzRouteUrl()); const cards = page.locator(".flight-card--clickable"); await expect(cards.first()).toBeVisible({ timeout: 30000 }); @@ -90,6 +90,6 @@ test.describe("Schedule flight details button", () => { const url = page.url(); expect(url).toContain("?request="); - expect(url).toContain("schedule-route-VVO-MJZ-20260518-20260524"); + expect(url).toContain(`schedule-route-VVO-MJZ-${dates.startYmd}-${dates.endYmd}`); }); }); diff --git a/tests/e2e/schedule-route-buy-button.spec.ts b/tests/e2e/schedule-route-buy-button.spec.ts index c0bd7474..9bfaa669 100644 --- a/tests/e2e/schedule-route-buy-button.spec.ts +++ b/tests/e2e/schedule-route-buy-button.spec.ts @@ -1,5 +1,6 @@ import { test, expect } from "./fixtures/console-gate"; import { routeScheduleVvoMjzFixtures } from "./helpers/api-fixtures"; +import { vvoMjzRouteUrl } from "./helpers/dates"; // Schedule search-results page must mirror Angular's // `` strip in each expanded flight body — @@ -37,7 +38,7 @@ test("schedule route page surfaces the buy ticket button inside an expanded flig // flight in the list is inside the buy window. Earlier-this-week URLs hit // a "today's first flight is < 2h out" edge case and the buy button hides // on that single row, even though the rest of the list shows it. - await page.goto("/ru-ru/schedule/route/VVO-MJZ-20260518-20260524"); + await page.goto(vvoMjzRouteUrl()); // Wait for the list to render. const cards = page.locator(".flight-card--clickable"); diff --git a/tests/e2e/schedule-su0634-aircraft-link.spec.ts b/tests/e2e/schedule-su0634-aircraft-link.spec.ts index 2239728f..8a973453 100644 --- a/tests/e2e/schedule-su0634-aircraft-link.spec.ts +++ b/tests/e2e/schedule-su0634-aircraft-link.spec.ts @@ -3,13 +3,26 @@ import { routeAppSettingsFixture, routeDictionaryFixtures, } from "./helpers/api-fixtures"; +import { + addDays, + formatIsoDate, + formatYmd, + vvoMjzScheduleDates, +} from "./helpers/dates"; // TIRREDESIGN-28: SU0634 KJA -> HKT departs after midnight local time while // the backend flightId.date remains on the previous service day. Angular builds // the schedule details URL from the leg's local scheduled departure date, so -// the details request receives 2026-05-19 and the aircraft link is rendered. +// the details request receives the local departure date and the aircraft link is rendered. -const ROUTE_URL = "/ru-ru/schedule/route/KJA-HKT-20260519-20260525-C0"; +const dates = vvoMjzScheduleDates(); +const serviceDate = dates.leg1; +const localDate = dates.leg2; +const routeEnd = addDays(localDate, 6); +const SERVICE_DATE_ISO = formatIsoDate(serviceDate); +const LOCAL_DATE_ISO = formatIsoDate(localDate); +const LOCAL_DATE_YMD = formatYmd(localDate); +const ROUTE_URL = `/ru-ru/schedule/route/KJA-HKT-${LOCAL_DATE_YMD}-${formatYmd(routeEnd)}-C0`; const PLANE_PARK_URL = "http://www.aeroflot.ru/cms/ru/flight/plane_park"; const su0634Search = [ @@ -22,8 +35,8 @@ const su0634Search = [ carrier: "SU", flightNumber: "0634", suffix: "", - date: "2026-05-18", - dateLT: "2026-05-19", + date: SERVICE_DATE_ISO, + dateLT: LOCAL_DATE_ISO, }, flyingTime: "08:10:00", leg: { @@ -38,8 +51,8 @@ const su0634Search = [ terminal: "1", times: { scheduledDeparture: { - utc: "2026-05-18T22:05:00Z", - local: "2026-05-19T05:05:00+07:00", + utc: `${SERVICE_DATE_ISO}T22:05:00Z`, + local: `${LOCAL_DATE_ISO}T05:05:00+07:00`, dayChange: { value: 0, title: "" }, localTime: "05:05", tzOffset: 420, @@ -57,8 +70,8 @@ const su0634Search = [ terminal: "I", times: { scheduledArrival: { - utc: "2026-05-19T06:15:00Z", - local: "2026-05-19T13:15:00+07:00", + utc: `${LOCAL_DATE_ISO}T06:15:00Z`, + local: `${LOCAL_DATE_ISO}T13:15:00+07:00`, dayChange: { value: 0, title: "" }, localTime: "13:15", tzOffset: 420, @@ -100,7 +113,12 @@ const su0634Details = { data: { routes: su0634Search, partners: [], - daysOfFlight: ["20260515", "20260519", "20260522", "20260526"], + daysOfFlight: [ + formatYmd(addDays(localDate, -4)), + LOCAL_DATE_YMD, + formatYmd(addDays(localDate, 3)), + formatYmd(addDays(localDate, 7)), + ], }, }; @@ -132,13 +150,13 @@ test("TIRREDESIGN-28: SU0634 schedule details uses local date and opens plane pa status: 200, contentType: "application/json", body: JSON.stringify( - date === "2026-05-19T00:00:00" + date === `${LOCAL_DATE_ISO}T00:00:00` ? su0634Details : { data: { routes: [], partners: [], daysOfFlight: [] } }, ), }); }); - await context.route(PLANE_PARK_URL, async (route) => { + await context.route(/https?:\/\/www\.aeroflot\.ru\/cms\/ru\/flight\/plane_park/, async (route) => { await route.fulfill({ status: 200, contentType: "text/html", @@ -156,7 +174,9 @@ test("TIRREDESIGN-28: SU0634 schedule details uses local date and opens plane pa await expect(detailsBtn).toBeVisible({ timeout: 10000 }); await detailsBtn.click(); - await expect(page).toHaveURL(/\/ru-ru\/schedule\/KJA\/SU0634-20260519\/HKT/); + await expect(page).toHaveURL( + new RegExp(`/ru-ru/schedule/KJA/SU0634-${LOCAL_DATE_YMD}/HKT`), + ); const details = page.locator('[data-testid="schedule-leg-details"]'); await expect(details).toBeVisible({ timeout: 15000 });