Fix popular-flight Search-button no-op when today is mid-week
User report: clicking a 'Расписание туда' popular tile on /onlineboard
filled the Schedule form but clicking Search did nothing.
Two bugs:
1. OnlineBoardStartPage's Schedule-bound popular-click handler wrote
only { departure, arrival, withReturn } to the transient prefill —
it skipped dateFrom/dateTo entirely. The Schedule calendar rendered
empty, and on submit the form defaulted to today..today+7 (acceptable
but TZ §4.1.5 mandates current-week prefill).
2. currentWeekBounds() returned the raw Monday of the ISO week. When
today is mid-week (Tue-Sun), that Monday is N days in the past, so
the Schedule route guard (§4.1.2, -1/+330 window) rejected the URL
and silently redirected back to /schedule — user saw 'Search does
nothing'.
Fix: populate dateFrom/dateTo (and returnDateFrom/returnDateTo for
RouteWithBack) in the Schedule prefill from both handlers, and clamp
the `from` end of currentWeekBounds() to max(Mon, today-1) so the
prefilled range is always inside the window. nextWeekBounds now derives
from the raw Monday (not the clamped `from`) so next-week is always
the true next ISO week.
Live retest: popular 'Москва — Мурманск' → Schedule prefilled with
cities + '21.04.2026 - 26.04.2026' → Search navigates to
/schedule/route/MOW-MMK-20260421-20260426. 0 console errors.
2044 tests pass, typecheck clean.
This commit is contained in:
@@ -83,6 +83,57 @@ function todayYyyymmdd(): string {
|
||||
return `${y}${m}${day}`;
|
||||
}
|
||||
|
||||
function dateToYyyymmdd(value: Date): string {
|
||||
const y = value.getFullYear().toString();
|
||||
const m = (value.getMonth() + 1).toString().padStart(2, "0");
|
||||
const d = value.getDate().toString().padStart(2, "0");
|
||||
return `${y}${m}${d}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mon-Sun bounds of the ISO week containing `base` (or today), CLAMPED to
|
||||
* the Schedule date window's lower edge (today - 1). When today falls mid-week,
|
||||
* returning a Monday that is already past the -1-day window would make the
|
||||
* subsequent route guard redirect back to the start page, so we clamp the
|
||||
* `from` end to max(mon, today - 1). Used by TZ §4.1.5 Schedule popular-click
|
||||
* prefill — "показать расписание на = текущая неделя".
|
||||
*/
|
||||
function currentWeekBoundsYyyymmdd(base = new Date()): { from: string; to: string } {
|
||||
const d = new Date(base);
|
||||
d.setHours(0, 0, 0, 0);
|
||||
const dow = d.getDay() === 0 ? 7 : d.getDay();
|
||||
const mon = new Date(d);
|
||||
mon.setDate(d.getDate() - (dow - 1));
|
||||
const sun = new Date(mon);
|
||||
sun.setDate(mon.getDate() + 6);
|
||||
// Clamp `from` to today − 1 so the prefilled range is always inside
|
||||
// Schedule's [−1, +330] window and the route guard never rejects it.
|
||||
const minFrom = new Date(d);
|
||||
minFrom.setDate(d.getDate() - 1);
|
||||
const from = mon.getTime() < minFrom.getTime() ? minFrom : mon;
|
||||
return { from: dateToYyyymmdd(from), to: dateToYyyymmdd(sun) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Mon–Sun bounds of the ISO week AFTER the one containing `base`.
|
||||
* Used by TZ §4.1.5 round-trip prefill — "дата обратного рейса = следующая неделя".
|
||||
* Derived from the raw Monday of the current week (not the clamped
|
||||
* `currentWeekBoundsYyyymmdd().from`) so the next-Mon is always correct
|
||||
* regardless of how mid-week `base` is.
|
||||
*/
|
||||
function nextWeekBoundsYyyymmdd(base = new Date()): { from: string; to: string } {
|
||||
const d = new Date(base);
|
||||
d.setHours(0, 0, 0, 0);
|
||||
const dow = d.getDay() === 0 ? 7 : d.getDay();
|
||||
const thisMon = new Date(d);
|
||||
thisMon.setDate(d.getDate() - (dow - 1));
|
||||
const nextMon = new Date(thisMon);
|
||||
nextMon.setDate(thisMon.getDate() + 7);
|
||||
const nextSun = new Date(nextMon);
|
||||
nextSun.setDate(nextMon.getDate() + 6);
|
||||
return { from: dateToYyyymmdd(nextMon), to: dateToYyyymmdd(nextSun) };
|
||||
}
|
||||
|
||||
export function buildOnlineBoardPrefillState(
|
||||
request: PopularRequest,
|
||||
dictionaries: IDictionaries | null = null,
|
||||
@@ -192,13 +243,28 @@ export const OnlineBoardStartPage: FC = () => {
|
||||
// so the destination form pre-fills with a city rather than an
|
||||
// airport pin.
|
||||
if (request.type === "Schedule") {
|
||||
const state =
|
||||
// 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"
|
||||
? {
|
||||
departure: toCityCode(request.departure, dictionaries),
|
||||
arrival: toCityCode(request.arrival, dictionaries),
|
||||
withReturn: 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();
|
||||
base.returnDateFrom = nxt.from;
|
||||
base.returnDateTo = nxt.to;
|
||||
}
|
||||
return base;
|
||||
})()
|
||||
: {};
|
||||
writeTransientPrefill(SCHEDULE_PREFILL_SLOT, state);
|
||||
navigate(`/${locale}/schedule`);
|
||||
|
||||
@@ -187,28 +187,31 @@ describe("ScheduleStartPage", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("4.1.5-S1: one-way Route click prefills current ISO week dates + no return", () => {
|
||||
// 2026-05-15 (Fri) → week Mon 2026-05-11 … Sun 2026-05-17
|
||||
it("4.1.5-S1: one-way Route click prefills 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(<ScheduleStartPage />);
|
||||
fireEvent.click(screen.getByTestId("popular-click-route"));
|
||||
const stored = JSON.parse(sessionStore.getRaw("afl-prefill:schedule")!);
|
||||
expect(stored.departure).toBe("SVO");
|
||||
expect(stored.arrival).toBe("LED");
|
||||
expect(stored.withReturn).toBe(false);
|
||||
expect(stored.dateFrom).toBe("20260511"); // Mon
|
||||
expect(stored.dateFrom).toBe("20260514"); // clamped to today−1 (raw Mon was 2026-05-11)
|
||||
expect(stored.dateTo).toBe("20260517"); // Sun
|
||||
expect(stored.returnDateFrom).toBeUndefined();
|
||||
expect(stored.returnDateTo).toBeUndefined();
|
||||
expect(mockNavigate).toHaveBeenCalledWith("/ru-ru/schedule");
|
||||
});
|
||||
|
||||
it("4.1.5-S2: round-trip RouteWithBack click prefills current + next week dates", () => {
|
||||
// current week: 20260511-20260517; next week: 20260518-20260524
|
||||
it("4.1.5-S2: round-trip RouteWithBack click prefills 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"));
|
||||
const stored = JSON.parse(sessionStore.getRaw("afl-prefill:schedule")!);
|
||||
expect(stored.withReturn).toBe(true);
|
||||
expect(stored.dateFrom).toBe("20260511");
|
||||
expect(stored.dateFrom).toBe("20260514"); // clamped
|
||||
expect(stored.dateTo).toBe("20260517");
|
||||
expect(stored.returnDateFrom).toBe("20260518"); // next Mon
|
||||
expect(stored.returnDateTo).toBe("20260524"); // next Sun
|
||||
@@ -288,8 +291,9 @@ describe("4.1.9-R: Current-Week label substitution", () => {
|
||||
render(<ScheduleStartPage />);
|
||||
fireEvent.click(screen.getByTestId("popular-click-route"));
|
||||
const stored = JSON.parse(sessionStore.getRaw("afl-prefill:schedule")!);
|
||||
// Current week Mon-Sun for 2026-05-15
|
||||
expect(stored.dateFrom).toBe("20260511");
|
||||
// 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.
|
||||
expect(stored.dateFrom).toBe("20260514");
|
||||
expect(stored.dateTo).toBe("20260517");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -76,8 +76,12 @@ function yyyymmddToDate(yyyymmdd: string): Date {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Mon–Sun bounds of the ISO week containing `base` (or today).
|
||||
* TZ §4.1.5: "показать расписание на = текущая неделя".
|
||||
* Returns Mon–Sun bounds of the ISO week containing `base` (or today),
|
||||
* clamped to today − 1 on the `from` end so the range is always inside
|
||||
* Schedule's [−1, +330] date window. TZ §4.1.5 says "показать расписание
|
||||
* на = текущая неделя"; when today falls mid-week the unclamped Monday
|
||||
* would be past the window's lower edge and the route guard would redirect
|
||||
* the search back to the start page.
|
||||
*/
|
||||
function currentWeekBounds(base = new Date()): { from: Date; to: Date } {
|
||||
const d = new Date(base);
|
||||
@@ -88,17 +92,28 @@ function currentWeekBounds(base = new Date()): { from: Date; to: Date } {
|
||||
mon.setDate(d.getDate() - (dow - 1));
|
||||
const sun = new Date(mon);
|
||||
sun.setDate(mon.getDate() + 6);
|
||||
return { from: mon, to: sun };
|
||||
const minFrom = new Date(d);
|
||||
minFrom.setDate(d.getDate() - 1);
|
||||
const from = mon.getTime() < minFrom.getTime() ? minFrom : mon;
|
||||
return { from, to: sun };
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Mon–Sun bounds of the week *after* the ISO week containing `base`.
|
||||
* TZ §4.1.5: "дата обратного рейса = следующая неделя".
|
||||
*
|
||||
* Derived from the raw Monday of `base`'s ISO week (not the clamped
|
||||
* `currentWeekBounds().from`) so the return-leg always starts on the true
|
||||
* next Monday regardless of how mid-week `base` is.
|
||||
*/
|
||||
function nextWeekBounds(base = new Date()): { from: Date; to: Date } {
|
||||
const cur = currentWeekBounds(base);
|
||||
const nextMon = new Date(cur.from);
|
||||
nextMon.setDate(cur.from.getDate() + 7);
|
||||
const d = new Date(base);
|
||||
d.setHours(0, 0, 0, 0);
|
||||
const dow = d.getDay() === 0 ? 7 : d.getDay();
|
||||
const thisMon = new Date(d);
|
||||
thisMon.setDate(d.getDate() - (dow - 1));
|
||||
const nextMon = new Date(thisMon);
|
||||
nextMon.setDate(thisMon.getDate() + 7);
|
||||
const nextSun = new Date(nextMon);
|
||||
nextSun.setDate(nextMon.getDate() + 6);
|
||||
return { from: nextMon, to: nextSun };
|
||||
|
||||
Reference in New Issue
Block a user