diff --git a/src/features/online-board/components/OnlineBoardStartPage.test.tsx b/src/features/online-board/components/OnlineBoardStartPage.test.tsx index 35804ec8..b8366095 100644 --- a/src/features/online-board/components/OnlineBoardStartPage.test.tsx +++ b/src/features/online-board/components/OnlineBoardStartPage.test.tsx @@ -17,6 +17,7 @@ import { setBoardFilter, type ScheduleFilterSnapshot, } from "@/shared/state/crossSectionNavigation.js"; +import { sessionStore } from "@/shared/storage.js"; // --------------------------------------------------------------------------- // Hook mocks for geo + viewport — controlled per test @@ -170,6 +171,7 @@ describe("buildOnlineBoardPrefillState", () => { describe("OnlineBoardStartPage", () => { beforeEach(() => { vi.clearAllMocks(); + sessionStore.clear(); }); it("renders start page with page layout structure", () => { @@ -272,15 +274,20 @@ describe("OnlineBoardStartPage", () => { expect(mockNavigate).not.toHaveBeenCalled(); }); - it("TIRREDESIGN-9: schedule Route popular click navigates directly to schedule results", () => { + it("TIRREDESIGN-9: schedule Route popular click opens Schedule with prefilled data", () => { vi.useFakeTimers(); vi.setSystemTime(new Date(2026, 4, 15, 12, 0, 0)); try { render(); fireEvent.click(screen.getByTestId("popular-click-schedule-route")); - expect(mockNavigate).toHaveBeenCalledWith( - "/ru-ru/schedule/route/MOW-MMK-20260514-20260517", - ); + expect(mockNavigate).toHaveBeenCalledWith("/ru-ru/schedule"); + expect(JSON.parse(sessionStore.getRaw("afl-prefill:schedule") ?? "{}")).toEqual({ + departure: "MOW", + arrival: "MMK", + withReturn: false, + dateFrom: "20260514", + dateTo: "20260517", + }); } finally { vi.useRealTimers(); } diff --git a/src/features/online-board/components/OnlineBoardStartPage.tsx b/src/features/online-board/components/OnlineBoardStartPage.tsx index 59abed15..cfd09b73 100644 --- a/src/features/online-board/components/OnlineBoardStartPage.tsx +++ b/src/features/online-board/components/OnlineBoardStartPage.tsx @@ -24,6 +24,7 @@ import { PopularRequestsPanel } from "@/features/popular-requests/components/Pop import type { PopularRequest } from "@/features/popular-requests/types.js"; import { readAndClearTransientPrefill, + writeTransientPrefill, } from "@/shared/state/transientPrefill.js"; import { getBoardFilter, @@ -38,7 +39,6 @@ import type { IDictionaries } from "@/shared/dictionaries/index.js"; import { useGeoCityDefault } from "@/shared/hooks/useGeoCityDefault.js"; import { useIsMobileViewport } from "@/shared/hooks/useIsMobileViewport.js"; import { getOnlineBoardDefaultTimeRange } from "../timeDefaults.js"; -import { buildScheduleUrl } from "@/features/schedule/url.js"; import "./OnlineBoardStartPage.scss"; /** @@ -244,37 +244,30 @@ export const OnlineBoardStartPage: FC = ({ today }) = const handlePopularRequestClick = useCallback( (request: PopularRequest) => { - // Schedule route requests execute immediately. The live Angular app - // only pre-fills `/schedule`, but TIRREDESIGN-9 is specifically about - // the popular "Москва - Мурманск" schedule search not running. + // Schedule-type requests open the Schedule start page with the form + // prefilled from the clicked popular item, matching Angular's shared + // filter-state behavior. if (request.type === "Schedule") { - if (request.mode === "Route" || request.mode === "RouteWithBack") { - const cur = currentWeekBoundsYyyymmdd(); - const outbound = { - departure: toCityCode(request.departure, dictionaries), - arrival: toCityCode(request.arrival, dictionaries), - dateFrom: cur.from, - dateTo: cur.to, - }; - const url = - request.mode === "RouteWithBack" - ? (() => { + const state: Record = + request.mode === "Route" || request.mode === "RouteWithBack" + ? (() => { + const base: Record = { + departure: toCityCode(request.departure, dictionaries), + arrival: toCityCode(request.arrival, dictionaries), + withReturn: request.mode === "RouteWithBack", + }; + const cur = currentWeekBoundsYyyymmdd(); + base.dateFrom = cur.from; + base.dateTo = cur.to; + if (request.mode === "RouteWithBack") { const nxt = nextWeekBoundsYyyymmdd(); - return buildScheduleUrl({ - type: "roundtrip", - outbound, - inbound: { - departure: outbound.arrival, - arrival: outbound.departure, - dateFrom: nxt.from, - dateTo: nxt.to, - }, - }); - })() - : buildScheduleUrl({ type: "route", outbound }); - navigate(`/${locale}/${url}`); - return; - } + base.returnDateFrom = nxt.from; + base.returnDateTo = nxt.to; + } + return base; + })() + : {}; + writeTransientPrefill(SCHEDULE_PREFILL_SLOT, state); navigate(`/${locale}/schedule`); return; } diff --git a/src/features/schedule/components/ScheduleStartPage.test.tsx b/src/features/schedule/components/ScheduleStartPage.test.tsx index 2ce0bd48..af18089f 100644 --- a/src/features/schedule/components/ScheduleStartPage.test.tsx +++ b/src/features/schedule/components/ScheduleStartPage.test.tsx @@ -191,20 +191,31 @@ describe("ScheduleStartPage", () => { }); }); - it("4.1.5-S1: one-way Route click executes current ISO week search (from clamped to today-1)", () => { + it("4.1.5-S1: one-way Route click populates form with current ISO week dates (from clamped to today-1) + no return", () => { // 2026-05-15 (Fri) → raw Mon 2026-05-11, raw Sun 2026-05-17 // `from` is clamped to today−1 = 2026-05-14 so the route guard does // not redirect the search back to the start page. render(); fireEvent.click(screen.getByTestId("popular-click-route")); + expect(mockNavigate).not.toHaveBeenCalled(); + expect((screen.getByTestId("schedule-departure-input") as HTMLInputElement).value).toBe("SVO"); + expect((screen.getByTestId("schedule-arrival-input") as HTMLInputElement).value).toBe("LED"); + expect((screen.getByTestId("round-trip-toggle") as HTMLInputElement).checked).toBe(false); + expect(screen.queryByTestId("return-date-range-input")).toBeNull(); + + fireEvent.submit(screen.getByTestId("schedule-search-form")); expect(mockNavigate).toHaveBeenCalledWith("/ru-ru/schedule/route/SVO-LED-20260514-20260517"); }); - it("4.1.5-S2: round-trip RouteWithBack click executes current + next week search (outbound from clamped)", () => { + it("4.1.5-S2: round-trip RouteWithBack click populates form with current + next week dates (outbound from clamped)", () => { // current week raw: 20260511-20260517 (clamped from: 20260514-20260517) // next week: 20260518-20260524 (unclamped — future) render(); fireEvent.click(screen.getByTestId("popular-click-roundtrip")); + expect(mockNavigate).not.toHaveBeenCalled(); + expect((screen.getByTestId("round-trip-toggle") as HTMLInputElement).checked).toBe(true); + + fireEvent.submit(screen.getByTestId("schedule-search-form")); expect(mockNavigate).toHaveBeenCalledWith( "/ru-ru/schedule/route/SVO-LED-20260514-20260517/LED-SVO-20260518-20260524", ); @@ -283,11 +294,12 @@ describe("4.1.9-R: Current-Week label substitution", () => { vi.useRealTimers(); }); - it("4.1.9-R: start page uses current week on Route popular search", () => { + it("4.1.9-R: start page populates date range with current week on Route click", () => { render(); fireEvent.click(screen.getByTestId("popular-click-route")); // Current week Sun for 2026-05-15 is 2026-05-17; `from` is clamped to // today−1 = 2026-05-14 so the range is inside Schedule's −1/+330 window. + fireEvent.submit(screen.getByTestId("schedule-search-form")); expect(mockNavigate).toHaveBeenCalledWith("/ru-ru/schedule/route/SVO-LED-20260514-20260517"); }); }); diff --git a/src/features/schedule/components/ScheduleStartPage.tsx b/src/features/schedule/components/ScheduleStartPage.tsx index 3ba7cf19..fb13085c 100644 --- a/src/features/schedule/components/ScheduleStartPage.tsx +++ b/src/features/schedule/components/ScheduleStartPage.tsx @@ -383,61 +383,26 @@ export const ScheduleStartPage: FC = () => { const handlePopularRequestClick = useCallback( (request: PopularRequest) => { - // Route popular requests execute immediately. TIRREDESIGN-9 covers - // "Расписание туда: Москва - Мурманск"; stopping at a prefilled form - // leaves that request visibly unperformed. + // Popular route clicks prefill the Schedule form from the clicked + // item. This mirrors Angular: users land on the Schedule page with + // route/date fields ready, then submit when they want results. switch (request.mode) { case "Route": case "RouteWithBack": { const curWeek = currentWeekBounds(); - const dep = toCityCode(request.departure, dictionaries); - const arr = toCityCode(request.arrival, dictionaries); - const outbound = { - departure: dep, - arrival: arr, - dateFrom: dateToYyyymmdd(curWeek.from), - dateTo: dateToYyyymmdd(curWeek.to), - }; + setDepartureCode(toCityCode(request.departure, dictionaries)); + setArrivalCode(toCityCode(request.arrival, dictionaries)); + setIsRoundTrip(request.mode === "RouteWithBack"); + setDateFrom(curWeek.from); + setDateTo(curWeek.to); if (request.mode === "RouteWithBack") { const nxt = nextWeekBounds(); - const inbound = { - departure: arr, - arrival: dep, - dateFrom: dateToYyyymmdd(nxt.from), - dateTo: dateToYyyymmdd(nxt.to), - }; - setScheduleFilter({ - mode: "route", - departure: dep, - arrival: arr, - dateFrom: outbound.dateFrom, - dateTo: outbound.dateTo, - timeFrom: "0000", - timeTo: "2400", - onlyDirect: false, - showReturn: true, - returnDateFrom: inbound.dateFrom, - returnDateTo: inbound.dateTo, - returnTimeFrom: "0000", - returnTimeTo: "2400", - searchExecuted: true, - }); - void navigate(`/${locale}/${buildScheduleUrl({ type: "roundtrip", outbound, inbound })}`); - break; + setReturnDateFrom(nxt.from); + setReturnDateTo(nxt.to); + } else { + setReturnDateFrom(null); + setReturnDateTo(null); } - setScheduleFilter({ - mode: "route", - departure: dep, - arrival: arr, - dateFrom: outbound.dateFrom, - dateTo: outbound.dateTo, - timeFrom: "0000", - timeTo: "2400", - onlyDirect: false, - showReturn: false, - searchExecuted: true, - }); - void navigate(`/${locale}/${buildScheduleUrl({ type: "route", outbound })}`); break; } case "Arrival": @@ -452,7 +417,7 @@ export const ScheduleStartPage: FC = () => { } if (sameCitiesError) setSameCitiesError(null); }, - [dictionaries, sameCitiesError, navigate, locale], + [dictionaries, sameCitiesError], ); const scheduleFilter = (