Audit Online-Board expanded row per TZ 4.1.13.4

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.
This commit is contained in:
2026-04-21 23:02:55 +03:00
parent 3b5ae9af85
commit 9f6623786f
4 changed files with 221 additions and 2 deletions
+2 -1
View File
@@ -80,7 +80,8 @@
"GATE": "Gate",
"BAG_BELT": "Baggage belt",
"CONFIGURATION": "Configuration",
"STATUS": "Status"
"STATUS": "Status",
"DISPATCH": "Transfer"
},
"BOARDING-STATUSES": {
"Expected": "Expected",
+2 -1
View File
@@ -80,7 +80,8 @@
"GATE": "Выход",
"BAG_BELT": "Лента выдачи багажа",
"CONFIGURATION": "Компоновка",
"STATUS": "Статус"
"STATUS": "Статус",
"DISPATCH": "Трансфер"
},
"BOARDING-STATUSES": {
"Expected": "Ожидается",
+182
View File
@@ -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<typeof makeFlight> & { leg: ReturnType<typeof makeLeg> }).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(<FlightCard flight={flight} expandable initialExpanded onViewDetails={() => {}} />);
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(<FlightCard flight={flight} expandable initialExpanded onViewDetails={() => {}} />);
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(<FlightCard flight={flight} expandable initialExpanded onViewDetails={() => {}} />);
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(<FlightCard flight={flight} expandable initialExpanded onViewDetails={() => {}} 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(<FlightCard flight={flight} expandable initialExpanded onViewDetails={() => {}} 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(<FlightCard flight={flight} expandable initialExpanded onViewDetails={() => {}} 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(<FlightCard flight={flight} expandable initialExpanded onViewDetails={() => {}} 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(<FlightCard flight={flight} expandable initialExpanded onViewDetails={() => {}} 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(<FlightCard flight={flight} expandable initialExpanded onViewDetails={() => {}} 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(<FlightCard flight={flight} expandable initialExpanded onViewDetails={() => {}} 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(<FlightCard flight={flight} expandable initialExpanded onViewDetails={() => {}} 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(<FlightCard flight={flight} expandable initialExpanded onViewDetails={() => {}} 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(<FlightCard flight={makeFlight({})} expandable initialExpanded onViewDetails={() => {}} />);
expect(screen.getByTestId("flight-share-button")).toBeTruthy();
});
it("4.1.13.4-R: Details button present in expanded actions row", () => {
render(<FlightCard flight={makeFlight({})} expandable initialExpanded onViewDetails={() => {}} />);
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(<FlightCard flight={flight} expandable initialExpanded onViewDetails={() => {}} />);
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(<FlightCard flight={flight} expandable initialExpanded onViewDetails={() => {}} />);
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");
});
});
+35
View File
@@ -199,6 +199,12 @@ export const FlightCard: FC<FlightCardProps> = ({
? 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<string, string> = {
Finished: "BOARDING-STATUSES.Finished",
Expected: "BOARDING-STATUSES.Expected",
@@ -420,6 +426,35 @@ export const FlightCard: FC<FlightCardProps> = ({
</span>
</div>
)}
{/* TZ §4.1.13.4.3: gate number (выход на посадку) when available */}
{boardingGate && (
<div data-testid="transition-gate">
<span className="flight-card__detail-caption">
{t("DETAILS.GATE")}
</span>
<span className="flight-card__detail-value">{boardingGate}</span>
</div>
)}
{/* TZ §4.1.13.4.3: dispatch type (трап/автобус) when available */}
{boardingDispatch && (
<div data-testid="transition-dispatch">
<span className="flight-card__detail-caption">
{t("DETAILS.DISPATCH")}
</span>
<span className="flight-card__detail-value">
{t(`DISPATCH.${boardingDispatch}`)}
</span>
</div>
)}
{/* TZ §4.1.13.4.3: baggage belt for deboarding (arrival) when available */}
{bagBelt && (
<div data-testid="transition-bag-belt">
<span className="flight-card__detail-caption">
{t("DETAILS.BAG_BELT")}
</span>
<span className="flight-card__detail-value">{bagBelt}</span>
</div>
)}
</div>
</div>
)}