diff --git a/src/features/online-board/components/BoardDetailsHeader/DetailsHeaderBadge.test.tsx b/src/features/online-board/components/BoardDetailsHeader/DetailsHeaderBadge.test.tsx index 956de2ad..b6a318a3 100644 --- a/src/features/online-board/components/BoardDetailsHeader/DetailsHeaderBadge.test.tsx +++ b/src/features/online-board/components/BoardDetailsHeader/DetailsHeaderBadge.test.tsx @@ -88,6 +88,13 @@ describe("DetailsHeaderBadge", () => { expect(screen.getByText("SU 0022")).toBeTruthy(); }); + it("TIRREDESIGN-13: renders suffix in primary flight number", () => { + const flight = makeDirect(); + flight.flightId = { ...flight.flightId, flightNumber: "0038", suffix: "D" }; + render(); + expect(screen.getByText("SU 0038D")).toBeTruthy(); + }); + it("renders operator-logo", () => { render(); expect(screen.getByTestId("operator-logo")).toBeTruthy(); diff --git a/src/features/online-board/components/BoardDetailsHeader/DetailsHeaderBadge.tsx b/src/features/online-board/components/BoardDetailsHeader/DetailsHeaderBadge.tsx index 699ee772..d13dcde9 100644 --- a/src/features/online-board/components/BoardDetailsHeader/DetailsHeaderBadge.tsx +++ b/src/features/online-board/components/BoardDetailsHeader/DetailsHeaderBadge.tsx @@ -39,7 +39,7 @@ export const DetailsHeaderBadge: FC = ({ }) => { const { t } = useTranslation(); const codeshareLegs = getCodeshareLegs(flight); - const primaryNumber = `${flight.flightId.carrier} ${flight.flightId.flightNumber}`; + const primaryNumber = `${flight.flightId.carrier} ${flight.flightId.flightNumber}${flight.flightId.suffix ?? ""}`; const carrier = operatingCarrier(flight.operatingBy) ?? flight.flightId.carrier; return ( diff --git a/src/features/online-board/components/FlightsMiniList/FlightsMiniListItem.test.tsx b/src/features/online-board/components/FlightsMiniList/FlightsMiniListItem.test.tsx index 04e0d163..cdc5aebf 100644 --- a/src/features/online-board/components/FlightsMiniList/FlightsMiniListItem.test.tsx +++ b/src/features/online-board/components/FlightsMiniList/FlightsMiniListItem.test.tsx @@ -60,6 +60,15 @@ describe("FlightsMiniListItem", () => { expect(screen.getByText(/SU\s*0022/)).toBeTruthy(); }); + it("TIRREDESIGN-13: renders suffix in flight number", () => { + const flight = makeDirectFlight({ + id: "SU0038D-20260514", + flightId: { carrier: "SU", flightNumber: "0038", suffix: "D", date: "20260514" }, + }); + render(); + expect(screen.getByText(/SU\s*0038D/)).toBeTruthy(); + }); + it("renders departure and arrival times", () => { const flight = makeDirectFlight(); render(); diff --git a/src/features/online-board/components/FlightsMiniList/FlightsMiniListItem.tsx b/src/features/online-board/components/FlightsMiniList/FlightsMiniListItem.tsx index f8771fb3..9739fa81 100644 --- a/src/features/online-board/components/FlightsMiniList/FlightsMiniListItem.tsx +++ b/src/features/online-board/components/FlightsMiniList/FlightsMiniListItem.tsx @@ -127,14 +127,14 @@ export const FlightsMiniListItem = forwardRef {(() => { const childIds = (flight as typeof flight & { - _childFlightIds?: { carrier: string; flightNumber: string }[]; + _childFlightIds?: { carrier: string; flightNumber: string; suffix?: string }[]; })._childFlightIds; if (childIds && childIds.length > 1) { return childIds - .map((c) => `${c.carrier} ${c.flightNumber}`) + .map((c) => `${c.carrier} ${c.flightNumber}${c.suffix ?? ""}`) .join(", "); } - return `${flight.flightId.carrier} ${flight.flightId.flightNumber}`; + return `${flight.flightId.carrier} ${flight.flightId.flightNumber}${flight.flightId.suffix ?? ""}`; })()} { expect(screen.getAllByText("SU 100").length).toBeGreaterThanOrEqual(1); }); + it("TIRREDESIGN-13: renders suffix in the details page flight number", () => { + const suffixFlight = { + ...mockFlight, + id: "SU0038D-20260514", + flightId: { carrier: "SU", flightNumber: "0038", suffix: "D", date: "20260514" }, + } as IDirectFlight; + mockState = { + flight: suffixFlight, + allFlights: [suffixFlight], + daysOfFlight: ["20260514"], + loading: false, + error: null, + }; + render( + , + ); + expect(screen.getAllByText("SU 0038D").length).toBeGreaterThanOrEqual(1); + }); + it("renders loading skeleton", () => { mockState = { flight: null, allFlights: [], daysOfFlight: [], loading: true, error: null }; render(); @@ -440,6 +463,20 @@ describe("OnlineBoardDetailsPage", () => { expect(link?.getAttribute("href")).toContain("/ru/onlineboard/flight/SU1234-20260515"); }); + it("TIRREDESIGN-13: flight context keeps suffix in breadcrumb back URL", () => { + mockSearchParamsInstance = new URLSearchParams("request=onlineboard-flight-SU0038D-20260514"); + const { container } = render( + , + ); + const link = getCrumbLink(container, "BREADCRUMBS.FLIGHT-NUMBER"); + expect(link).toBeTruthy(); + expect(link?.getAttribute("href")).toContain("/ru/onlineboard/flight/SU0038D-20260514"); + }); + it("route context → leaf 'Маршрут: …' linking back to /route/", () => { mockSearchParamsInstance = new URLSearchParams("request=onlineboard-route-MOW-LED-20260515"); const { container } = render( diff --git a/src/features/online-board/components/OnlineBoardDetailsPage.tsx b/src/features/online-board/components/OnlineBoardDetailsPage.tsx index 34a2d0d8..30706f44 100644 --- a/src/features/online-board/components/OnlineBoardDetailsPage.tsx +++ b/src/features/online-board/components/OnlineBoardDetailsPage.tsx @@ -21,7 +21,7 @@ import { useLiveFlightDetails } from "../hooks/useLiveFlightDetails.js"; import { useOnlineBoard } from "../hooks/useOnlineBoard.js"; import { parseDetailsRequestParam } from "@/shared/detailsRequestParam.js"; import { buildFlightJsonLd } from "../json-ld.js"; -import { buildOnlineBoardUrl } from "../url.js"; +import { buildOnlineBoardUrl, parseFlightUrlParams } from "../url.js"; import { getFlightSearchDate } from "../flightSearchDate.js"; import { useCityName, useStationDisplayName } from "@/shared/hooks/useDictionaries.js"; import { FlightDetailsAccordion } from "./details-panels/FlightDetailsAccordion.js"; @@ -65,6 +65,10 @@ export interface OnlineBoardDetailsPageProps { canonicalOrigin: string; } +function parseParentFlightRequest(flightNumber: string, date: string): IParsedFlightId | null { + return parseFlightUrlParams(`${flightNumber}-${date}`); +} + /** * One side of a leg's station block — station code + airport name + city * + terminal, plus scheduled / expected / actual times formatted as @@ -449,13 +453,14 @@ export const OnlineBoardDetailsPage: FC = ({ const backUrl = (() => { switch (parentRequest.kind) { case "flight": { - const m = parentRequest.flightNumber.match(/^([A-Z]{2,3})(\d+)$/); - if (!m || !m[1] || !m[2]) return `/${locale}/onlineboard`; + const parsed = parseParentFlightRequest(parentRequest.flightNumber, parentRequest.date); + if (!parsed) return `/${locale}/onlineboard`; return `/${locale}/${buildOnlineBoardUrl({ type: "flight", - carrier: m[1], - flightNumber: m[2], - date: parentRequest.date, + carrier: parsed.carrier, + flightNumber: parsed.flightNumber, + ...(parsed.suffix ? { suffix: parsed.suffix } : {}), + date: parsed.date, })}`; } case "departure": @@ -483,8 +488,10 @@ export const OnlineBoardDetailsPage: FC = ({ switch (parentRequest.kind) { case "flight": { // Angular renders "Рейс: SU 6188" — carrier and number space-separated - const m = parentRequest.flightNumber.match(/^([A-Z]{2,3})(\d+)$/); - const formatted = m?.[1] && m?.[2] ? `${m[1]} ${m[2]}` : parentRequest.flightNumber; + const parsed = parseParentFlightRequest(parentRequest.flightNumber, parentRequest.date); + const formatted = parsed + ? `${parsed.carrier} ${parsed.flightNumber}${parsed.suffix ?? ""}` + : parentRequest.flightNumber; return t("BREADCRUMBS.FLIGHT-NUMBER", { flightNumber: formatted }); } case "departure": @@ -594,7 +601,7 @@ export const OnlineBoardDetailsPage: FC = ({ } const legs = getLegs(displayFlight); - const flightNumber = `${displayFlight.flightId.carrier} ${displayFlight.flightId.flightNumber}`; + const flightNumber = `${displayFlight.flightId.carrier} ${displayFlight.flightId.flightNumber}${displayFlight.flightId.suffix ?? ""}`; const firstLeg = legs[0]; const lastLeg = legs[legs.length - 1]; diff --git a/src/features/online-board/json-ld.test.ts b/src/features/online-board/json-ld.test.ts index 96eb419d..afeb25a0 100644 --- a/src/features/online-board/json-ld.test.ts +++ b/src/features/online-board/json-ld.test.ts @@ -94,6 +94,14 @@ describe("buildFlightJsonLd", () => { expect(result.flightNumber).toBe("SU0100"); }); + it("TIRREDESIGN-13: includes suffix in flightNumber", () => { + const flight = makeDirectFlight({ carrier: "SU", flightNumber: "0038" }); + flight.flightId.suffix = "D"; + const result = buildFlightJsonLd(flight); + + expect(result.flightNumber).toBe("SU0038D"); + }); + it("maps departure airport", () => { const flight = makeDirectFlight({ depCode: "SVO", depAirport: "Sheremetyevo" }); const result = buildFlightJsonLd(flight); diff --git a/src/features/online-board/json-ld.ts b/src/features/online-board/json-ld.ts index f3ebc3c2..1281ebcf 100644 --- a/src/features/online-board/json-ld.ts +++ b/src/features/online-board/json-ld.ts @@ -48,11 +48,11 @@ export function buildFlightJsonLd(flight: ISimpleFlight): Flight { const firstLeg = getFirstLeg(flight); const lastLeg = getLastLeg(flight); - const { carrier, flightNumber } = flight.flightId; + const { carrier, flightNumber, suffix } = flight.flightId; const result: Flight = { "@type": "Flight", - flightNumber: `${carrier}${flightNumber}`, + flightNumber: `${carrier}${flightNumber}${suffix ?? ""}`, provider: { "@type": "Airline", name: "Aeroflot", diff --git a/src/ui/flights/FlightCard.test.tsx b/src/ui/flights/FlightCard.test.tsx index 9a5572a8..4b7ad9b0 100644 --- a/src/ui/flights/FlightCard.test.tsx +++ b/src/ui/flights/FlightCard.test.tsx @@ -170,6 +170,15 @@ describe("4.1.13.3 Table 23 — Direct flight collapsed row", () => { expect(screen.getByTestId("flight-carrier-number").textContent).toContain("SU 0022"); }); + it("TIRREDESIGN-13: renders suffix in collapsed row flight number", () => { + const flight = { + ...makeFlight({}), + flightId: { carrier: "SU", flightNumber: "0038", suffix: "D", date: "20260514" }, + } as ISimpleFlight; + render(); + expect(screen.getByTestId("flight-carrier-number").textContent).toContain("SU 0038D"); + }); + it("T23-C2: renders operator logo (full, not mini/round) for direct flight", () => { render(); const logo = screen.getByTestId("operator-logo"); diff --git a/src/ui/flights/FlightCard.tsx b/src/ui/flights/FlightCard.tsx index 25e9c154..7f7d891d 100644 --- a/src/ui/flights/FlightCard.tsx +++ b/src/ui/flights/FlightCard.tsx @@ -184,7 +184,7 @@ export const FlightCard: FC = ({ ? childFlightIds .map((id) => `${id.carrier} ${id.flightNumber}${id.suffix ?? ""}`) .join(", ") - : `${flight.flightId.carrier} ${flight.flightId.flightNumber}`; + : `${flight.flightId.carrier} ${flight.flightId.flightNumber}${flight.flightId.suffix ?? ""}`; // TZ §4.1.22: when OperatingBy is null, resolve carrier from flight-number // ranges (SU1-2999→SU, SU5000-5399→DP, SU5400-5799→HZ, etc.). // Falls back to the flight's own carrier code for non-SU flights or when