From f1ab6563054b360e7cc017e1a4384f11c18960c7 Mon Sep 17 00:00:00 2001 From: gnezim Date: Mon, 18 May 2026 18:28:40 +0300 Subject: [PATCH] Implement online board stale data timers --- modern.config.ts | 8 ++- src/env/index.ts | 6 ++ .../components/OnlineBoardDetailsPage.tsx | 6 ++ .../components/OnlineBoardSearchPage.tsx | 6 ++ .../components/StaleDataOverlay.scss | 16 +++++ .../components/StaleDataOverlay.tsx | 17 +++++ .../hooks/useStaleDataTimers.test.ts | 72 +++++++++++++++++++ .../online-board/hooks/useStaleDataTimers.ts | 40 +++++++++++ src/i18n/locales/de/common.json | 1 + src/i18n/locales/en/common.json | 1 + src/i18n/locales/es/common.json | 1 + src/i18n/locales/fr/common.json | 1 + src/i18n/locales/it/common.json | 1 + src/i18n/locales/ja/common.json | 1 + src/i18n/locales/ko/common.json | 1 + src/i18n/locales/ru/common.json | 1 + src/i18n/locales/zh/common.json | 1 + tests/e2e/helpers/dates.ts | 4 ++ tests/e2e/helpers/onlineboard-fixtures.ts | 57 +++++++++++++++ tests/e2e/online-board.spec.ts | 49 +++++++++++++ tests/e2e/onlineboard-aircraft-link.spec.ts | 7 +- ...oard-details-transition-visibility.spec.ts | 15 ++-- ...le-details-day-tabs-operating-days.spec.ts | 1 - tests/e2e/schedule-route-buy-button.spec.ts | 20 ++++++ 24 files changed, 323 insertions(+), 10 deletions(-) create mode 100644 src/features/online-board/components/StaleDataOverlay.scss create mode 100644 src/features/online-board/components/StaleDataOverlay.tsx create mode 100644 src/features/online-board/hooks/useStaleDataTimers.test.ts create mode 100644 src/features/online-board/hooks/useStaleDataTimers.ts create mode 100644 tests/e2e/helpers/onlineboard-fixtures.ts diff --git a/modern.config.ts b/modern.config.ts index 24801015..63ea14b2 100644 --- a/modern.config.ts +++ b/modern.config.ts @@ -22,7 +22,13 @@ const plugins = [ // '{z}/{x}/{y}', so we serialize the whole payload to base64 and decode // it in the browser at runtime. Base64 output is A-Z/a-z/0-9/+/=/ — no // braces for the template engine to grab. -const PUBLIC_ENV_KEYS = ["MAP_TILE_URL", "API_BASE_URL", "SIGNALR_HUB_URL"] as const; +const PUBLIC_ENV_KEYS = [ + "MAP_TILE_URL", + "API_BASE_URL", + "SIGNALR_HUB_URL", + "REFRESH_PAUSE_MIN", + "REFRESH_STOP_MIN", +] as const; const PUBLIC_ENV: Record = {}; for (const k of PUBLIC_ENV_KEYS) { const v = process.env[k]; diff --git a/src/env/index.ts b/src/env/index.ts index dd22e12a..7f0c093a 100644 --- a/src/env/index.ts +++ b/src/env/index.ts @@ -20,6 +20,8 @@ const EnvSchema = z.object({ // skip the connection when blank so the browser does not emit CORS // errors for an unreachable placeholder host. SIGNALR_HUB_URL: z.string().default(""), + REFRESH_PAUSE_MIN: z.coerce.number().nonnegative().default(15), + REFRESH_STOP_MIN: z.coerce.number().nonnegative().default(60), OTEL_EXPORTER_OTLP_ENDPOINT: z.string().url().optional(), OTEL_EXPORTER_OTLP_HEADERS: z.string().optional(), LOGS_ENDPOINT: z.string().url().optional(), @@ -45,6 +47,8 @@ export interface Env { PROD_ORIGIN: string; API_BASE_URL: string; SIGNALR_HUB_URL: string; + REFRESH_PAUSE_MIN: number; + REFRESH_STOP_MIN: number; OTEL_EXPORTER_OTLP_ENDPOINT?: string; OTEL_EXPORTER_OTLP_HEADERS?: string; LOGS_ENDPOINT?: string; @@ -92,6 +96,8 @@ export function getEnv(): Env { PROD_ORIGIN: raw.PROD_ORIGIN, API_BASE_URL: raw.API_BASE_URL, SIGNALR_HUB_URL: raw.SIGNALR_HUB_URL, + REFRESH_PAUSE_MIN: raw.REFRESH_PAUSE_MIN, + REFRESH_STOP_MIN: raw.REFRESH_STOP_MIN, ANALYTICS_ENABLED: { metrica: raw.ANALYTICS_METRICA, ctm: raw.ANALYTICS_CTM, diff --git a/src/features/online-board/components/OnlineBoardDetailsPage.tsx b/src/features/online-board/components/OnlineBoardDetailsPage.tsx index 30706f44..0937cc75 100644 --- a/src/features/online-board/components/OnlineBoardDetailsPage.tsx +++ b/src/features/online-board/components/OnlineBoardDetailsPage.tsx @@ -18,6 +18,7 @@ import { PageLayout } from "@/ui/layout/PageLayout.js"; import { useAppSettings } from "@/shared/hooks/useAppSettings.js"; import { useFlightDetails } from "../hooks/useFlightDetails.js"; import { useLiveFlightDetails } from "../hooks/useLiveFlightDetails.js"; +import { useStaleDataTimers } from "../hooks/useStaleDataTimers.js"; import { useOnlineBoard } from "../hooks/useOnlineBoard.js"; import { parseDetailsRequestParam } from "@/shared/detailsRequestParam.js"; import { buildFlightJsonLd } from "../json-ld.js"; @@ -31,6 +32,7 @@ import { BoardDetailsHeader } from "./BoardDetailsHeader/index.js"; import { DetailsBackButton } from "./DetailsBackButton/index.js"; import { FlightSchedule } from "./FlightSchedule/index.js"; import { FullRouteTimeline } from "./FullRouteTimeline/index.js"; +import { StaleDataOverlay } from "./StaleDataOverlay.js"; import { TransferBar } from "./TransferBar/index.js"; import type { IParsedFlightId, IFlightLeg, FlightStatus as FlightStatusType } from "../types.js"; import { @@ -403,6 +405,7 @@ export const OnlineBoardDetailsPage: FC = ({ flight, refresh, ); + const isStale = useStaleDataTimers(); const displayFlight = connectionStatus === "live" && liveFlight ? liveFlight : flight; @@ -619,6 +622,9 @@ export const OnlineBoardDetailsPage: FC = ({ return ( <> + {isStale && ( + + )} } title={

{pageTitle}

} diff --git a/src/features/online-board/components/OnlineBoardSearchPage.tsx b/src/features/online-board/components/OnlineBoardSearchPage.tsx index 9d4164e8..aa385522 100644 --- a/src/features/online-board/components/OnlineBoardSearchPage.tsx +++ b/src/features/online-board/components/OnlineBoardSearchPage.tsx @@ -32,6 +32,7 @@ import "./OnlineBoardSearchPage.scss"; import { JsonLdRenderer } from "@/shared/seo/json-ld.js"; import { useOnlineBoard } from "../hooks/useOnlineBoard.js"; import { useLiveBoardSearch } from "../hooks/useLiveBoardSearch.js"; +import { useStaleDataTimers } from "../hooks/useStaleDataTimers.js"; import { useCalendarDays } from "../hooks/useCalendarDays.js"; import { buildOnlineBoardUrl } from "../url.js"; import { buildFlightListJsonLd } from "../json-ld.js"; @@ -41,6 +42,7 @@ import { PobedaAuroraBanner, shouldShowPobedaAuroraBanner, } from "./PobedaAuroraBanner.js"; +import { StaleDataOverlay } from "./StaleDataOverlay.js"; import type { SortMode } from "../sortFlights.js"; import type { OnlineBoardParams } from "../url.js"; import type { SearchFlightsParams, CalendarParams } from "../api.js"; @@ -377,6 +379,7 @@ export const OnlineBoardSearchPage: FC = ({ flights, refresh, ); + const isStale = useStaleDataTimers(); // Calendar days const calendarParams = toCalendarParams(params); @@ -463,6 +466,9 @@ export const OnlineBoardSearchPage: FC = ({ data-searching={loading ? "true" : undefined} > {jsonLd && } + {isStale && ( + + )} } title={ diff --git a/src/features/online-board/components/StaleDataOverlay.scss b/src/features/online-board/components/StaleDataOverlay.scss new file mode 100644 index 00000000..209334c3 --- /dev/null +++ b/src/features/online-board/components/StaleDataOverlay.scss @@ -0,0 +1,16 @@ +.stale-data-overlay { + position: fixed; + inset: 0; + z-index: 9999; + display: flex; + align-items: flex-start; + justify-content: center; + box-sizing: border-box; + padding: 20vh 24px 24px; + background: rgb(255 255 255 / 70%); + color: #111827; + font-size: 18px; + font-weight: 600; + line-height: 1.4; + text-align: center; +} diff --git a/src/features/online-board/components/StaleDataOverlay.tsx b/src/features/online-board/components/StaleDataOverlay.tsx new file mode 100644 index 00000000..c6a0873e --- /dev/null +++ b/src/features/online-board/components/StaleDataOverlay.tsx @@ -0,0 +1,17 @@ +import type { FC } from "react"; +import "./StaleDataOverlay.scss"; + +interface StaleDataOverlayProps { + message: string; +} + +export const StaleDataOverlay: FC = ({ message }) => ( +
+ {message} +
+); diff --git a/src/features/online-board/hooks/useStaleDataTimers.test.ts b/src/features/online-board/hooks/useStaleDataTimers.test.ts new file mode 100644 index 00000000..acd3288c --- /dev/null +++ b/src/features/online-board/hooks/useStaleDataTimers.test.ts @@ -0,0 +1,72 @@ +/** + * @vitest-environment jsdom + */ +import { renderHook, act } from "@testing-library/react"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { useStaleDataTimers } from "./useStaleDataTimers.js"; + +describe("useStaleDataTimers", () => { + const originalLocation = window.location; + const assign = vi.fn(); + + beforeEach(() => { + vi.useFakeTimers(); + assign.mockClear(); + Object.defineProperty(window, "location", { + value: { assign, pathname: "/ru/onlineboard" }, + writable: true, + }); + }); + + afterEach(() => { + vi.useRealTimers(); + Object.defineProperty(window, "location", { + value: originalLocation, + writable: true, + }); + }); + + it("marks data stale after the pause timeout", () => { + const { result } = renderHook(() => + useStaleDataTimers({ pauseMinutes: 0.001, stopMinutes: 1 }), + ); + + expect(result.current).toBe(false); + + act(() => { + vi.advanceTimersByTime(60); + }); + + expect(result.current).toBe(true); + }); + + it("redirects after the stop timeout", () => { + renderHook(() => + useStaleDataTimers({ + pauseMinutes: 1, + stopMinutes: 0.001, + redirectTo: "/", + }), + ); + + act(() => { + vi.advanceTimersByTime(60); + }); + + expect(assign).toHaveBeenCalledWith("/"); + }); + + it("clears timers on unmount", () => { + const { unmount } = renderHook(() => + useStaleDataTimers({ pauseMinutes: 0.001, stopMinutes: 0.002 }), + ); + + unmount(); + + act(() => { + vi.advanceTimersByTime(120); + }); + + expect(assign).not.toHaveBeenCalled(); + }); +}); diff --git a/src/features/online-board/hooks/useStaleDataTimers.ts b/src/features/online-board/hooks/useStaleDataTimers.ts new file mode 100644 index 00000000..9e13a933 --- /dev/null +++ b/src/features/online-board/hooks/useStaleDataTimers.ts @@ -0,0 +1,40 @@ +import { useEffect, useState } from "react"; +import { getEnv } from "@/env/index.js"; + +const MILLIS_PER_MINUTE = 60_000; + +export interface StaleDataTimersOptions { + pauseMinutes?: number; + stopMinutes?: number; + redirectTo?: string; +} + +function toDelay(minutes: number): number { + return Math.max(0, minutes * MILLIS_PER_MINUTE); +} + +export function useStaleDataTimers(options: StaleDataTimersOptions = {}): boolean { + const env = getEnv(); + const pauseMinutes = options.pauseMinutes ?? env.REFRESH_PAUSE_MIN; + const stopMinutes = options.stopMinutes ?? env.REFRESH_STOP_MIN; + const redirectTo = options.redirectTo ?? "/"; + const [stale, setStale] = useState(false); + + useEffect(() => { + setStale(false); + + const pauseTimer = window.setTimeout(() => { + setStale(true); + }, toDelay(pauseMinutes)); + const stopTimer = window.setTimeout(() => { + window.location.assign(redirectTo); + }, toDelay(stopMinutes)); + + return () => { + window.clearTimeout(pauseTimer); + window.clearTimeout(stopTimer); + }; + }, [pauseMinutes, redirectTo, stopMinutes]); + + return stale; +} diff --git a/src/i18n/locales/de/common.json b/src/i18n/locales/de/common.json index a5396ec5..caf78941 100644 --- a/src/i18n/locales/de/common.json +++ b/src/i18n/locales/de/common.json @@ -392,6 +392,7 @@ "CONNECTION-LIVE": "Live", "CONNECTION-RECONNECTING": "Reconnecting…", "CONNECTION-OFFLINE": "Offline", + "STALE-DATA-REFRESH": "Data is outdated, refresh the page.", "INVALID-PARAMS": "Invalid URL parameters.", "LOADING": "Loading…", "A11Y-PREV-PAGE": "Previous page", diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index 43709a9f..edc1c8d7 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -432,6 +432,7 @@ "CONNECTION-LIVE": "Live", "CONNECTION-RECONNECTING": "Reconnecting…", "CONNECTION-OFFLINE": "Offline", + "STALE-DATA-REFRESH": "Data is outdated, refresh the page.", "INVALID-PARAMS": "Invalid URL parameters.", "LOADING": "Loading…", "A11Y-PREV-PAGE": "Previous page", diff --git a/src/i18n/locales/es/common.json b/src/i18n/locales/es/common.json index 751c31a9..48744acc 100644 --- a/src/i18n/locales/es/common.json +++ b/src/i18n/locales/es/common.json @@ -392,6 +392,7 @@ "CONNECTION-LIVE": "Live", "CONNECTION-RECONNECTING": "Reconnecting…", "CONNECTION-OFFLINE": "Offline", + "STALE-DATA-REFRESH": "Data is outdated, refresh the page.", "INVALID-PARAMS": "Invalid URL parameters.", "LOADING": "Loading…", "A11Y-PREV-PAGE": "Previous page", diff --git a/src/i18n/locales/fr/common.json b/src/i18n/locales/fr/common.json index f0d3d831..d3c7893e 100644 --- a/src/i18n/locales/fr/common.json +++ b/src/i18n/locales/fr/common.json @@ -392,6 +392,7 @@ "CONNECTION-LIVE": "Live", "CONNECTION-RECONNECTING": "Reconnecting…", "CONNECTION-OFFLINE": "Offline", + "STALE-DATA-REFRESH": "Data is outdated, refresh the page.", "INVALID-PARAMS": "Invalid URL parameters.", "LOADING": "Loading…", "A11Y-PREV-PAGE": "Previous page", diff --git a/src/i18n/locales/it/common.json b/src/i18n/locales/it/common.json index f61cce22..46006f54 100644 --- a/src/i18n/locales/it/common.json +++ b/src/i18n/locales/it/common.json @@ -392,6 +392,7 @@ "CONNECTION-LIVE": "Live", "CONNECTION-RECONNECTING": "Reconnecting…", "CONNECTION-OFFLINE": "Offline", + "STALE-DATA-REFRESH": "Data is outdated, refresh the page.", "INVALID-PARAMS": "Invalid URL parameters.", "LOADING": "Loading…", "A11Y-PREV-PAGE": "Previous page", diff --git a/src/i18n/locales/ja/common.json b/src/i18n/locales/ja/common.json index 5b6d90fe..af13b965 100644 --- a/src/i18n/locales/ja/common.json +++ b/src/i18n/locales/ja/common.json @@ -392,6 +392,7 @@ "CONNECTION-LIVE": "Live", "CONNECTION-RECONNECTING": "Reconnecting…", "CONNECTION-OFFLINE": "Offline", + "STALE-DATA-REFRESH": "Data is outdated, refresh the page.", "INVALID-PARAMS": "Invalid URL parameters.", "LOADING": "Loading…", "A11Y-PREV-PAGE": "Previous page", diff --git a/src/i18n/locales/ko/common.json b/src/i18n/locales/ko/common.json index 3cacab8b..e4cdc0d0 100644 --- a/src/i18n/locales/ko/common.json +++ b/src/i18n/locales/ko/common.json @@ -392,6 +392,7 @@ "CONNECTION-LIVE": "Live", "CONNECTION-RECONNECTING": "Reconnecting…", "CONNECTION-OFFLINE": "Offline", + "STALE-DATA-REFRESH": "Data is outdated, refresh the page.", "INVALID-PARAMS": "Invalid URL parameters.", "LOADING": "Loading…", "A11Y-PREV-PAGE": "Previous page", diff --git a/src/i18n/locales/ru/common.json b/src/i18n/locales/ru/common.json index 8210acf6..aeacdb02 100644 --- a/src/i18n/locales/ru/common.json +++ b/src/i18n/locales/ru/common.json @@ -432,6 +432,7 @@ "CONNECTION-LIVE": "Онлайн", "CONNECTION-RECONNECTING": "Соединение…", "CONNECTION-OFFLINE": "Нет связи", + "STALE-DATA-REFRESH": "Данные устарели, обновите страницу!", "INVALID-PARAMS": "Неверные параметры URL.", "LOADING": "Загрузка…", "A11Y-PREV-PAGE": "Предыдущая страница", diff --git a/src/i18n/locales/zh/common.json b/src/i18n/locales/zh/common.json index 98b39154..ea38a08e 100644 --- a/src/i18n/locales/zh/common.json +++ b/src/i18n/locales/zh/common.json @@ -392,6 +392,7 @@ "CONNECTION-LIVE": "Live", "CONNECTION-RECONNECTING": "Reconnecting…", "CONNECTION-OFFLINE": "Offline", + "STALE-DATA-REFRESH": "Data is outdated, refresh the page.", "INVALID-PARAMS": "Invalid URL parameters.", "LOADING": "Loading…", "A11Y-PREV-PAGE": "Previous page", diff --git a/tests/e2e/helpers/dates.ts b/tests/e2e/helpers/dates.ts index 27e8b612..1ad83bc3 100644 --- a/tests/e2e/helpers/dates.ts +++ b/tests/e2e/helpers/dates.ts @@ -9,6 +9,10 @@ export function formatYmd(date: Date): string { return `${date.getFullYear()}${String(date.getMonth() + 1).padStart(2, "0")}${String(date.getDate()).padStart(2, "0")}`; } +export function formatIsoDate(date: Date): string { + return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`; +} + export function formatRuDate(date: Date): string { return `${String(date.getDate()).padStart(2, "0")}.${String(date.getMonth() + 1).padStart(2, "0")}.${date.getFullYear()}`; } diff --git a/tests/e2e/helpers/onlineboard-fixtures.ts b/tests/e2e/helpers/onlineboard-fixtures.ts new file mode 100644 index 00000000..fd7eb415 --- /dev/null +++ b/tests/e2e/helpers/onlineboard-fixtures.ts @@ -0,0 +1,57 @@ +import { addDays, formatIsoDate, formatYmd } from "./dates"; + +type DateTimeValue = { + utc?: string; + local?: string; + localTime?: string; +}; + +type OnlineboardDetailsFixture = { + data: { + routes: Array<{ + flightId: { + dateLT?: string; + date: string; + }; + leg: { + departure: { times: Record }; + arrival: { times: Record }; + daysForTabs?: string[]; + }; + }>; + }; +}; + +function replaceDatePart(value: string | undefined, isoDate: string): string | undefined { + return value?.replace(/^\d{4}-\d{2}-\d{2}/, isoDate); +} + +export function nextOnlineboardDetailsFixture(raw: string): { + body: string; + compactDate: string; +} { + const date = addDays(new Date(), 1); + const isoDate = formatIsoDate(date); + const compactDate = formatYmd(date); + const fixture = JSON.parse(raw) as OnlineboardDetailsFixture; + + for (const route of fixture.data.routes) { + route.flightId.date = isoDate; + route.flightId.dateLT = isoDate; + + for (const point of [route.leg.departure, route.leg.arrival]) { + for (const value of Object.values(point.times)) { + const utc = replaceDatePart(value.utc, isoDate); + if (utc !== undefined) value.utc = utc; + const local = replaceDatePart(value.local, isoDate); + if (local !== undefined) value.local = local; + } + } + + route.leg.daysForTabs = Array.from({ length: 15 }, (_, index) => + formatYmd(addDays(date, index)), + ); + } + + return { body: JSON.stringify(fixture), compactDate }; +} diff --git a/tests/e2e/online-board.spec.ts b/tests/e2e/online-board.spec.ts index cfa09df9..da6433a2 100644 --- a/tests/e2e/online-board.spec.ts +++ b/tests/e2e/online-board.spec.ts @@ -264,6 +264,55 @@ test.describe("Online Board", () => { await expect(arrInput).toHaveValue("Самара"); }); + test("route results show stale-data overlay after inactivity timeout", async ({ + page, + consoleMessages, + }) => { + await page.addInitScript(() => { + const target = window as unknown as { __ENV__?: Record }; + target.__ENV__ = { + ...(target.__ENV__ ?? {}), + REFRESH_PAUSE_MIN: "0.01", + REFRESH_STOP_MIN: "1", + }; + }); + await routeDictionaryFixtures(page); + await routeOnlineboardRouteFixtures(page); + + await page.goto(`/ru/onlineboard/route/MOW-KUF-${formatYmd(new Date())}`); + await expect(page.locator('[data-testid="online-board-search"]')).toBeVisible({ + timeout: 10000, + }); + + await expect(page.locator('[data-testid="stale-data-overlay"]')).toHaveText( + "Данные устарели, обновите страницу!", + { timeout: 2000 }, + ); + }); + + test("route results redirect to main page after stale-data stop timeout", async ({ + page, + consoleMessages, + }) => { + await page.addInitScript(() => { + const target = window as unknown as { __ENV__?: Record }; + target.__ENV__ = { + ...(target.__ENV__ ?? {}), + REFRESH_PAUSE_MIN: "0.001", + REFRESH_STOP_MIN: "0.02", + }; + }); + await routeDictionaryFixtures(page); + await routeOnlineboardRouteFixtures(page); + + await page.goto(`/ru/onlineboard/route/MOW-KUF-${formatYmd(new Date())}`); + await expect(page.locator('[data-testid="online-board-search"]')).toBeVisible({ + timeout: 10000, + }); + + await page.waitForURL(/\/(ru\/onlineboard)?$/, { timeout: 4000 }); + }); + // Requires live API (calendar days endpoint). // Skipped when WAF blocks flights.test.aeroflot.ru. test.skip("route search results page shows calendar strip with day numbers", async ({ diff --git a/tests/e2e/onlineboard-aircraft-link.spec.ts b/tests/e2e/onlineboard-aircraft-link.spec.ts index 252a8d17..b91d319c 100644 --- a/tests/e2e/onlineboard-aircraft-link.spec.ts +++ b/tests/e2e/onlineboard-aircraft-link.spec.ts @@ -2,6 +2,7 @@ import { test, expect } from "./fixtures/console-gate"; import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; +import { nextOnlineboardDetailsFixture } from "./helpers/onlineboard-fixtures"; // TIRREDESIGN-24 — in the online-board flight details card, the aircraft // title under "Борт" must be a clickable external link that opens in a new tab. @@ -22,11 +23,13 @@ test("Onlineboard details aircraft title opens Aeroflot plane park in a new tab" context, consoleMessages, }) => { + const details = nextOnlineboardDetailsFixture(onlineboardDetails); + await page.route("**/api/flights/v1.1/ru/onlineboard/details?**", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", - body: onlineboardDetails, + body: details.body, }); }); @@ -38,7 +41,7 @@ test("Onlineboard details aircraft title opens Aeroflot plane park in a new tab" }); }); - await page.goto("/ru-ru/onlineboard/SU6951-20260514"); + await page.goto(`/ru-ru/onlineboard/SU6951-${details.compactDate}`); const aircraftRow = page.locator('[data-testid="details-row-aircraft"]'); await expect(aircraftRow).toBeVisible({ timeout: 15000 }); diff --git a/tests/e2e/onlineboard-details-transition-visibility.spec.ts b/tests/e2e/onlineboard-details-transition-visibility.spec.ts index 63e24826..8a73626e 100644 --- a/tests/e2e/onlineboard-details-transition-visibility.spec.ts +++ b/tests/e2e/onlineboard-details-transition-visibility.spec.ts @@ -2,6 +2,7 @@ import { test, expect } from "./fixtures/console-gate"; import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; +import { nextOnlineboardDetailsFixture } from "./helpers/onlineboard-fixtures"; const FIXTURE_DIR = path.resolve( path.dirname(fileURLToPath(import.meta.url)), @@ -16,24 +17,26 @@ test("TIRREDESIGN-7: onlineboard details shows non-scheduled transition even whe page, consoleMessages, }) => { - const details = structuredClone(baseDetails); + const shifted = nextOnlineboardDetailsFixture(JSON.stringify(baseDetails)); + const details = JSON.parse(shifted.body); const flight = details.data.routes[0]; const leg = flight.leg; + const isoDate = `${shifted.compactDate.slice(0, 4)}-${shifted.compactDate.slice(4, 6)}-${shifted.compactDate.slice(6, 8)}`; flight.status = "InFlight"; leg.status = "InFlight"; leg.transition = { registration: { start: { - utc: "2026-05-14T07:00:00Z", - local: "2026-05-14T10:00:00+03:00", + utc: `${isoDate}T07:00:00Z`, + local: `${isoDate}T10:00:00+03:00`, dayChange: { value: 0, title: "" }, localTime: "10:00", tzOffset: 180, }, end: { - utc: "2026-05-14T07:30:00Z", - local: "2026-05-14T10:30:00+03:00", + utc: `${isoDate}T07:30:00Z`, + local: `${isoDate}T10:30:00+03:00`, dayChange: { value: 0, title: "" }, localTime: "10:30", tzOffset: 180, @@ -51,7 +54,7 @@ test("TIRREDESIGN-7: onlineboard details shows non-scheduled transition even whe }); }); - await page.goto("/ru-ru/onlineboard/SU6951-20260514"); + await page.goto(`/ru-ru/onlineboard/SU6951-${shifted.compactDate}`); const registrationRow = page.locator('[data-testid="details-row-registration"]'); await expect(registrationRow).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 da07e7ac..24858fa3 100644 --- a/tests/e2e/schedule-details-day-tabs-operating-days.spec.ts +++ b/tests/e2e/schedule-details-day-tabs-operating-days.spec.ts @@ -52,7 +52,6 @@ test("TIRREDESIGN-26: schedule details day tabs disable non-operating flight dat await expect(page.getByTestId("day-tabs")).toBeVisible({ timeout: 15000 }); await expect(page.getByTestId("day-tab-20260519")).toBeEnabled(); - await page.getByTestId("day-tabs-next").click(); const nonOperatingFriday = page.getByTestId("day-tab-20260522"); await expect(nonOperatingFriday).toBeDisabled(); await expect(page.getByTestId("day-tab-20260523")).toBeEnabled(); diff --git a/tests/e2e/schedule-route-buy-button.spec.ts b/tests/e2e/schedule-route-buy-button.spec.ts index 83880450..c0bd7474 100644 --- a/tests/e2e/schedule-route-buy-button.spec.ts +++ b/tests/e2e/schedule-route-buy-button.spec.ts @@ -12,6 +12,26 @@ import { routeScheduleVvoMjzFixtures } from "./helpers/api-fixtures"; test("schedule route page surfaces the buy ticket button inside an expanded flight body", async ({ page, }) => { + await page.addInitScript(() => { + const fixedTime = new Date("2026-05-17T00:00:00+03:00").getTime(); + const RealDate = Date; + const FixedDate = function fixedDate( + this: Date, + ...args: unknown[] + ) { + return args.length === 0 + ? new RealDate(fixedTime) + : Reflect.construct(RealDate, args); + } as unknown as DateConstructor; + FixedDate.now = () => fixedTime; + FixedDate.parse = RealDate.parse; + FixedDate.UTC = RealDate.UTC; + Object.defineProperty(FixedDate, "prototype", { + value: RealDate.prototype, + }); + Object.setPrototypeOf(FixedDate, RealDate); + window.Date = FixedDate; + }); await routeScheduleVvoMjzFixtures(page); // Pick a calendar week well clear of the 2h pre-departure cutoff so every // flight in the list is inside the buy window. Earlier-this-week URLs hit