diff --git a/src/features/online-board/components/OnlineBoardDetailsPage.test.tsx b/src/features/online-board/components/OnlineBoardDetailsPage.test.tsx index d111ad10..70c3b314 100644 --- a/src/features/online-board/components/OnlineBoardDetailsPage.test.tsx +++ b/src/features/online-board/components/OnlineBoardDetailsPage.test.tsx @@ -76,6 +76,7 @@ const mockFlight: IDirectFlight = { // Mutable state for test control let mockState = { flight: mockFlight as IDirectFlight | null, + allFlights: [mockFlight] as IDirectFlight[], loading: false, error: null as Error | null, }; @@ -98,10 +99,27 @@ vi.mock("@/i18n/provider.js", () => ({ }), })); +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" }), +})); + +vi.mock("@/ui/layout/PageTabs.js", () => ({ + PageTabs: () =>
, +})); + +vi.mock("@/features/flights-map/hooks/useFeatureFlag.js", () => ({ + useFeatureFlag: () => false, +})); + describe("OnlineBoardDetailsPage", () => { beforeEach(() => { mockState = { flight: mockFlight, + allFlights: [mockFlight], loading: false, error: null, }; @@ -115,19 +133,19 @@ describe("OnlineBoardDetailsPage", () => { }); it("renders loading skeleton", () => { - mockState = { flight: null, loading: true, error: null }; + mockState = { flight: null, allFlights: [], loading: true, error: null }; render(); expect(screen.queryByTestId("flight-details")).toBeNull(); }); it("renders error state", () => { - mockState = { flight: null, loading: false, error: new Error("fail") }; + mockState = { flight: null, allFlights: [], loading: false, error: new Error("fail") }; render(); expect(screen.getByTestId("flight-details-error")).toBeTruthy(); }); it("renders not-found state", () => { - mockState = { flight: null, loading: false, error: null }; + mockState = { flight: null, allFlights: [], loading: false, error: null }; render(); expect(screen.getByTestId("flight-details-not-found")).toBeTruthy(); }); @@ -190,9 +208,31 @@ describe("OnlineBoardDetailsPage", () => { }, }, }; - mockState = { flight: flightWithTransition, loading: false, error: null }; + mockState = { flight: flightWithTransition, allFlights: [flightWithTransition], loading: false, error: null }; render(); expect(screen.getByTestId("flight-details-accordion")).toBeTruthy(); }); }); + + describe("mini-list integration", () => { + it("renders mini-list when multiple flights are returned", () => { + const first = mockFlight; + const second = { ...mockFlight, id: "SU0022-20260417", flightId: { ...mockFlight.flightId, date: "20260417" } }; + mockState = { flight: first, allFlights: [first, second], loading: false, error: null }; + render(); + expect(screen.getByTestId("flights-mini-list")).toBeTruthy(); + }); + + it("does not render mini-list when only one flight is returned", () => { + mockState = { flight: mockFlight, allFlights: [mockFlight], loading: false, error: null }; + render(); + expect(screen.queryByTestId("flights-mini-list")).toBeNull(); + }); + + it("renders inside PageLayout (has page-layout class)", () => { + mockState = { flight: mockFlight, allFlights: [mockFlight], loading: false, error: null }; + const { container } = render(); + expect(container.querySelector(".page-layout")).toBeTruthy(); + }); + }); }); diff --git a/src/features/online-board/components/OnlineBoardDetailsPage.tsx b/src/features/online-board/components/OnlineBoardDetailsPage.tsx index 5b74dc34..889ab7ef 100644 --- a/src/features/online-board/components/OnlineBoardDetailsPage.tsx +++ b/src/features/online-board/components/OnlineBoardDetailsPage.tsx @@ -14,11 +14,14 @@ import { FlightCard } from "@/ui/flights/FlightCard.js"; import { FlightListSkeleton } from "@/ui/flights/FlightListSkeleton.js"; import { SeoHead } from "@/ui/seo/SeoHead.js"; import { JsonLdRenderer } from "@/shared/seo/json-ld.js"; +import { PageLayout } from "@/ui/layout/PageLayout.js"; +import { PageTabs } from "@/ui/layout/PageTabs.js"; import { useFlightDetails } from "../hooks/useFlightDetails.js"; import { useLiveFlightDetails } from "../hooks/useLiveFlightDetails.js"; import { buildFlightDetailsSeo } from "../seo.js"; import { buildFlightJsonLd } from "../json-ld.js"; import { FlightDetailsAccordion } from "./details-panels/FlightDetailsAccordion.js"; +import { FlightsMiniList } from "./FlightsMiniList/index.js"; import type { IParsedFlightId, IFlightLeg } from "../types.js"; export interface OnlineBoardDetailsPageProps { @@ -149,7 +152,7 @@ export const OnlineBoardDetailsPage: FC = ({ flights: `${flightId.carrier}${flightId.flightNumber}${flightId.suffix ?? ""}`, dates: `${flightId.date.slice(0, 4)}-${flightId.date.slice(4, 6)}-${flightId.date.slice(6, 8)}`, }; - const { flight, loading, error } = useFlightDetails(detailsParams); + const { flight, allFlights, loading, error } = useFlightDetails(detailsParams); // Live updates via SignalR const { flight: liveFlight, connectionStatus } = useLiveFlightDetails( @@ -159,23 +162,39 @@ export const OnlineBoardDetailsPage: FC = ({ const displayFlight = connectionStatus === "live" && liveFlight ? liveFlight : flight; + const onlineboardHref = `/${locale}/onlineboard`; + const commonLayoutProps = { + headerLeft: , + breadcrumbs: [ + { label: t("BREADCRUMBS.ONLINEBOARD"), url: onlineboardHref }, + ], + }; + if (loading) { - return ; + return ( + + + + ); } if (error) { return ( -
-

Failed to load flight details. Please try again.

-
+ +
+

Failed to load flight details. Please try again.

+
+
); } if (!displayFlight) { return ( -
-

Flight not found.

-
+ +
+

Flight not found.

+
+
); } @@ -186,48 +205,65 @@ export const OnlineBoardDetailsPage: FC = ({ const jsonLd = buildFlightJsonLd(displayFlight); return ( -
+ <> - {/* Connection status */} -
- {connectionStatus === "live" && ( - Live - )} - {connectionStatus === "reconnecting" && ( - Reconnecting... - )} - {connectionStatus === "offline" && ( - Offline - )} -
+ } + title={

{flightNumber}

} + breadcrumbs={[ + { label: t("BREADCRUMBS.ONLINEBOARD"), url: onlineboardHref }, + { label: flightNumber }, + ]} + contentLeft={ + + } + > +
+ {/* Connection status */} +
+ {connectionStatus === "live" && ( + Live + )} + {connectionStatus === "reconnecting" && ( + Reconnecting... + )} + {connectionStatus === "offline" && ( + Offline + )} +
- {/* Flight header */} -
-

{flightNumber}

- {displayFlight.status} -
+ {/* Overall status (h1 moved to PageLayout title) */} +
+ {displayFlight.status} +
- {/* Summary card */} - + {/* Summary card */} + - {/* Operating carrier */} - {displayFlight.operatingBy.carrier && ( -
- Operated by: {displayFlight.operatingBy.carrier} - {displayFlight.operatingBy.flightNumber - ? ` ${displayFlight.operatingBy.flightNumber}` - : ""} + {/* Operating carrier */} + {displayFlight.operatingBy.carrier && ( +
+ Operated by: {displayFlight.operatingBy.carrier} + {displayFlight.operatingBy.flightNumber + ? ` ${displayFlight.operatingBy.flightNumber}` + : ""} +
+ )} + + {/* Detailed leg information */} + + + {/* Flying time */} +
+ Total flying time: {displayFlight.flyingTime} +
- )} - - {/* Detailed leg information */} - - - {/* Flying time */} -
- Total flying time: {displayFlight.flyingTime} -
-
+
+ ); }; diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index 887fd6a7..d165a16c 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -37,6 +37,9 @@ "TITLE": "Online Timetable", "YOU_SEARCH": "You searched" }, + "BREADCRUMBS": { + "ONLINEBOARD": "Online Board" + }, "DETAILS": { "REGISTRATION": "Check-in", "BOARDING": "Boarding", diff --git a/src/i18n/locales/ru/common.json b/src/i18n/locales/ru/common.json index f4f6f09b..e3ea2ec8 100644 --- a/src/i18n/locales/ru/common.json +++ b/src/i18n/locales/ru/common.json @@ -37,6 +37,9 @@ "TITLE": "Онлайн-Табло", "YOU_SEARCH": "Вы искали" }, + "BREADCRUMBS": { + "ONLINEBOARD": "Онлайн-табло" + }, "DETAILS": { "REGISTRATION": "Регистрация", "BOARDING": "Посадка", diff --git a/tests/integration/online-board/flight-details.test.tsx b/tests/integration/online-board/flight-details.test.tsx index 44a14d6b..e3095b7e 100644 --- a/tests/integration/online-board/flight-details.test.tsx +++ b/tests/integration/online-board/flight-details.test.tsx @@ -20,6 +20,17 @@ import { DIRECT_FLIGHT, MULTI_LEG_FLIGHT } from "./fixtures.js"; vi.mock("@modern-js/runtime/router", () => ({ useNavigate: () => vi.fn(), useParams: () => ({ lang: "ru" }), + Link: ({ children, to, ...props }: { children: React.ReactNode; to: string; className?: string; [k: string]: unknown }) => ( + {children} + ), +})); + +vi.mock("@/ui/layout/PageTabs.js", () => ({ + PageTabs: () =>
, +})); + +vi.mock("@/features/flights-map/hooks/useFeatureFlag.js", () => ({ + useFeatureFlag: () => false, })); vi.mock("@/i18n/provider.js", () => ({ @@ -52,6 +63,7 @@ const FLIGHT_ID: IParsedFlightId = { function setupWithFlight(flight: ISimpleFlight = DIRECT_FLIGHT) { mockUseFlightDetails.mockReturnValue({ flight, + allFlights: [flight], loading: false, error: null, }); @@ -166,6 +178,7 @@ describe("Flight details page integration", () => { it("renders error state when API fails", () => { mockUseFlightDetails.mockReturnValue({ flight: null, + allFlights: [], loading: false, error: new Error("API error"), }); @@ -187,6 +200,7 @@ describe("Flight details page integration", () => { it("renders not-found state when flight is null without error", () => { mockUseFlightDetails.mockReturnValue({ flight: null, + allFlights: [], loading: false, error: null, });