Audit intermediate landing + transfer per TZ 4.1.15.6 + 4.1.16.7
Three concrete gaps fixed:
1. TransferBar (Online-Board §4.1.15.6): duration now uses actual/estimated
UTC times when viewType=Onlineboard instead of always scheduled UTC.
Adds isIntermediateLanding prop (default true) so the label can switch
between "Промежуточная посадка" and "Пересадка" based on flight-number
identity rather than being hardcoded. StationChange now always rendered
(not only when separated) so city/airport/terminal are always shown.
2. ScheduleFlightBody (§4.1.16.7): transferDuration previously computed
ground time from .local strings ("HH:MM:SS" without timezone), making
new Date() result timezone-dependent and often NaN. Switched to .utc
(ISO 8601 with Z suffix) for a correct, deterministic diff.
Tests: 53 pass (8 TransferBar + 32+3 ScheduleFlightBody + 10 computeTransferTime).
New test cases: isIntermediateLanding=false label, StationChange always-on,
--separated class, UTC-based 90-min duration, label distinction per TZ.
This commit is contained in:
@@ -106,6 +106,13 @@ describe("TransferBar", () => {
|
||||
expect(screen.getByText("11:30")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders SHARED.FLIGHT-TRANSFER-PLURAL-ONE label when isIntermediateLanding=false", () => {
|
||||
const leg = makeLeg({ arrLocal: "10:00", arrUtc: "2026-04-17T10:00:00Z" });
|
||||
const nextLeg = makeLeg({ depLocal: "11:30", depUtc: "2026-04-17T11:30:00Z" });
|
||||
render(<TransferBar leg={leg} nextLeg={nextLeg} viewType="Schedule" isIntermediateLanding={false} />);
|
||||
expect(screen.getByText("SHARED.FLIGHT-TRANSFER-PLURAL-ONE")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders TransferTime component", () => {
|
||||
const leg = makeLeg({ arrUtc: "2026-04-17T10:00:00Z" });
|
||||
const nextLeg = makeLeg({ depUtc: "2026-04-17T11:30:00Z" });
|
||||
@@ -113,6 +120,13 @@ describe("TransferBar", () => {
|
||||
expect(screen.getByTestId("transfer-time")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("always renders StationChange (station info shown even for same station)", () => {
|
||||
const leg = makeLeg({ arrCity: "Moscow", arrCityCode: "MOW", arrAirportCode: "SVO" });
|
||||
const nextLeg = makeLeg({ depCity: "Moscow", depCityCode: "MOW", depAirportCode: "SVO" });
|
||||
render(<TransferBar leg={leg} nextLeg={nextLeg} viewType="Schedule" />);
|
||||
expect(screen.getByTestId("station-change")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders StationChange when arrival/departure cities differ", () => {
|
||||
const leg = makeLeg({ arrCity: "Moscow", arrCityCode: "MOW" });
|
||||
const nextLeg = makeLeg({ depCity: "Saint Petersburg", depCityCode: "LED", depAirportCode: "LED" });
|
||||
@@ -127,4 +141,12 @@ describe("TransferBar", () => {
|
||||
const bar = container.querySelector(".transfer-bar");
|
||||
expect(bar?.className).not.toContain("transfer-bar--separated");
|
||||
});
|
||||
|
||||
it("adds --separated class when arrival/departure airports differ", () => {
|
||||
const leg = makeLeg({ arrCity: "Moscow", arrCityCode: "MOW", arrAirportCode: "SVO" });
|
||||
const nextLeg = makeLeg({ depCity: "Saint Petersburg", depCityCode: "LED", depAirportCode: "LED" });
|
||||
const { container } = render(<TransferBar leg={leg} nextLeg={nextLeg} viewType="Schedule" />);
|
||||
const bar = container.querySelector(".transfer-bar");
|
||||
expect(bar?.className).toContain("transfer-bar--separated");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,25 +10,59 @@ export interface TransferBarProps {
|
||||
leg: IFlightLeg;
|
||||
nextLeg: IFlightLeg;
|
||||
viewType: "Onlineboard" | "Schedule";
|
||||
/**
|
||||
* When true (default) the bar is labelled "Промежуточная посадка" (same
|
||||
* flight number, multi-segment). When false it is labelled "Пересадка"
|
||||
* (different flight numbers, connecting). TZ §4.1.15.6 / §4.1.16.7.
|
||||
*/
|
||||
isIntermediateLanding?: boolean;
|
||||
}
|
||||
|
||||
function arrivalLocal(leg: IFlightLeg, viewType: "Onlineboard" | "Schedule"): string {
|
||||
const t = leg.arrival.times;
|
||||
if (viewType === "Schedule") return t.scheduledArrival.local;
|
||||
return t.actualBlockOn?.local ?? t.scheduledArrival.local;
|
||||
if (viewType === "Schedule") return t.scheduledArrival.localTime ?? t.scheduledArrival.local;
|
||||
return (t.actualBlockOn ?? t.estimatedBlockOn ?? t.scheduledArrival).localTime
|
||||
?? (t.actualBlockOn ?? t.estimatedBlockOn ?? t.scheduledArrival).local;
|
||||
}
|
||||
|
||||
function departureLocal(leg: IFlightLeg, viewType: "Onlineboard" | "Schedule"): string {
|
||||
const t = leg.departure.times;
|
||||
if (viewType === "Schedule") return t.scheduledDeparture.local;
|
||||
return t.actualBlockOff?.local ?? t.scheduledDeparture.local;
|
||||
if (viewType === "Schedule") return t.scheduledDeparture.localTime ?? t.scheduledDeparture.local;
|
||||
return (t.actualBlockOff ?? t.estimatedBlockOff ?? t.scheduledDeparture).localTime
|
||||
?? (t.actualBlockOff ?? t.estimatedBlockOff ?? t.scheduledDeparture).local;
|
||||
}
|
||||
|
||||
export const TransferBar: FC<TransferBarProps> = ({ leg, nextLeg, viewType }) => {
|
||||
/**
|
||||
* Pick the most-current UTC timestamp for transfer duration calculation.
|
||||
* TZ §4.1.15.6: "одно из актуальных на момент отображения времен расписанию,
|
||||
* ожидаемому, фактическому по каждой позиции вылет/прилет."
|
||||
*/
|
||||
function arrivalUtc(leg: IFlightLeg, viewType: "Onlineboard" | "Schedule"): string {
|
||||
const t = leg.arrival.times;
|
||||
if (viewType === "Schedule") return t.scheduledArrival.utc;
|
||||
return (t.actualBlockOn ?? t.estimatedBlockOn ?? t.scheduledArrival).utc;
|
||||
}
|
||||
|
||||
function departureUtc(leg: IFlightLeg, viewType: "Onlineboard" | "Schedule"): string {
|
||||
const t = leg.departure.times;
|
||||
if (viewType === "Schedule") return t.scheduledDeparture.utc;
|
||||
return (t.actualBlockOff ?? t.estimatedBlockOff ?? t.scheduledDeparture).utc;
|
||||
}
|
||||
|
||||
export const TransferBar: FC<TransferBarProps> = ({
|
||||
leg,
|
||||
nextLeg,
|
||||
viewType,
|
||||
isIntermediateLanding = true,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const stationChange = detectStationChange(leg.arrival, nextLeg.departure);
|
||||
const separated = stationChange !== "noChange";
|
||||
|
||||
const labelKey = isIntermediateLanding
|
||||
? "SHARED.INTERMEDIATE-LANDING-PLURAL-ONE"
|
||||
: "SHARED.FLIGHT-TRANSFER-PLURAL-ONE";
|
||||
|
||||
const className = `transfer-bar${separated ? " transfer-bar--separated" : ""}`;
|
||||
|
||||
return (
|
||||
@@ -39,11 +73,11 @@ export const TransferBar: FC<TransferBarProps> = ({ leg, nextLeg, viewType }) =>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="transfer-bar__content">
|
||||
<span className="transfer-bar__type">{t("SHARED.INTERMEDIATE-LANDING-PLURAL-ONE")}</span>
|
||||
<span className="transfer-bar__type">{t(labelKey)}</span>
|
||||
<div className="transfer-bar__duration">
|
||||
<TransferTime
|
||||
arrivalUtc={leg.arrival.times.scheduledArrival.utc}
|
||||
departureUtc={nextLeg.departure.times.scheduledDeparture.utc}
|
||||
arrivalUtc={arrivalUtc(leg, viewType)}
|
||||
departureUtc={departureUtc(nextLeg, viewType)}
|
||||
/>
|
||||
</div>
|
||||
<div className="transfer-bar__times">
|
||||
@@ -51,11 +85,9 @@ export const TransferBar: FC<TransferBarProps> = ({ leg, nextLeg, viewType }) =>
|
||||
<span> — </span>
|
||||
<span>{departureLocal(nextLeg, viewType)}</span>
|
||||
</div>
|
||||
{separated && (
|
||||
<div className="transfer-bar__station-change">
|
||||
<StationChange from={leg.arrival} to={nextLeg.departure} />
|
||||
</div>
|
||||
)}
|
||||
<div className="transfer-bar__station-change">
|
||||
<StationChange from={leg.arrival} to={nextLeg.departure} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -478,4 +478,44 @@ describe("ScheduleFlightBody – TZ §4.1.14.4", () => {
|
||||
expect(onStatus).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// TZ §4.1.16.7 — Intermediate landing vs transfer duration (UTC fix)
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe("Transfer duration – TZ §4.1.16.7 (UTC-based, not local)", () => {
|
||||
it("computes ground time from UTC (90 min = 1ч. 30мин.)", () => {
|
||||
// leg1 arr UTC: 10:00Z, leg2 dep UTC: 11:30Z → 90 min
|
||||
const depUtc = "2026-08-10T08:00:00Z";
|
||||
const midUtc = "2026-08-10T10:00:00Z"; // arr leg1
|
||||
const midNextUtc = "2026-08-10T11:30:00Z"; // dep leg2
|
||||
const arrUtc = "2026-08-10T14:30:00Z";
|
||||
const flight: ISimpleFlight = {
|
||||
routeType: "MultiLeg",
|
||||
id: "duration-test",
|
||||
flyingTime: "06:30:00",
|
||||
status: "Scheduled",
|
||||
flightId: { carrier: "SU", flightNumber: "9999", date: "2026-08-10" },
|
||||
operatingBy: {},
|
||||
legs: [
|
||||
makeLeg(depUtc, "11:00", midUtc, "13:00", "SVO", "KJA"),
|
||||
makeLeg(midNextUtc, "14:30", arrUtc, "17:30", "KJA", "LED"),
|
||||
],
|
||||
} as unknown as ISimpleFlight;
|
||||
render(<ScheduleFlightBody flight={flight} />);
|
||||
// 90 min = "1ч. 30мин." (ru locale)
|
||||
const transfer = screen.getByTestId("flight-transfer");
|
||||
expect(transfer.textContent).toContain("1ч. 30мин.");
|
||||
});
|
||||
|
||||
it("label is Промежуточная посадка for same flight-number multi-leg (§4.1.16.7)", () => {
|
||||
render(<ScheduleFlightBody flight={makeMultiLegFlight(FUTURE_10D)} />);
|
||||
expect(screen.getByText("SHARED.INTERMEDIATE-LANDING-PLURAL-ONE")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("label is Пересадка for connecting (different flight numbers) (§4.1.16.7)", () => {
|
||||
render(<ScheduleFlightBody flight={makeConnectingFlight(FUTURE_10D)} />);
|
||||
expect(screen.getByText("SHARED.FLIGHT-TRANSFER")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -67,10 +67,14 @@ function formatDuration(value: string | undefined, language: string): string {
|
||||
return isRu ? `${h}ч. ${m}мин.` : `${h}h ${m}m`;
|
||||
}
|
||||
|
||||
/** Compute ground-transfer minutes between two consecutive legs. */
|
||||
/**
|
||||
* Compute ground-transfer duration string between two consecutive legs.
|
||||
* Uses UTC timestamps (not local, which lack timezone) for correct diff.
|
||||
* TZ §4.1.16.7: duration = departure UTC − arrival UTC.
|
||||
*/
|
||||
function transferDuration(prev: IFlightLeg, next: IFlightLeg): string {
|
||||
const prevArr = prev.arrival.times.scheduledArrival.local;
|
||||
const nextDep = next.departure.times.scheduledDeparture.local;
|
||||
const prevArr = prev.arrival.times.scheduledArrival.utc;
|
||||
const nextDep = next.departure.times.scheduledDeparture.utc;
|
||||
if (!prevArr || !nextDep) return "";
|
||||
const a = new Date(prevArr).getTime();
|
||||
const b = new Date(nextDep).getTime();
|
||||
|
||||
Reference in New Issue
Block a user