From e444b6e2615fdfccacb2145fcb754fc249438d89 Mon Sep 17 00:00:00 2001 From: gnezim Date: Wed, 22 Apr 2026 17:09:14 +0300 Subject: [PATCH] =?UTF-8?q?Enforce=20outbound=E2=86=94return=20week=20coup?= =?UTF-8?q?ling=20in=20Schedule=20filter=20(TZ=20=C2=A74.1.9.4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Tie return calendar minDate to outbound dateTo so earlier days grey out in the picker (Table 16: return cannot start before outbound ends; same week allowed). - Auto-clear the return range when the user moves outbound forward and strands the previously-chosen return in an invalid state. - Clearing outbound via the X button now cascades to the return range. Rewrote two tests that previously asserted the submit-time error path; the new proactive clearing makes that path unreachable for this case, which is closer to the intent of the TZ. --- .../components/ScheduleFilter.test.tsx | 30 +++++++++-------- .../schedule/components/ScheduleFilter.tsx | 32 ++++++++++++++++++- 2 files changed, 47 insertions(+), 15 deletions(-) diff --git a/src/features/schedule/components/ScheduleFilter.test.tsx b/src/features/schedule/components/ScheduleFilter.test.tsx index ae9e485e..e943e400 100644 --- a/src/features/schedule/components/ScheduleFilter.test.tsx +++ b/src/features/schedule/components/ScheduleFilter.test.tsx @@ -305,7 +305,7 @@ describe("ScheduleFilter – validation per TZ §4.1.9.4", () => { // §4.1.9.4 return date must be >= outbound dateTo // ------------------------------------------------------------------------- - it("4.1.9.4-R: return date before outbound dateTo shows error + blocks submit", () => { + it("4.1.9.4-R: return date before outbound dateTo auto-clears on mount (Table 16)", () => { render( { initialReturnDateTo="20260610" />, ); - fireEvent.submit(screen.getByTestId("search-form")); - expect(screen.queryByTestId("schedule-return-before-outbound-error")).toBeTruthy(); - expect(mockNavigate).not.toHaveBeenCalled(); + // TZ §4.1.9.4 Table 16: an invalid retFrom < outTo combination is + // blanked before the user can even submit, rather than only raising + // an error after submit. The return X button is therefore hidden. + expect(screen.queryByTestId("schedule-return-date-clear")).toBeNull(); + expect(screen.queryByTestId("schedule-return-before-outbound-error")).toBeNull(); }); it("4.1.9.4-R: return date equal to outbound dateTo passes validation", () => { @@ -356,7 +358,7 @@ describe("ScheduleFilter – validation per TZ §4.1.9.4", () => { expect(mockNavigate).toHaveBeenCalled(); }); - it("4.1.9.4-R: return error clears after user clears the return date range", () => { + it("4.1.9.4-R: clearing outbound also clears return (Table 16 cascade)", () => { render( { initialDateFrom="20260601" initialDateTo="20260607" initialReturnFlights={true} - initialReturnDateFrom="20260603" - initialReturnDateTo="20260610" + initialReturnDateFrom="20260608" + initialReturnDateTo="20260614" />, ); - fireEvent.submit(screen.getByTestId("search-form")); - expect(screen.queryByTestId("schedule-return-before-outbound-error")).toBeTruthy(); + // Sanity: both ranges are populated. + expect(screen.queryByTestId("schedule-date-clear")).toBeTruthy(); + expect(screen.queryByTestId("schedule-return-date-clear")).toBeTruthy(); - const clearBtn = screen.queryByTestId("schedule-return-date-clear"); - if (clearBtn) { - fireEvent.click(clearBtn); - expect(screen.queryByTestId("schedule-return-before-outbound-error")).toBeNull(); - } + // Clicking the outbound X must clear both ranges. + fireEvent.click(screen.getByTestId("schedule-date-clear")); + expect(screen.queryByTestId("schedule-date-clear")).toBeNull(); + expect(screen.queryByTestId("schedule-return-date-clear")).toBeNull(); }); }); diff --git a/src/features/schedule/components/ScheduleFilter.tsx b/src/features/schedule/components/ScheduleFilter.tsx index e5013873..9c74abd9 100644 --- a/src/features/schedule/components/ScheduleFilter.tsx +++ b/src/features/schedule/components/ScheduleFilter.tsx @@ -230,6 +230,27 @@ export const ScheduleFilter: FC = ({ [submitLockedUntil, nowTs], ); + // TZ §4.1.9.4 Table 16: when the outbound range moves forward such + // that the already-chosen return starts before the new outbound + // dateTo, blank the return picker and any coupled error so the user + // isn't left with an invalid combo. Intentionally does not fire when + // outbound is cleared — that's a separate "user actively cleared" flow + // handled on the outbound X button (see the clear handler below). + useEffect(() => { + if (!returnFlights) return; + const [, outTo] = dateRange; + const [retFrom] = returnDateRange; + if (!retFrom || !outTo) return; + const retFromDay = new Date(retFrom); + retFromDay.setHours(0, 0, 0, 0); + const outToDay = new Date(outTo); + outToDay.setHours(0, 0, 0, 0); + if (retFromDay.getTime() < outToDay.getTime()) { + setReturnDateRange([null, null]); + setReturnBeforeOutboundError(null); + } + }, [dateRange, returnFlights, returnDateRange]); + // Swap the Calendar input's displayed text to "Текущая неделя" per // TZ §4.1.9 Table 14 when the selected range equals Mon-Sun of the // current week. Uses inputRef + useEffect to override PrimeReact's @@ -452,8 +473,13 @@ export const ScheduleFilter: FC = ({ aria-label={t("SHARED.A11Y-CLEAR")} data-testid="schedule-date-clear" onClick={() => { + // TZ §4.1.9.4 Table 16: clearing outbound also + // clears return (return picker is only meaningful + // relative to the outbound range). setDateRange([null, null]); setRangeError(null); + setReturnDateRange([null, null]); + setReturnBeforeOutboundError(null); }} > × @@ -543,7 +569,11 @@ export const ScheduleFilter: FC = ({ if (returnBeforeOutboundError) setReturnBeforeOutboundError(null); }} selectionMode="range" - minDate={scheduleMinDate} + // TZ §4.1.9.4: return cannot start before outbound's + // dateTo. Tie the return picker's minDate to it so + // earlier days grey out in the calendar UI. Same + // week (retFrom = outTo) is allowed. + minDate={dateRange[1] ?? scheduleMinDate} maxDate={scheduleMaxDate} disabledDates={returnDisabledDates} dateFormat="dd.mm.yy"