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}