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 { render, screen } from "@testing-library/react";
|
||||||
import { OnlineBoardDetailsPage } from "./OnlineBoardDetailsPage.js";
|
import { OnlineBoardDetailsPage } from "./OnlineBoardDetailsPage.js";
|
||||||
import type { IParsedFlightId, IDirectFlight, IMultiLegFlight, IFlightLeg } from "../types.js";
|
import type { IParsedFlightId, IDirectFlight, IMultiLegFlight, IFlightLeg } from "../types.js";
|
||||||
|
import type { SearchFlightsParams } from "../api.js";
|
||||||
|
|
||||||
const mockFlightId: IParsedFlightId = {
|
const mockFlightId: IParsedFlightId = {
|
||||||
carrier: "SU",
|
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", () => ({
|
vi.mock("@modern-js/runtime/router", () => ({
|
||||||
Link: ({ children, to, ...props }: { children: React.ReactNode; to: string; className?: string; [k: string]: unknown }) => (
|
Link: ({ children, to, ...props }: { children: React.ReactNode; to: string; className?: string; [k: string]: unknown }) => (
|
||||||
<a href={to} {...props}>{children}</a>
|
<a href={to} {...props}>{children}</a>
|
||||||
),
|
),
|
||||||
useNavigate: () => vi.fn(),
|
useNavigate: () => vi.fn(),
|
||||||
useParams: () => ({ lang: "ru-ru" }),
|
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", () => ({
|
vi.mock("@/features/online-board/hooks/useOnlineBoard.js", () => ({
|
||||||
useOnlineBoard: () => ({
|
useOnlineBoard: (params: SearchFlightsParams) => mockUseOnlineBoard(params),
|
||||||
flights: [],
|
|
||||||
loading: false,
|
|
||||||
error: null,
|
|
||||||
refresh: vi.fn(),
|
|
||||||
}),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("@/ui/layout/PageTabs.js", () => ({
|
vi.mock("@/ui/layout/PageTabs.js", () => ({
|
||||||
@@ -146,6 +152,8 @@ describe("OnlineBoardDetailsPage", () => {
|
|||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
};
|
};
|
||||||
|
mockSearchParamsInstance = new URLSearchParams();
|
||||||
|
mockUseOnlineBoard.mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders flight details", () => {
|
it("renders flight details", () => {
|
||||||
@@ -378,4 +386,48 @@ describe("OnlineBoardDetailsPage", () => {
|
|||||||
expect(screen.queryByTestId("flight-schedule")).toBeNull();
|
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 { useFlightDetails } from "../hooks/useFlightDetails.js";
|
||||||
import { useLiveFlightDetails } from "../hooks/useLiveFlightDetails.js";
|
import { useLiveFlightDetails } from "../hooks/useLiveFlightDetails.js";
|
||||||
import { useOnlineBoard } from "../hooks/useOnlineBoard.js";
|
import { useOnlineBoard } from "../hooks/useOnlineBoard.js";
|
||||||
|
import { parseDetailsRequestParam } from "@/shared/detailsRequestParam.js";
|
||||||
import { buildFlightJsonLd } from "../json-ld.js";
|
import { buildFlightJsonLd } from "../json-ld.js";
|
||||||
import { buildOnlineBoardUrl } from "../url.js";
|
import { buildOnlineBoardUrl } from "../url.js";
|
||||||
import { FlightDetailsAccordion } from "./details-panels/FlightDetailsAccordion.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
|
// 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`
|
// departures for the day). The URL carries that context in the `request`
|
||||||
// query param — 'onlineboard-<type>-<iata>-<yyyymmdd>' — so we parse it
|
// query param — per TZ §4.1.2 Table 5 row 6 — so we parse it via the
|
||||||
// and dispatch a second fetch via useOnlineBoard to feed the sidebar.
|
// shared codec and dispatch a second fetch via useOnlineBoard to feed
|
||||||
|
// the sidebar.
|
||||||
const parentRequest = useMemo(() => {
|
const parentRequest = useMemo(() => {
|
||||||
const raw = searchParams.get("request");
|
const raw = searchParams.get("request");
|
||||||
if (!raw) return null;
|
return raw ? parseDetailsRequestParam(raw) : 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;
|
|
||||||
}, [searchParams]);
|
}, [searchParams]);
|
||||||
|
|
||||||
const parentParams = useMemo(() => {
|
const parentParams = useMemo(() => {
|
||||||
if (!parentRequest) return null;
|
if (!parentRequest) return null;
|
||||||
return {
|
const d = parentRequest.date;
|
||||||
...(parentRequest.type === "departure"
|
const isoDate = `${d.slice(0, 4)}-${d.slice(4, 6)}-${d.slice(6, 8)}`;
|
||||||
? { departure: parentRequest.departure }
|
const dateFrom = `${isoDate}T00:00:00`;
|
||||||
: { arrival: parentRequest.arrival }),
|
const dateTo = `${isoDate}T23:59:59`;
|
||||||
dateFrom: `${parentRequest.date}T00:00:00`,
|
switch (parentRequest.kind) {
|
||||||
dateTo: `${parentRequest.date}T23:59:59`,
|
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]);
|
}, [parentRequest]);
|
||||||
|
|
||||||
// When there's no parent request context, fall back to allFlights (this
|
// When there's no parent request context, fall back to allFlights (this
|
||||||
|
|||||||
Reference in New Issue
Block a user