Pre-fill schedule form from popular request query params

Add buildSchedulePopularRequestQueryParams to convert Route/RouteWithBack
popular requests into URL search params. ScheduleStartPage now reads
departure/arrival/return from query params to initialize form state, and
the popular request click handler navigates with appropriate params for
both Schedule and Onlineboard request types.
This commit is contained in:
2026-04-16 18:29:56 +03:00
parent 1aaebc5176
commit 68f7c239dc
2 changed files with 201 additions and 7 deletions
@@ -0,0 +1,155 @@
/**
* Tests for ScheduleStartPage component.
*
* Verifies query param building from popular requests and
* form state initialization from URL search params.
*
* @vitest-environment jsdom
*/
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import { ScheduleStartPage, buildSchedulePopularRequestQueryParams } from "./ScheduleStartPage.js";
import type { PopularRequest } from "@/features/popular-requests/types.js";
const mockNavigate = vi.fn();
let mockSearchParams = new URLSearchParams();
vi.mock("@modern-js/runtime/router", () => ({
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>
),
}));
vi.mock("@/i18n/provider.js", () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}));
vi.mock("@/features/popular-requests/components/PopularRequestsPanel.js", () => ({
PopularRequestsPanel: ({ onRequestClick }: { onRequestClick?: (r: PopularRequest) => void }) => (
<div data-testid="popular-requests">
<button
data-testid="popular-click-route"
onClick={() => onRequestClick?.({ mode: "Route", departure: "SVO", arrival: "LED", type: "Schedule" })}
>
Route
</button>
<button
data-testid="popular-click-onlineboard"
onClick={() => onRequestClick?.({ mode: "Departure", departure: "LED", type: "Onlineboard" })}
>
Onlineboard
</button>
</div>
),
}));
vi.mock("@/features/online-board/components/OnlineBoardStartPage.js", () => ({
buildPopularRequestQueryParams: (request: PopularRequest) => {
const params = new URLSearchParams();
if (request.mode === "Departure") {
params.set("tab", "route");
params.set("departure", (request as { departure: string }).departure);
}
return params;
},
}));
vi.mock("@/shared/hooks/useCitySearch.js", () => ({
useCitySearch: () => ({ suggestions: [], search: vi.fn() }),
}));
vi.mock("@/ui/layout/SearchHistory.js", () => ({
SearchHistory: () => <div data-testid="search-history" />,
}));
// ---------------------------------------------------------------------------
// Pure function: buildSchedulePopularRequestQueryParams
// ---------------------------------------------------------------------------
describe("buildSchedulePopularRequestQueryParams", () => {
it("builds params for Route mode with departure and arrival", () => {
const request: PopularRequest = {
mode: "Route",
departure: "SVO",
arrival: "LED",
type: "Schedule",
};
const params = buildSchedulePopularRequestQueryParams(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 with return flag", () => {
const request: PopularRequest = {
mode: "RouteWithBack",
departure: "SVO",
arrival: "LED",
type: "Schedule",
};
const params = buildSchedulePopularRequestQueryParams(request);
expect(params.get("departure")).toBe("SVO");
expect(params.get("arrival")).toBe("LED");
expect(params.get("return")).toBe("true");
});
it("returns empty params for non-route modes", () => {
const request: PopularRequest = {
mode: "Arrival",
arrival: "VKO",
type: "Onlineboard",
};
const params = buildSchedulePopularRequestQueryParams(request);
expect(params.toString()).toBe("");
});
});
// ---------------------------------------------------------------------------
// Component tests
// ---------------------------------------------------------------------------
describe("ScheduleStartPage", () => {
beforeEach(() => {
vi.clearAllMocks();
mockSearchParams = new URLSearchParams();
});
it("renders the start page", () => {
render(<ScheduleStartPage />);
expect(screen.getByTestId("schedule-start")).toBeTruthy();
});
it("navigates to schedule page on Schedule-type popular request click", () => {
render(<ScheduleStartPage />);
fireEvent.click(screen.getByTestId("popular-click-route"));
expect(mockNavigate).toHaveBeenCalledWith(
expect.objectContaining({
pathname: "/ru/schedule",
search: expect.stringContaining("departure=SVO"),
}),
);
});
it("navigates to onlineboard on Onlineboard-type popular request click", () => {
render(<ScheduleStartPage />);
fireEvent.click(screen.getByTestId("popular-click-onlineboard"));
expect(mockNavigate).toHaveBeenCalledWith(
expect.objectContaining({
pathname: "/ru/online-board",
}),
);
});
it("initializes form from search params", () => {
mockSearchParams = new URLSearchParams("departure=SVO&arrival=LED&return=true");
render(<ScheduleStartPage />);
const roundTripCheckbox = screen.getByTestId("round-trip-toggle");
expect((roundTripCheckbox as HTMLInputElement).checked).toBe(true);
});
});
@@ -8,7 +8,7 @@
*/
import { type FC, useState, useCallback, type FormEvent } from "react";
import { useNavigate, useParams } from "@modern-js/runtime/router";
import { useNavigate, useParams, useSearchParams } from "@modern-js/runtime/router";
import { Calendar } from "primereact/calendar";
import { Slider, type SliderChangeEvent } from "primereact/slider";
import { AutoComplete, type AutoCompleteCompleteEvent } from "primereact/autocomplete";
@@ -19,6 +19,7 @@ import { PageTabs } from "@/ui/layout/PageTabs.js";
import { SearchHistory } from "@/ui/layout/SearchHistory.js";
import { PopularRequestsPanel } from "@/features/popular-requests/components/PopularRequestsPanel.js";
import type { PopularRequest } from "@/features/popular-requests/types.js";
import { buildPopularRequestQueryParams } from "@/features/online-board/components/OnlineBoardStartPage.js";
import { buildScheduleUrl } from "../url.js";
import "./ScheduleStartPage.scss";
@@ -41,21 +42,42 @@ function addDays(base: Date, days: number): Date {
return result;
}
/**
* Build URL search params for a schedule-type popular request.
*
* Only Route and RouteWithBack modes produce params; other modes
* return empty params (they belong to the online board).
*/
export function buildSchedulePopularRequestQueryParams(
request: PopularRequest,
): URLSearchParams {
const params = new URLSearchParams();
if (request.mode === "Route" || request.mode === "RouteWithBack") {
params.set("departure", request.departure);
params.set("arrival", request.arrival);
if (request.mode === "RouteWithBack") {
params.set("return", "true");
}
}
return params;
}
export const ScheduleStartPage: FC = () => {
const navigate = useNavigate();
const { t } = useTranslation();
const routeParams = useParams<{ lang: string }>();
const lang = routeParams.lang ?? "ru";
const [searchParams] = useSearchParams();
const today = new Date();
const [departureAirport, setDepartureAirport] = useState<CitySuggestion | string>("");
const [arrivalAirport, setArrivalAirport] = useState<CitySuggestion | string>("");
const [departureAirport, setDepartureAirport] = useState<CitySuggestion | string>(searchParams.get("departure") ?? "");
const [arrivalAirport, setArrivalAirport] = useState<CitySuggestion | string>(searchParams.get("arrival") ?? "");
const [dateFrom, setDateFrom] = useState<Date | null>(today);
const [dateTo, setDateTo] = useState<Date | null>(addDays(today, 7));
const [timeRange, setTimeRange] = useState<[number, number]>([0, 1440]);
const [directOnly, setDirectOnly] = useState(false);
const [isRoundTrip, setIsRoundTrip] = useState(false);
const [isRoundTrip, setIsRoundTrip] = useState(searchParams.get("return") === "true");
const [returnDateFrom, setReturnDateFrom] = useState<Date | null>(addDays(today, 7));
const [returnDateTo, setReturnDateTo] = useState<Date | null>(addDays(today, 14));
const [returnTimeRange, setReturnTimeRange] = useState<[number, number]>([0, 1440]);
@@ -124,9 +146,26 @@ export const ScheduleStartPage: FC = () => {
[departureAirport, arrivalAirport, dateFrom, dateTo, timeRange, directOnly, isRoundTrip, returnDateFrom, returnDateTo, returnTimeRange, navigate, lang],
);
const handlePopularRequestClick = useCallback((_request: PopularRequest) => {
// Navigation handled by PopularRequestItem internally
}, []);
const handlePopularRequestClick = useCallback(
(request: PopularRequest) => {
if (request.type === "Onlineboard") {
const params = buildPopularRequestQueryParams(request);
navigate({
pathname: `/${lang}/online-board`,
search: params.toString(),
});
return;
}
// Schedule-type: build params and navigate to schedule with pre-fill
const params = buildSchedulePopularRequestQueryParams(request);
navigate({
pathname: `/${lang}/schedule`,
search: params.toString(),
});
},
[navigate, lang],
);
const scheduleFilter = (
<form