Skip to content

Commit 8ce6054

Browse files
Add tests for month and year logic and improve coverage
Introduces new test suites for month and year logic helpers, covering keyboard navigation, disabled states, and focus management. Adds additional tests for edge cases in date utilities, click outside wrapper, shadow root, and exposes new helpers via the main entry point. Improves robustness and coverage of calendar and datepicker components.
1 parent 00e3fb0 commit 8ce6054

9 files changed

+320
-1
lines changed

src/date_utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1090,6 +1090,7 @@ export function isTimeInDisabledRange(
10901090
try {
10911091
valid = !isWithinInterval(baseTime, { start: min, end: max });
10921092
} catch (err) {
1093+
/* istanbul ignore next - date-fns historically threw on invalid intervals */
10931094
valid = false;
10941095
}
10951096
return valid;

src/test/calendar_container.test.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { render } from "@testing-library/react";
22
import React from "react";
33

44
import CalendarContainer from "../calendar_container";
5+
import { CalendarContainer as CalendarContainerFromIndex } from "../index";
56

67
describe("CalendarContainer", () => {
78
it("renders with default props", () => {
@@ -18,6 +19,16 @@ describe("CalendarContainer", () => {
1819
expect(dialog?.textContent).toBe("Test Content");
1920
});
2021

22+
it("exposes CalendarContainer via the package entry point", () => {
23+
const { container } = render(
24+
<CalendarContainerFromIndex>
25+
<div>Entry Content</div>
26+
</CalendarContainerFromIndex>,
27+
);
28+
29+
expect(container.querySelector('[role="dialog"]')).toBeTruthy();
30+
});
31+
2132
it("renders with showTimeSelectOnly prop", () => {
2233
const { container } = render(
2334
<CalendarContainer showTimeSelectOnly>

src/test/click_outside_wrapper.test.tsx

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,4 +225,58 @@ describe("ClickOutsideWrapper", () => {
225225
addEventListenerSpy.mockRestore();
226226
removeEventListenerSpy.mockRestore();
227227
});
228+
229+
it("falls back to event.target when composedPath does not return nodes", () => {
230+
const addEventListenerSpy = jest.spyOn(document, "addEventListener");
231+
render(
232+
<ClickOutsideWrapper onClickOutside={onClickOutsideMock}>
233+
<div>Inside</div>
234+
</ClickOutsideWrapper>,
235+
);
236+
237+
const handlerEntry = addEventListenerSpy.mock.calls.find(
238+
([type]) => type === "mousedown",
239+
);
240+
const handler = handlerEntry?.[1] as EventListener;
241+
242+
const outsideNode = document.createElement("div");
243+
const mockEvent = {
244+
composed: true,
245+
composedPath: () => [{}],
246+
target: outsideNode,
247+
} as unknown as MouseEvent;
248+
249+
handler(mockEvent);
250+
251+
expect(onClickOutsideMock).toHaveBeenCalledTimes(1);
252+
addEventListenerSpy.mockRestore();
253+
});
254+
255+
it("does not treat non-HTMLElement targets as ignored elements", () => {
256+
const addEventListenerSpy = jest.spyOn(document, "addEventListener");
257+
render(
258+
<ClickOutsideWrapper
259+
onClickOutside={onClickOutsideMock}
260+
ignoreClass="ignore-me"
261+
>
262+
<div>Inside</div>
263+
</ClickOutsideWrapper>,
264+
);
265+
266+
const handlerEntry = addEventListenerSpy.mock.calls.find(
267+
([type]) => type === "mousedown",
268+
);
269+
const handler = handlerEntry?.[1] as EventListener;
270+
271+
const textNode = document.createTextNode("outside");
272+
const mockEvent = {
273+
composed: false,
274+
target: textNode,
275+
} as unknown as MouseEvent;
276+
277+
handler(mockEvent);
278+
279+
expect(onClickOutsideMock).toHaveBeenCalledTimes(1);
280+
addEventListenerSpy.mockRestore();
281+
});
228282
});

src/test/date_utils_test.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1148,6 +1148,14 @@ describe("date_utils", () => {
11481148

11491149
expect(isMonthInRange(startDate, endDate, 1, day)).toBe(false);
11501150
});
1151+
1152+
it("should return false when the start year is after the end year", () => {
1153+
const day = newDate("2025-01-01");
1154+
const startDate = newDate("2026-01-01");
1155+
const endDate = newDate("2024-01-01");
1156+
1157+
expect(isMonthInRange(startDate, endDate, 1, day)).toBe(false);
1158+
});
11511159
});
11521160

11531161
describe("getStartOfYear", () => {
@@ -1190,6 +1198,14 @@ describe("date_utils", () => {
11901198

11911199
expect(isQuarterInRange(startDate, endDate, 1, day)).toBe(false);
11921200
});
1201+
1202+
it("should return false when the start year is after the end year", () => {
1203+
const day = newDate("2025-01-01");
1204+
const startDate = newDate("2026-01-01");
1205+
const endDate = newDate("2024-04-01");
1206+
1207+
expect(isQuarterInRange(startDate, endDate, 1, day)).toBe(false);
1208+
});
11931209
});
11941210

11951211
describe("isYearInRange", () => {
@@ -1214,6 +1230,12 @@ describe("date_utils", () => {
12141230
it("should return false if range isn't passed", () => {
12151231
expect(isYearInRange(2016)).toBe(false);
12161232
});
1233+
1234+
it("should return false if provided dates are invalid", () => {
1235+
expect(
1236+
isYearInRange(2016, new Date("invalid"), new Date("invalid")),
1237+
).toBe(false);
1238+
});
12171239
});
12181240

12191241
describe("getYearsPeriod", () => {

src/test/datepicker_test.test.tsx

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@ import {
2626
subWeeks,
2727
subYears,
2828
} from "../date_utils";
29-
import DatePicker, { registerLocale } from "../index";
29+
import DatePicker, {
30+
registerLocale,
31+
setDefaultLocale,
32+
getDefaultLocale,
33+
} from "../index";
3034

3135
import CustomInput from "./helper_components/custom_input";
3236
import ShadowRoot from "./helper_components/shadow_root";
@@ -121,6 +125,73 @@ describe("DatePicker", () => {
121125
jest.resetAllMocks();
122126
});
123127

128+
it("exposes locale helpers via the main entry point", () => {
129+
const originalLocale = getDefaultLocale();
130+
try {
131+
expect(getDefaultLocale()).toBe(originalLocale);
132+
setDefaultLocale("en-GB");
133+
expect(getDefaultLocale()).toBe("en-GB");
134+
} finally {
135+
setDefaultLocale(originalLocale);
136+
}
137+
});
138+
139+
it("does not trigger selection changes when readOnly", () => {
140+
const onChange = jest.fn();
141+
const { instance } = renderDatePickerWithRef({
142+
readOnly: true,
143+
onChange,
144+
});
145+
146+
act(() => {
147+
instance?.handleSelect(newDate("2024-05-01"));
148+
});
149+
150+
expect(onChange).not.toHaveBeenCalled();
151+
});
152+
153+
it("skips updating preSelection when readOnly", () => {
154+
const selected = newDate("2024-01-01");
155+
const { instance } = renderDatePickerWithRef({
156+
readOnly: true,
157+
selected,
158+
});
159+
160+
const originalPreSelection = instance?.state.preSelection;
161+
162+
act(() => {
163+
instance?.setPreSelection(newDate("2024-02-01"));
164+
});
165+
166+
expect(instance?.state.preSelection).toBe(originalPreSelection);
167+
});
168+
169+
it("short-circuits day key navigation when keyboard navigation is disabled", () => {
170+
const onKeyDown = jest.fn();
171+
const preSelection = newDate("2024-06-15");
172+
const { instance } = renderDatePickerWithRef({
173+
disabledKeyboardNavigation: true,
174+
onKeyDown,
175+
inline: true,
176+
selected: preSelection,
177+
});
178+
179+
act(() => {
180+
instance?.setState({ preSelection });
181+
});
182+
183+
act(() => {
184+
instance?.onDayKeyDown({
185+
key: "ArrowRight",
186+
shiftKey: false,
187+
preventDefault: jest.fn(),
188+
} as unknown as React.KeyboardEvent<HTMLDivElement>);
189+
});
190+
191+
expect(onKeyDown).toHaveBeenCalled();
192+
expect(instance?.state.preSelection).toBe(preSelection);
193+
});
194+
124195
it("should retain the calendar open status when the document visibility change", () => {
125196
const { container } = render(<DatePicker />);
126197
const input = safeQuerySelector(container, "input");

src/test/month_logic.test.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import type React from "react";
2+
import Month from "../month";
3+
import { KeyType, newDate } from "../date_utils";
4+
5+
type MonthComponentProps = React.ComponentProps<typeof Month>;
6+
7+
const buildProps = (
8+
override: Partial<MonthComponentProps> = {},
9+
): MonthComponentProps =>
10+
({
11+
day: newDate("2024-01-01"),
12+
onDayClick: jest.fn(),
13+
onDayMouseEnter: jest.fn(),
14+
onMouseLeave: jest.fn(),
15+
setPreSelection: jest.fn(),
16+
preSelection: newDate("2024-01-01"),
17+
showFourColumnMonthYearPicker: false,
18+
showTwoColumnMonthYearPicker: false,
19+
disabledKeyboardNavigation: false,
20+
...override,
21+
}) as MonthComponentProps;
22+
23+
describe("Month logic helpers", () => {
24+
it("short-circuits keyboard navigation when there is no preSelection", () => {
25+
const props = buildProps({ preSelection: undefined });
26+
const instance = new Month(props);
27+
const getVerticalOffsetSpy = jest.spyOn(instance, "getVerticalOffset");
28+
29+
instance.handleKeyboardNavigation(
30+
{
31+
preventDefault: jest.fn(),
32+
} as unknown as React.KeyboardEvent<HTMLDivElement>,
33+
KeyType.ArrowRight,
34+
1,
35+
);
36+
37+
expect(getVerticalOffsetSpy).not.toHaveBeenCalled();
38+
expect(props.setPreSelection).not.toHaveBeenCalled();
39+
});
40+
41+
it("prevents quarter navigation when the destination date is disabled", () => {
42+
const props = buildProps();
43+
const instance = new Month(props);
44+
jest.spyOn(instance, "isDisabled").mockReturnValue(true);
45+
jest.spyOn(instance, "isExcluded").mockReturnValue(false);
46+
47+
instance.handleQuarterNavigation(2, newDate("2024-04-01"));
48+
49+
expect(props.setPreSelection).not.toHaveBeenCalled();
50+
});
51+
52+
it("does not handle quarter arrow keys without a preSelection value", () => {
53+
const props = buildProps({ preSelection: undefined });
54+
const instance = new Month(props);
55+
const navigationSpy = jest.spyOn(instance, "handleQuarterNavigation");
56+
57+
instance.onQuarterKeyDown(
58+
{
59+
key: KeyType.ArrowRight,
60+
preventDefault: jest.fn(),
61+
} as unknown as React.KeyboardEvent<HTMLDivElement>,
62+
2,
63+
);
64+
65+
instance.onQuarterKeyDown(
66+
{
67+
key: KeyType.ArrowLeft,
68+
preventDefault: jest.fn(),
69+
} as unknown as React.KeyboardEvent<HTMLDivElement>,
70+
2,
71+
);
72+
73+
expect(navigationSpy).not.toHaveBeenCalled();
74+
});
75+
});

src/test/shadow_root.test.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,16 @@ describe("ShadowRoot", () => {
7575

7676
expect(container.querySelector("div")).not.toBeNull();
7777
});
78+
79+
it("should avoid re-initializing when effect runs multiple times", () => {
80+
const { container } = render(
81+
<React.StrictMode>
82+
<ShadowRoot>
83+
<div>Strict Content</div>
84+
</ShadowRoot>
85+
</React.StrictMode>,
86+
);
87+
88+
expect(container.querySelector("div")).not.toBeNull();
89+
});
7890
});

src/test/test_utils.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import { KeyType } from "../date_utils";
12
import {
23
SafeElementWrapper,
34
safeQuerySelector,
45
safeQuerySelectorAll,
6+
getKey,
57
} from "./test_utils";
68

79
describe("test_utils", () => {
@@ -37,6 +39,12 @@ describe("test_utils", () => {
3739
});
3840
});
3941

42+
describe("getKey", () => {
43+
it("should throw when key is not supported", () => {
44+
expect(() => getKey("?" as KeyType)).toThrow("Unknown key");
45+
});
46+
});
47+
4048
describe("safeQuerySelectorAll", () => {
4149
let container: HTMLElement;
4250

0 commit comments

Comments
 (0)