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.
This commit is contained in:
@@ -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;
|
||||||
|
}) => <a href={to} {...props}>{children}</a>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/i18n/provider.js", () => ({
|
||||||
|
useTranslation: () => ({
|
||||||
|
t: (key: string, vars?: Record<string, unknown>) => {
|
||||||
|
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 }[];
|
||||||
|
}) => (
|
||||||
|
<div>
|
||||||
|
<nav data-testid="breadcrumbs">
|
||||||
|
{breadcrumbs?.map((b, i) => (
|
||||||
|
<span key={i} data-testid={`crumb-${i}`} data-url={b.url}>
|
||||||
|
{b.label}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/ui/layout/PageTabs.js", () => ({
|
||||||
|
PageTabs: () => <div />,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/ui/flights/FlightListSkeleton.js", () => ({
|
||||||
|
FlightListSkeleton: () => <div data-testid="skeleton" />,
|
||||||
|
}));
|
||||||
|
|
||||||
|
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(
|
||||||
|
<ScheduleDetailsPage
|
||||||
|
flights={[flightId]}
|
||||||
|
locale="ru-ru"
|
||||||
|
canonicalOrigin="https://example.com"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
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(
|
||||||
|
<ScheduleDetailsPage
|
||||||
|
flights={[flightId]}
|
||||||
|
locale="ru-ru"
|
||||||
|
canonicalOrigin="https://example.com"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
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(
|
||||||
|
<ScheduleDetailsPage
|
||||||
|
flights={[flightId]}
|
||||||
|
locale="ru-ru"
|
||||||
|
canonicalOrigin="https://example.com"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should stay at 1-item trail — no leaf
|
||||||
|
expect(screen.getByTestId("crumb-0")).toBeTruthy();
|
||||||
|
expect(screen.queryByTestId("crumb-1")).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -268,7 +268,7 @@ export const ScheduleDetailsPage: FC<ScheduleDetailsPageProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
title={<h1 className="text--white page-title">{title}</h1>}
|
title={<h1 className="text--white page-title">{title}</h1>}
|
||||||
breadcrumbs={[{ label: t("SCHEDULE.TITLE"), url: scheduleHref }]}
|
breadcrumbs={breadcrumbs}
|
||||||
>
|
>
|
||||||
<SeoHead {...seoProps} />
|
<SeoHead {...seoProps} />
|
||||||
{/* Angular renders `flight-details-full-route` once at the top for
|
{/* Angular renders `flight-details-full-route` once at the top for
|
||||||
|
|||||||
Reference in New Issue
Block a user