Use shared detailsRequestParam codec for mini-list parent-request (route + flight kinds)
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user