This commit is contained in:
@@ -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 (
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user