From 9f6623786fecac2d1f53fc62f831fd8c063f9c17 Mon Sep 17 00:00:00 2001 From: gnezim Date: Tue, 21 Apr 2026 23:02:55 +0300 Subject: [PATCH] Audit Online-Board expanded row per TZ 4.1.13.4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gap audit against §4.1.13.4.3 (Tables 29/30) found that the inline boarding/deboarding row in FlightCard's default expanded body was missing three attributes: - departure.gate / arrival.gate (boarding gate number) - departure.dispatch (трап/автобус transfer type) - arrival.bagBelt (baggage belt, deboarding only) Add all three as conditional fields in the transition block, guarded by the existing isArrival flag so departure shows gate+dispatch and arrival shows gate+bagBelt. Add DETAILS.DISPATCH i18n label (ru + en). Add 16 assertion tests covering time rows, transition status/times, gate, dispatch, bagBelt, and the share/details buttons. Deferred (DONE_WITH_CONCERNS): - Check-in counter number: API type has checkingStatus string but no counter number field; requires backend extension. - Aircraft tail number: field (aircraft.registration) exists in types but is only shown in the details-page AircraftPanel, not in the FlightCard expanded body; deferred to details-page parity task. - Code-share chips in expanded segment body: currently merged into the collapsed header number column via _childFlightIds; per-segment expanded display deferred to multi-leg task. --- src/i18n/locales/en/common.json | 3 +- src/i18n/locales/ru/common.json | 3 +- src/ui/flights/FlightCard.test.tsx | 182 +++++++++++++++++++++++++++++ src/ui/flights/FlightCard.tsx | 35 ++++++ 4 files changed, 221 insertions(+), 2 deletions(-) diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index d74ba65a..7acc2af1 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -80,7 +80,8 @@ "GATE": "Gate", "BAG_BELT": "Baggage belt", "CONFIGURATION": "Configuration", - "STATUS": "Status" + "STATUS": "Status", + "DISPATCH": "Transfer" }, "BOARDING-STATUSES": { "Expected": "Expected", diff --git a/src/i18n/locales/ru/common.json b/src/i18n/locales/ru/common.json index c5509bbf..0f5ba6ef 100644 --- a/src/i18n/locales/ru/common.json +++ b/src/i18n/locales/ru/common.json @@ -80,7 +80,8 @@ "GATE": "Выход", "BAG_BELT": "Лента выдачи багажа", "CONFIGURATION": "Компоновка", - "STATUS": "Статус" + "STATUS": "Статус", + "DISPATCH": "Трансфер" }, "BOARDING-STATUSES": { "Expected": "Ожидается", diff --git a/src/ui/flights/FlightCard.test.tsx b/src/ui/flights/FlightCard.test.tsx index d4d8c7ae..adf5766e 100644 --- a/src/ui/flights/FlightCard.test.tsx +++ b/src/ui/flights/FlightCard.test.tsx @@ -471,3 +471,185 @@ describe("4.1.13.3 — Collapsed row state", () => { expect(document.querySelector("[data-testid='flight-card-expanded']")).toBeNull(); }); }); + +// --------------------------------------------------------------------------- +// §4.1.13.4 — Expanded representation (Table 25 / §4.1.13.4.3) +// --------------------------------------------------------------------------- + +function makeTransitionItem(status: "InProgress" | "Finished" | "Expected" = "InProgress") { + return { + start: { + dayChange: { value: 0, title: "" }, + local: "2026-04-15T09:00:00+03:00", + localTime: "09:00", + tzOffset: 3, + utc: "2026-04-15T06:00:00Z", + }, + end: { + dayChange: { value: 0, title: "" }, + local: "2026-04-15T09:30:00+03:00", + localTime: "09:30", + tzOffset: 3, + utc: "2026-04-15T06:30:00Z", + }, + status, + isActual: true, + } as const; +} + +/** + * Build a full ISimpleFlight with boarding transition + gate/dispatch override. + */ +function makeFlightWithBoarding(opts: { + gate?: string; + dispatch?: string; + bagBelt?: string; + direction?: "departure" | "arrival"; +}): ISimpleFlight { + const base = makeFlight({}); + const transition = makeTransitionItem(); + // `makeFlight` always returns a Direct flight so `leg` is safe to access + const baseLeg = (base as ReturnType & { leg: ReturnType }).leg; + return { + ...base, + leg: { + ...baseLeg, + transition: { + boarding: transition, + deboarding: transition, + }, + departure: { + ...baseLeg.departure, + gate: opts.gate, + dispatch: opts.dispatch, + }, + arrival: { + ...baseLeg.arrival, + gate: opts.gate, + bagBelt: opts.bagBelt, + }, + }, + } as unknown as ISimpleFlight; +} + +describe("4.1.13.4 — Expanded row content (TZ §4.1.13.4.3)", () => { + + it("4.1.13.4-R: expanded row shows scheduled departure time with label", () => { + const flight = makeFlight({ depLocal: "2026-04-15T10:00:00+03:00" }); + render( {}} />); + const expanded = screen.getByTestId("flight-card-expanded"); + // Scheduled time in expanded body + const text = expanded.textContent ?? ""; + expect(text).toContain("10:00"); + }); + + it("4.1.13.4-R: expanded row shows scheduled arrival time with label", () => { + const flight = makeFlight({ arrLocal: "2026-04-15T12:30:00+03:00" }); + render( {}} />); + const expanded = screen.getByTestId("flight-card-expanded"); + expect(expanded.textContent).toContain("12:30"); + }); + + it("4.1.13.4-R: expanded row shows actual departure time when available with SHARED.ACTUAL caption", () => { + const flight = makeFlight({ + depLocal: "2026-04-15T10:00:00+03:00", + depActualLocal: "2026-04-15T11:15:00+03:00", + }); + render( {}} />); + const expanded = screen.getByTestId("flight-card-expanded"); + // Actual time and caption key + expect(expanded.textContent).toContain("11:15"); + expect(expanded.textContent).toContain("SHARED.ACTUAL"); + }); + + it("4.1.13.4-R: boarding row shows status + start + end times when transition present", () => { + const flight = makeFlightWithBoarding({}); + render( {}} direction="departure" />); + const expanded = screen.getByTestId("flight-card-expanded"); + // Label key + expect(expanded.textContent).toContain("DETAILS.BOARDING"); + // Status key + expect(expanded.textContent).toContain("BOARDING-STATUSES.InProgress"); + }); + + it("4.1.13.4-R: deboarding row shown on arrival direction", () => { + const flight = makeFlightWithBoarding({}); + render( {}} direction="arrival" />); + const expanded = screen.getByTestId("flight-card-expanded"); + expect(expanded.textContent).toContain("DETAILS.DEBOARDING"); + }); + + it("4.1.13.4-R: boarding gate rendered when departure.gate is set (§4.1.13.4.3 Table 29)", () => { + const flight = makeFlightWithBoarding({ gate: "B12", direction: "departure" }); + render( {}} direction="departure" />); + expect(screen.getByTestId("transition-gate")).toBeTruthy(); + expect(screen.getByTestId("transition-gate").textContent).toContain("B12"); + }); + + it("4.1.13.4-R: gate is NOT rendered when gate is absent", () => { + const flight = makeFlightWithBoarding({}); + render( {}} direction="departure" />); + expect(document.querySelector("[data-testid='transition-gate']")).toBeNull(); + }); + + it("4.1.13.4-R: dispatch (трап/автобус) rendered when departure.dispatch is set (§4.1.13.4.3 Table 29)", () => { + const flight = makeFlightWithBoarding({ dispatch: "Bridge" }); + render( {}} direction="departure" />); + expect(screen.getByTestId("transition-dispatch")).toBeTruthy(); + // The dispatch value goes through t("DISPATCH.Bridge") which returns "DISPATCH.Bridge" in mock + expect(screen.getByTestId("transition-dispatch").textContent).toContain("Bridge"); + }); + + it("4.1.13.4-R: dispatch NOT rendered when departure.dispatch absent", () => { + const flight = makeFlightWithBoarding({}); + render( {}} direction="departure" />); + expect(document.querySelector("[data-testid='transition-dispatch']")).toBeNull(); + }); + + it("4.1.13.4-R: baggage belt rendered for deboarding (arrival) when arrival.bagBelt is set (§4.1.13.4.3 Table 30)", () => { + const flight = makeFlightWithBoarding({ bagBelt: "3", direction: "arrival" }); + render( {}} direction="arrival" />); + expect(screen.getByTestId("transition-bag-belt")).toBeTruthy(); + expect(screen.getByTestId("transition-bag-belt").textContent).toContain("3"); + }); + + it("4.1.13.4-R: baggage belt NOT rendered for departure direction", () => { + const flight = makeFlightWithBoarding({ bagBelt: "3" }); + render( {}} direction="departure" />); + expect(document.querySelector("[data-testid='transition-bag-belt']")).toBeNull(); + }); + + it("4.1.13.4-R: arrival gate rendered in deboarding row when arrival.gate is set", () => { + const flight = makeFlightWithBoarding({ gate: "C7" }); + render( {}} direction="arrival" />); + expect(screen.getByTestId("transition-gate")).toBeTruthy(); + expect(screen.getByTestId("transition-gate").textContent).toContain("C7"); + }); + + it("4.1.13.4-R: share button always present in expanded actions row", () => { + render( {}} />); + expect(screen.getByTestId("flight-share-button")).toBeTruthy(); + }); + + it("4.1.13.4-R: Details button present in expanded actions row", () => { + render( {}} />); + expect(screen.getByTestId("flight-details-button")).toBeTruthy(); + }); + + it("4.1.13.4-R: aircraft type shown under operator column when expanded (TZ Table 25 C3)", () => { + const flight = makeFlight({ aircraftTitle: "Airbus A320" }); + render( {}} />); + const number = screen.getByTestId("flight-carrier-number"); + // Aircraft name rendered inside the number/header column when expanded + expect(number.textContent).toContain("Airbus A320"); + }); + + it("4.1.13.4-R: no transition row shown when no transition data", () => { + const flight = makeFlight({}); + render( {}} />); + const expanded = screen.getByTestId("flight-card-expanded"); + // Neither boarding nor deboarding label should appear when transition is absent + expect(expanded.textContent).not.toContain("DETAILS.BOARDING"); + expect(expanded.textContent).not.toContain("DETAILS.DEBOARDING"); + }); +}); diff --git a/src/ui/flights/FlightCard.tsx b/src/ui/flights/FlightCard.tsx index c643a63f..f2b8dfcb 100644 --- a/src/ui/flights/FlightCard.tsx +++ b/src/ui/flights/FlightCard.tsx @@ -199,6 +199,12 @@ export const FlightCard: FC = ({ ? arrivalLeg.transition?.deboarding : departureLeg.transition?.boarding; const transitionLabelKey = isArrival ? "DETAILS.DEBOARDING" : "DETAILS.BOARDING"; + + // TZ §4.1.13.4.3: boarding block shows gate + dispatch (трап/автобус); + // deboarding block shows gate + baggage belt from arrival station. + const boardingGate = isArrival ? arrStation.gate : depStation.gate; + const boardingDispatch = isArrival ? arrStation.dispatch : depStation.dispatch; + const bagBelt = isArrival ? arrStation.bagBelt : undefined; const BOARDING_STATUS_KEY: Record = { Finished: "BOARDING-STATUSES.Finished", Expected: "BOARDING-STATUSES.Expected", @@ -420,6 +426,35 @@ export const FlightCard: FC = ({ )} + {/* TZ §4.1.13.4.3: gate number (выход на посадку) when available */} + {boardingGate && ( +
+ + {t("DETAILS.GATE")} + + {boardingGate} +
+ )} + {/* TZ §4.1.13.4.3: dispatch type (трап/автобус) when available */} + {boardingDispatch && ( +
+ + {t("DETAILS.DISPATCH")} + + + {t(`DISPATCH.${boardingDispatch}`)} + +
+ )} + {/* TZ §4.1.13.4.3: baggage belt for deboarding (arrival) when available */} + {bagBelt && ( +
+ + {t("DETAILS.BAG_BELT")} + + {bagBelt} +
+ )} )}