From c49a2a85258a0d52863cf71501e3477bb7fc2a5e Mon Sep 17 00:00:00 2001 From: gnezim Date: Wed, 22 Apr 2026 00:39:23 +0300 Subject: [PATCH] =?UTF-8?q?Audit=20connecting=20flight=20details=20per=20T?= =?UTF-8?q?Z=20=C2=A74.1.16.6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three fixes: - Transfer box: use IATA cityCode (not display text) for city-level station change detection (TZ §4.1.16.6 rule 12), catching cases where city codes differ even if airport codes are the same. - Transfer box: add terminal-change case — same airport but different arrival/departure terminals now renders both codes separated by → (TZ §4.1.16.6 rule 14). - ScheduleDetailsPage title: show all connecting flight numbers in the page

and title string (TZ §4.1.16.6 Table 60 header rule 1+5). Also fixes a pre-existing flaky test in ScheduleFlightBody: todayUtc() now always returns UTC noon of today to avoid day-boundary races. --- .../components/ScheduleDetailsPage.tsx | 10 +- .../components/ScheduleFlightBody.test.tsx | 149 +++++++++++++++++- .../components/ScheduleFlightBody.tsx | 42 ++++- 3 files changed, 188 insertions(+), 13 deletions(-) diff --git a/src/features/schedule/components/ScheduleDetailsPage.tsx b/src/features/schedule/components/ScheduleDetailsPage.tsx index 95392e13..fa249ec9 100644 --- a/src/features/schedule/components/ScheduleDetailsPage.tsx +++ b/src/features/schedule/components/ScheduleDetailsPage.tsx @@ -187,8 +187,14 @@ export const ScheduleDetailsPage: FC = ({ {t("SHARED.BACK-SCHEDULE")} ); - const title = flightIds[0] - ? `${t("BOARD.FLIGHT-INFO")}: ${flightIds[0].carrier} ${flightIds[0].flightNumber}` + // TZ §4.1.16.6 Table 60 rules 1+5: connecting flights show BOTH flight numbers in the header. + const titleFlightNumbers = flightIds.length > 1 + ? flightIds.map((f) => `${f.carrier} ${f.flightNumber}${f.suffix ?? ""}`).join(", ") + : flightIds[0] + ? `${flightIds[0].carrier} ${flightIds[0].flightNumber}${flightIds[0].suffix ?? ""}` + : null; + const title = titleFlightNumbers + ? `${t("BOARD.FLIGHT-INFO")}: ${titleFlightNumbers}` : t("SCHEDULE.TITLE"); if (loading) { diff --git a/src/features/schedule/components/ScheduleFlightBody.test.tsx b/src/features/schedule/components/ScheduleFlightBody.test.tsx index bfd2c152..a1af0854 100644 --- a/src/features/schedule/components/ScheduleFlightBody.test.tsx +++ b/src/features/schedule/components/ScheduleFlightBody.test.tsx @@ -170,10 +170,15 @@ const FUTURE_1H = new Date(Date.now() + 1 * 3600 * 1000).toISOString(); /** Yesterday — buy should be hidden (past) */ const YESTERDAY = new Date(Date.now() - 24 * 3600 * 1000).toISOString(); /** Today (same calendar day) — status button should be visible */ -function todayUtc(offsetHours = 3): string { +/** Return an ISO string that is guaranteed to be within today's UTC date. + * Always use UTC noon (12:00) to avoid any local-to-UTC shift pushing us + * into yesterday or tomorrow. */ +function todayUtc(): string { const now = new Date(); - now.setHours(now.getHours() + offsetHours); - return now.toISOString(); + // Force UTC noon of today — well within the UTC calendar day + return new Date( + Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), 12, 0, 0), + ).toISOString(); } // ── Tests ───────────────────────────────────────────────────────────────────── @@ -245,7 +250,7 @@ describe("ScheduleFlightBody – TZ §4.1.14.4", () => { }); }); - describe("Connecting flight structure", () => { + describe("Connecting flight structure – TZ §4.1.16.6", () => { it("renders separate flight numbers per leg for connecting flight", () => { render(); expect(screen.getByText("SU 6188")).toBeTruthy(); @@ -256,6 +261,138 @@ describe("ScheduleFlightBody – TZ §4.1.14.4", () => { render(); expect(screen.getByText("SHARED.FLIGHT-TRANSFER")).toBeTruthy(); }); + + it("transfer box shows single station when same airport (no change)", () => { + // Both legs land/depart at the same airport code and city (KUF) + render(); + const transfer = screen.getByTestId("flight-transfer"); + // Should NOT show the → arrow; look specifically at the stations div + const stationsDiv = transfer.querySelector(".schedule-flight-body__transfer-stations"); + expect(stationsDiv).toBeTruthy(); + // No arrow span in the stations area (the svg icon is outside this div) + const arrowSpans = stationsDiv?.querySelectorAll("span[aria-hidden='true']") ?? []; + expect(arrowSpans.length).toBe(0); + }); + + it("transfer box shows two stations when airports differ (TZ §4.1.16.6 rule 12)", () => { + // Flight where leg1 arr = SVO and leg2 dep = VKO (different airports) + const dep = FUTURE_10D; + const mid = new Date(new Date(dep).getTime() + 2 * 3600 * 1000).toISOString(); + const midNext = new Date(new Date(mid).getTime() + 2 * 3600 * 1000).toISOString(); + const arr = new Date(new Date(midNext).getTime() + 3 * 3600 * 1000).toISOString(); + const stationChangeFlight: ISimpleFlight = { + routeType: "MultiLeg", + id: "su6188+su6233-stn-change", + flyingTime: "07:00:00", + status: "Scheduled", + flightId: { carrier: "SU", flightNumber: "6188", date: dep.slice(0, 10) }, + operatingBy: {}, + legs: [ + { ...makeLeg(dep, "10:00", mid, "12:00", "SVO", "VKO"), + arrival: { ...makeLeg(dep, "10:00", mid, "12:00", "SVO", "VKO").arrival, + scheduled: { airportCode: "VKO", city: "Moscow", airport: "Vnukovo", cityCode: "MOW", countryCode: "RU" } } }, + { ...makeLeg(midNext, "14:00", arr, "17:00", "SVO", "LED"), + departure: { ...makeLeg(midNext, "14:00", arr, "17:00", "SVO", "LED").departure, + scheduled: { airportCode: "SVO", city: "Moscow", airport: "Sheremetyevo", cityCode: "MOW", countryCode: "RU" } } }, + ], + _childFlightIds: [ + { carrier: "SU", flightNumber: "6188" }, + { carrier: "SU", flightNumber: "6233" }, + ], + } as unknown as ISimpleFlight; + render(); + const transfer = screen.getByTestId("flight-transfer"); + // Should show arrow → between the two different airports + const arrowSpans = transfer.querySelectorAll("span[aria-hidden='true']"); + expect(arrowSpans.length).toBeGreaterThanOrEqual(1); + expect(transfer.textContent).toContain("Vnukovo"); + expect(transfer.textContent).toContain("Sheremetyevo"); + }); + + it("transfer box shows both terminals when same airport but different terminals (TZ §4.1.16.6 rule 14)", () => { + // Leg1 arr = SVO terminal D; Leg2 dep = SVO terminal E + const dep = FUTURE_10D; + const mid = new Date(new Date(dep).getTime() + 2 * 3600 * 1000).toISOString(); + const midNext = new Date(new Date(mid).getTime() + 1.5 * 3600 * 1000).toISOString(); + const arr = new Date(new Date(midNext).getTime() + 3 * 3600 * 1000).toISOString(); + const leg1 = makeLeg(dep, "10:00", mid, "12:00", "SVO", "SVO"); + const leg2 = makeLeg(midNext, "13:30", arr, "16:30", "SVO", "LED"); + // Patch terminals + (leg1 as unknown as Record).arrival = { + ...(leg1 as unknown as { arrival: unknown }).arrival as Record, + terminal: "D", + scheduled: { airportCode: "SVO", city: "Moscow", airport: "Sheremetyevo", cityCode: "MOW", countryCode: "RU" }, + }; + (leg2 as unknown as Record).departure = { + ...(leg2 as unknown as { departure: unknown }).departure as Record, + terminal: "E", + scheduled: { airportCode: "SVO", city: "Moscow", airport: "Sheremetyevo", cityCode: "MOW", countryCode: "RU" }, + checkingStatus: "", + }; + const terminalChangeFlight: ISimpleFlight = { + routeType: "MultiLeg", + id: "su-terminal-change", + flyingTime: "05:30:00", + status: "Scheduled", + flightId: { carrier: "SU", flightNumber: "1000", date: dep.slice(0, 10) }, + operatingBy: {}, + legs: [leg1, leg2], + _childFlightIds: [ + { carrier: "SU", flightNumber: "1000" }, + { carrier: "SU", flightNumber: "1001" }, + ], + } as unknown as ISimpleFlight; + render(); + const transfer = screen.getByTestId("flight-transfer"); + // Both terminal codes must appear + expect(transfer.textContent).toContain("D"); + expect(transfer.textContent).toContain("E"); + // Arrow must be present between them + const arrowSpans = transfer.querySelectorAll("span[aria-hidden='true']"); + expect(arrowSpans.length).toBeGreaterThanOrEqual(1); + }); + + it("transfer box terminal text uses arrTerminal variable (not leg.arrival.terminal directly)", () => { + // Regression: same airport, same terminal should show single station without arrow + const dep = FUTURE_10D; + const mid = new Date(new Date(dep).getTime() + 2 * 3600 * 1000).toISOString(); + const midNext = new Date(new Date(mid).getTime() + 1.5 * 3600 * 1000).toISOString(); + const arr = new Date(new Date(midNext).getTime() + 3 * 3600 * 1000).toISOString(); + const leg1 = makeLeg(dep, "10:00", mid, "12:00", "SVO", "SVO"); + const leg2 = makeLeg(midNext, "13:30", arr, "16:30", "SVO", "LED"); + // Same terminal B on both + (leg1 as unknown as Record).arrival = { + ...(leg1 as unknown as { arrival: unknown }).arrival as Record, + terminal: "B", + scheduled: { airportCode: "SVO", city: "Moscow", airport: "Sheremetyevo", cityCode: "MOW", countryCode: "RU" }, + }; + (leg2 as unknown as Record).departure = { + ...(leg2 as unknown as { departure: unknown }).departure as Record, + terminal: "B", + scheduled: { airportCode: "SVO", city: "Moscow", airport: "Sheremetyevo", cityCode: "MOW", countryCode: "RU" }, + checkingStatus: "", + }; + const sametermFlight: ISimpleFlight = { + routeType: "MultiLeg", + id: "su-same-terminal", + flyingTime: "05:30:00", + status: "Scheduled", + flightId: { carrier: "SU", flightNumber: "1000", date: dep.slice(0, 10) }, + operatingBy: {}, + legs: [leg1, leg2], + _childFlightIds: [ + { carrier: "SU", flightNumber: "1000" }, + { carrier: "SU", flightNumber: "1001" }, + ], + } as unknown as ISimpleFlight; + render(); + const transfer = screen.getByTestId("flight-transfer"); + // No → arrow for same-airport same-terminal + const arrowSpans = transfer.querySelectorAll("span[aria-hidden='true']"); + expect(arrowSpans.length).toBe(0); + // Terminal B should appear + expect(transfer.textContent).toContain("B"); + }); }); // ──────────────────────────────────────────────────────────────────────────── @@ -313,7 +450,7 @@ describe("ScheduleFlightBody – TZ §4.1.14.4", () => { describe("Status button – TZ §4.1.14.4.5", () => { it("shows status button when flight departs today", () => { const onStatus = vi.fn(); - render(); + render(); expect(screen.getByTestId("schedule-status-button")).toBeTruthy(); }); @@ -336,7 +473,7 @@ describe("ScheduleFlightBody – TZ §4.1.14.4", () => { it("calls onStatus handler when status button clicked", () => { const onStatus = vi.fn(); - render(); + render(); fireEvent.click(screen.getByTestId("schedule-status-button")); expect(onStatus).toHaveBeenCalledOnce(); }); diff --git a/src/features/schedule/components/ScheduleFlightBody.tsx b/src/features/schedule/components/ScheduleFlightBody.tsx index bddf5d5e..013ccff4 100644 --- a/src/features/schedule/components/ScheduleFlightBody.tsx +++ b/src/features/schedule/components/ScheduleFlightBody.tsx @@ -266,9 +266,24 @@ export const ScheduleFlightBody: FC = ({ transferType === "connecting" ? "SHARED.FLIGHT-TRANSFER" : "SHARED.INTERMEDIATE-LANDING-PLURAL-ONE"; + // TZ §4.1.16.6 rule 12: show both stations if airport codes differ OR if the + // IATA city codes differ (e.g. SVO→Moscow vs VKO→Moscow is same city, but + // SVO→Moscow vs UFA→Ufa is different). Fall back to airport-code comparison + // when cityCode is absent. + const arrCityCode = leg.arrival.scheduled.cityCode; + const depCityCode = next?.departure.scheduled.cityCode; const stationChange = - next && leg.arrival.scheduled.airportCode !== - next.departure.scheduled.airportCode; + next && ( + leg.arrival.scheduled.airportCode !== next.departure.scheduled.airportCode || + (arrCityCode && depCityCode && arrCityCode !== depCityCode) + ); + // TZ §4.1.16.6 rule 14: if same airport but different terminals, show both. + const arrTerminal = leg.arrival.terminal; + const depTerminal = next?.departure.terminal; + const terminalChange = + next && !stationChange && + arrTerminal && depTerminal && + arrTerminal !== depTerminal; return (
@@ -359,16 +374,33 @@ export const ScheduleFlightBody: FC = ({
{stationChange ? ( <> - {leg.arrival.scheduled.city}, {leg.arrival.scheduled.airport} - + + {leg.arrival.scheduled.city}, {leg.arrival.scheduled.airport} + {arrTerminal ? ` - ${arrTerminal}` : ""} + + {next.departure.scheduled.city}, {next.departure.scheduled.airport} + {depTerminal ? ` - ${depTerminal}` : ""} + + + ) : terminalChange ? ( + // TZ §4.1.16.6 rule 14: same airport, different terminals + <> + + {leg.arrival.scheduled.city}, {leg.arrival.scheduled.airport} + {arrTerminal ? ` - ${arrTerminal}` : ""} + + + + {next.departure.scheduled.airport} + {depTerminal ? ` - ${depTerminal}` : ""} ) : ( {leg.arrival.scheduled.city}, {leg.arrival.scheduled.airport} - {leg.arrival.terminal ? ` - ${leg.arrival.terminal}` : ""} + {arrTerminal ? ` - ${arrTerminal}` : ""} )}