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:
2026-04-16 18:24:53 +03:00
parent dfe32fdee1
commit 1aaebc5176
3 changed files with 218 additions and 11 deletions
@@ -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>
),