Allow changed time range resubmission
ci-deploy / build-deploy-test (push) Successful in 1m51s

This commit is contained in:
2026-05-05 19:04:03 +03:00
parent 04a71192fa
commit 4afecd23a6
3 changed files with 91 additions and 4 deletions
@@ -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(
<OnlineBoardFilter
initialTab="route"
initialDeparture="MOW"
initialArrival="LED"
initialDate="20260515"
/>,
);
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(
<OnlineBoardFilter
initialTab="route"
initialDeparture="MOW"
initialArrival="LED"
initialDate="20260515"
/>,
);
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();
@@ -254,6 +254,7 @@ export const OnlineBoardFilter: FC<OnlineBoardFilterProps> = ({
// 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<string | null>(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<OnlineBoardFilterProps> = ({
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<OnlineBoardFilterProps> = ({
});
// 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<OnlineBoardFilterProps> = ({
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 (
+13
View File
@@ -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),
);
});
});