From b8e595dc25afdf49a76ba34b120441d9617b911f Mon Sep 17 00:00:00 2001 From: gnezim Date: Sun, 19 Apr 2026 16:51:31 +0300 Subject: [PATCH] URL surface parity with Angular for /popular and start-page prefill - Drop the React-only standalone /popular route (and its e2e smoketest). Angular returns 404 for /ru-ru/popular; popular requests are surfaced inline on onlineboard/schedule start pages via PopularRequestsPanel (which stays). Matching the URL surface is a contractual requirement for the MF remote. - Replace ?tab/?departure/?arrival/?return query-string prefill on the onlineboard and schedule start pages with a sessionStorage transient slot. Mirrors Angular's OnlineBoardFiltersStateService / ScheduleFiltersStateService cross-page singletons: URLs stay clean of query strings, the start-page form still seeds itself from a popular-request click, and a fresh page reload (which bypasses the in-memory state in Angular) lands on a pristine form. Same-page popular clicks remount the filter via key bump so the useState initializers pick up the new prefill. --- .../components/OnlineBoardStartPage.test.tsx | 113 +++++--------- .../components/OnlineBoardStartPage.tsx | 143 +++++++++--------- .../components/ScheduleStartPage.test.tsx | 88 +++-------- .../schedule/components/ScheduleStartPage.tsx | 79 ++++++---- src/routes/[lang]/popular/page.tsx | 57 ------- src/shared/state/transientPrefill.ts | 36 +++++ tests/e2e/popular.spec.ts | 17 --- .../online-board/start-page.test.tsx | 2 +- 8 files changed, 211 insertions(+), 324 deletions(-) delete mode 100644 src/routes/[lang]/popular/page.tsx create mode 100644 src/shared/state/transientPrefill.ts delete mode 100644 tests/e2e/popular.spec.ts diff --git a/src/features/online-board/components/OnlineBoardStartPage.test.tsx b/src/features/online-board/components/OnlineBoardStartPage.test.tsx index 726b5a53..5007006e 100644 --- a/src/features/online-board/components/OnlineBoardStartPage.test.tsx +++ b/src/features/online-board/components/OnlineBoardStartPage.test.tsx @@ -9,16 +9,14 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import { render, screen, fireEvent } from "@testing-library/react"; -import { OnlineBoardStartPage, buildPopularRequestQueryParams } from "./OnlineBoardStartPage.js"; +import { OnlineBoardStartPage, buildOnlineBoardPrefillState } 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: () => mockNavigate, useParams: () => ({ lang: "ru" }), - useSearchParams: () => [mockSearchParams], Link: ({ children, to, ...props }: { children: React.ReactNode; to: string; className?: string; [k: string]: unknown }) => ( {children} ), @@ -65,88 +63,52 @@ vi.mock("@/ui/city-autocomplete/index.js", () => ({ })); // --------------------------------------------------------------------------- -// Pure function: buildPopularRequestQueryParams +// Pure function: buildOnlineBoardPrefillState // --------------------------------------------------------------------------- -describe("buildPopularRequestQueryParams", () => { - it("builds params for FlightNumber mode", () => { +describe("buildOnlineBoardPrefillState", () => { + it("builds state 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); + expect(buildOnlineBoardPrefillState(request)).toEqual({ + tab: "flight", + flightNumber: "SU0654", + }); }); - 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 state for Departure mode", () => { + expect( + buildOnlineBoardPrefillState({ + mode: "Departure", + departure: "LED", + type: "Onlineboard", + }), + ).toEqual({ tab: "route", departure: "LED" }); }); - 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 state for Arrival mode", () => { + expect( + buildOnlineBoardPrefillState({ + mode: "Arrival", + arrival: "VKO", + type: "Onlineboard", + }), + ).toEqual({ tab: "route", arrival: "VKO" }); }); - 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"); + it("builds state for Route mode", () => { + expect( + buildOnlineBoardPrefillState({ + mode: "Route", + departure: "LED", + arrival: "KRR", + type: "Onlineboard", + }), + ).toEqual({ tab: "route", departure: "LED", arrival: "KRR" }); }); }); @@ -211,14 +173,11 @@ describe("OnlineBoardStartPage", () => { expect(screen.getByTestId("feedback-button")).toBeTruthy(); }); - it("navigates on popular request click (Onlineboard type)", () => { + it("does not navigate on same-page popular click (Onlineboard type)", () => { render(); const btn = screen.getByTestId("popular-click"); fireEvent.click(btn); - expect(mockNavigate).toHaveBeenCalledWith( - expect.objectContaining({ - search: expect.stringContaining("tab=flight"), - }), - ); + // Same-page click — the filter remounts via key bump, no nav. + expect(mockNavigate).not.toHaveBeenCalled(); }); }); diff --git a/src/features/online-board/components/OnlineBoardStartPage.tsx b/src/features/online-board/components/OnlineBoardStartPage.tsx index 6c678c2a..529cfba0 100644 --- a/src/features/online-board/components/OnlineBoardStartPage.tsx +++ b/src/features/online-board/components/OnlineBoardStartPage.tsx @@ -12,8 +12,8 @@ * @module */ -import { type FC, useCallback, useMemo } from "react"; -import { useNavigate, useParams, useSearchParams } from "@modern-js/runtime/router"; +import { type FC, useCallback, useState } from "react"; +import { useNavigate, useParams } 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"; @@ -21,47 +21,52 @@ import { SearchHistory } from "@/ui/layout/SearchHistory.js"; import { OnlineBoardFilter } from "./OnlineBoardFilter.js"; import { PopularRequestsPanel } from "@/features/popular-requests/components/PopularRequestsPanel.js"; import type { PopularRequest } from "@/features/popular-requests/types.js"; +import { + readAndClearTransientPrefill, + writeTransientPrefill, +} from "@/shared/state/transientPrefill.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(); +export const ONLINE_BOARD_PREFILL_SLOT = "online-board"; +export const SCHEDULE_PREFILL_SLOT = "schedule"; +/** + * Transient prefill state handed from a popular-request click to the + * start page form. Mirrors Angular's `OnlineBoardFiltersStateService` + * cross-page singleton: URLs stay clean of query strings while the + * start-page form still seeds itself from the click. Held in + * sessionStorage and read-then-cleared on mount, so a fresh tab or + * page reload renders an empty form (matching Angular's behavior of a + * pristine service after navigation). + */ +export interface OnlineBoardPrefillState { + tab?: "flight" | "route"; + departure?: string; + arrival?: string; + flightNumber?: string; +} + +export function buildOnlineBoardPrefillState( + request: PopularRequest, +): OnlineBoardPrefillState { switch (request.mode) { case "FlightNumber": - params.set("tab", "flight"); - params.set("carrier", request.carrier); - params.set("flight", request.flightNumber); - break; + return { + tab: "flight", + flightNumber: `${request.carrier}${request.flightNumber}`, + }; case "Departure": - params.set("tab", "route"); - params.set("departure", request.departure); - break; + return { tab: "route", departure: request.departure }; case "Arrival": - params.set("tab", "route"); - params.set("arrival", request.arrival); - break; + return { tab: "route", arrival: request.arrival }; 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 { + tab: "route", + departure: request.departure, + arrival: request.arrival, + }; } - - return params; } export const OnlineBoardStartPage: FC = () => { @@ -69,53 +74,49 @@ export const OnlineBoardStartPage: FC = () => { const navigate = useNavigate(); const routeParams = useParams<{ lang: string }>(); const lang = routeParams.lang ?? "ru"; - const [searchParams] = useSearchParams(); - /** 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"); + // Read-and-clear any prefill the previous page wrote. Stored in + // useState (with a one-shot initializer) so React strict mode's + // double-render doesn't lose the value on the second pass. + const [prefill, setPrefill] = useState( + () => + readAndClearTransientPrefill( + ONLINE_BOARD_PREFILL_SLOT, + ) ?? {}, + ); + // Same-page popular clicks need to re-mount the filter so its + // useState initial values pick up the new prefill. Key bump does it. + const [filterKey, setFilterKey] = useState(0); - // 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 filterInitialProps = { + ...(prefill.tab ? { initialTab: prefill.tab } : {}), + ...(prefill.departure ? { initialDeparture: prefill.departure } : {}), + ...(prefill.arrival ? { initialArrival: prefill.arrival } : {}), + ...(prefill.flightNumber ? { initialFlightNumber: prefill.flightNumber } : {}), + }; 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(), - }); + const state = + request.mode === "Route" || request.mode === "RouteWithBack" + ? { + departure: request.departure, + arrival: request.arrival, + withReturn: request.mode === "RouteWithBack", + } + : {}; + writeTransientPrefill(SCHEDULE_PREFILL_SLOT, state); + navigate(`/${lang}/schedule`); return; } - // Onlineboard requests stay on this page with query params - navigate({ - pathname: `/${lang}/onlineboard`, - search: params.toString(), - }); + // Onlineboard request — same page. Update local prefill + + // remount the filter via key bump so its useState initializers + // see the new initial* props. + setPrefill(buildOnlineBoardPrefillState(request)); + setFilterKey((n) => n + 1); }, [navigate, lang], ); @@ -134,7 +135,7 @@ export const OnlineBoardStartPage: FC = () => { breadcrumbs={[]} contentLeft={ <> - + } diff --git a/src/features/schedule/components/ScheduleStartPage.test.tsx b/src/features/schedule/components/ScheduleStartPage.test.tsx index 1fa67031..ae08f620 100644 --- a/src/features/schedule/components/ScheduleStartPage.test.tsx +++ b/src/features/schedule/components/ScheduleStartPage.test.tsx @@ -9,16 +9,14 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import { render, screen, fireEvent } from "@testing-library/react"; -import { ScheduleStartPage, buildSchedulePopularRequestQueryParams } from "./ScheduleStartPage.js"; +import { ScheduleStartPage } 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} ), @@ -50,14 +48,14 @@ vi.mock("@/features/popular-requests/components/PopularRequestsPanel.js", () => })); vi.mock("@/features/online-board/components/OnlineBoardStartPage.js", () => ({ - buildPopularRequestQueryParams: (request: PopularRequest) => { - const params = new URLSearchParams(); + buildOnlineBoardPrefillState: (request: PopularRequest) => { if (request.mode === "Departure") { - params.set("tab", "route"); - params.set("departure", (request as { departure: string }).departure); + return { tab: "route", departure: request.departure }; } - return params; + return {}; }, + ONLINE_BOARD_PREFILL_SLOT: "online-board", + SCHEDULE_PREFILL_SLOT: "schedule", })); vi.mock("@/shared/hooks/useCitySearch.js", () => ({ @@ -68,56 +66,10 @@ 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(); + sessionStorage.clear(); }); it("renders the start page", () => { @@ -125,29 +77,29 @@ describe("ScheduleStartPage", () => { expect(screen.getByTestId("schedule-start")).toBeTruthy(); }); - it("navigates to schedule page on Schedule-type popular request click", () => { + it("writes prefill + navigates to schedule on Schedule-type popular click", () => { render(); fireEvent.click(screen.getByTestId("popular-click-route")); - expect(mockNavigate).toHaveBeenCalledWith( - expect.objectContaining({ - pathname: "/ru/schedule", - search: expect.stringContaining("departure=SVO"), - }), + expect(sessionStorage.getItem("afl-prefill:schedule")).toBe( + JSON.stringify({ departure: "SVO", arrival: "LED", withReturn: false }), ); + expect(mockNavigate).toHaveBeenCalledWith("/ru/schedule"); }); - it("navigates to onlineboard on Onlineboard-type popular request click", () => { + it("writes prefill + navigates to onlineboard on Onlineboard-type popular click", () => { render(); fireEvent.click(screen.getByTestId("popular-click-onlineboard")); - expect(mockNavigate).toHaveBeenCalledWith( - expect.objectContaining({ - pathname: "/ru/onlineboard", - }), + expect(sessionStorage.getItem("afl-prefill:online-board")).toBe( + JSON.stringify({ tab: "route", departure: "LED" }), ); + expect(mockNavigate).toHaveBeenCalledWith("/ru/onlineboard"); }); - it("initializes form from search params", () => { - mockSearchParams = new URLSearchParams("departure=SVO&arrival=LED&return=true"); + it("initializes form from sessionStorage prefill", () => { + sessionStorage.setItem( + "afl-prefill:schedule", + JSON.stringify({ departure: "SVO", arrival: "LED", withReturn: 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 af453c7e..32fecb06 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, useSearchParams } from "@modern-js/runtime/router"; +import { useNavigate, useParams } 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,7 +19,15 @@ 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 { + buildOnlineBoardPrefillState, + ONLINE_BOARD_PREFILL_SLOT, + SCHEDULE_PREFILL_SLOT, +} from "@/features/online-board/components/OnlineBoardStartPage.js"; +import { + readAndClearTransientPrefill, + writeTransientPrefill, +} from "@/shared/state/transientPrefill.js"; import { buildScheduleUrl } from "../url.js"; import "./ScheduleStartPage.scss"; @@ -43,23 +51,16 @@ function addDays(base: Date, days: number): Date { } /** - * 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). + * Transient prefill state handed from a popular-request click via + * sessionStorage. Mirrors Angular's `ScheduleFiltersStateService` + * cross-page singleton: URL stays clean of query strings while the + * form seeds from the click. Read-and-cleared on mount, so reload + * gives a pristine form (matching Angular). */ -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 interface SchedulePrefillState { + departure?: string; + arrival?: string; + withReturn?: boolean; } export const ScheduleStartPage: FC = () => { @@ -67,17 +68,24 @@ export const ScheduleStartPage: FC = () => { const { t } = useTranslation(); const routeParams = useParams<{ lang: string }>(); const lang = routeParams.lang ?? "ru"; - const [searchParams] = useSearchParams(); + + // One-shot read of any prefill the previous page wrote. + const [prefill] = useState( + () => + readAndClearTransientPrefill( + SCHEDULE_PREFILL_SLOT, + ) ?? {}, + ); const today = new Date(); - const [departureAirport, setDepartureAirport] = useState(searchParams.get("departure") ?? ""); - const [arrivalAirport, setArrivalAirport] = useState(searchParams.get("arrival") ?? ""); + const [departureAirport, setDepartureAirport] = useState(prefill.departure ?? ""); + const [arrivalAirport, setArrivalAirport] = useState(prefill.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(searchParams.get("return") === "true"); + const [isRoundTrip, setIsRoundTrip] = useState(prefill.withReturn === true); const [returnDateFrom, setReturnDateFrom] = useState(addDays(today, 7)); const [returnDateTo, setReturnDateTo] = useState(addDays(today, 14)); const [returnTimeRange, setReturnTimeRange] = useState<[number, number]>([0, 1440]); @@ -149,20 +157,25 @@ export const ScheduleStartPage: FC = () => { const handlePopularRequestClick = useCallback( (request: PopularRequest) => { if (request.type === "Onlineboard") { - const params = buildPopularRequestQueryParams(request); - navigate({ - pathname: `/${lang}/onlineboard`, - search: params.toString(), - }); + writeTransientPrefill( + ONLINE_BOARD_PREFILL_SLOT, + buildOnlineBoardPrefillState(request), + ); + navigate(`/${lang}/onlineboard`); return; } - // Schedule-type: build params and navigate to schedule with pre-fill - const params = buildSchedulePopularRequestQueryParams(request); - navigate({ - pathname: `/${lang}/schedule`, - search: params.toString(), - }); + // Schedule-type: only Route / RouteWithBack carry city info. + const state: SchedulePrefillState = + request.mode === "Route" || request.mode === "RouteWithBack" + ? { + departure: request.departure, + arrival: request.arrival, + withReturn: request.mode === "RouteWithBack", + } + : {}; + writeTransientPrefill(SCHEDULE_PREFILL_SLOT, state); + navigate(`/${lang}/schedule`); }, [navigate, lang], ); diff --git a/src/routes/[lang]/popular/page.tsx b/src/routes/[lang]/popular/page.tsx deleted file mode 100644 index 277e265e..00000000 --- a/src/routes/[lang]/popular/page.tsx +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Popular Requests standalone page route. - * - * Provides a standalone view of the PopularRequestsPanel. In Angular, - * popular requests were embedded in OnlineBoard and Schedule start pages. - * This route provides a direct-access path for the MF remote and allows - * independent rendering/testing. - * - * URL: /{lang}/popular - */ - -import { lazy, Suspense, useCallback } from "react"; -import { useParams, useNavigate } from "@modern-js/runtime/router"; -import { useTranslation } from "@/i18n/provider.js"; -import type { PopularRequest } from "@/features/popular-requests/types.js"; - -const PopularRequestsPanel = lazy(() => - import("@/features/popular-requests/components/PopularRequestsPanel.js").then( - (m) => ({ default: m.PopularRequestsPanel }), - ), -); - -export default function PopularPage(): JSX.Element { - const { t } = useTranslation(); - const routeParams = useParams<{ lang: string }>(); - const lang = routeParams.lang ?? "ru"; - const navigate = useNavigate(); - - const handleRequestClick = useCallback( - (request: PopularRequest) => { - switch (request.mode) { - case "FlightNumber": - case "Arrival": - case "Departure": - void navigate(`/${lang}/onlineboard`); - return; - case "Route": - if (request.type === "Onlineboard") { - void navigate(`/${lang}/onlineboard`); - } else { - void navigate(`/${lang}/schedule`); - } - return; - case "RouteWithBack": - void navigate(`/${lang}/schedule`); - return; - } - }, - [lang, navigate], - ); - - return ( - {t("SHARED.LOADING")}
}> - - - ); -} diff --git a/src/shared/state/transientPrefill.ts b/src/shared/state/transientPrefill.ts new file mode 100644 index 00000000..c5257816 --- /dev/null +++ b/src/shared/state/transientPrefill.ts @@ -0,0 +1,36 @@ +/** + * Cross-page transient prefill store. + * + * Mirrors Angular's `OnlineBoardFiltersStateService` / + * `ScheduleFiltersStateService` — singleton state used to hand + * popular-request data from one page to another without polluting the + * URL with query strings. Reload clears it (matches Angular's behavior: + * a fresh navigation to the page bypasses the service-held state). + * + * sessionStorage is the host: same-tab persistence, gone on tab close, + * works around react-router v6's same-path navigation collapsing + * `state` updates without re-rendering useLocation consumers. + */ + +const KEY_PREFIX = "afl-prefill:"; + +export function writeTransientPrefill(slot: string, value: T): void { + if (typeof sessionStorage === "undefined") return; + try { + sessionStorage.setItem(KEY_PREFIX + slot, JSON.stringify(value)); + } catch { + // ignore quota/disabled + } +} + +export function readAndClearTransientPrefill(slot: string): T | null { + if (typeof sessionStorage === "undefined") return null; + try { + const raw = sessionStorage.getItem(KEY_PREFIX + slot); + if (!raw) return null; + sessionStorage.removeItem(KEY_PREFIX + slot); + return JSON.parse(raw) as T; + } catch { + return null; + } +} diff --git a/tests/e2e/popular.spec.ts b/tests/e2e/popular.spec.ts deleted file mode 100644 index 70876e66..00000000 --- a/tests/e2e/popular.spec.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { test, expect } from "@playwright/test"; - -test.describe("Popular Requests", () => { - test("/ru/popular renders", async ({ page }) => { - await page.goto("/ru/popular"); - await page.waitForLoadState("domcontentloaded"); - - // The page should render without crashing. The PopularRequestsPanel - // is lazy-loaded, so we wait for either the panel or the loading fallback. - await expect( - page.locator("main, [aria-busy='true'], body"), - ).not.toBeEmpty(); - - // URL should be correct - expect(page.url()).toContain("/ru/popular"); - }); -}); diff --git a/tests/integration/online-board/start-page.test.tsx b/tests/integration/online-board/start-page.test.tsx index 9ef6c61f..d96335c9 100644 --- a/tests/integration/online-board/start-page.test.tsx +++ b/tests/integration/online-board/start-page.test.tsx @@ -20,7 +20,7 @@ const navigateSpy = vi.fn(); vi.mock("@modern-js/runtime/router", () => ({ useNavigate: () => navigateSpy, useParams: () => ({ lang: "ru" }), - useSearchParams: () => [new URLSearchParams()], + useLocation: () => ({ state: null, pathname: "/ru/onlineboard" }), Link: ({ children, to, ...props }: { children: React.ReactNode; to: string; [k: string]: unknown }) => ( {children} ),