Wire popular request clicks to pre-fill OnlineBoardFilter via query params
Clicking a popular request now builds URLSearchParams and navigates with them, so the filter initializes with the correct tab/fields pre-filled. Schedule-type requests redirect to the schedule feature instead.
This commit is contained in:
@@ -1,18 +1,24 @@
|
||||
/**
|
||||
* Tests for OnlineBoardStartPage component.
|
||||
*
|
||||
* Verifies page layout rendering with filter and info sections.
|
||||
* Verifies page layout rendering with filter and info sections,
|
||||
* query param building from popular requests, and navigation on click.
|
||||
*
|
||||
* @vitest-environment jsdom
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { OnlineBoardStartPage } from "./OnlineBoardStartPage.js";
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import { OnlineBoardStartPage, buildPopularRequestQueryParams } from "./OnlineBoardStartPage.js";
|
||||
import type { PopularRequest } from "@/features/popular-requests/types.js";
|
||||
|
||||
const mockNavigate = vi.fn();
|
||||
const mockSearchParams = new URLSearchParams();
|
||||
|
||||
vi.mock("@modern-js/runtime/router", () => ({
|
||||
useNavigate: () => vi.fn(),
|
||||
useNavigate: () => mockNavigate,
|
||||
useParams: () => ({ lang: "ru" }),
|
||||
useSearchParams: () => [mockSearchParams],
|
||||
Link: ({ children, to, ...props }: { children: React.ReactNode; to: string; className?: string; [k: string]: unknown }) => (
|
||||
<a href={to} {...props}>{children}</a>
|
||||
),
|
||||
@@ -25,7 +31,16 @@ vi.mock("@/i18n/provider.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("@/features/popular-requests/components/PopularRequestsPanel.js", () => ({
|
||||
PopularRequestsPanel: () => <div data-testid="popular-requests">Popular</div>,
|
||||
PopularRequestsPanel: ({ onRequestClick }: { onRequestClick?: (r: PopularRequest) => void }) => (
|
||||
<div data-testid="popular-requests">
|
||||
<button
|
||||
data-testid="popular-click"
|
||||
onClick={() => onRequestClick?.({ mode: "FlightNumber", carrier: "SU", flightNumber: "0654", type: "Onlineboard" })}
|
||||
>
|
||||
Popular
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/shared/hooks/useCitySearch.js", () => ({
|
||||
@@ -36,6 +51,96 @@ vi.mock("@/shared/hooks/useSearchHistory.js", () => ({
|
||||
useSearchHistory: () => ({ items: [], add: vi.fn(), clear: vi.fn() }),
|
||||
}));
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pure function: buildPopularRequestQueryParams
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("buildPopularRequestQueryParams", () => {
|
||||
it("builds params for FlightNumber mode", () => {
|
||||
const request: PopularRequest = {
|
||||
mode: "FlightNumber",
|
||||
carrier: "SU",
|
||||
flightNumber: "0654",
|
||||
type: "Onlineboard",
|
||||
};
|
||||
const params = buildPopularRequestQueryParams(request);
|
||||
expect(params.get("tab")).toBe("flight");
|
||||
expect(params.get("carrier")).toBe("SU");
|
||||
expect(params.get("flight")).toBe("0654");
|
||||
expect(params.has("departure")).toBe(false);
|
||||
expect(params.has("arrival")).toBe(false);
|
||||
});
|
||||
|
||||
it("builds params for Departure mode", () => {
|
||||
const request: PopularRequest = {
|
||||
mode: "Departure",
|
||||
departure: "LED",
|
||||
type: "Onlineboard",
|
||||
};
|
||||
const params = buildPopularRequestQueryParams(request);
|
||||
expect(params.get("tab")).toBe("route");
|
||||
expect(params.get("departure")).toBe("LED");
|
||||
expect(params.has("arrival")).toBe(false);
|
||||
expect(params.has("carrier")).toBe(false);
|
||||
});
|
||||
|
||||
it("builds params for Arrival mode", () => {
|
||||
const request: PopularRequest = {
|
||||
mode: "Arrival",
|
||||
arrival: "VKO",
|
||||
type: "Onlineboard",
|
||||
};
|
||||
const params = buildPopularRequestQueryParams(request);
|
||||
expect(params.get("tab")).toBe("route");
|
||||
expect(params.get("arrival")).toBe("VKO");
|
||||
expect(params.has("departure")).toBe(false);
|
||||
});
|
||||
|
||||
it("builds params for Route mode (Onlineboard)", () => {
|
||||
const request: PopularRequest = {
|
||||
mode: "Route",
|
||||
departure: "LED",
|
||||
arrival: "KRR",
|
||||
type: "Onlineboard",
|
||||
};
|
||||
const params = buildPopularRequestQueryParams(request);
|
||||
expect(params.get("tab")).toBe("route");
|
||||
expect(params.get("departure")).toBe("LED");
|
||||
expect(params.get("arrival")).toBe("KRR");
|
||||
expect(params.has("return")).toBe(false);
|
||||
});
|
||||
|
||||
it("builds params for Route mode (Schedule)", () => {
|
||||
const request: PopularRequest = {
|
||||
mode: "Route",
|
||||
departure: "SVO",
|
||||
arrival: "LED",
|
||||
type: "Schedule",
|
||||
};
|
||||
const params = buildPopularRequestQueryParams(request);
|
||||
expect(params.get("departure")).toBe("SVO");
|
||||
expect(params.get("arrival")).toBe("LED");
|
||||
expect(params.has("return")).toBe(false);
|
||||
});
|
||||
|
||||
it("builds params for RouteWithBack mode (Schedule) with return flag", () => {
|
||||
const request: PopularRequest = {
|
||||
mode: "RouteWithBack",
|
||||
departure: "SVO",
|
||||
arrival: "LED",
|
||||
type: "Schedule",
|
||||
};
|
||||
const params = buildPopularRequestQueryParams(request);
|
||||
expect(params.get("departure")).toBe("SVO");
|
||||
expect(params.get("arrival")).toBe("LED");
|
||||
expect(params.get("return")).toBe("true");
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Component tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("OnlineBoardStartPage", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
@@ -91,4 +196,15 @@ describe("OnlineBoardStartPage", () => {
|
||||
render(<OnlineBoardStartPage />);
|
||||
expect(screen.getByTestId("feedback-button")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("navigates on popular request click (Onlineboard type)", () => {
|
||||
render(<OnlineBoardStartPage />);
|
||||
const btn = screen.getByTestId("popular-click");
|
||||
fireEvent.click(btn);
|
||||
expect(mockNavigate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
search: expect.stringContaining("tab=flight"),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { type FC, useCallback } from "react";
|
||||
import { type FC, useCallback, useMemo } from "react";
|
||||
import { useNavigate, useParams, useSearchParams } from "@modern-js/runtime/router";
|
||||
import { useTranslation } from "@/i18n/provider.js";
|
||||
import { PageLayout } from "@/ui/layout/PageLayout.js";
|
||||
import { PageTabs } from "@/ui/layout/PageTabs.js";
|
||||
@@ -22,13 +23,102 @@ import { PopularRequestsPanel } from "@/features/popular-requests/components/Pop
|
||||
import type { PopularRequest } from "@/features/popular-requests/types.js";
|
||||
import "./OnlineBoardStartPage.scss";
|
||||
|
||||
/**
|
||||
* Build URL search params for a given popular request.
|
||||
*
|
||||
* Schedule-type requests produce params for the schedule page;
|
||||
* Onlineboard-type requests produce params for the online board filter.
|
||||
*/
|
||||
export function buildPopularRequestQueryParams(
|
||||
request: PopularRequest,
|
||||
): URLSearchParams {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
switch (request.mode) {
|
||||
case "FlightNumber":
|
||||
params.set("tab", "flight");
|
||||
params.set("carrier", request.carrier);
|
||||
params.set("flight", request.flightNumber);
|
||||
break;
|
||||
case "Departure":
|
||||
params.set("tab", "route");
|
||||
params.set("departure", request.departure);
|
||||
break;
|
||||
case "Arrival":
|
||||
params.set("tab", "route");
|
||||
params.set("arrival", request.arrival);
|
||||
break;
|
||||
case "Route":
|
||||
case "RouteWithBack":
|
||||
if (request.type === "Onlineboard") {
|
||||
params.set("tab", "route");
|
||||
}
|
||||
params.set("departure", request.departure);
|
||||
params.set("arrival", request.arrival);
|
||||
if (request.mode === "RouteWithBack" && request.type === "Schedule") {
|
||||
params.set("return", "true");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
export const OnlineBoardStartPage: FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const routeParams = useParams<{ lang: string }>();
|
||||
const lang = routeParams.lang ?? "ru";
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const handlePopularRequestClick = useCallback((_request: PopularRequest) => {
|
||||
// Navigation is handled by PopularRequestItem internally;
|
||||
// this callback is available for analytics or custom behavior.
|
||||
}, []);
|
||||
/** Derive initial filter props from current URL query params */
|
||||
const filterInitialProps = useMemo(() => {
|
||||
const tab = searchParams.get("tab");
|
||||
const departure = searchParams.get("departure");
|
||||
const arrival = searchParams.get("arrival");
|
||||
const carrier = searchParams.get("carrier");
|
||||
const flight = searchParams.get("flight");
|
||||
const date = searchParams.get("date");
|
||||
|
||||
// No query params — use defaults
|
||||
if (!tab && !departure && !arrival && !carrier && !flight) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
...(tab === "flight" || tab === "route"
|
||||
? { initialTab: tab as "flight" | "route" }
|
||||
: {}),
|
||||
...(departure ? { initialDeparture: departure } : {}),
|
||||
...(arrival ? { initialArrival: arrival } : {}),
|
||||
...(date ? { initialDate: date } : {}),
|
||||
...(carrier && flight
|
||||
? { initialFlightNumber: `${carrier}${flight}` }
|
||||
: {}),
|
||||
};
|
||||
}, [searchParams]);
|
||||
|
||||
const handlePopularRequestClick = useCallback(
|
||||
(request: PopularRequest) => {
|
||||
const params = buildPopularRequestQueryParams(request);
|
||||
|
||||
// Schedule-type requests navigate to the schedule feature
|
||||
if (request.type === "Schedule") {
|
||||
navigate({
|
||||
pathname: `/${lang}/schedule`,
|
||||
search: params.toString(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Onlineboard requests stay on this page with query params
|
||||
navigate({
|
||||
pathname: `/${lang}/online-board`,
|
||||
search: params.toString(),
|
||||
});
|
||||
},
|
||||
[navigate, lang],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="online-board-start-page" data-testid="online-board-start">
|
||||
@@ -44,7 +134,7 @@ export const OnlineBoardStartPage: FC = () => {
|
||||
breadcrumbs={[]}
|
||||
contentLeft={
|
||||
<>
|
||||
<OnlineBoardFilter />
|
||||
<OnlineBoardFilter {...filterInitialProps} />
|
||||
<SearchHistory />
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ const navigateSpy = vi.fn();
|
||||
vi.mock("@modern-js/runtime/router", () => ({
|
||||
useNavigate: () => navigateSpy,
|
||||
useParams: () => ({ lang: "ru" }),
|
||||
useSearchParams: () => [new URLSearchParams()],
|
||||
Link: ({ children, to, ...props }: { children: React.ReactNode; to: string; [k: string]: unknown }) => (
|
||||
<a href={to} {...props}>{children}</a>
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user