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 = (