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),
+ );
});
});