diff --git a/src/features/online-board/components/OnlineBoardDetailsPage.test.tsx b/src/features/online-board/components/OnlineBoardDetailsPage.test.tsx
index e7783ac9..cff3ff23 100644
--- a/src/features/online-board/components/OnlineBoardDetailsPage.test.tsx
+++ b/src/features/online-board/components/OnlineBoardDetailsPage.test.tsx
@@ -10,6 +10,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen } from "@testing-library/react";
import { OnlineBoardDetailsPage } from "./OnlineBoardDetailsPage.js";
import type { IParsedFlightId, IDirectFlight, IMultiLegFlight, IFlightLeg } from "../types.js";
+import type { SearchFlightsParams } from "../api.js";
const mockFlightId: IParsedFlightId = {
carrier: "SU",
@@ -111,22 +112,27 @@ vi.mock("@/i18n/provider.js", () => ({
}),
}));
+// Mutable so individual tests can inject ?request= params
+let mockSearchParamsInstance = new URLSearchParams();
+
vi.mock("@modern-js/runtime/router", () => ({
Link: ({ children, to, ...props }: { children: React.ReactNode; to: string; className?: string; [k: string]: unknown }) => (
{children}
),
useNavigate: () => vi.fn(),
useParams: () => ({ lang: "ru-ru" }),
- useSearchParams: () => [new URLSearchParams()],
+ useSearchParams: () => [mockSearchParamsInstance],
+}));
+
+const mockUseOnlineBoard = vi.fn((_params: SearchFlightsParams) => ({
+ flights: [],
+ loading: false,
+ error: null,
+ refresh: vi.fn(),
}));
vi.mock("@/features/online-board/hooks/useOnlineBoard.js", () => ({
- useOnlineBoard: () => ({
- flights: [],
- loading: false,
- error: null,
- refresh: vi.fn(),
- }),
+ useOnlineBoard: (params: SearchFlightsParams) => mockUseOnlineBoard(params),
}));
vi.mock("@/ui/layout/PageTabs.js", () => ({
@@ -146,6 +152,8 @@ describe("OnlineBoardDetailsPage", () => {
loading: false,
error: null,
};
+ mockSearchParamsInstance = new URLSearchParams();
+ mockUseOnlineBoard.mockClear();
});
it("renders flight details", () => {
@@ -378,4 +386,48 @@ describe("OnlineBoardDetailsPage", () => {
expect(screen.queryByTestId("flight-schedule")).toBeNull();
});
});
+
+ describe("parent-request codec (TZ §4.1.2 Table 5 row 6)", () => {
+ it("4.1.2-R-Request-route: hydrates mini-list from route parent-request", () => {
+ mockSearchParamsInstance = new URLSearchParams(
+ "request=onlineboard-route-MOW-LED-20260515",
+ );
+ render(
+ ,
+ );
+ // The first call to useOnlineBoard carries the parentParams derived from
+ // the route-kind request; assert it received departure + arrival + dates.
+ const calls = mockUseOnlineBoard.mock.calls;
+ const routeCall = calls.find(
+ ([p]) =>
+ p.departure === "MOW" &&
+ p.arrival === "LED" &&
+ p.dateFrom === "2026-05-15T00:00:00" &&
+ p.dateTo === "2026-05-15T23:59:59",
+ );
+ expect(routeCall).toBeTruthy();
+ });
+
+ it("4.1.2-R-Request-flight: flight-number parent-request does not dispatch secondary mini-list fetch", () => {
+ mockSearchParamsInstance = new URLSearchParams(
+ "request=onlineboard-flight-SU1234-20260515",
+ );
+ render(
+ ,
+ );
+ // flight kind → parentParams is null → useOnlineBoard falls back to
+ // the empty-params sentinel { dateFrom: "", dateTo: "" } and skips the fetch.
+ const calls = mockUseOnlineBoard.mock.calls;
+ const hasNonEmptyDateFrom = calls.some(([p]) => p.dateFrom !== "");
+ expect(hasNonEmptyDateFrom).toBe(false);
+ });
+ });
});
diff --git a/src/features/online-board/components/OnlineBoardDetailsPage.tsx b/src/features/online-board/components/OnlineBoardDetailsPage.tsx
index b6e39d45..c6936693 100644
--- a/src/features/online-board/components/OnlineBoardDetailsPage.tsx
+++ b/src/features/online-board/components/OnlineBoardDetailsPage.tsx
@@ -19,6 +19,7 @@ import { useAppSettings } from "@/shared/hooks/useAppSettings.js";
import { useFlightDetails } from "../hooks/useFlightDetails.js";
import { useLiveFlightDetails } from "../hooks/useLiveFlightDetails.js";
import { useOnlineBoard } from "../hooks/useOnlineBoard.js";
+import { parseDetailsRequestParam } from "@/shared/detailsRequestParam.js";
import { buildFlightJsonLd } from "../json-ld.js";
import { buildOnlineBoardUrl } from "../url.js";
import { FlightDetailsAccordion } from "./details-panels/FlightDetailsAccordion.js";
@@ -405,34 +406,37 @@ export const OnlineBoardDetailsPage: FC = ({
// Angular's mini-list is populated from the PARENT search (e.g. all LED
// departures for the day). The URL carries that context in the `request`
- // query param — 'onlineboard---' — so we parse it
- // and dispatch a second fetch via useOnlineBoard to feed the sidebar.
+ // query param — per TZ §4.1.2 Table 5 row 6 — so we parse it via the
+ // shared codec and dispatch a second fetch via useOnlineBoard to feed
+ // the sidebar.
const parentRequest = useMemo(() => {
const raw = searchParams.get("request");
- if (!raw) return null;
- const parts = raw.split("-");
- if (parts.length < 4 || parts[0] !== "onlineboard") return null;
- const [, kind, iata, yyyymmdd] = parts;
- if (!iata || !yyyymmdd || yyyymmdd.length !== 8) return null;
- const isoDate = `${yyyymmdd.slice(0, 4)}-${yyyymmdd.slice(4, 6)}-${yyyymmdd.slice(6, 8)}`;
- if (kind === "departure") {
- return { type: "departure" as const, departure: iata, date: isoDate };
- }
- if (kind === "arrival") {
- return { type: "arrival" as const, arrival: iata, date: isoDate };
- }
- return null;
+ return raw ? parseDetailsRequestParam(raw) : null;
}, [searchParams]);
const parentParams = useMemo(() => {
if (!parentRequest) return null;
- return {
- ...(parentRequest.type === "departure"
- ? { departure: parentRequest.departure }
- : { arrival: parentRequest.arrival }),
- dateFrom: `${parentRequest.date}T00:00:00`,
- dateTo: `${parentRequest.date}T23:59:59`,
- };
+ const d = parentRequest.date;
+ const isoDate = `${d.slice(0, 4)}-${d.slice(4, 6)}-${d.slice(6, 8)}`;
+ const dateFrom = `${isoDate}T00:00:00`;
+ const dateTo = `${isoDate}T23:59:59`;
+ switch (parentRequest.kind) {
+ case "departure":
+ return { departure: parentRequest.station, dateFrom, dateTo };
+ case "arrival":
+ return { arrival: parentRequest.station, dateFrom, dateTo };
+ case "route":
+ return {
+ departure: parentRequest.departure,
+ arrival: parentRequest.arrival,
+ dateFrom,
+ dateTo,
+ };
+ case "flight":
+ // Flight-number parent: mini-list is already produced by the existing
+ // flight-details fetch (by-flight-number-and-date). No extra fetch.
+ return null;
+ }
}, [parentRequest]);
// When there's no parent request context, fall back to allFlights (this