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