Implement online board stale data timers
This commit is contained in:
+7
-1
@@ -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<string, string> = {};
|
||||
for (const k of PUBLIC_ENV_KEYS) {
|
||||
const v = process.env[k];
|
||||
|
||||
Vendored
+6
@@ -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,
|
||||
|
||||
@@ -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<OnlineBoardDetailsPageProps> = ({
|
||||
flight,
|
||||
refresh,
|
||||
);
|
||||
const isStale = useStaleDataTimers();
|
||||
|
||||
const displayFlight = connectionStatus === "live" && liveFlight ? liveFlight : flight;
|
||||
|
||||
@@ -619,6 +622,9 @@ export const OnlineBoardDetailsPage: FC<OnlineBoardDetailsPageProps> = ({
|
||||
return (
|
||||
<>
|
||||
<JsonLdRenderer data={jsonLd} />
|
||||
{isStale && (
|
||||
<StaleDataOverlay message={t("SHARED.STALE-DATA-REFRESH")} />
|
||||
)}
|
||||
<PageLayout
|
||||
headerLeft={<DetailsBackButton locale={locale} />}
|
||||
title={<h1 className="flight-details__flight-number">{pageTitle}</h1>}
|
||||
|
||||
@@ -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<OnlineBoardSearchPageProps> = ({
|
||||
flights,
|
||||
refresh,
|
||||
);
|
||||
const isStale = useStaleDataTimers();
|
||||
|
||||
// Calendar days
|
||||
const calendarParams = toCalendarParams(params);
|
||||
@@ -463,6 +466,9 @@ export const OnlineBoardSearchPage: FC<OnlineBoardSearchPageProps> = ({
|
||||
data-searching={loading ? "true" : undefined}
|
||||
>
|
||||
{jsonLd && <JsonLdRenderer data={jsonLd} />}
|
||||
{isStale && (
|
||||
<StaleDataOverlay message={t("SHARED.STALE-DATA-REFRESH")} />
|
||||
)}
|
||||
<PageLayout
|
||||
headerLeft={<PageTabs viewType="onlineboard" />}
|
||||
title={
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { FC } from "react";
|
||||
import "./StaleDataOverlay.scss";
|
||||
|
||||
interface StaleDataOverlayProps {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export const StaleDataOverlay: FC<StaleDataOverlayProps> = ({ message }) => (
|
||||
<div
|
||||
className="stale-data-overlay"
|
||||
data-testid="stale-data-overlay"
|
||||
role="alert"
|
||||
aria-live="assertive"
|
||||
>
|
||||
{message}
|
||||
</div>
|
||||
);
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -432,6 +432,7 @@
|
||||
"CONNECTION-LIVE": "Онлайн",
|
||||
"CONNECTION-RECONNECTING": "Соединение…",
|
||||
"CONNECTION-OFFLINE": "Нет связи",
|
||||
"STALE-DATA-REFRESH": "Данные устарели, обновите страницу!",
|
||||
"INVALID-PARAMS": "Неверные параметры URL.",
|
||||
"LOADING": "Загрузка…",
|
||||
"A11Y-PREV-PAGE": "Предыдущая страница",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()}`;
|
||||
}
|
||||
|
||||
@@ -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<string, DateTimeValue> };
|
||||
arrival: { times: Record<string, DateTimeValue> };
|
||||
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 };
|
||||
}
|
||||
@@ -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<string, string> };
|
||||
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<string, string> };
|
||||
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 ({
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user