Skip to content

Commit c592381

Browse files
Add coverage tests for calendar interactions
1 parent 3c79e39 commit c592381

File tree

7 files changed

+477
-7
lines changed

7 files changed

+477
-7
lines changed

src/test/click_outside_wrapper.test.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,4 +194,35 @@ describe("ClickOutsideWrapper", () => {
194194

195195
removeEventListenerSpy.mockRestore();
196196
});
197+
198+
it("invokes handler registered on document with composedPath target", () => {
199+
const addEventListenerSpy = jest.spyOn(document, "addEventListener");
200+
const removeEventListenerSpy = jest.spyOn(document, "removeEventListener");
201+
202+
const { unmount } = render(
203+
<ClickOutsideWrapper onClickOutside={onClickOutsideMock}>
204+
<div>Inside</div>
205+
</ClickOutsideWrapper>,
206+
);
207+
208+
const handlerEntry = addEventListenerSpy.mock.calls.find(
209+
([type]) => type === "mousedown",
210+
);
211+
const handler = handlerEntry?.[1] as EventListener;
212+
213+
const outsideNode = document.createElement("div");
214+
const mockEvent = {
215+
composed: true,
216+
composedPath: () => [outsideNode],
217+
target: outsideNode,
218+
} as unknown as MouseEvent;
219+
220+
handler(mockEvent);
221+
222+
expect(onClickOutsideMock).toHaveBeenCalledTimes(1);
223+
224+
unmount();
225+
addEventListenerSpy.mockRestore();
226+
removeEventListenerSpy.mockRestore();
227+
});
197228
});

src/test/date_utils_test.test.ts

Lines changed: 76 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1105,6 +1105,14 @@ describe("date_utils", () => {
11051105

11061106
expect(isMonthInRange(startDate, endDate, 5, day)).toBe(true);
11071107
});
1108+
1109+
it("should return false when the start date is after the end date", () => {
1110+
const day = newDate("2024-01-01");
1111+
const startDate = newDate("2024-02-01");
1112+
const endDate = newDate("2024-01-01");
1113+
1114+
expect(isMonthInRange(startDate, endDate, 1, day)).toBe(false);
1115+
});
11081116
});
11091117

11101118
describe("getStartOfYear", () => {
@@ -1139,6 +1147,50 @@ describe("date_utils", () => {
11391147

11401148
expect(isQuarterInRange(startDate, endDate, 5, day)).toBe(true);
11411149
});
1150+
1151+
it("should return false when the start quarter is after the end quarter", () => {
1152+
const day = newDate("2024-01-01");
1153+
const startDate = newDate("2024-10-01");
1154+
const endDate = newDate("2024-04-01");
1155+
1156+
expect(isQuarterInRange(startDate, endDate, 1, day)).toBe(false);
1157+
});
1158+
});
1159+
1160+
describe("isTimeInDisabledRange", () => {
1161+
it("throws when either minTime or maxTime is missing", () => {
1162+
expect(() =>
1163+
isTimeInDisabledRange(newDate(), { minTime: newDate() }),
1164+
).toThrow("Both minTime and maxTime props required");
1165+
});
1166+
1167+
it("returns false when isWithinInterval throws", () => {
1168+
jest.isolateModules(() => {
1169+
jest.doMock("date-fns", () => {
1170+
const actual = jest.requireActual("date-fns");
1171+
return {
1172+
...actual,
1173+
isWithinInterval: () => {
1174+
throw new Error("boom");
1175+
},
1176+
};
1177+
});
1178+
1179+
// eslint-disable-next-line @typescript-eslint/no-var-requires -- isolated mock import
1180+
const {
1181+
isTimeInDisabledRange: mockedIsTimeInDisabledRange,
1182+
} = require("../date_utils");
1183+
expect(
1184+
mockedIsTimeInDisabledRange(new Date(), {
1185+
minTime: new Date(),
1186+
maxTime: new Date(),
1187+
}),
1188+
).toBe(false);
1189+
1190+
jest.resetModules();
1191+
jest.dontMock("date-fns");
1192+
});
1193+
});
11421194
});
11431195

11441196
describe("isYearInRange", () => {
@@ -1581,13 +1633,30 @@ describe("date_utils", () => {
15811633

15821634
describe("isDayInRange error handling", () => {
15831635
it("returns false when isWithinInterval throws", () => {
1584-
const testDate = new Date("2024-01-15");
1585-
const invalidStartDate = new Date("invalid");
1586-
const invalidEndDate = new Date("also-invalid");
1587-
1588-
const result = isDayInRange(testDate, invalidStartDate, invalidEndDate);
1589-
1590-
expect(result).toBe(false);
1636+
jest.isolateModules(() => {
1637+
jest.doMock("date-fns", () => {
1638+
const actual = jest.requireActual("date-fns");
1639+
return {
1640+
...actual,
1641+
isWithinInterval: () => {
1642+
throw new Error("boom");
1643+
},
1644+
};
1645+
});
1646+
1647+
// eslint-disable-next-line @typescript-eslint/no-var-requires -- isolated mock import
1648+
const { isDayInRange: mockedIsDayInRange } = require("../date_utils");
1649+
expect(
1650+
mockedIsDayInRange(
1651+
new Date("2024-01-15"),
1652+
new Date("2024-01-10"),
1653+
new Date("2024-01-20"),
1654+
),
1655+
).toBe(false);
1656+
1657+
jest.resetModules();
1658+
jest.dontMock("date-fns");
1659+
});
15911660
});
15921661

15931662
it("returns true for dates inside a valid range", () => {

src/test/datepicker_test.test.tsx

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,22 @@ import {
3636
setupMockResizeObserver,
3737
} from "./test_utils";
3838

39+
const renderDatePickerWithRef = (
40+
props: React.ComponentProps<typeof DatePicker>,
41+
) => {
42+
let instance: DatePicker | null = null;
43+
const result = render(
44+
<DatePicker
45+
ref={(node) => {
46+
instance = node;
47+
}}
48+
{...props}
49+
/>,
50+
);
51+
52+
return { ...result, instance: instance as DatePicker | null };
53+
};
54+
3955
function getSelectedDayNode(container: HTMLElement) {
4056
return (
4157
container.querySelector('.react-datepicker__day[tabindex="0"]') ?? undefined
@@ -5014,6 +5030,195 @@ describe("DatePicker", () => {
50145030
jest.useRealTimers();
50155031
});
50165032

5033+
it("deferFocusInput cancels pending timeouts before focusing input", () => {
5034+
jest.useFakeTimers();
5035+
const { instance } = renderDatePickerWithRef({
5036+
selected: newDate(),
5037+
onChange: () => {},
5038+
});
5039+
5040+
expect(instance).not.toBeNull();
5041+
5042+
const setFocusSpy = jest
5043+
.spyOn(instance as DatePicker, "setFocus")
5044+
.mockImplementation(() => undefined);
5045+
5046+
act(() => {
5047+
instance?.deferFocusInput();
5048+
instance?.deferFocusInput();
5049+
});
5050+
5051+
jest.advanceTimersByTime(1);
5052+
5053+
expect(setFocusSpy).toHaveBeenCalledTimes(2);
5054+
5055+
setFocusSpy.mockRestore();
5056+
jest.useRealTimers();
5057+
});
5058+
5059+
it("clears ranges when changed date is null and start date exists", () => {
5060+
const onChange = jest.fn();
5061+
const startDate = new Date("2024-01-15T00:00:00");
5062+
const { instance } = renderDatePickerWithRef({
5063+
inline: true,
5064+
selectsRange: true,
5065+
startDate,
5066+
endDate: null,
5067+
selected: null,
5068+
onChange,
5069+
});
5070+
5071+
act(() => {
5072+
instance?.setSelected(null);
5073+
});
5074+
5075+
expect(onChange).toHaveBeenCalledWith([null, null], undefined);
5076+
});
5077+
5078+
it("reports input errors when escaping with invalid preSelection", () => {
5079+
const onInputError = jest.fn();
5080+
const { container, instance } = renderDatePickerWithRef({
5081+
selected: null,
5082+
onChange: () => {},
5083+
onInputError,
5084+
});
5085+
5086+
act(() => {
5087+
instance?.setState({ preSelection: "invalid-date" as unknown as Date });
5088+
});
5089+
5090+
const input = safeQuerySelector<HTMLInputElement>(container, "input");
5091+
fireEvent.keyDown(input, { key: "Escape" });
5092+
5093+
expect(onInputError).toHaveBeenCalled();
5094+
});
5095+
5096+
it("reports input errors when input key down completes with invalid preSelection", () => {
5097+
const onInputError = jest.fn();
5098+
const { container, instance } = renderDatePickerWithRef({
5099+
selected: null,
5100+
onChange: () => {},
5101+
onInputError,
5102+
});
5103+
5104+
act(() => {
5105+
instance?.setState({ preSelection: "invalid-date" as unknown as Date });
5106+
});
5107+
5108+
const input = safeQuerySelector<HTMLInputElement>(container, "input");
5109+
act(() => {
5110+
fireEvent.keyDown(input, { key: "Tab" });
5111+
});
5112+
5113+
expect(onInputError).toHaveBeenCalled();
5114+
});
5115+
5116+
it("reports input errors when unsupported key is pressed in calendar grid", () => {
5117+
const onInputError = jest.fn();
5118+
const { instance } = renderDatePickerWithRef({
5119+
selected: newDate(),
5120+
onChange: () => {},
5121+
onInputError,
5122+
inline: true,
5123+
});
5124+
5125+
act(() => {
5126+
instance?.setState({ preSelection: newDate() });
5127+
});
5128+
5129+
act(() => {
5130+
instance?.onDayKeyDown({
5131+
key: "A",
5132+
shiftKey: false,
5133+
preventDefault: jest.fn(),
5134+
target: document.createElement("div"),
5135+
} as unknown as React.KeyboardEvent<HTMLDivElement>);
5136+
});
5137+
5138+
expect(onInputError).toHaveBeenCalled();
5139+
});
5140+
5141+
it("reports input errors when escape is pressed within the calendar grid", () => {
5142+
const onInputError = jest.fn();
5143+
const { instance } = renderDatePickerWithRef({
5144+
selected: null,
5145+
onChange: () => {},
5146+
onInputError,
5147+
inline: true,
5148+
});
5149+
5150+
act(() => {
5151+
instance?.setState({ preSelection: "invalid-date" as unknown as Date });
5152+
});
5153+
5154+
act(() => {
5155+
instance?.onDayKeyDown({
5156+
key: "Escape",
5157+
shiftKey: false,
5158+
preventDefault: jest.fn(),
5159+
target: document.createElement("div"),
5160+
} as unknown as React.KeyboardEvent<HTMLDivElement>);
5161+
});
5162+
5163+
expect(onInputError).toHaveBeenCalled();
5164+
});
5165+
5166+
describe("aria-live messaging", () => {
5167+
it("describes range selections", () => {
5168+
const startDate = new Date("2024-01-01T00:00:00");
5169+
const endDate = new Date("2024-01-02T00:00:00");
5170+
const { instance } = renderDatePickerWithRef({
5171+
selectsRange: true,
5172+
startDate,
5173+
endDate,
5174+
selected: endDate,
5175+
});
5176+
5177+
const message = instance?.renderAriaLiveRegion();
5178+
expect(message?.props.children).toContain("Selected start date");
5179+
});
5180+
5181+
it("describes time-only selections", () => {
5182+
const { instance } = renderDatePickerWithRef({
5183+
showTimeSelectOnly: true,
5184+
selected: new Date("2024-01-01T12:00:00"),
5185+
});
5186+
5187+
const message = instance?.renderAriaLiveRegion();
5188+
expect(message?.props.children).toContain("Selected time:");
5189+
});
5190+
5191+
it("describes year picker selections", () => {
5192+
const { instance } = renderDatePickerWithRef({
5193+
showYearPicker: true,
5194+
selected: new Date("2024-01-01T00:00:00"),
5195+
});
5196+
5197+
const message = instance?.renderAriaLiveRegion();
5198+
expect(message?.props.children).toContain("Selected year:");
5199+
});
5200+
5201+
it("describes month-year picker selections", () => {
5202+
const { instance } = renderDatePickerWithRef({
5203+
showMonthYearPicker: true,
5204+
selected: new Date("2024-03-01T00:00:00"),
5205+
});
5206+
5207+
const message = instance?.renderAriaLiveRegion();
5208+
expect(message?.props.children).toContain("Selected month:");
5209+
});
5210+
5211+
it("describes quarter picker selections", () => {
5212+
const { instance } = renderDatePickerWithRef({
5213+
showQuarterYearPicker: true,
5214+
selected: new Date("2024-06-01T00:00:00"),
5215+
});
5216+
5217+
const message = instance?.renderAriaLiveRegion();
5218+
expect(message?.props.children).toContain("Selected quarter:");
5219+
});
5220+
});
5221+
50175222
it("should handle focus on year dropdown", () => {
50185223
const { container } = render(
50195224
<DatePicker

0 commit comments

Comments
 (0)