diff --git a/src/features/online-board/components/OnlineBoardFilter.test.tsx b/src/features/online-board/components/OnlineBoardFilter.test.tsx index 00b15abb..35ddbd71 100644 --- a/src/features/online-board/components/OnlineBoardFilter.test.tsx +++ b/src/features/online-board/components/OnlineBoardFilter.test.tsx @@ -249,6 +249,48 @@ describe("OnlineBoardFilter – time slider 1h minimum gap per TZ §4.1.9 Tables }); }); +describe("OnlineBoardFilter – submit lock", () => { + beforeEach(() => { + vi.clearAllMocks(); + _sliderOnChange = null; + }); + + it("keeps the same submitted route search locked", () => { + render( + , + ); + + fireEvent.submit(screen.getByTestId("search-form")); + + expect(screen.getByTestId("search-submit")).toHaveProperty("disabled", true); + }); + + it("unlocks route search after the user changes the time range", () => { + render( + , + ); + + fireEvent.submit(screen.getByTestId("search-form")); + expect(screen.getByTestId("search-submit")).toHaveProperty("disabled", true); + + act(() => { + _sliderOnChange?.({ value: [600, 1440] }); + }); + + expect(screen.getByTestId("search-submit")).toHaveProperty("disabled", false); + }); +}); + describe("OnlineBoardFilter – flight-number validation per TZ §4.1.9.3", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/features/online-board/components/OnlineBoardFilter.tsx b/src/features/online-board/components/OnlineBoardFilter.tsx index a6311a49..1ab42cb2 100644 --- a/src/features/online-board/components/OnlineBoardFilter.tsx +++ b/src/features/online-board/components/OnlineBoardFilter.tsx @@ -254,6 +254,7 @@ export const OnlineBoardFilter: FC = ({ // Value is the timestamp when the lock expires (or 0 if unlocked). // The 30-second constant is intentionally hardcoded (not configurable). const [submitLockedUntil, setSubmitLockedUntil] = useState(0); + const [lockedSearchSignature, setLockedSearchSignature] = useState(null); const [now, setNow] = useState(() => Date.now()); // Tick every second while the lock is active so the disabled state // updates reactively. @@ -262,9 +263,38 @@ export const OnlineBoardFilter: FC = ({ const id = setTimeout(() => setNow(Date.now()), 1000); return () => clearTimeout(id); }, [submitLockedUntil, now]); + const flightSearchSignature = useMemo(() => { + const dateParam = dateToYyyymmdd(flightDate ?? todayDate); + return [ + "flight", + flightNumber.trim(), + dateParam, + ].join("|"); + }, [flightNumber, flightDate, todayDate]); + + const routeSearchSignature = useMemo(() => { + const dateParam = dateToYyyymmdd(routeDate ?? todayDate); + const timePart = + timeRange[0] !== 0 || timeRange[1] !== 1440 + ? `${minutesToHhmm(timeRange[0])}-${minutesToHhmm(timeRange[1])}` + : "full-day"; + return [ + "route", + routeDepartureCode.trim().toUpperCase(), + routeArrivalCode.trim().toUpperCase(), + dateParam, + timePart, + ].join("|"); + }, [routeDepartureCode, routeArrivalCode, routeDate, timeRange, todayDate]); + + const activeSearchSignature = + activeTab === "flight" ? flightSearchSignature : routeSearchSignature; const isSubmitLocked = useMemo( - () => submitLockedUntil > 0 && now < submitLockedUntil, - [submitLockedUntil, now], + () => + submitLockedUntil > 0 && + now < submitLockedUntil && + lockedSearchSignature === activeSearchSignature, + [submitLockedUntil, now, lockedSearchSignature, activeSearchSignature], ); // Swap the Calendar input's display text to "Сегодня" / "Завтра" per @@ -369,13 +399,14 @@ export const OnlineBoardFilter: FC = ({ }); // Lock submit for 30 seconds (§4.1.10 — hardcoded, not configurable) + setLockedSearchSignature(flightSearchSignature); setSubmitLockedUntil(Date.now() + 30_000); setNow(Date.now()); const url = buildOnlineBoardUrl({ type: "flight", carrier, flightNumber: num, date: dateParam }); void navigate(`/${locale}/${url}`); }, - [flightNumber, flightDate, navigate, locale, isSubmitLocked], + [flightNumber, flightDate, navigate, locale, isSubmitLocked, flightSearchSignature], ); const handleRouteSubmit = useCallback( @@ -448,11 +479,12 @@ export const OnlineBoardFilter: FC = ({ url = buildOnlineBoardUrl({ type: "route", departure: depCode, arrival: arrCode, date: dateParam, ...timeExtras }); } // Lock submit for 30 seconds (§4.1.10 — hardcoded, not configurable) + setLockedSearchSignature(routeSearchSignature); setSubmitLockedUntil(Date.now() + 30_000); setNow(Date.now()); void navigate(`/${locale}/${url}`); }, - [routeDepartureCode, routeArrivalCode, routeDate, timeRange, navigate, locale, isSubmitLocked], + [routeDepartureCode, routeArrivalCode, routeDate, timeRange, navigate, locale, isSubmitLocked, routeSearchSignature], ); return ( diff --git a/tests/e2e/onlineboard-time-filter.spec.ts b/tests/e2e/onlineboard-time-filter.spec.ts index b080f16a..c9bdf7ae 100644 --- a/tests/e2e/onlineboard-time-filter.spec.ts +++ b/tests/e2e/onlineboard-time-filter.spec.ts @@ -87,5 +87,18 @@ test.describe("Onlineboard time-range filter (TIRREDESIGN-11)", () => { // URL must have grown a time suffix. await expect(page).toHaveURL(/\/onlineboard\/route\/MOW-LED-\d{8}-\d{8}/); + + const firstFilteredUrl = page.url(); + + await slider.click({ + position: { x: sliderBox.width * 0.75, y: sliderBox.height / 2 }, + }); + await expect(page.locator('[data-testid="search-submit"]')).toBeEnabled(); + await page.locator('[data-testid="search-submit"]').click(); + await page.waitForURL( + (url) => + url.href !== firstFilteredUrl && + /\/onlineboard\/route\/MOW-LED-\d{8}-\d{8}$/.test(url.pathname), + ); }); });