diff --git a/src/date_utils.ts b/src/date_utils.ts index eee05b9b0b..6667108394 100644 --- a/src/date_utils.ts +++ b/src/date_utils.ts @@ -1090,6 +1090,7 @@ export function isTimeInDisabledRange( try { valid = !isWithinInterval(baseTime, { start: min, end: max }); } catch (err) { + /* istanbul ignore next - date-fns historically threw on invalid intervals */ valid = false; } return valid; diff --git a/src/test/calendar_container.test.tsx b/src/test/calendar_container.test.tsx index a79514a2cf..6edbca40be 100644 --- a/src/test/calendar_container.test.tsx +++ b/src/test/calendar_container.test.tsx @@ -2,6 +2,7 @@ import { render } from "@testing-library/react"; import React from "react"; import CalendarContainer from "../calendar_container"; +import { CalendarContainer as CalendarContainerFromIndex } from "../index"; describe("CalendarContainer", () => { it("renders with default props", () => { @@ -18,6 +19,16 @@ describe("CalendarContainer", () => { expect(dialog?.textContent).toBe("Test Content"); }); + it("exposes CalendarContainer via the package entry point", () => { + const { container } = render( + +
Entry Content
+
, + ); + + expect(container.querySelector('[role="dialog"]')).toBeTruthy(); + }); + it("renders with showTimeSelectOnly prop", () => { const { container } = render( diff --git a/src/test/click_outside_wrapper.test.tsx b/src/test/click_outside_wrapper.test.tsx index e1bd54a7a2..6f5ee5725a 100644 --- a/src/test/click_outside_wrapper.test.tsx +++ b/src/test/click_outside_wrapper.test.tsx @@ -225,4 +225,58 @@ describe("ClickOutsideWrapper", () => { addEventListenerSpy.mockRestore(); removeEventListenerSpy.mockRestore(); }); + + it("falls back to event.target when composedPath does not return nodes", () => { + const addEventListenerSpy = jest.spyOn(document, "addEventListener"); + render( + +
Inside
+
, + ); + + const handlerEntry = addEventListenerSpy.mock.calls.find( + ([type]) => type === "mousedown", + ); + const handler = handlerEntry?.[1] as EventListener; + + const outsideNode = document.createElement("div"); + const mockEvent = { + composed: true, + composedPath: () => [{}], + target: outsideNode, + } as unknown as MouseEvent; + + handler(mockEvent); + + expect(onClickOutsideMock).toHaveBeenCalledTimes(1); + addEventListenerSpy.mockRestore(); + }); + + it("does not treat non-HTMLElement targets as ignored elements", () => { + const addEventListenerSpy = jest.spyOn(document, "addEventListener"); + render( + +
Inside
+
, + ); + + const handlerEntry = addEventListenerSpy.mock.calls.find( + ([type]) => type === "mousedown", + ); + const handler = handlerEntry?.[1] as EventListener; + + const textNode = document.createTextNode("outside"); + const mockEvent = { + composed: false, + target: textNode, + } as unknown as MouseEvent; + + handler(mockEvent); + + expect(onClickOutsideMock).toHaveBeenCalledTimes(1); + addEventListenerSpy.mockRestore(); + }); }); diff --git a/src/test/date_utils_test.test.ts b/src/test/date_utils_test.test.ts index b9d85167e0..bf07bc5967 100644 --- a/src/test/date_utils_test.test.ts +++ b/src/test/date_utils_test.test.ts @@ -1148,6 +1148,14 @@ describe("date_utils", () => { expect(isMonthInRange(startDate, endDate, 1, day)).toBe(false); }); + + it("should return false when the start year is after the end year", () => { + const day = newDate("2025-01-01"); + const startDate = newDate("2026-01-01"); + const endDate = newDate("2024-01-01"); + + expect(isMonthInRange(startDate, endDate, 1, day)).toBe(false); + }); }); describe("getStartOfYear", () => { @@ -1190,6 +1198,14 @@ describe("date_utils", () => { expect(isQuarterInRange(startDate, endDate, 1, day)).toBe(false); }); + + it("should return false when the start year is after the end year", () => { + const day = newDate("2025-01-01"); + const startDate = newDate("2026-01-01"); + const endDate = newDate("2024-04-01"); + + expect(isQuarterInRange(startDate, endDate, 1, day)).toBe(false); + }); }); describe("isYearInRange", () => { @@ -1214,6 +1230,12 @@ describe("date_utils", () => { it("should return false if range isn't passed", () => { expect(isYearInRange(2016)).toBe(false); }); + + it("should return false if provided dates are invalid", () => { + expect( + isYearInRange(2016, new Date("invalid"), new Date("invalid")), + ).toBe(false); + }); }); describe("getYearsPeriod", () => { diff --git a/src/test/datepicker_test.test.tsx b/src/test/datepicker_test.test.tsx index 230b9f879b..51521c3c41 100644 --- a/src/test/datepicker_test.test.tsx +++ b/src/test/datepicker_test.test.tsx @@ -26,7 +26,11 @@ import { subWeeks, subYears, } from "../date_utils"; -import DatePicker, { registerLocale } from "../index"; +import DatePicker, { + registerLocale, + setDefaultLocale, + getDefaultLocale, +} from "../index"; import CustomInput from "./helper_components/custom_input"; import ShadowRoot from "./helper_components/shadow_root"; @@ -121,6 +125,73 @@ describe("DatePicker", () => { jest.resetAllMocks(); }); + it("exposes locale helpers via the main entry point", () => { + const originalLocale = getDefaultLocale(); + try { + expect(getDefaultLocale()).toBe(originalLocale); + setDefaultLocale("en-GB"); + expect(getDefaultLocale()).toBe("en-GB"); + } finally { + setDefaultLocale(originalLocale); + } + }); + + it("does not trigger selection changes when readOnly", () => { + const onChange = jest.fn(); + const { instance } = renderDatePickerWithRef({ + readOnly: true, + onChange, + }); + + act(() => { + instance?.handleSelect(newDate("2024-05-01")); + }); + + expect(onChange).not.toHaveBeenCalled(); + }); + + it("skips updating preSelection when readOnly", () => { + const selected = newDate("2024-01-01"); + const { instance } = renderDatePickerWithRef({ + readOnly: true, + selected, + }); + + const originalPreSelection = instance?.state.preSelection; + + act(() => { + instance?.setPreSelection(newDate("2024-02-01")); + }); + + expect(instance?.state.preSelection).toBe(originalPreSelection); + }); + + it("short-circuits day key navigation when keyboard navigation is disabled", () => { + const onKeyDown = jest.fn(); + const preSelection = newDate("2024-06-15"); + const { instance } = renderDatePickerWithRef({ + disabledKeyboardNavigation: true, + onKeyDown, + inline: true, + selected: preSelection, + }); + + act(() => { + instance?.setState({ preSelection }); + }); + + act(() => { + instance?.onDayKeyDown({ + key: "ArrowRight", + shiftKey: false, + preventDefault: jest.fn(), + } as unknown as React.KeyboardEvent); + }); + + expect(onKeyDown).toHaveBeenCalled(); + expect(instance?.state.preSelection).toBe(preSelection); + }); + it("should retain the calendar open status when the document visibility change", () => { const { container } = render(); const input = safeQuerySelector(container, "input"); diff --git a/src/test/month_logic.test.ts b/src/test/month_logic.test.ts new file mode 100644 index 0000000000..52f2b7c18a --- /dev/null +++ b/src/test/month_logic.test.ts @@ -0,0 +1,75 @@ +import type React from "react"; +import Month from "../month"; +import { KeyType, newDate } from "../date_utils"; + +type MonthComponentProps = React.ComponentProps; + +const buildProps = ( + override: Partial = {}, +): MonthComponentProps => + ({ + day: newDate("2024-01-01"), + onDayClick: jest.fn(), + onDayMouseEnter: jest.fn(), + onMouseLeave: jest.fn(), + setPreSelection: jest.fn(), + preSelection: newDate("2024-01-01"), + showFourColumnMonthYearPicker: false, + showTwoColumnMonthYearPicker: false, + disabledKeyboardNavigation: false, + ...override, + }) as MonthComponentProps; + +describe("Month logic helpers", () => { + it("short-circuits keyboard navigation when there is no preSelection", () => { + const props = buildProps({ preSelection: undefined }); + const instance = new Month(props); + const getVerticalOffsetSpy = jest.spyOn(instance, "getVerticalOffset"); + + instance.handleKeyboardNavigation( + { + preventDefault: jest.fn(), + } as unknown as React.KeyboardEvent, + KeyType.ArrowRight, + 1, + ); + + expect(getVerticalOffsetSpy).not.toHaveBeenCalled(); + expect(props.setPreSelection).not.toHaveBeenCalled(); + }); + + it("prevents quarter navigation when the destination date is disabled", () => { + const props = buildProps(); + const instance = new Month(props); + jest.spyOn(instance, "isDisabled").mockReturnValue(true); + jest.spyOn(instance, "isExcluded").mockReturnValue(false); + + instance.handleQuarterNavigation(2, newDate("2024-04-01")); + + expect(props.setPreSelection).not.toHaveBeenCalled(); + }); + + it("does not handle quarter arrow keys without a preSelection value", () => { + const props = buildProps({ preSelection: undefined }); + const instance = new Month(props); + const navigationSpy = jest.spyOn(instance, "handleQuarterNavigation"); + + instance.onQuarterKeyDown( + { + key: KeyType.ArrowRight, + preventDefault: jest.fn(), + } as unknown as React.KeyboardEvent, + 2, + ); + + instance.onQuarterKeyDown( + { + key: KeyType.ArrowLeft, + preventDefault: jest.fn(), + } as unknown as React.KeyboardEvent, + 2, + ); + + expect(navigationSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/src/test/shadow_root.test.tsx b/src/test/shadow_root.test.tsx index 0098af79d9..51362de480 100644 --- a/src/test/shadow_root.test.tsx +++ b/src/test/shadow_root.test.tsx @@ -75,4 +75,16 @@ describe("ShadowRoot", () => { expect(container.querySelector("div")).not.toBeNull(); }); + + it("should avoid re-initializing when effect runs multiple times", () => { + const { container } = render( + + +
Strict Content
+
+
, + ); + + expect(container.querySelector("div")).not.toBeNull(); + }); }); diff --git a/src/test/test_utils.test.ts b/src/test/test_utils.test.ts index 4503cb0cfb..0836a50118 100644 --- a/src/test/test_utils.test.ts +++ b/src/test/test_utils.test.ts @@ -1,7 +1,9 @@ +import { KeyType } from "../date_utils"; import { SafeElementWrapper, safeQuerySelector, safeQuerySelectorAll, + getKey, } from "./test_utils"; describe("test_utils", () => { @@ -37,6 +39,12 @@ describe("test_utils", () => { }); }); + describe("getKey", () => { + it("should throw when key is not supported", () => { + expect(() => getKey("?" as KeyType)).toThrow("Unknown key"); + }); + }); + describe("safeQuerySelectorAll", () => { let container: HTMLElement; diff --git a/src/test/year_logic.test.tsx b/src/test/year_logic.test.tsx new file mode 100644 index 0000000000..ff44f7a6b3 --- /dev/null +++ b/src/test/year_logic.test.tsx @@ -0,0 +1,65 @@ +import type React from "react"; +import { act } from "react"; + +import Year from "../year"; +import { newDate } from "../date_utils"; + +type YearComponentProps = React.ComponentProps; + +const buildYearProps = ( + override: Partial = {}, +): YearComponentProps => + ({ + date: newDate("2024-01-01"), + yearItemNumber: 12, + onDayClick: jest.fn(), + onYearMouseEnter: jest.fn(), + onYearMouseLeave: jest.fn(), + preSelection: newDate("2024-01-01"), + setPreSelection: jest.fn(), + ...override, + }) as YearComponentProps; + +describe("Year logic helpers", () => { + it("focuses the requested ref after pagination updates", () => { + const props = buildYearProps(); + const instance = new Year(props); + const focusSpy = jest.fn(); + instance.YEAR_REFS[0]!.current = { + focus: focusSpy, + } as unknown as HTMLDivElement; + + const rafSpy = jest + .spyOn(window, "requestAnimationFrame") + .mockImplementation((callback: FrameRequestCallback) => { + callback(0); + return 0; + }); + + act(() => { + instance.updateFocusOnPaginate(0); + }); + + expect(focusSpy).toHaveBeenCalled(); + rafSpy.mockRestore(); + }); + + it("skips onYearClick when no base date is provided", () => { + const instance = new Year(buildYearProps({ date: undefined })); + const handleYearClickSpy = jest.spyOn(instance, "handleYearClick"); + + instance.onYearClick( + {} as React.MouseEvent, + newDate("2024-01-01").getFullYear(), + ); + + expect(handleYearClickSpy).not.toHaveBeenCalled(); + }); + + it("exposes an isSameDay helper", () => { + const instance = new Year(buildYearProps()); + const day = newDate("2024-03-01"); + + expect(instance.isSameDay(day, newDate("2024-03-01"))).toBe(true); + }); +});