Align schedule filter submit parity
This commit is contained in:
@@ -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(
|
||||
<ScheduleFilter
|
||||
initialDeparture="SVO"
|
||||
initialArrival="LED"
|
||||
initialDateFrom="20260601"
|
||||
initialDateTo="20260607"
|
||||
initialReturnFlights={true}
|
||||
initialReturnDateFrom="20260608"
|
||||
initialReturnDateTo="20260614"
|
||||
/>,
|
||||
);
|
||||
|
||||
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(
|
||||
<ScheduleFilter
|
||||
initialDeparture="SVO"
|
||||
@@ -494,7 +513,10 @@ describe("ScheduleFilter – repeated-submit lock", () => {
|
||||
|
||||
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",
|
||||
);
|
||||
|
||||
@@ -251,47 +251,6 @@ export const ScheduleFilter: FC<ScheduleFilterProps> = ({
|
||||
[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<ScheduleFilterProps> = ({
|
||||
const handleSubmit = useCallback(
|
||||
(e: FormEvent<HTMLFormElement>) => {
|
||||
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<ScheduleFilterProps> = ({
|
||||
: {}),
|
||||
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<ScheduleFilterProps> = ({
|
||||
returnTimeRange,
|
||||
navigate,
|
||||
locale,
|
||||
isSubmitLocked,
|
||||
formSearchKey,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -691,7 +643,11 @@ export const ScheduleFilter: FC<ScheduleFilterProps> = ({
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={returnFlights}
|
||||
onChange={(e) => setReturnFlights(e.target.checked)}
|
||||
onChange={(e) => {
|
||||
setReturnFlights(e.target.checked);
|
||||
setReturnDateRange([null, null]);
|
||||
setReturnBeforeOutboundError(null);
|
||||
}}
|
||||
/>
|
||||
<span>{t("SHARED.RETURN_FLIGHT_VIEW")}</span>
|
||||
</label>
|
||||
@@ -794,8 +750,6 @@ export const ScheduleFilter: FC<ScheduleFilterProps> = ({
|
||||
type="submit"
|
||||
className="search-button"
|
||||
data-testid="search-submit"
|
||||
disabled={isSubmitLocked}
|
||||
aria-disabled={isSubmitLocked}
|
||||
>
|
||||
<span>{t("SHARED.SCHEDULES_VIEW")}</span>
|
||||
</button>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user