Wire FlightsMiniList into OnlineBoardDetailsPage via PageLayout
This commit is contained in:
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -37,6 +37,9 @@
|
||||
"TITLE": "Online Timetable",
|
||||
"YOU_SEARCH": "You searched"
|
||||
},
|
||||
"BREADCRUMBS": {
|
||||
"ONLINEBOARD": "Online Board"
|
||||
},
|
||||
"DETAILS": {
|
||||
"REGISTRATION": "Check-in",
|
||||
"BOARDING": "Boarding",
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user