Use shared detailsRequestParam codec for mini-list parent-request (route + flight kinds)

This commit is contained in:
2026-04-21 16:37:32 +03:00
parent 531ace6abc
commit 8b22f0601f
2 changed files with 85 additions and 29 deletions
@@ -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 }) => (
<a href={to} {...props}>{children}</a>
),
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(
<OnlineBoardDetailsPage
flightId={mockFlightId}
locale="ru"
canonicalOrigin="https://example.com"
/>,
);
// 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(
<OnlineBoardDetailsPage
flightId={mockFlightId}
locale="ru"
canonicalOrigin="https://example.com"
/>,
);
// 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);
});
});
});
@@ -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<OnlineBoardDetailsPageProps> = ({
// 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-<type>-<iata>-<yyyymmdd>' — 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