Preserve online board flight suffixes

This commit is contained in:
2026-05-14 14:41:11 +03:00
parent 7fd8faf202
commit 30f1ee7873
10 changed files with 93 additions and 16 deletions
@@ -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(<DetailsHeaderBadge flight={flight} locale="ru" />);
expect(screen.getByText("SU 0038D")).toBeTruthy();
});
it("renders operator-logo", () => {
render(<DetailsHeaderBadge flight={makeDirect()} locale="ru" />);
expect(screen.getByTestId("operator-logo")).toBeTruthy();
@@ -39,7 +39,7 @@ export const DetailsHeaderBadge: FC<DetailsHeaderBadgeProps> = ({
}) => {
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 (
@@ -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(<FlightsMiniListItem flight={flight} isSelected={false} lang="ru" />);
expect(screen.getByText(/SU\s*0038D/)).toBeTruthy();
});
it("renders departure and arrival times", () => {
const flight = makeDirectFlight();
render(<FlightsMiniListItem flight={flight} isSelected={false} lang="ru" />);
@@ -127,14 +127,14 @@ export const FlightsMiniListItem = forwardRef<HTMLAnchorElement, FlightsMiniList
<div className="mini-list__flight-number">
{(() => {
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 ?? ""}`;
})()}
<span
className={`mini-list__status-icon ${iconColor}`}
@@ -173,6 +173,29 @@ describe("OnlineBoardDetailsPage", () => {
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(
<OnlineBoardDetailsPage
flightId={{ carrier: "SU", flightNumber: "0038", suffix: "D", date: "20260514" }}
locale="ru"
canonicalOrigin="https://www.aeroflot.ru"
/>,
);
expect(screen.getAllByText("SU 0038D").length).toBeGreaterThanOrEqual(1);
});
it("renders loading skeleton", () => {
mockState = { flight: null, allFlights: [], daysOfFlight: [], loading: true, error: null };
render(<OnlineBoardDetailsPage flightId={mockFlightId} locale="ru" canonicalOrigin="https://www.aeroflot.ru" />);
@@ -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(
<OnlineBoardDetailsPage
flightId={mockFlightId}
locale="ru"
canonicalOrigin="https://example.com"
/>,
);
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(
@@ -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<OnlineBoardDetailsPageProps> = ({
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<OnlineBoardDetailsPageProps> = ({
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<OnlineBoardDetailsPageProps> = ({
}
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];
@@ -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);
+2 -2
View File
@@ -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",
+9
View File
@@ -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(<FlightCard flight={flight} expandable />);
expect(screen.getByTestId("flight-carrier-number").textContent).toContain("SU 0038D");
});
it("T23-C2: renders operator logo (full, not mini/round) for direct flight", () => {
render(<FlightCard flight={makeFlight({})} expandable />);
const logo = screen.getByTestId("operator-logo");
+1 -1
View File
@@ -184,7 +184,7 @@ export const FlightCard: FC<FlightCardProps> = ({
? 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