From 266a6f910ca3bcb600ed87fe2bfe03ecc28f0bad Mon Sep 17 00:00:00 2001 From: gnezim Date: Tue, 21 Apr 2026 17:50:57 +0300 Subject: [PATCH] Fix ScheduleDetailsPage happy-path breadcrumb + add missing breadcrumb tests The final (success) return branch was passing a hardcoded 1-item breadcrumb array instead of the computed `breadcrumbs` variable, so the leaf crumb built from ?request= was silently dropped for loaded flights. Loading/error/empty branches were already correct. Adds 3 unit tests to lock the wiring. --- .../components/ScheduleDetailsPage.test.tsx | 166 ++++++++++++++++++ .../components/ScheduleDetailsPage.tsx | 2 +- 2 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 src/features/schedule/components/ScheduleDetailsPage.test.tsx diff --git a/src/features/schedule/components/ScheduleDetailsPage.test.tsx b/src/features/schedule/components/ScheduleDetailsPage.test.tsx new file mode 100644 index 00000000..e7f1fd81 --- /dev/null +++ b/src/features/schedule/components/ScheduleDetailsPage.test.tsx @@ -0,0 +1,166 @@ +/** + * Tests for ScheduleDetailsPage breadcrumb wiring. + * + * Verifies TZ §4.1.4 Table 7 rows 11-13: the leaf breadcrumb is built + * from the `?request=` query param when area === "schedule", and falls + * back to a single-item trail when no request param is present (share-link + * scenario). + * + * @vitest-environment jsdom + */ + +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { render, screen } from "@testing-library/react"; +import { ScheduleDetailsPage } from "./ScheduleDetailsPage.js"; +import type { IScheduleFlightId } from "../types.js"; + +// --------------------------------------------------------------------------- +// Controlled useSearchParams — overridden per test +// --------------------------------------------------------------------------- +let mockSearchParamsGet: (key: string) => string | null = () => null; + +vi.mock("@modern-js/runtime/router", () => ({ + useSearchParams: () => [{ get: (k: string) => mockSearchParamsGet(k) }], + Link: ({ + children, + to, + ...props + }: { + children: React.ReactNode; + to: string; + [k: string]: unknown; + }) => {children}, +})); + +vi.mock("@/i18n/provider.js", () => ({ + useTranslation: () => ({ + t: (key: string, vars?: Record) => { + if (vars) { + return Object.entries(vars).reduce( + (s: string, [k, v]) => s.replace(`{${k}}`, String(v)), + key, + ); + } + return key; + }, + }), +})); + +vi.mock("@/i18n/resolver.js", () => ({ + localeToLanguage: () => "ru", + normalizeLocaleParam: (l: string) => l, + DEFAULT_LANGUAGE: "ru", +})); + +vi.mock("../hooks/useScheduleDetails.js", () => ({ + useScheduleDetails: () => ({ flights: [], loading: true, error: null }), +})); + +vi.mock("../seo.js", () => ({ + buildScheduleDetailsSeo: () => ({}), +})); + +vi.mock("@/ui/layout/PageLayout.js", () => ({ + PageLayout: ({ + children, + breadcrumbs, + }: { + children?: React.ReactNode; + breadcrumbs?: { label: string; url?: string }[]; + }) => ( +
+ + {children} +
+ ), +})); + +vi.mock("@/ui/layout/PageTabs.js", () => ({ + PageTabs: () =>
, +})); + +vi.mock("@/ui/flights/FlightListSkeleton.js", () => ({ + FlightListSkeleton: () =>
, +})); + +vi.mock("@/ui/seo/SeoHead.js", () => ({ + SeoHead: () => null, +})); + +// --------------------------------------------------------------------------- + +const flightId: IScheduleFlightId = { + carrier: "SU", + flightNumber: "1234", + date: "20260515", +}; + +describe("ScheduleDetailsPage breadcrumbs", () => { + beforeEach(() => { + mockSearchParamsGet = () => null; + }); + + it("shows 1-item trail when no ?request= param (share-link)", () => { + render( + , + ); + expect(screen.queryByTestId("crumb-0")).toBeTruthy(); + expect(screen.queryByTestId("crumb-1")).toBeNull(); + }); + + it("shows 2-item trail with route leaf when ?request= carries schedule area (one-way)", () => { + mockSearchParamsGet = (k) => + k === "request" + ? "schedule-route-NBC-KHV-20220307-20220313" + : null; + + render( + , + ); + + const crumb0 = screen.getByTestId("crumb-0"); + const crumb1 = screen.getByTestId("crumb-1"); + expect(crumb0.textContent).toContain("SCHEDULE.TITLE"); + // Leaf label key is BREADCRUMBS.SCHEDULE-ROUTE (the mock t() returns the + // key since it has no literal {…} placeholders in the key string itself) + expect(crumb1.textContent).toContain("BREADCRUMBS.SCHEDULE-ROUTE"); + // Back URL must rebuild a schedule search URL with the IATA codes + const backUrl = crumb1.getAttribute("data-url") ?? ""; + expect(backUrl).toContain("/schedule/"); + expect(backUrl).toContain("NBC"); + expect(backUrl).toContain("KHV"); + }); + + it("ignores ?request= that carries onlineboard area", () => { + mockSearchParamsGet = (k) => + k === "request" + ? "onlineboard-route-MOW-LED-20260515" + : null; + + render( + , + ); + + // Should stay at 1-item trail — no leaf + expect(screen.getByTestId("crumb-0")).toBeTruthy(); + expect(screen.queryByTestId("crumb-1")).toBeNull(); + }); +}); diff --git a/src/features/schedule/components/ScheduleDetailsPage.tsx b/src/features/schedule/components/ScheduleDetailsPage.tsx index 786ff7e0..95392e13 100644 --- a/src/features/schedule/components/ScheduleDetailsPage.tsx +++ b/src/features/schedule/components/ScheduleDetailsPage.tsx @@ -268,7 +268,7 @@ export const ScheduleDetailsPage: FC = ({
} title={

{title}

} - breadcrumbs={[{ label: t("SCHEDULE.TITLE"), url: scheduleHref }]} + breadcrumbs={breadcrumbs} > {/* Angular renders `flight-details-full-route` once at the top for