Prefill schedule popular route requests

This commit is contained in:
2026-05-05 23:43:32 +03:00
parent 421a960a82
commit cb48dcc706
4 changed files with 63 additions and 86 deletions
@@ -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(<OnlineBoardStartPage />);
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();
}
@@ -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<OnlineBoardStartPageProps> = ({ 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<string, unknown> =
request.mode === "Route" || request.mode === "RouteWithBack"
? (() => {
const base: Record<string, unknown> = {
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;
}
@@ -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 today1 = 2026-05-14 so the route guard does
// not redirect the search back to the start page.
render(<ScheduleStartPage />);
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(<ScheduleStartPage />);
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(<ScheduleStartPage />);
fireEvent.click(screen.getByTestId("popular-click-route"));
// Current week Sun for 2026-05-15 is 2026-05-17; `from` is clamped to
// today1 = 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");
});
});
@@ -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 = (