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} +
+ )} )}