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