Enforce 1h minimum gap on time-range slider per TZ 4.1.9 Tables 12/14
This commit is contained in:
@@ -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: () => <div data-testid="slider-stub" />,
|
||||
Slider: (props: Record<string, unknown>) => {
|
||||
_sliderOnChange = props["onChange"] as (e: { value: number[] }) => void;
|
||||
return <div data-testid="slider-stub" />;
|
||||
},
|
||||
}));
|
||||
|
||||
// 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(<OnlineBoardFilter initialTab="route" initialTimeFrom="0600" initialTimeTo="0660" />);
|
||||
// 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(<OnlineBoardFilter initialTab="route" />);
|
||||
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(<OnlineBoardFilter initialTab="route" />);
|
||||
act(() => {
|
||||
_sliderOnChange?.({ value: [1440, 1440] });
|
||||
});
|
||||
const value = screen.getByTestId("time-selector").querySelector(".time-selector__value");
|
||||
expect(value?.textContent).toBe("23:00 — 24:00");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -587,7 +587,21 @@ export const OnlineBoardFilter: FC<OnlineBoardFilterProps> = ({
|
||||
<div className="time-selector">
|
||||
<Slider
|
||||
value={timeRange}
|
||||
onChange={(e: SliderChangeEvent) => 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}
|
||||
|
||||
@@ -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: () => <div data-testid="slider-stub" />,
|
||||
Slider: (props: Record<string, unknown>) => {
|
||||
const cb = props["onChange"] as (e: { value: number[] }) => void;
|
||||
_sliderOnChange = cb;
|
||||
_sliderOnChanges.push(cb);
|
||||
return <div data-testid="slider-stub" />;
|
||||
},
|
||||
}));
|
||||
|
||||
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(<ScheduleFilter initialTimeFrom="0600" initialTimeTo="0700" />);
|
||||
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(<ScheduleFilter />);
|
||||
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(<ScheduleFilter />);
|
||||
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(<ScheduleFilter initialReturnFlights={true} />);
|
||||
// _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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -346,9 +346,21 @@ export const ScheduleFilter: FC<ScheduleFilterProps> = ({
|
||||
<div className="time-selector">
|
||||
<Slider
|
||||
value={timeRange}
|
||||
onChange={(e: SliderChangeEvent) =>
|
||||
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<ScheduleFilterProps> = ({
|
||||
<div className="time-selector">
|
||||
<Slider
|
||||
value={returnTimeRange}
|
||||
onChange={(e: SliderChangeEvent) =>
|
||||
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}
|
||||
|
||||
Reference in New Issue
Block a user