Wire FullRouteTimeline and TransferBar into OnlineBoardDetailsPage

Multi-leg flights now render a full-route timeline header and interleave
a transfer-bar between consecutive legs, surfacing station change and
intermediate-landing duration inline with the leg details.
This commit is contained in:
2026-04-17 02:38:24 +03:00
parent 4c87a3b362
commit 6805b8fe4d
2 changed files with 90 additions and 8 deletions
@@ -9,7 +9,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 } from "../types.js";
import type { IParsedFlightId, IDirectFlight, IMultiLegFlight, IFlightLeg } from "../types.js";
const mockFlightId: IParsedFlightId = {
carrier: "SU",
@@ -75,8 +75,8 @@ const mockFlight: IDirectFlight = {
// Mutable state for test control
let mockState = {
flight: mockFlight as IDirectFlight | null,
allFlights: [mockFlight] as IDirectFlight[],
flight: mockFlight as IDirectFlight | IMultiLegFlight | null,
allFlights: [mockFlight] as (IDirectFlight | IMultiLegFlight)[],
daysOfFlight: ["20250115"] as string[],
loading: false,
error: null as Error | null,
@@ -273,6 +273,70 @@ describe("OnlineBoardDetailsPage", () => {
});
});
describe("multi-leg timeline integration", () => {
function makeLeg(index: number, overrides: Partial<IFlightLeg> = {}): IFlightLeg {
return {
...mockFlight.leg,
index,
...overrides,
} as IFlightLeg;
}
it("does not render FullRouteTimeline for Direct flights", () => {
render(
<OnlineBoardDetailsPage flightId={mockFlightId} locale="ru" canonicalOrigin="https://example.com" />,
);
expect(screen.queryByTestId("full-route-timeline")).toBeNull();
expect(screen.queryByTestId("transfer-bar")).toBeNull();
});
it("renders FullRouteTimeline for MultiLeg flights", () => {
const multiLeg: IMultiLegFlight = {
id: "SU100-20250115",
flightId: { carrier: "SU", flightNumber: "100", suffix: "", date: "20250115" },
routeType: "MultiLeg",
status: "Scheduled",
flyingTime: "12:00",
operatingBy: {},
legs: [makeLeg(0), makeLeg(1)],
};
mockState = {
flight: multiLeg,
allFlights: [multiLeg],
daysOfFlight: ["20250115"],
loading: false,
error: null,
};
render(
<OnlineBoardDetailsPage flightId={mockFlightId} locale="ru" canonicalOrigin="https://example.com" />,
);
expect(screen.getByTestId("full-route-timeline")).toBeTruthy();
});
it("interleaves one TransferBar between two legs", () => {
const multiLeg: IMultiLegFlight = {
id: "SU100-20250115",
flightId: { carrier: "SU", flightNumber: "100", suffix: "", date: "20250115" },
routeType: "MultiLeg",
status: "Scheduled",
flyingTime: "12:00",
operatingBy: {},
legs: [makeLeg(0), makeLeg(1)],
};
mockState = {
flight: multiLeg,
allFlights: [multiLeg],
daysOfFlight: ["20250115"],
loading: false,
error: null,
};
render(
<OnlineBoardDetailsPage flightId={mockFlightId} locale="ru" canonicalOrigin="https://example.com" />,
);
expect(screen.getAllByTestId("transfer-bar")).toHaveLength(1);
});
});
describe("flight schedule integration", () => {
it("renders FlightSchedule when firstLeg.daysOfWeek is present", () => {
const flightWithDaysOfWeek = {
@@ -7,7 +7,7 @@
* @module
*/
import { useCallback, type FC } from "react";
import { Fragment, useCallback, type FC } from "react";
import { useNavigate } from "@modern-js/runtime/router";
import { useTranslation } from "@/i18n/provider.js";
import "./OnlineBoardDetailsPage.scss";
@@ -28,6 +28,8 @@ import { DayTabs } from "./DayTabs/index.js";
import { BoardDetailsHeader } from "./BoardDetailsHeader/index.js";
import { DetailsBackButton } from "./DetailsBackButton/index.js";
import { FlightSchedule } from "./FlightSchedule/index.js";
import { FullRouteTimeline } from "./FullRouteTimeline/index.js";
import { TransferBar } from "./TransferBar/index.js";
import type { IParsedFlightId, IFlightLeg } from "../types.js";
export interface OnlineBoardDetailsPageProps {
@@ -41,12 +43,20 @@ export interface OnlineBoardDetailsPageProps {
/**
* Render all legs of a flight with departure/arrival station details.
* For multi-leg flights, interleaves a TransferBar between consecutive legs.
*/
function FlightLegs({ legs }: { legs: IFlightLeg[] }): JSX.Element {
function FlightLegs({
legs,
viewType,
}: {
legs: IFlightLeg[];
viewType: "Onlineboard" | "Schedule";
}): JSX.Element {
return (
<div className="flight-details__legs" data-testid="flight-legs">
{legs.map((leg) => (
<div key={leg.index} className="flight-details__leg" data-testid={`flight-leg-${leg.index}`}>
{legs.map((leg, i) => (
<Fragment key={leg.index}>
<div className="flight-details__leg" data-testid={`flight-leg-${leg.index}`}>
<div className="flight-details__leg-header">
<span className="flight-details__leg-index">Leg {leg.index + 1}</span>
<span className="flight-details__leg-status">{leg.status}</span>
@@ -124,6 +134,10 @@ function FlightLegs({ legs }: { legs: IFlightLeg[] }): JSX.Element {
)}
<FlightDetailsAccordion leg={leg} viewType="Onlineboard" />
</div>
{i < legs.length - 1 && (
<TransferBar leg={leg} nextLeg={legs[i + 1]!} viewType={viewType} />
)}
</Fragment>
))}
</div>
);
@@ -278,6 +292,10 @@ export const OnlineBoardDetailsPage: FC<OnlineBoardDetailsPageProps> = ({
<BoardDetailsHeader flight={displayFlight} locale={locale} />
{displayFlight.routeType === "MultiLeg" && (
<FullRouteTimeline legs={displayFlight.legs} viewType="Onlineboard" />
)}
{/* Summary card */}
<FlightCard flight={displayFlight} />
@@ -292,7 +310,7 @@ export const OnlineBoardDetailsPage: FC<OnlineBoardDetailsPageProps> = ({
)}
{/* Detailed leg information */}
<FlightLegs legs={legs} />
<FlightLegs legs={legs} viewType="Onlineboard" />
{/* Flying time */}
<div className="flight-details__flying-time" data-testid="flying-time">