From 1b183c334de7ae2015de4946d54bc364f6c70325 Mon Sep 17 00:00:00 2001 From: gnezim Date: Thu, 14 May 2026 22:49:45 +0300 Subject: [PATCH] Align schedule filter submit parity --- .../components/ScheduleFilter.test.tsx | 30 ++++++++-- .../schedule/components/ScheduleFilter.tsx | 56 ++----------------- .../components/ScheduleSearchPage.scss | 8 +-- tests/e2e/schedule-filter-resubmit.spec.ts | 2 +- 4 files changed, 36 insertions(+), 60 deletions(-) diff --git a/src/features/schedule/components/ScheduleFilter.test.tsx b/src/features/schedule/components/ScheduleFilter.test.tsx index 81b048c1..a2403a4a 100644 --- a/src/features/schedule/components/ScheduleFilter.test.tsx +++ b/src/features/schedule/components/ScheduleFilter.test.tsx @@ -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( + , + ); + + 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( { 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", ); diff --git a/src/features/schedule/components/ScheduleFilter.tsx b/src/features/schedule/components/ScheduleFilter.tsx index 1b1ba477..8c0987ca 100644 --- a/src/features/schedule/components/ScheduleFilter.tsx +++ b/src/features/schedule/components/ScheduleFilter.tsx @@ -251,47 +251,6 @@ export const ScheduleFilter: FC = ({ [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 = ({ const handleSubmit = useCallback( (e: FormEvent) => { 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 = ({ : {}), 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 = ({ returnTimeRange, navigate, locale, - isSubmitLocked, - formSearchKey, ], ); @@ -691,7 +643,11 @@ export const ScheduleFilter: FC = ({ setReturnFlights(e.target.checked)} + onChange={(e) => { + setReturnFlights(e.target.checked); + setReturnDateRange([null, null]); + setReturnBeforeOutboundError(null); + }} /> {t("SHARED.RETURN_FLIGHT_VIEW")} @@ -794,8 +750,6 @@ export const ScheduleFilter: FC = ({ type="submit" className="search-button" data-testid="search-submit" - disabled={isSubmitLocked} - aria-disabled={isSubmitLocked} > {t("SHARED.SCHEDULES_VIEW")} diff --git a/src/features/schedule/components/ScheduleSearchPage.scss b/src/features/schedule/components/ScheduleSearchPage.scss index e9f3c57d..eeeaee28 100644 --- a/src/features/schedule/components/ScheduleSearchPage.scss +++ b/src/features/schedule/components/ScheduleSearchPage.scss @@ -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; } diff --git a/tests/e2e/schedule-filter-resubmit.spec.ts b/tests/e2e/schedule-filter-resubmit.spec.ts index 5d0c6809..bdc5479c 100644 --- a/tests/e2e/schedule-filter-resubmit.spec.ts +++ b/tests/e2e/schedule-filter-resubmit.spec.ts @@ -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();