Execute schedule popular route searches
This commit is contained in:
@@ -63,6 +63,12 @@ vi.mock("@/features/popular-requests/components/PopularRequestsPanel.js", () =>
|
||||
>
|
||||
Popular
|
||||
</button>
|
||||
<button
|
||||
data-testid="popular-click-schedule-route"
|
||||
onClick={() => onRequestClick?.({ mode: "Route", departure: "MOW", arrival: "MMK", type: "Schedule" })}
|
||||
>
|
||||
Schedule Route
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
@@ -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(<OnlineBoardStartPage />);
|
||||
fireEvent.click(screen.getByTestId("popular-click-schedule-route"));
|
||||
expect(mockNavigate).toHaveBeenCalledWith(
|
||||
"/ru-ru/schedule/route/MOW-MMK-20260514-20260517",
|
||||
);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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<OnlineBoardStartPageProps> = ({ 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<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") {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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(<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();
|
||||
|
||||
// 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(<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",
|
||||
);
|
||||
@@ -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(<ScheduleStartPage />);
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
Reference in New Issue
Block a user