diff --git a/src/features/online-board/components/OnlineBoardFilter.test.tsx b/src/features/online-board/components/OnlineBoardFilter.test.tsx
index 69d3201c..6aae52d8 100644
--- a/src/features/online-board/components/OnlineBoardFilter.test.tsx
+++ b/src/features/online-board/components/OnlineBoardFilter.test.tsx
@@ -6,7 +6,7 @@
*/
import { describe, it, expect, vi, beforeEach } from "vitest";
-import { render, screen, fireEvent } from "@testing-library/react";
+import { render, screen, fireEvent, act } from "@testing-library/react";
import { OnlineBoardFilter } from "./OnlineBoardFilter.js";
// ---------------------------------------------------------------------------
@@ -51,9 +51,14 @@ vi.mock("primereact/calendar", () => ({
},
}));
-// PrimeReact Slider stub
+// PrimeReact Slider stub — captures onChange so tests can fire range events
+let _sliderOnChange: ((e: { value: number[] }) => void) | null = null;
+
vi.mock("primereact/slider", () => ({
- Slider: () =>
,
+ Slider: (props: Record) => {
+ _sliderOnChange = props["onChange"] as (e: { value: number[] }) => void;
+ return ;
+ },
}));
// DayQuickPick — not needed for these tests
@@ -197,3 +202,40 @@ describe("OnlineBoardFilter – clear-button (X) per TZ §4.1.9 Tables 11/12", (
expect(screen.queryByTestId("route-date-clear")).toBeNull();
});
});
+
+describe("OnlineBoardFilter – time slider 1h minimum gap per TZ §4.1.9 Tables 12", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ _sliderOnChange = null;
+ });
+
+ it("4.1.9-R: time slider enforces 1h minimum gap (zero gap → to clamped to from + 60)", () => {
+ render();
+ // Fire slider onChange with zero-gap [600, 600]
+ act(() => {
+ _sliderOnChange?.({ value: [600, 600] });
+ });
+ // The displayed time value should show from=10:00 and to=11:00
+ const value = screen.getByTestId("time-selector").querySelector(".time-selector__value");
+ expect(value?.textContent).toBe("10:00 — 11:00");
+ });
+
+ it("4.1.9-R: time slider cannot set timeTo before timeFrom (inverted range is normalised)", () => {
+ render();
+ act(() => {
+ _sliderOnChange?.({ value: [800, 600] });
+ });
+ // After normalisation from = min(800,600)=600, to = max(800,600)=800; gap >= 60 → kept
+ const value = screen.getByTestId("time-selector").querySelector(".time-selector__value");
+ expect(value?.textContent).toBe("10:00 — 13:20");
+ });
+
+ it("4.1.9-R: time slider clamps from at right edge (24:00 zero gap → from = 23:00, to = 24:00)", () => {
+ render();
+ act(() => {
+ _sliderOnChange?.({ value: [1440, 1440] });
+ });
+ const value = screen.getByTestId("time-selector").querySelector(".time-selector__value");
+ expect(value?.textContent).toBe("23:00 — 24:00");
+ });
+});
diff --git a/src/features/online-board/components/OnlineBoardFilter.tsx b/src/features/online-board/components/OnlineBoardFilter.tsx
index 4b2c70a9..0411cbc3 100644
--- a/src/features/online-board/components/OnlineBoardFilter.tsx
+++ b/src/features/online-board/components/OnlineBoardFilter.tsx
@@ -587,7 +587,21 @@ export const OnlineBoardFilter: FC = ({
setTimeRange(e.value as [number, number])}
+ onChange={(e: SliderChangeEvent) => {
+ const raw = e.value as [number, number];
+ const MIN_GAP = 60;
+ let from = Math.min(raw[0], raw[1]);
+ let to = Math.max(raw[0], raw[1]);
+ if (to - from < MIN_GAP) {
+ if (from + MIN_GAP <= 1440) {
+ to = from + MIN_GAP;
+ } else {
+ to = 1440;
+ from = to - MIN_GAP;
+ }
+ }
+ setTimeRange([from, to]);
+ }}
range
min={0}
max={1440}
diff --git a/src/features/schedule/components/ScheduleFilter.test.tsx b/src/features/schedule/components/ScheduleFilter.test.tsx
index 5a9b49d5..0c806041 100644
--- a/src/features/schedule/components/ScheduleFilter.test.tsx
+++ b/src/features/schedule/components/ScheduleFilter.test.tsx
@@ -6,7 +6,7 @@
*/
import { describe, it, expect, vi, beforeEach } from "vitest";
-import { render, screen, fireEvent } from "@testing-library/react";
+import { render, screen, fireEvent, act } from "@testing-library/react";
import { ScheduleFilter } from "./ScheduleFilter.js";
// ---------------------------------------------------------------------------
@@ -47,9 +47,20 @@ vi.mock("primereact/calendar", () => ({
},
}));
-// PrimeReact Slider stub
+// PrimeReact Slider stub — captures onChange per testId so tests can fire events.
+// The last-mounted Slider's onChange is tracked; for the return-time slider tests
+// we render with returnFlights=true so the second Slider mounts last.
+let _sliderOnChange: ((e: { value: number[] }) => void) | null = null;
+// All mounted sliders — keyed by render order; index 0 = outbound, 1 = return
+const _sliderOnChanges: Array<((e: { value: number[] }) => void) | null> = [];
+
vi.mock("primereact/slider", () => ({
- Slider: () => ,
+ Slider: (props: Record) => {
+ const cb = props["onChange"] as (e: { value: number[] }) => void;
+ _sliderOnChange = cb;
+ _sliderOnChanges.push(cb);
+ return ;
+ },
}));
vi.mock("@/ui/city-autocomplete/index.js", () => ({
@@ -76,6 +87,8 @@ vi.mock("@/shared/dateWindow.js", () => ({
describe("ScheduleFilter – clear-button (X) per TZ §4.1.9 Tables 13/14", () => {
beforeEach(() => {
vi.clearAllMocks();
+ _sliderOnChange = null;
+ _sliderOnChanges.length = 0;
});
// -------------------------------------------------------------------------
@@ -170,3 +183,48 @@ describe("ScheduleFilter – clear-button (X) per TZ §4.1.9 Tables 13/14", () =
expect(screen.queryByTestId("time-slider-clear")).toBeNull();
});
});
+
+describe("ScheduleFilter – time slider 1h minimum gap per TZ §4.1.9 Table 14", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ _sliderOnChange = null;
+ _sliderOnChanges.length = 0;
+ });
+
+ it("4.1.9-R: outbound time slider enforces 1h minimum gap (zero gap → to clamped to from + 60)", () => {
+ render();
+ act(() => {
+ _sliderOnChanges[0]?.({ value: [600, 600] });
+ });
+ const value = screen.getByTestId("time-selector").querySelector(".time-selector__value");
+ expect(value?.textContent).toBe("10:00 — 11:00");
+ });
+
+ it("4.1.9-R: outbound time slider cannot set timeTo before timeFrom (inverted range normalised)", () => {
+ render();
+ act(() => {
+ _sliderOnChanges[0]?.({ value: [800, 600] });
+ });
+ const value = screen.getByTestId("time-selector").querySelector(".time-selector__value");
+ expect(value?.textContent).toBe("10:00 — 13:20");
+ });
+
+ it("4.1.9-R: outbound time slider clamps from at right edge (24:00 zero gap → 23:00 – 24:00)", () => {
+ render();
+ act(() => {
+ _sliderOnChanges[0]?.({ value: [1440, 1440] });
+ });
+ const value = screen.getByTestId("time-selector").querySelector(".time-selector__value");
+ expect(value?.textContent).toBe("23:00 — 24:00");
+ });
+
+ it("4.1.9-R: return time slider enforces 1h minimum gap", () => {
+ render();
+ // _sliderOnChanges[0] = outbound, _sliderOnChanges[1] = return
+ act(() => {
+ _sliderOnChanges[1]?.({ value: [720, 720] });
+ });
+ const value = screen.getByTestId("return-time-selector").querySelector(".time-selector__value");
+ expect(value?.textContent).toBe("12:00 — 13:00");
+ });
+});
diff --git a/src/features/schedule/components/ScheduleFilter.tsx b/src/features/schedule/components/ScheduleFilter.tsx
index 7dc38753..3b1a4342 100644
--- a/src/features/schedule/components/ScheduleFilter.tsx
+++ b/src/features/schedule/components/ScheduleFilter.tsx
@@ -346,9 +346,21 @@ export const ScheduleFilter: FC = ({
- setTimeRange(e.value as [number, number])
- }
+ onChange={(e: SliderChangeEvent) => {
+ const raw = e.value as [number, number];
+ const MIN_GAP = 60;
+ let from = Math.min(raw[0], raw[1]);
+ let to = Math.max(raw[0], raw[1]);
+ if (to - from < MIN_GAP) {
+ if (from + MIN_GAP <= 1440) {
+ to = from + MIN_GAP;
+ } else {
+ to = 1440;
+ from = to - MIN_GAP;
+ }
+ }
+ setTimeRange([from, to]);
+ }}
range
min={0}
max={1440}
@@ -429,9 +441,21 @@ export const ScheduleFilter: FC = ({
- setReturnTimeRange(e.value as [number, number])
- }
+ onChange={(e: SliderChangeEvent) => {
+ const raw = e.value as [number, number];
+ const MIN_GAP = 60;
+ let from = Math.min(raw[0], raw[1]);
+ let to = Math.max(raw[0], raw[1]);
+ if (to - from < MIN_GAP) {
+ if (from + MIN_GAP <= 1440) {
+ to = from + MIN_GAP;
+ } else {
+ to = 1440;
+ from = to - MIN_GAP;
+ }
+ }
+ setReturnTimeRange([from, to]);
+ }}
range
min={0}
max={1440}