Show all active transition blocks inline + gate on isActual (TIRREDESIGN-7)
The inline expanded flight card used to pick one of boarding / deboarding based on the search direction and show just that block. Angular's board-flight-body renders registration, boarding and deboarding side-by-side, each gated on the API payload's isActual flag — TIRREDESIGN-7 expects the same so a flight mid-boarding can still show that its registration already finished. - FlightCard now iterates registration/boarding/deboarding and renders each row when its isActual flag is set. Gate + dispatch still come from depStation on boarding, gate + bag-belt from arrStation on deboarding. - shared.shouldShowTransition swaps 'status != Scheduled' for the isActual flag to match Angular's same-payload semantics on the details accordion. The Schedule/Cancelled short-circuits stand. - Test fixture makeFlightWithBoarding scopes its transition to the direction under test so the two blocks don't collide on testids.
This commit is contained in:
@@ -27,12 +27,16 @@ describe("shouldShowTransition", () => {
|
||||
expect(shouldShowTransition(undefined, "Scheduled", "Onlineboard")).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when item.status is Scheduled", () => {
|
||||
const scheduled = { ...validItem, status: "Scheduled" as const };
|
||||
expect(shouldShowTransition(scheduled, "Scheduled", "Onlineboard")).toBe(false);
|
||||
// TIRREDESIGN-7: the rule now reads the API's isActual flag rather
|
||||
// than inferring from status, so a transition marked inactive hides
|
||||
// even if its status has left Scheduled (e.g. a cancelled boarding
|
||||
// phase that hasn't been cleared to Scheduled).
|
||||
it("returns false when item.isActual is false", () => {
|
||||
const inactive = { ...validItem, isActual: false };
|
||||
expect(shouldShowTransition(inactive, "Scheduled", "Onlineboard")).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true for active transition on Onlineboard", () => {
|
||||
it("returns true when item.isActual is true on Onlineboard", () => {
|
||||
expect(shouldShowTransition(validItem, "Scheduled", "Onlineboard")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,9 +8,14 @@ import type {
|
||||
export type DetailsViewType = "Onlineboard" | "Schedule";
|
||||
|
||||
/**
|
||||
* Matches Angular's `showBoardProperty` in flight-details-wrapper.component.ts.
|
||||
* Transition panels are hidden for Schedule mode, Cancelled flights,
|
||||
* missing data, or when the transition hasn't started.
|
||||
* Matches Angular's `showBoardProperty` in flight-details-wrapper.component.ts,
|
||||
* tightened for TIRREDESIGN-7 to use the payload's `isActual` flag. The
|
||||
* API sets `isActual=true` precisely when a transition block is in its
|
||||
* current operational phase — the backend computes that from status +
|
||||
* clocked times, so consumers shouldn't rediscover it locally.
|
||||
*
|
||||
* Transition panels remain hidden for Schedule mode or Cancelled legs
|
||||
* regardless of the flag (those contexts never show transition detail).
|
||||
*/
|
||||
export function shouldShowTransition(
|
||||
item: IFlightTransitionItem | undefined,
|
||||
@@ -20,8 +25,7 @@ export function shouldShowTransition(
|
||||
if (viewType === "Schedule") return false;
|
||||
if (legStatus === "Cancelled") return false;
|
||||
if (!item) return false;
|
||||
if (item.status === "Scheduled") return false;
|
||||
return true;
|
||||
return item.isActual === true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -504,28 +504,36 @@ function makeFlightWithBoarding(opts: {
|
||||
gate?: string;
|
||||
dispatch?: string;
|
||||
bagBelt?: string;
|
||||
/**
|
||||
* Which transition phase to simulate as isActual. Both blocks now
|
||||
* render side-by-side when both are active (TIRREDESIGN-7), so tests
|
||||
* that assert on a single `transition-gate` testid need to scope to
|
||||
* one direction at a time.
|
||||
*/
|
||||
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;
|
||||
const transitionMap =
|
||||
opts.direction === "arrival"
|
||||
? { deboarding: transition }
|
||||
: { boarding: transition };
|
||||
const depGate = opts.direction === "arrival" ? undefined : opts.gate;
|
||||
const arrGate = opts.direction === "arrival" ? opts.gate : undefined;
|
||||
return {
|
||||
...base,
|
||||
leg: {
|
||||
...baseLeg,
|
||||
transition: {
|
||||
boarding: transition,
|
||||
deboarding: transition,
|
||||
},
|
||||
transition: transitionMap,
|
||||
departure: {
|
||||
...baseLeg.departure,
|
||||
gate: opts.gate,
|
||||
gate: depGate,
|
||||
dispatch: opts.dispatch,
|
||||
},
|
||||
arrival: {
|
||||
...baseLeg.arrival,
|
||||
gate: opts.gate,
|
||||
gate: arrGate,
|
||||
bagBelt: opts.bagBelt,
|
||||
},
|
||||
},
|
||||
@@ -572,8 +580,8 @@ describe("4.1.13.4 — Expanded row content (TZ §4.1.13.4.3)", () => {
|
||||
expect(expanded.textContent).toContain("BOARDING-STATUSES.InProgress");
|
||||
});
|
||||
|
||||
it("4.1.13.4-R: deboarding row shown on arrival direction", () => {
|
||||
const flight = makeFlightWithBoarding({});
|
||||
it("4.1.13.4-R: deboarding row shown when deboarding transition is active (§4.1.13.4.3)", () => {
|
||||
const flight = makeFlightWithBoarding({ direction: "arrival" });
|
||||
render(<FlightCard flight={flight} expandable initialExpanded onViewDetails={() => {}} direction="arrival" />);
|
||||
const expanded = screen.getByTestId("flight-card-expanded");
|
||||
expect(expanded.textContent).toContain("DETAILS.DEBOARDING");
|
||||
@@ -620,7 +628,7 @@ describe("4.1.13.4 — Expanded row content (TZ §4.1.13.4.3)", () => {
|
||||
});
|
||||
|
||||
it("4.1.13.4-R: arrival gate rendered in deboarding row when arrival.gate is set", () => {
|
||||
const flight = makeFlightWithBoarding({ gate: "C7" });
|
||||
const flight = makeFlightWithBoarding({ gate: "C7", direction: "arrival" });
|
||||
render(<FlightCard flight={flight} expandable initialExpanded onViewDetails={() => {}} direction="arrival" />);
|
||||
expect(screen.getByTestId("transition-gate")).toBeTruthy();
|
||||
expect(screen.getByTestId("transition-gate").textContent).toContain("C7");
|
||||
|
||||
@@ -214,19 +214,21 @@ export const FlightCard: FC<FlightCardProps> = ({
|
||||
? "SHARED.ACTUAL"
|
||||
: "SHARED.EXPECTED";
|
||||
|
||||
// Arrival pages show the deboarding (Высадка) transition; departure /
|
||||
// route / flight-number views show boarding (Посадка). Matches Angular.
|
||||
const isArrival = direction === "arrival";
|
||||
const transition = isArrival
|
||||
? arrivalLeg.transition?.deboarding
|
||||
: departureLeg.transition?.boarding;
|
||||
const transitionLabelKey = isArrival ? "DETAILS.DEBOARDING" : "DETAILS.BOARDING";
|
||||
// TIRREDESIGN-7 / TZ §4.1.13.4.1-3: the inline expanded panel renders
|
||||
// registration (check-in), boarding and deboarding rows side-by-side,
|
||||
// each visible independently when its `isActual` flag is true. Matches
|
||||
// Angular's `board-flight-body` template which ngIfs on
|
||||
// `transition?.X?.isActual` for all three blocks. `direction` no
|
||||
// longer gates which single block wins — gate/dispatch/bag-belt
|
||||
// still come from the appropriate end of the leg though.
|
||||
const registration = departureLeg.transition?.registration;
|
||||
const boarding = departureLeg.transition?.boarding;
|
||||
const deboarding = arrivalLeg.transition?.deboarding;
|
||||
const showRegistration = Boolean(registration?.isActual);
|
||||
const showBoarding = Boolean(boarding?.isActual);
|
||||
const showDeboarding = Boolean(deboarding?.isActual);
|
||||
const anyTransition = showRegistration || showBoarding || showDeboarding;
|
||||
|
||||
// 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",
|
||||
@@ -235,6 +237,79 @@ export const FlightCard: FC<FlightCardProps> = ({
|
||||
Scheduled: "BOARDING-STATUSES.Scheduled",
|
||||
};
|
||||
|
||||
const renderTransitionRow = (
|
||||
key: "registration" | "boarding" | "deboarding",
|
||||
item: NonNullable<typeof boarding>,
|
||||
): JSX.Element => {
|
||||
const labelKey =
|
||||
key === "registration"
|
||||
? "DETAILS.REGISTRATION"
|
||||
: key === "boarding"
|
||||
? "DETAILS.BOARDING"
|
||||
: "DETAILS.DEBOARDING";
|
||||
// "Время начала" / "Время окончания" apply to all three rows —
|
||||
// Angular reuses the same SHARED.BOARDING-START/END strings for
|
||||
// registration rows too.
|
||||
const startKey = "SHARED.BOARDING-START";
|
||||
const endKey = "SHARED.BOARDING-END";
|
||||
// Gate + dispatch surface on boarding (departure side); gate + bag
|
||||
// belt surface on deboarding (arrival side). Registration has no
|
||||
// gate-like affordance in the API payload.
|
||||
const gate = key === "boarding" ? depStation.gate : key === "deboarding" ? arrStation.gate : undefined;
|
||||
const dispatch = key === "boarding" ? depStation.dispatch : undefined;
|
||||
const bagBelt = key === "deboarding" ? arrStation.bagBelt : undefined;
|
||||
return (
|
||||
<div className="flight-card__detail-row" key={key} data-transition={key}>
|
||||
<div className="flight-card__detail-label">{t(labelKey)}</div>
|
||||
<div className="flight-card__detail-group">
|
||||
<div>
|
||||
<span className="flight-card__detail-caption">{t("DETAILS.STATUS")}</span>
|
||||
<span
|
||||
className={`flight-card__detail-value flight-card__detail-status flight-card__detail-status--${item.status.toLowerCase()}`}
|
||||
>
|
||||
<span className="flight-card__status-dot" aria-hidden="true" />
|
||||
{t(BOARDING_STATUS_KEY[item.status] ?? item.status)}
|
||||
</span>
|
||||
</div>
|
||||
{item.start?.local && (
|
||||
<div>
|
||||
<span className="flight-card__detail-caption">{t(startKey)}</span>
|
||||
<span className="flight-card__detail-value">
|
||||
{formatLocalTime(item.start.local)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{item.end?.local && (
|
||||
<div>
|
||||
<span className="flight-card__detail-caption">{t(endKey)}</span>
|
||||
<span className="flight-card__detail-value">
|
||||
{formatLocalTime(item.end.local)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{gate && (
|
||||
<div data-testid="transition-gate">
|
||||
<span className="flight-card__detail-caption">{t("DETAILS.GATE")}</span>
|
||||
<span className="flight-card__detail-value">{gate}</span>
|
||||
</div>
|
||||
)}
|
||||
{dispatch && (
|
||||
<div data-testid="transition-dispatch">
|
||||
<span className="flight-card__detail-caption">{t("DETAILS.DISPATCH")}</span>
|
||||
<span className="flight-card__detail-value">{t(`DISPATCH.${dispatch}`)}</span>
|
||||
</div>
|
||||
)}
|
||||
{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>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flight-card${rowClickable ? " flight-card--clickable" : ""}${expanded ? " flight-card--expanded" : ""}`}
|
||||
@@ -485,74 +560,12 @@ export const FlightCard: FC<FlightCardProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{transition && (
|
||||
<div className="flight-card__detail-row">
|
||||
<div className="flight-card__detail-label">
|
||||
{t(transitionLabelKey)}
|
||||
</div>
|
||||
<div className="flight-card__detail-group">
|
||||
<div>
|
||||
<span className="flight-card__detail-caption">
|
||||
{t("DETAILS.STATUS")}
|
||||
</span>
|
||||
<span
|
||||
className={`flight-card__detail-value flight-card__detail-status flight-card__detail-status--${transition.status.toLowerCase()}`}
|
||||
>
|
||||
<span className="flight-card__status-dot" aria-hidden="true" />
|
||||
{t(BOARDING_STATUS_KEY[transition.status] ?? transition.status)}
|
||||
</span>
|
||||
</div>
|
||||
{transition.start?.local && (
|
||||
<div>
|
||||
<span className="flight-card__detail-caption">
|
||||
{t("SHARED.BOARDING-START")}
|
||||
</span>
|
||||
<span className="flight-card__detail-value">
|
||||
{formatLocalTime(transition.start.local)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{transition.end?.local && (
|
||||
<div>
|
||||
<span className="flight-card__detail-caption">
|
||||
{t("SHARED.BOARDING-END")}
|
||||
</span>
|
||||
<span className="flight-card__detail-value">
|
||||
{formatLocalTime(transition.end.local)}
|
||||
</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>
|
||||
{anyTransition && (
|
||||
<>
|
||||
{showRegistration && registration && renderTransitionRow("registration", registration)}
|
||||
{showBoarding && boarding && renderTransitionRow("boarding", boarding)}
|
||||
{showDeboarding && deboarding && renderTransitionRow("deboarding", deboarding)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{onViewDetails && (
|
||||
|
||||
Reference in New Issue
Block a user