Wire FlightsMiniList into OnlineBoardDetailsPage via PageLayout

This commit is contained in:
2026-04-16 23:35:29 +03:00
parent bfe14012c7
commit cf08541256
5 changed files with 145 additions and 49 deletions
@@ -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 }) => (
<a href={to} {...props}>{children}</a>
),
useNavigate: () => vi.fn(),
useParams: () => ({ lang: "ru" }),
}));
vi.mock("@/ui/layout/PageTabs.js", () => ({
PageTabs: () => <div data-testid="page-tabs" />,
}));
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(<OnlineBoardDetailsPage flightId={mockFlightId} locale="ru" canonicalOrigin="https://www.aeroflot.ru" />);
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(<OnlineBoardDetailsPage flightId={mockFlightId} locale="ru" canonicalOrigin="https://www.aeroflot.ru" />);
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(<OnlineBoardDetailsPage flightId={mockFlightId} locale="ru" canonicalOrigin="https://www.aeroflot.ru" />);
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(<OnlineBoardDetailsPage flightId={mockFlightId} locale="ru" canonicalOrigin="https://example.com" />);
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(<OnlineBoardDetailsPage flightId={mockFlightId} locale="ru" canonicalOrigin="https://example.com" />);
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(<OnlineBoardDetailsPage flightId={mockFlightId} locale="ru" canonicalOrigin="https://example.com" />);
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(<OnlineBoardDetailsPage flightId={mockFlightId} locale="ru" canonicalOrigin="https://example.com" />);
expect(container.querySelector(".page-layout")).toBeTruthy();
});
});
});
@@ -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<OnlineBoardDetailsPageProps> = ({
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<OnlineBoardDetailsPageProps> = ({
const displayFlight = connectionStatus === "live" && liveFlight ? liveFlight : flight;
const onlineboardHref = `/${locale}/onlineboard`;
const commonLayoutProps = {
headerLeft: <PageTabs viewType="onlineboard" />,
breadcrumbs: [
{ label: t("BREADCRUMBS.ONLINEBOARD"), url: onlineboardHref },
],
};
if (loading) {
return <FlightListSkeleton count={1} />;
return (
<PageLayout {...commonLayoutProps}>
<FlightListSkeleton count={1} />
</PageLayout>
);
}
if (error) {
return (
<div className="flight-details flight-details--error" data-testid="flight-details-error">
<p>Failed to load flight details. Please try again.</p>
</div>
<PageLayout {...commonLayoutProps}>
<div className="flight-details flight-details--error" data-testid="flight-details-error">
<p>Failed to load flight details. Please try again.</p>
</div>
</PageLayout>
);
}
if (!displayFlight) {
return (
<div className="flight-details flight-details--not-found" data-testid="flight-details-not-found">
<p>Flight not found.</p>
</div>
<PageLayout {...commonLayoutProps}>
<div className="flight-details flight-details--not-found" data-testid="flight-details-not-found">
<p>Flight not found.</p>
</div>
</PageLayout>
);
}
@@ -186,48 +205,65 @@ export const OnlineBoardDetailsPage: FC<OnlineBoardDetailsPageProps> = ({
const jsonLd = buildFlightJsonLd(displayFlight);
return (
<div className="flight-details" data-testid="flight-details">
<>
<SeoHead {...seoProps} />
<JsonLdRenderer data={jsonLd} />
{/* Connection status */}
<div className="flight-details__status" data-testid="connection-status">
{connectionStatus === "live" && (
<span className="connection-badge connection-badge--live">Live</span>
)}
{connectionStatus === "reconnecting" && (
<span className="connection-badge connection-badge--reconnecting">Reconnecting...</span>
)}
{connectionStatus === "offline" && (
<span className="connection-badge connection-badge--offline">Offline</span>
)}
</div>
<PageLayout
headerLeft={<PageTabs viewType="onlineboard" />}
title={<h1 className="flight-details__flight-number">{flightNumber}</h1>}
breadcrumbs={[
{ label: t("BREADCRUMBS.ONLINEBOARD"), url: onlineboardHref },
{ label: flightNumber },
]}
contentLeft={
<FlightsMiniList
flights={allFlights}
currentFlight={displayFlight}
lang={locale}
/>
}
>
<div className="flight-details" data-testid="flight-details">
{/* Connection status */}
<div className="flight-details__status" data-testid="connection-status">
{connectionStatus === "live" && (
<span className="connection-badge connection-badge--live">Live</span>
)}
{connectionStatus === "reconnecting" && (
<span className="connection-badge connection-badge--reconnecting">Reconnecting...</span>
)}
{connectionStatus === "offline" && (
<span className="connection-badge connection-badge--offline">Offline</span>
)}
</div>
{/* Flight header */}
<div className="flight-details__header">
<h1 className="flight-details__flight-number">{flightNumber}</h1>
<span className="flight-details__overall-status">{displayFlight.status}</span>
</div>
{/* Overall status (h1 moved to PageLayout title) */}
<div className="flight-details__header">
<span className="flight-details__overall-status">{displayFlight.status}</span>
</div>
{/* Summary card */}
<FlightCard flight={displayFlight} />
{/* Summary card */}
<FlightCard flight={displayFlight} />
{/* Operating carrier */}
{displayFlight.operatingBy.carrier && (
<div className="flight-details__operating" data-testid="operating-carrier">
Operated by: {displayFlight.operatingBy.carrier}
{displayFlight.operatingBy.flightNumber
? ` ${displayFlight.operatingBy.flightNumber}`
: ""}
{/* Operating carrier */}
{displayFlight.operatingBy.carrier && (
<div className="flight-details__operating" data-testid="operating-carrier">
Operated by: {displayFlight.operatingBy.carrier}
{displayFlight.operatingBy.flightNumber
? ` ${displayFlight.operatingBy.flightNumber}`
: ""}
</div>
)}
{/* Detailed leg information */}
<FlightLegs legs={legs} />
{/* Flying time */}
<div className="flight-details__flying-time" data-testid="flying-time">
Total flying time: {displayFlight.flyingTime}
</div>
</div>
)}
{/* Detailed leg information */}
<FlightLegs legs={legs} />
{/* Flying time */}
<div className="flight-details__flying-time" data-testid="flying-time">
Total flying time: {displayFlight.flyingTime}
</div>
</div>
</PageLayout>
</>
);
};
+3
View File
@@ -37,6 +37,9 @@
"TITLE": "Online Timetable",
"YOU_SEARCH": "You searched"
},
"BREADCRUMBS": {
"ONLINEBOARD": "Online Board"
},
"DETAILS": {
"REGISTRATION": "Check-in",
"BOARDING": "Boarding",
+3
View File
@@ -37,6 +37,9 @@
"TITLE": "Онлайн-Табло",
"YOU_SEARCH": "Вы искали"
},
"BREADCRUMBS": {
"ONLINEBOARD": "Онлайн-табло"
},
"DETAILS": {
"REGISTRATION": "Регистрация",
"BOARDING": "Посадка",
@@ -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 }) => (
<a href={to} {...props}>{children}</a>
),
}));
vi.mock("@/ui/layout/PageTabs.js", () => ({
PageTabs: () => <div data-testid="page-tabs" />,
}));
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,
});