From 68f7c239dca288eb724e04d123bfb2df79a526c4 Mon Sep 17 00:00:00 2001 From: gnezim Date: Thu, 16 Apr 2026 18:29:56 +0300 Subject: [PATCH] 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. --- .../components/ScheduleStartPage.test.tsx | 155 ++++++++++++++++++ .../schedule/components/ScheduleStartPage.tsx | 53 +++++- 2 files changed, 201 insertions(+), 7 deletions(-) create mode 100644 src/features/schedule/components/ScheduleStartPage.test.tsx diff --git a/src/features/schedule/components/ScheduleStartPage.test.tsx b/src/features/schedule/components/ScheduleStartPage.test.tsx new file mode 100644 index 00000000..25ee58e6 --- /dev/null +++ b/src/features/schedule/components/ScheduleStartPage.test.tsx @@ -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 }) => ( + {children} + ), +})); + +vi.mock("@/i18n/provider.js", () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})); + +vi.mock("@/features/popular-requests/components/PopularRequestsPanel.js", () => ({ + PopularRequestsPanel: ({ onRequestClick }: { onRequestClick?: (r: PopularRequest) => void }) => ( +
+ + +
+ ), +})); + +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: () =>
, +})); + +// --------------------------------------------------------------------------- +// 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(); + expect(screen.getByTestId("schedule-start")).toBeTruthy(); + }); + + it("navigates to schedule page on Schedule-type popular request click", () => { + render(); + 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(); + 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(); + const roundTripCheckbox = screen.getByTestId("round-trip-toggle"); + expect((roundTripCheckbox as HTMLInputElement).checked).toBe(true); + }); +}); diff --git a/src/features/schedule/components/ScheduleStartPage.tsx b/src/features/schedule/components/ScheduleStartPage.tsx index 2bf0d259..a7ed6b30 100644 --- a/src/features/schedule/components/ScheduleStartPage.tsx +++ b/src/features/schedule/components/ScheduleStartPage.tsx @@ -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(""); - const [arrivalAirport, setArrivalAirport] = useState(""); + const [departureAirport, setDepartureAirport] = useState(searchParams.get("departure") ?? ""); + const [arrivalAirport, setArrivalAirport] = useState(searchParams.get("arrival") ?? ""); const [dateFrom, setDateFrom] = useState(today); const [dateTo, setDateTo] = useState(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(addDays(today, 7)); const [returnDateTo, setReturnDateTo] = useState(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 = (