From ade7feb7151c9debf9011e71ff33f635f29be48b Mon Sep 17 00:00:00 2001 From: gnezim Date: Fri, 17 Apr 2026 01:34:29 +0300 Subject: [PATCH] Add DetailsHeaderBadge with flight number and codesharing --- .../DetailsHeaderBadge.test.tsx | 119 ++++++++++++++++++ .../BoardDetailsHeader/DetailsHeaderBadge.tsx | 47 +++++++ 2 files changed, 166 insertions(+) create mode 100644 src/features/online-board/components/BoardDetailsHeader/DetailsHeaderBadge.test.tsx create mode 100644 src/features/online-board/components/BoardDetailsHeader/DetailsHeaderBadge.tsx diff --git a/src/features/online-board/components/BoardDetailsHeader/DetailsHeaderBadge.test.tsx b/src/features/online-board/components/BoardDetailsHeader/DetailsHeaderBadge.test.tsx new file mode 100644 index 00000000..956de2ad --- /dev/null +++ b/src/features/online-board/components/BoardDetailsHeader/DetailsHeaderBadge.test.tsx @@ -0,0 +1,119 @@ +// @vitest-environment jsdom +import { describe, it, expect, vi } from "vitest"; +import { render, screen } from "@testing-library/react"; +import { DetailsHeaderBadge } from "./DetailsHeaderBadge.js"; +import type { ISimpleFlight, IFlightLeg } from "../../types.js"; + +vi.mock("@/i18n/provider.js", () => ({ useTranslation: () => ({ t: (k: string) => k }) })); + +function makeLeg(overrides: Partial = {}): IFlightLeg { + return { + arrival: { + scheduled: { airport: "", airportCode: "LED", city: "", cityCode: "", countryCode: "" }, + latest: { airport: "", airportCode: "LED", city: "", cityCode: "", countryCode: "" }, + dispatch: "", + gate: "", + terminal: "", + times: { + scheduledArrival: { + dayChange: { value: 0, title: "" }, + local: "", + localTime: "", + tzOffset: 0, + utc: "", + }, + }, + }, + dayChange: 0, + departure: { + scheduled: { airport: "", airportCode: "SVO", city: "", cityCode: "", countryCode: "" }, + latest: { airport: "", airportCode: "SVO", city: "", cityCode: "", countryCode: "" }, + dispatch: "", + gate: "", + terminal: "", + checkingStatus: "Scheduled", + parkingStand: "", + times: { + scheduledDeparture: { + dayChange: { value: 0, title: "" }, + local: "", + localTime: "", + tzOffset: 0, + utc: "2026-04-20T10:00:00Z", + }, + }, + }, + equipment: {}, + flags: { checkinAvailable: false, returnToAirport: false, routeChanged: false }, + flyingTime: "1h", + index: 0, + operatingBy: {}, + status: "Scheduled", + updated: "", + ...overrides, + } as IFlightLeg; +} + +function makeDirect(): ISimpleFlight { + return { + id: "X", + routeType: "Direct", + flyingTime: "1h", + status: "Scheduled", + flightId: { carrier: "SU", flightNumber: "0022", suffix: "", date: "20260417" }, + operatingBy: { carrier: "SU", flightNumber: "0022" }, + leg: makeLeg(), + } as ISimpleFlight; +} + +function makeMultiLeg(): ISimpleFlight { + return { + id: "Y", + routeType: "MultiLeg", + flyingTime: "2h", + status: "Scheduled", + flightId: { carrier: "SU", flightNumber: "0022", suffix: "", date: "20260417" }, + operatingBy: { carrier: "SU", flightNumber: "0022" }, + legs: [ + makeLeg({ operatingBy: { carrier: "KL", flightNumber: "1234" } }), + makeLeg({ operatingBy: { carrier: "AF", flightNumber: "5678" } }), + makeLeg({ operatingBy: { carrier: "SU", flightNumber: "0022" } }), + ], + } as ISimpleFlight; +} + +describe("DetailsHeaderBadge", () => { + it("renders primary flight number SU 0022", () => { + render(); + expect(screen.getByText("SU 0022")).toBeTruthy(); + }); + + it("renders operator-logo", () => { + render(); + expect(screen.getByTestId("operator-logo")).toBeTruthy(); + }); + + it("does NOT render codesharing for Direct", () => { + render(); + expect(screen.queryByTestId("codesharing")).toBeNull(); + }); + + it("renders codesharing list for MultiLeg with partner carriers", () => { + render(); + const cs = screen.getByTestId("codesharing"); + expect(cs).toBeTruthy(); + expect(cs.textContent).toContain("KL 1234"); + expect(cs.textContent).toContain("AF 5678"); + expect(cs.textContent).not.toContain("SU 0022"); + }); + + it("renders small status button when showStatus=true", () => { + render(); + expect(screen.getByTestId("flight-status-button")).toBeTruthy(); + }); + + it("does not render status button by default", () => { + render(); + expect(screen.queryByTestId("flight-status-button")).toBeNull(); + }); +}); diff --git a/src/features/online-board/components/BoardDetailsHeader/DetailsHeaderBadge.tsx b/src/features/online-board/components/BoardDetailsHeader/DetailsHeaderBadge.tsx new file mode 100644 index 00000000..fc82f821 --- /dev/null +++ b/src/features/online-board/components/BoardDetailsHeader/DetailsHeaderBadge.tsx @@ -0,0 +1,47 @@ +import type { FC } from "react"; +import type { ISimpleFlight, IFlightLeg } from "../../types.js"; +import { OperatorLogo } from "./OperatorLogo.js"; +import { FlightStatusButton } from "./FlightStatusButton.js"; + +export interface DetailsHeaderBadgeProps { + flight: ISimpleFlight; + locale: string; + large?: boolean; + round?: boolean; + showStatus?: boolean; +} + +function getCodeshareLegs(flight: ISimpleFlight): IFlightLeg[] { + if (flight.routeType !== "MultiLeg") return []; + return flight.legs.filter( + (l) => l.operatingBy?.carrier && l.operatingBy.carrier !== flight.flightId.carrier, + ); +} + +export const DetailsHeaderBadge: FC = ({ + flight, + locale, + large = true, + round = false, + showStatus = false, +}) => { + const codeshareLegs = getCodeshareLegs(flight); + const primaryNumber = `${flight.flightId.carrier} ${flight.flightId.flightNumber}`; + + return ( +
+
+
{primaryNumber}
+ {codeshareLegs.length > 0 && ( +
+ {codeshareLegs + .map((l) => `${l.operatingBy.carrier} ${l.operatingBy.flightNumber}`) + .join(", ")} +
+ )} +
+ + {showStatus && } +
+ ); +};