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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user