Skip to content

Commit 06f1906

Browse files
Merge pull request #6009 from Hacker0x01/close-coverage-gaps
Add coverage tests for calendar interactions
2 parents fec3673 + 45d7780 commit 06f1906

11 files changed

+531
-13
lines changed

src/calendar.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,7 @@ export default class Calendar extends Component<CalendarProps, CalendarState> {
266266
componentDidUpdate(prevProps: CalendarProps) {
267267
if (
268268
this.props.preSelection &&
269+
isValid(this.props.preSelection) &&
269270
(!isSameDay(this.props.preSelection, prevProps.preSelection) ||
270271
this.props.monthSelectedIn !== prevProps.monthSelectedIn)
271272
) {
@@ -468,6 +469,11 @@ export default class Calendar extends Component<CalendarProps, CalendarState> {
468469
};
469470

470471
header = (date: Date = this.state.date): React.ReactElement[] => {
472+
// Return empty array if date is invalid
473+
if (!isValid(date)) {
474+
return [];
475+
}
476+
471477
const disabled = this.props.disabled;
472478
const startOfWeek = getStartOfWeek(
473479
date,
@@ -781,7 +787,9 @@ export default class Calendar extends Component<CalendarProps, CalendarState> {
781787
}
782788
return (
783789
<h2 className={classes.join(" ")}>
784-
{formatDate(date, this.props.dateFormat, this.props.locale)}
790+
{isValid(date)
791+
? formatDate(date, this.props.dateFormat, this.props.locale)
792+
: ""}
785793
</h2>
786794
);
787795
};
@@ -1112,6 +1120,17 @@ export default class Calendar extends Component<CalendarProps, CalendarState> {
11121120
};
11131121

11141122
renderAriaLiveRegion = (): React.ReactElement => {
1123+
// Don't render aria-live message if date is invalid
1124+
if (!isValid(this.state.date)) {
1125+
return (
1126+
<span
1127+
role="alert"
1128+
aria-live="polite"
1129+
className="react-datepicker__aria-live"
1130+
/>
1131+
);
1132+
}
1133+
11151134
const { startPeriod, endPeriod } = getYearsPeriod(
11161135
this.state.date,
11171136
this.props.yearItemNumber ?? Calendar.defaultProps.yearItemNumber,

src/month.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
isSameMonth,
2828
isSameQuarter,
2929
isSpaceKeyDown,
30+
isValid,
3031
newDate,
3132
setMonth,
3233
setQuarter,
@@ -447,6 +448,11 @@ export default class Month extends Component<MonthProps> {
447448
};
448449

449450
renderWeeks = () => {
451+
// Return empty array if day is invalid
452+
if (!isValid(this.props.day)) {
453+
return [];
454+
}
455+
450456
const weeks = [];
451457
const isFixedHeight = this.props.fixedHeight;
452458

@@ -1138,6 +1144,11 @@ export default class Month extends Component<MonthProps> {
11381144
? ariaLabelPrefix.trim() + " "
11391145
: "";
11401146

1147+
// Format aria-label, return empty string if date is invalid
1148+
const formattedAriaLabel = isValid(day)
1149+
? `${formattedAriaLabelPrefix}${formatDate(day, "MMMM, yyyy", this.props.locale)}`
1150+
: "";
1151+
11411152
const shouldUseListboxRole = showMonthYearPicker || showQuarterYearPicker;
11421153

11431154
if (shouldUseListboxRole) {
@@ -1150,7 +1161,7 @@ export default class Month extends Component<MonthProps> {
11501161
onPointerLeave={
11511162
this.props.usePointerEvent ? this.handleMouseLeave : undefined
11521163
}
1153-
aria-label={`${formattedAriaLabelPrefix}${formatDate(day, "MMMM, yyyy", this.props.locale)}`}
1164+
aria-label={formattedAriaLabel}
11541165
role="listbox"
11551166
>
11561167
{showMonthYearPicker ? this.renderMonths() : this.renderQuarters()}
@@ -1172,7 +1183,7 @@ export default class Month extends Component<MonthProps> {
11721183
onPointerLeave={
11731184
this.props.usePointerEvent ? this.handleMouseLeave : undefined
11741185
}
1175-
aria-label={`${formattedAriaLabelPrefix}${formatDate(day, "MMMM, yyyy", this.props.locale)}`}
1186+
aria-label={formattedAriaLabel}
11761187
role="rowgroup"
11771188
>
11781189
{this.renderWeeks()}

src/test/calendar_test.test.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
* @jest-environment jsdom
33
*/
44

5-
import { render, fireEvent, act, waitFor } from "@testing-library/react";
5+
import { render, fireEvent, waitFor } from "@testing-library/react";
6+
import { act } from "react";
67
import {
78
setDate,
89
startOfMonth,

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: 77 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -926,6 +926,41 @@ describe("date_utils", () => {
926926
});
927927
});
928928

929+
describe("isTimeInDisabledRange edge cases", () => {
930+
it("throws when either minTime or maxTime is missing", () => {
931+
expect(() =>
932+
isTimeInDisabledRange(newDate(), { minTime: newDate() }),
933+
).toThrow("Both minTime and maxTime props required");
934+
});
935+
936+
it("returns false when isWithinInterval throws", async () => {
937+
jest.doMock("date-fns", () => {
938+
const actual = jest.requireActual("date-fns");
939+
return {
940+
...actual,
941+
isWithinInterval: () => {
942+
throw new Error("boom");
943+
},
944+
};
945+
});
946+
947+
try {
948+
const { isTimeInDisabledRange: mockedIsTimeInDisabledRange } =
949+
await import("../date_utils");
950+
951+
expect(
952+
mockedIsTimeInDisabledRange(new Date(), {
953+
minTime: new Date(),
954+
maxTime: new Date(),
955+
}),
956+
).toBe(false);
957+
} finally {
958+
jest.resetModules();
959+
jest.dontMock("date-fns");
960+
}
961+
});
962+
});
963+
929964
describe("isDayInRange", () => {
930965
it("should tell if day is in range", () => {
931966
const day = newDate("2016-02-15 09:40");
@@ -1105,6 +1140,14 @@ describe("date_utils", () => {
11051140

11061141
expect(isMonthInRange(startDate, endDate, 5, day)).toBe(true);
11071142
});
1143+
1144+
it("should return false when the start date is after the end date", () => {
1145+
const day = newDate("2024-01-01");
1146+
const startDate = newDate("2024-02-01");
1147+
const endDate = newDate("2024-01-01");
1148+
1149+
expect(isMonthInRange(startDate, endDate, 1, day)).toBe(false);
1150+
});
11081151
});
11091152

11101153
describe("getStartOfYear", () => {
@@ -1139,6 +1182,14 @@ describe("date_utils", () => {
11391182

11401183
expect(isQuarterInRange(startDate, endDate, 5, day)).toBe(true);
11411184
});
1185+
1186+
it("should return false when the start quarter is after the end quarter", () => {
1187+
const day = newDate("2024-01-01");
1188+
const startDate = newDate("2024-10-01");
1189+
const endDate = newDate("2024-04-01");
1190+
1191+
expect(isQuarterInRange(startDate, endDate, 1, day)).toBe(false);
1192+
});
11421193
});
11431194

11441195
describe("isYearInRange", () => {
@@ -1580,14 +1631,33 @@ describe("date_utils", () => {
15801631
});
15811632

15821633
describe("isDayInRange error handling", () => {
1583-
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);
1634+
it("returns false when isWithinInterval throws", async () => {
1635+
jest.doMock("date-fns", () => {
1636+
const actual = jest.requireActual("date-fns");
1637+
return {
1638+
...actual,
1639+
isWithinInterval: () => {
1640+
throw new Error("boom");
1641+
},
1642+
};
1643+
});
15891644

1590-
expect(result).toBe(false);
1645+
try {
1646+
const { isDayInRange: mockedIsDayInRange } = await import(
1647+
"../date_utils"
1648+
);
1649+
1650+
expect(
1651+
mockedIsDayInRange(
1652+
new Date("2024-01-15"),
1653+
new Date("2024-01-10"),
1654+
new Date("2024-01-20"),
1655+
),
1656+
).toBe(false);
1657+
} finally {
1658+
jest.resetModules();
1659+
jest.dontMock("date-fns");
1660+
}
15911661
});
15921662

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

0 commit comments

Comments
 (0)