Align schedule filter submit parity

This commit is contained in:
2026-05-14 22:49:45 +03:00
parent 43ef9bb710
commit 1b183c334d
4 changed files with 36 additions and 60 deletions
@@ -470,9 +470,28 @@ describe("ScheduleFilter validation per TZ §4.1.9.4", () => {
expect(screen.queryByTestId("schedule-date-clear")).toBeNull();
expect(screen.queryByTestId("schedule-return-date-clear")).toBeNull();
});
it("clears stale return dates when return flights toggle changes", () => {
render(
<ScheduleFilter
initialDeparture="SVO"
initialArrival="LED"
initialDateFrom="20260601"
initialDateTo="20260607"
initialReturnFlights={true}
initialReturnDateFrom="20260608"
initialReturnDateTo="20260614"
/>,
);
expect(screen.queryByTestId("schedule-return-date-clear")).toBeTruthy();
fireEvent.click(screen.getByTestId("schedule-return-flights").querySelector("input")!);
fireEvent.click(screen.getByTestId("schedule-return-flights").querySelector("input")!);
expect(screen.queryByTestId("schedule-return-date-clear")).toBeNull();
});
});
describe("ScheduleFilter repeated-submit lock", () => {
describe("ScheduleFilter submit parity", () => {
beforeEach(() => {
vi.clearAllMocks();
scheduleCalendarMock.days = [];
@@ -482,7 +501,7 @@ describe("ScheduleFilter repeated-submit lock", () => {
_sliderOnChanges.length = 0;
});
it("keeps identical resubmits locked but allows an immediate changed search", () => {
it("allows immediate repeated submits like Angular", () => {
render(
<ScheduleFilter
initialDeparture="SVO"
@@ -494,7 +513,10 @@ describe("ScheduleFilter repeated-submit lock", () => {
fireEvent.submit(screen.getByTestId("search-form"));
expect(mockNavigate).toHaveBeenCalledTimes(1);
expect((screen.getByTestId("search-submit") as HTMLButtonElement).disabled).toBe(true);
expect((screen.getByTestId("search-submit") as HTMLButtonElement).disabled).toBe(false);
fireEvent.submit(screen.getByTestId("search-form"));
expect(mockNavigate).toHaveBeenCalledTimes(2);
fireEvent.change(screen.getByTestId("schedule-arrival-input"), {
target: { value: "KUF" },
@@ -502,7 +524,7 @@ describe("ScheduleFilter repeated-submit lock", () => {
expect((screen.getByTestId("search-submit") as HTMLButtonElement).disabled).toBe(false);
fireEvent.submit(screen.getByTestId("search-form"));
expect(mockNavigate).toHaveBeenCalledTimes(2);
expect(mockNavigate).toHaveBeenCalledTimes(3);
expect(mockNavigate).toHaveBeenLastCalledWith(
"/ru-ru/schedule/route/SVO-KUF-20260601-20260607",
);
@@ -251,47 +251,6 @@ export const ScheduleFilter: FC<ScheduleFilterProps> = ({
[returnAvailableDays, returnCalendarLoaded, scheduleMinDate, scheduleMaxDate],
);
// §4.1.11 — submit button locked for 30 seconds after each search.
// The 30-second constant is intentionally hardcoded (not configurable).
const [submitLock, setSubmitLock] = useState<{ until: number; key: string } | null>(null);
const [nowTs, setNowTs] = useState(() => Date.now());
useEffect(() => {
if (!submitLock || nowTs >= submitLock.until) return;
const id = setTimeout(() => setNowTs(Date.now()), 1000);
return () => clearTimeout(id);
}, [submitLock, nowTs]);
const formSearchKey = useMemo(
() =>
JSON.stringify({
departure: departure.trim().toUpperCase(),
arrival: arrival.trim().toUpperCase(),
dateFrom: dateRange[0] ? dateToYyyymmdd(dateRange[0]) : "",
dateTo: dateRange[1] ? dateToYyyymmdd(dateRange[1]) : "",
timeFrom: timeRange[0],
timeTo: timeRange[1],
directOnly,
returnFlights,
returnDateFrom: returnDateRange[0] ? dateToYyyymmdd(returnDateRange[0]) : "",
returnDateTo: returnDateRange[1] ? dateToYyyymmdd(returnDateRange[1]) : "",
returnTimeFrom: returnTimeRange[0],
returnTimeTo: returnTimeRange[1],
}),
[
departure,
arrival,
dateRange,
timeRange,
directOnly,
returnFlights,
returnDateRange,
returnTimeRange,
],
);
const isSubmitLocked = useMemo(
() => Boolean(submitLock && nowTs < submitLock.until && submitLock.key === formSearchKey),
[submitLock, nowTs, formSearchKey],
);
// 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
@@ -393,7 +352,6 @@ export const ScheduleFilter: FC<ScheduleFilterProps> = ({
const handleSubmit = useCallback(
(e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (isSubmitLocked) return;
const dep = departure.trim().toUpperCase();
const arr = arrival.trim().toUpperCase();
if (!dep || !arr) return;
@@ -516,10 +474,6 @@ export const ScheduleFilter: FC<ScheduleFilterProps> = ({
: {}),
searchExecuted: true,
});
// Lock submit for 30 seconds (§4.1.11 — hardcoded, not configurable)
const submittedAt = Date.now();
setSubmitLock({ until: submittedAt + 30_000, key: formSearchKey });
setNowTs(submittedAt);
void navigate(`/${locale}/${url}`);
},
[
@@ -533,8 +487,6 @@ export const ScheduleFilter: FC<ScheduleFilterProps> = ({
returnTimeRange,
navigate,
locale,
isSubmitLocked,
formSearchKey,
],
);
@@ -691,7 +643,11 @@ export const ScheduleFilter: FC<ScheduleFilterProps> = ({
<input
type="checkbox"
checked={returnFlights}
onChange={(e) => setReturnFlights(e.target.checked)}
onChange={(e) => {
setReturnFlights(e.target.checked);
setReturnDateRange([null, null]);
setReturnBeforeOutboundError(null);
}}
/>
<span>{t("SHARED.RETURN_FLIGHT_VIEW")}</span>
</label>
@@ -794,8 +750,6 @@ export const ScheduleFilter: FC<ScheduleFilterProps> = ({
type="submit"
className="search-button"
data-testid="search-submit"
disabled={isSubmitLocked}
aria-disabled={isSubmitLocked}
>
<span>{t("SHARED.SCHEDULES_VIEW")}</span>
</button>
@@ -40,11 +40,11 @@
}
}
// §4.1.11 — block filter/tabs/breadcrumbs while loading
// Angular parity: loading blocks only the sticky results controls.
// The sidebar filter stays interactive so users can correct criteria
// while the previous request is still in-flight.
&[data-searching="true"] {
.page-layout__left,
.page-layout__sticky,
.page-layout__breadcrumbs {
.page-layout__sticky {
pointer-events: none;
opacity: 0.6;
}
+1 -1
View File
@@ -15,7 +15,7 @@ test.describe("Schedule results filter", () => {
const submit = page.getByTestId("search-submit");
await expect(submit).toBeEnabled();
await submit.click();
await expect(submit).toBeDisabled();
await expect(submit).toBeEnabled();
await page.getByTestId("swap-cities-button").click();