Enforce outbound↔return week coupling in Schedule filter (TZ §4.1.9.4)

- 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.
This commit is contained in:
2026-04-22 17:09:14 +03:00
parent e7eca164f0
commit e444b6e261
2 changed files with 47 additions and 15 deletions
@@ -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(
<ScheduleFilter
initialDeparture="SVO"
@@ -317,9 +317,11 @@ describe("ScheduleFilter validation per TZ §4.1.9.4", () => {
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(
<ScheduleFilter
initialDeparture="SVO"
@@ -364,17 +366,17 @@ describe("ScheduleFilter validation per TZ §4.1.9.4", () => {
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();
});
});
@@ -230,6 +230,27 @@ export const ScheduleFilter: FC<ScheduleFilterProps> = ({
[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<ScheduleFilterProps> = ({
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);
}}
>
&times;
@@ -543,7 +569,11 @@ export const ScheduleFilter: FC<ScheduleFilterProps> = ({
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"