From 421a960a8205cf9720563d98d021578304158b11 Mon Sep 17 00:00:00 2001 From: gnezim Date: Tue, 5 May 2026 23:10:20 +0300 Subject: [PATCH] Execute schedule popular route searches --- .../components/OnlineBoardStartPage.test.tsx | 21 ++++++ .../components/OnlineBoardStartPage.tsx | 59 +++++++++-------- .../components/ScheduleStartPage.test.tsx | 22 +------ .../schedule/components/ScheduleStartPage.tsx | 66 ++++++++++++++----- 4 files changed, 103 insertions(+), 65 deletions(-) diff --git a/src/features/online-board/components/OnlineBoardStartPage.test.tsx b/src/features/online-board/components/OnlineBoardStartPage.test.tsx index f0f0ac4c..35804ec8 100644 --- a/src/features/online-board/components/OnlineBoardStartPage.test.tsx +++ b/src/features/online-board/components/OnlineBoardStartPage.test.tsx @@ -63,6 +63,12 @@ vi.mock("@/features/popular-requests/components/PopularRequestsPanel.js", () => > Popular + ), })); @@ -77,6 +83,7 @@ vi.mock("@/shared/hooks/useSearchHistory.js", () => ({ vi.mock("@/shared/dictionaries/index.js", () => ({ useDictionaries: () => ({ dictionaries: null, loading: false, error: null }), + getCityCodeByAirportCode: () => undefined, })); vi.mock("../hooks/useCalendarDays.js", () => ({ @@ -264,6 +271,20 @@ describe("OnlineBoardStartPage", () => { // Same-page click — the filter remounts via key bump, no nav. expect(mockNavigate).not.toHaveBeenCalled(); }); + + it("TIRREDESIGN-9: schedule Route popular click navigates directly to schedule results", () => { + 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", + ); + } finally { + vi.useRealTimers(); + } + }); }); // --------------------------------------------------------------------------- diff --git a/src/features/online-board/components/OnlineBoardStartPage.tsx b/src/features/online-board/components/OnlineBoardStartPage.tsx index 7e888b67..59abed15 100644 --- a/src/features/online-board/components/OnlineBoardStartPage.tsx +++ b/src/features/online-board/components/OnlineBoardStartPage.tsx @@ -24,7 +24,6 @@ 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, @@ -39,6 +38,7 @@ 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,36 +244,37 @@ export const OnlineBoardStartPage: FC = ({ today }) = const handlePopularRequestClick = useCallback( (request: PopularRequest) => { - // Schedule-type requests navigate to the schedule feature. City - // codes come off the popular-request API as airport codes in some - // cases (SVO/LED instead of MOW/LED); resolve them to owning city - // so the destination form pre-fills with a city rather than an - // airport pin. + // 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. if (request.type === "Schedule") { - // TZ §4.1.5: Schedule-bound popular clicks prefill the date range - // to the current ISO week (Mon-Sun); round-trip additionally sets - // the return range to the following ISO week. Without these the - // Schedule calendar renders empty until submit. - 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") { + 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 nxt = nextWeekBoundsYyyymmdd(); - base.returnDateFrom = nxt.from; - base.returnDateTo = nxt.to; - } - return base; - })() - : {}; - writeTransientPrefill(SCHEDULE_PREFILL_SLOT, state); + 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; + } navigate(`/${locale}/schedule`); return; } diff --git a/src/features/schedule/components/ScheduleStartPage.test.tsx b/src/features/schedule/components/ScheduleStartPage.test.tsx index 9a02e6cb..2ce0bd48 100644 --- a/src/features/schedule/components/ScheduleStartPage.test.tsx +++ b/src/features/schedule/components/ScheduleStartPage.test.tsx @@ -191,35 +191,20 @@ describe("ScheduleStartPage", () => { }); }); - it("4.1.5-S1: one-way Route click populates form with current ISO week dates (from clamped to today-1) + no return", () => { + it("4.1.5-S1: one-way Route click executes current ISO week search (from clamped to today-1)", () => { // 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. - // Same-page Schedule click updates form state directly (navigate to - // the same route would no-op), so we assert visible form state and - // submit the form to verify the dates landed in component state. 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(); - - // Submit drives the dates from state into the URL — proves they were set. - 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 populates form with current + next week dates (outbound from clamped)", () => { + it("4.1.5-S2: round-trip RouteWithBack click executes current + next week search (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", ); @@ -298,12 +283,11 @@ describe("4.1.9-R: Current-Week label substitution", () => { vi.useRealTimers(); }); - it("4.1.9-R: start page populates date range with current week on Route click", () => { + it("4.1.9-R: start page uses current week on Route popular search", () => { 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 2742f034..3ba7cf19 100644 --- a/src/features/schedule/components/ScheduleStartPage.tsx +++ b/src/features/schedule/components/ScheduleStartPage.tsx @@ -383,29 +383,61 @@ export const ScheduleStartPage: FC = () => { const handlePopularRequestClick = useCallback( (request: PopularRequest) => { - // Deviation from Angular: every popular-request click on Schedule - // populates the form in-place and stays on the page. Angular - // redirects Arrival/Departure/FlightNumber clicks to /onlineboard - // unconditionally; we instead treat the click as a Schedule prefill - // because users land here to plan a trip, not to switch sections. - // Codes sometimes arrive as airport (SVO/LED) — normalize to city. + // Route popular requests execute immediately. TIRREDESIGN-9 covers + // "Расписание туда: Москва - Мурманск"; stopping at a prefilled form + // leaves that request visibly unperformed. switch (request.mode) { case "Route": case "RouteWithBack": { const curWeek = currentWeekBounds(); - setDepartureCode(toCityCode(request.departure, dictionaries)); - setArrivalCode(toCityCode(request.arrival, dictionaries)); - setIsRoundTrip(request.mode === "RouteWithBack"); - setDateFrom(curWeek.from); - setDateTo(curWeek.to); + 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), + }; if (request.mode === "RouteWithBack") { const nxt = nextWeekBounds(); - setReturnDateFrom(nxt.from); - setReturnDateTo(nxt.to); - } else { - setReturnDateFrom(null); - setReturnDateTo(null); + 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; } + 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": @@ -420,7 +452,7 @@ export const ScheduleStartPage: FC = () => { } if (sameCitiesError) setSameCitiesError(null); }, - [dictionaries, sameCitiesError], + [dictionaries, sameCitiesError, navigate, locale], ); const scheduleFilter = (