Audit direct flight details per TZ §4.1.15.4/.10/.11

Gaps found and closed:
- Departure gate (§4.1.15.4): rendered in boarding accordion row body,
  sourced from leg.departure.gate (Angular parity: flight-details-boarding L21)
- Departure transfer type / dispatch (§4.1.15.4): rendered in boarding row body
  from leg.departure.dispatch (Angular parity: flight-details-boarding L17)
- Aircraft tail number (§4.1.15.4): rendered in AircraftPanel from
  aircraft.registration field; AIRPLANE.TAIL-NUMBER added to all 9 locales
- BoardingPanel accepts optional departure prop for gate/dispatch display;
  legacy hidden panel keeps existing testids without duplication
- §4.1.15.10 meals + §4.1.15.11 services already implemented (assertion tests
  already cover icon rendering, links, fallback icon)

Tests added: 4 in BoardingPanel, 2 in AircraftPanel, 3 in FlightDetailsAccordion
This commit is contained in:
2026-04-22 00:26:39 +03:00
parent 1740af682c
commit 21b6c90d0f
15 changed files with 156 additions and 12 deletions
@@ -67,6 +67,25 @@ describe("AircraftPanel", () => {
expect(screen.getByTestId("aircraft-panel")).toBeTruthy();
});
// §4.1.15.4: tail number (registration mark) in aircraft panel
it("4.1.15.4-TailNumber: renders tail number when aircraft.registration is present", () => {
const eq: IEquipmentFull = {
aircraft: { actual: { title: "Airbus A320", registration: "VP-BQS" } },
};
render(<AircraftPanel equipment={eq} />);
expect(screen.getByText("VP-BQS")).toBeTruthy();
// label key is AIRPLANE.TAIL-NUMBER (t mock returns key)
expect(screen.getByText("AIRPLANE.TAIL-NUMBER")).toBeTruthy();
});
it("4.1.15.4-TailNumber-Absent: omits tail-number row when registration is not present", () => {
const eq: IEquipmentFull = {
aircraft: { actual: { title: "Airbus A320" } },
};
render(<AircraftPanel equipment={eq} />);
expect(screen.queryByText("AIRPLANE.TAIL-NUMBER")).toBeNull();
});
// §4.1.15.9: previous-flight chip must be a link (opens new tab) when the
// flight's scheduled departure date is more recent than today 2 days.
describe("previous-flight link (§4.1.15.9)", () => {
@@ -112,6 +112,11 @@ export const AircraftPanel: FC<AircraftPanelProps> = ({
className?: string;
}> = [];
if (aircraft?.name) props.push({ label: t("AIRPLANE.NAME"), value: aircraft.name });
// §4.1.15.4: tail number (registration mark, e.g. "VP-BQS") from the
// aircraft.actual.registration field. Shown when present.
if (aircraft?.registration) {
props.push({ label: t("AIRPLANE.TAIL-NUMBER"), value: aircraft.registration });
}
if (total > 0) props.push({ label: t("AIRPLANE.SEATS-TOTAL"), value: total });
if (economy > 0) props.push({ label: t("AIRPLANE.SEATS-ECONOMY"), value: economy });
if (comfort > 0) props.push({ label: t("AIRPLANE.SEATS-COMFORT"), value: comfort });
@@ -43,4 +43,31 @@ describe("BoardingPanel", () => {
render(<BoardingPanel item={baseItem} />);
expect(screen.getByTestId("boarding-panel")).toBeTruthy();
});
// §4.1.15.4: departure gate + transfer type in boarding panel (Angular parity)
it("4.1.15.4-Boarding-Gate: renders gate row when departure.gate is present", () => {
render(<BoardingPanel item={baseItem} departure={{ dispatch: undefined, gate: "D5" }} />);
expect(screen.getByTestId("boarding-gate")).toBeTruthy();
expect(screen.getByText("D5")).toBeTruthy();
expect(screen.getByText("SHARED.NUMBER-EXIT")).toBeTruthy();
});
it("4.1.15.4-Boarding-Dispatch: renders dispatch type row when departure.dispatch is present", () => {
render(<BoardingPanel item={baseItem} departure={{ dispatch: "Bus", gate: undefined }} />);
expect(screen.getByTestId("boarding-dispatch")).toBeTruthy();
// t mock returns key — dispatch key is "DISPATCH.Bus"
expect(screen.getByText("DISPATCH.Bus")).toBeTruthy();
expect(screen.getByText("SHARED.LANDING-TRANSFER")).toBeTruthy();
});
it("4.1.15.4-Boarding-NoGate: omits gate row when departure.gate is absent", () => {
render(<BoardingPanel item={baseItem} departure={{ dispatch: undefined, gate: undefined }} />);
expect(screen.queryByTestId("boarding-gate")).toBeNull();
});
it("4.1.15.4-Boarding-NoDeparture: omits gate/dispatch rows when departure prop is not passed", () => {
render(<BoardingPanel item={baseItem} />);
expect(screen.queryByTestId("boarding-gate")).toBeNull();
expect(screen.queryByTestId("boarding-dispatch")).toBeNull();
});
});
@@ -5,7 +5,7 @@ import {
formatUtcOffset,
formatDayMonthYear,
} from "@/shared/utils/datetime/index.js";
import type { IFlightTransitionItem, FlightTransitionStatus } from "../../types.js";
import type { IFlightTransitionItem, IFlightLegDepartureStation, FlightTransitionStatus } from "../../types.js";
import "./panels.scss";
function formatTransitionTimestamp(iso: string): string {
@@ -25,9 +25,11 @@ const STATUS_KEYS: Record<FlightTransitionStatus, string> = {
export interface BoardingPanelProps {
item: IFlightTransitionItem;
/** Departure station — used to show transfer type + gate (§4.1.15.4). */
departure?: Pick<IFlightLegDepartureStation, "dispatch" | "gate">;
}
export const BoardingPanel: FC<BoardingPanelProps> = ({ item }) => {
export const BoardingPanel: FC<BoardingPanelProps> = ({ item, departure }) => {
const { t } = useTranslation();
const hasEnd = Boolean(item.end?.local);
@@ -47,6 +49,20 @@ export const BoardingPanel: FC<BoardingPanelProps> = ({ item }) => {
<span className="details-panel__value">{formatTransitionTimestamp(item.end.local)}</span>
</div>
)}
{/* §4.1.15.4: dispatch type (Bus / Bridge) and departure gate from boarding panel
Angular parity: flight-details-boarding.component.html lines 1724 */}
{departure?.dispatch && (
<div className="details-panel__row" data-testid="boarding-dispatch">
<span className="details-panel__label">{t("SHARED.LANDING-TRANSFER")}</span>
<span className="details-panel__value">{t(`DISPATCH.${departure.dispatch}`)}</span>
</div>
)}
{departure?.gate && (
<div className="details-panel__row" data-testid="boarding-gate">
<span className="details-panel__label">{t("SHARED.NUMBER-EXIT")}</span>
<span className="details-panel__value">{departure.gate}</span>
</div>
)}
</div>
);
};
@@ -40,6 +40,29 @@ function makeLeg(overrides: Partial<IFlightLeg> = {}): IFlightLeg {
return { ...base, ...overrides };
}
function makeLegWithBoarding(gate?: string, dispatch?: string): IFlightLeg {
return makeLeg({
departure: {
scheduled: { airport: "JFK", airportCode: "JFK", city: "New York", cityCode: "NYC", countryCode: "US" },
dispatch,
gate,
terminal: "",
checkingStatus: "Scheduled",
parkingStand: "",
times: { scheduledDeparture: { dayChange: { value: 0, title: "" }, local: "", localTime: "", tzOffset: 0, utc: "" } },
},
transition: {
boarding: {
start: { dayChange: { value: 0, title: "" }, local: "11:00", localTime: "11:00", tzOffset: 0, utc: "" },
end: { dayChange: { value: 0, title: "" }, local: "11:30", localTime: "11:30", tzOffset: 0, utc: "" },
status: "InProgress",
isActual: true,
},
},
status: "InFlight",
});
}
describe("FlightDetailsAccordion", () => {
it("returns null when no panels should be visible", () => {
const leg = makeLeg();
@@ -106,4 +129,28 @@ describe("FlightDetailsAccordion", () => {
render(<FlightDetailsAccordion leg={leg} viewType="Onlineboard" />);
expect(screen.getByText("DETAILS.ON_BOARD_SERVICES")).toBeTruthy();
});
// §4.1.15.4: departure gate + transfer type in boarding panel row
describe("boarding row gate + dispatch (§4.1.15.4)", () => {
it("4.1.15.4-AccordionBoarding-Gate: shows gate in boarding body when departure.gate is set", () => {
const leg = makeLegWithBoarding("D5", undefined);
render(<FlightDetailsAccordion leg={leg} viewType="Onlineboard" />);
expect(screen.getByTestId("boarding-gate")).toBeTruthy();
expect(screen.getByText("D5")).toBeTruthy();
});
it("4.1.15.4-AccordionBoarding-Dispatch: shows dispatch in boarding body when departure.dispatch is set", () => {
const leg = makeLegWithBoarding(undefined, "Bus");
render(<FlightDetailsAccordion leg={leg} viewType="Onlineboard" />);
expect(screen.getByTestId("boarding-dispatch")).toBeTruthy();
expect(screen.getByText("DISPATCH.Bus")).toBeTruthy();
});
it("4.1.15.4-AccordionBoarding-NoFields: omits gate/dispatch rows when departure has neither", () => {
const leg = makeLegWithBoarding(undefined, undefined);
render(<FlightDetailsAccordion leg={leg} viewType="Onlineboard" />);
expect(screen.queryByTestId("boarding-gate")).toBeNull();
expect(screen.queryByTestId("boarding-dispatch")).toBeNull();
});
});
});
@@ -161,7 +161,25 @@ export const FlightDetailsAccordion: FC<FlightDetailsAccordionProps> = ({ leg, v
icon: ICON_BOARDING,
title: t("DETAILS.BOARDING"),
statusStatus: boarding.status,
body: <TransitionTimes item={boarding} testId="boarding-times" />,
body: (
<>
<TransitionTimes item={boarding} testId="boarding-times" />
{/* §4.1.15.4: dispatch type (Bus / Bridge) and departure gate.
Angular parity: flight-details-boarding.component.html lines 1724 */}
{leg.departure.dispatch && (
<div className="details-panel__row" data-testid="boarding-dispatch">
<span className="details-panel__label">{t("SHARED.LANDING-TRANSFER")}</span>
<span className="details-panel__value">{t(`DISPATCH.${leg.departure.dispatch}`)}</span>
</div>
)}
{leg.departure.gate && (
<div className="details-panel__row" data-testid="boarding-gate">
<span className="details-panel__label">{t("SHARED.NUMBER-EXIT")}</span>
<span className="details-panel__value">{leg.departure.gate}</span>
</div>
)}
</>
),
legacyTestId: "boarding-panel",
isTransition: true,
});
@@ -229,6 +247,9 @@ export const FlightDetailsAccordion: FC<FlightDetailsAccordionProps> = ({ leg, v
if (row.id === "boarding" && leg.transition?.boarding) {
legacyPanels.push(
<div key="legacy-boarding" className="visually-hidden">
{/* departure prop intentionally omitted — dispatch/gate already
rendered in the visible accordion row body above to avoid
duplicate data-testid="boarding-gate/dispatch" in the DOM. */}
<BoardingPanel item={leg.transition.boarding} />
</div>,
);
+2 -1
View File
@@ -4,7 +4,8 @@
"SEATS-BUSINESS": "",
"SEATS-COMFORT": "",
"SEATS-ECONOMY": "",
"SEATS-TOTAL": ""
"SEATS-TOTAL": "",
"TAIL-NUMBER": "Kennzeichen"
},
"BOARD": {
"ARRIVAL": "Ankunft",
+2 -1
View File
@@ -4,7 +4,8 @@
"SEATS-BUSINESS": "Business",
"SEATS-COMFORT": "Comfort",
"SEATS-ECONOMY": "Economy",
"SEATS-TOTAL": "Number of seats"
"SEATS-TOTAL": "Number of seats",
"TAIL-NUMBER": "Tail number"
},
"BOARD": {
"ARRIVAL": "Arrival",
+2 -1
View File
@@ -4,7 +4,8 @@
"SEATS-BUSINESS": "",
"SEATS-COMFORT": "",
"SEATS-ECONOMY": "",
"SEATS-TOTAL": ""
"SEATS-TOTAL": "",
"TAIL-NUMBER": "Matrícula"
},
"BOARD": {
"ARRIVAL": "Llegada",
+2 -1
View File
@@ -4,7 +4,8 @@
"SEATS-BUSINESS": "",
"SEATS-COMFORT": "",
"SEATS-ECONOMY": "",
"SEATS-TOTAL": ""
"SEATS-TOTAL": "",
"TAIL-NUMBER": "Immatriculation"
},
"BOARD": {
"ARRIVAL": "Arrivée",
+2 -1
View File
@@ -4,7 +4,8 @@
"SEATS-BUSINESS": "",
"SEATS-COMFORT": "",
"SEATS-ECONOMY": "",
"SEATS-TOTAL": ""
"SEATS-TOTAL": "",
"TAIL-NUMBER": "Numero di coda"
},
"BOARD": {
"ARRIVAL": "Arrivo",
+2 -1
View File
@@ -4,7 +4,8 @@
"SEATS-BUSINESS": "",
"SEATS-COMFORT": "",
"SEATS-ECONOMY": "",
"SEATS-TOTAL": ""
"SEATS-TOTAL": "",
"TAIL-NUMBER": "機体番号"
},
"BOARD": {
"ARRIVAL": "到着",
+2 -1
View File
@@ -4,7 +4,8 @@
"SEATS-BUSINESS": "",
"SEATS-COMFORT": "",
"SEATS-ECONOMY": "",
"SEATS-TOTAL": ""
"SEATS-TOTAL": "",
"TAIL-NUMBER": "꼬리번호"
},
"BOARD": {
"ARRIVAL": "도착",
+2 -1
View File
@@ -4,7 +4,8 @@
"SEATS-BUSINESS": "Бизнес",
"SEATS-COMFORT": "Комфорт",
"SEATS-ECONOMY": "Эконом",
"SEATS-TOTAL": "Количество мест"
"SEATS-TOTAL": "Количество мест",
"TAIL-NUMBER": "Бортовой номер"
},
"BOARD": {
"ARRIVAL": "Прилет",
+2 -1
View File
@@ -4,7 +4,8 @@
"SEATS-BUSINESS": "",
"SEATS-COMFORT": "",
"SEATS-ECONOMY": "",
"SEATS-TOTAL": ""
"SEATS-TOTAL": "",
"TAIL-NUMBER": "机尾号"
},
"BOARD": {
"ARRIVAL": "到达",