import { render, waitFor, fireEvent } from "@testing-library/react"; import { act } from "react"; import { userEvent } from "@testing-library/user-event"; import { enUS, enGB } from "date-fns/locale"; import React, { useState } from "react"; import { OUTSIDE_CLICK_IGNORE_CLASS } from "../calendar"; import { KeyType, addDays, addMonths, addWeeks, addYears, formatDate, getDay, getEndOfWeek, getHours, getMinutes, getSeconds, getStartOfWeek, isSameDay, newDate, safeDateFormat, subDays, subMonths, subWeeks, subYears, } from "../date_utils"; import DatePicker, { registerLocale, setDefaultLocale, getDefaultLocale, } from "../index"; import CustomInput from "./helper_components/custom_input"; import ShadowRoot from "./helper_components/shadow_root"; import TestWrapper from "./helper_components/test_wrapper"; import { getKey, safeQuerySelector, setupMockResizeObserver, } from "./test_utils"; const renderDatePickerWithRef = ( props: React.ComponentProps, ) => { let instance: DatePicker | null = null; const result = render( { instance = node; }} {...props} />, ); return { ...result, instance: instance as DatePicker | null }; }; function getSelectedDayNode(container: HTMLElement) { return ( container.querySelector('.react-datepicker__day[tabindex="0"]') ?? undefined ); } function findSelectedDay(container: HTMLElement, targetDate: Date) { const days = Array.from( container.querySelectorAll(".react-datepicker__day"), ); return days.find( (d) => d.getAttribute("aria-label") === `Choose ${formatDate(targetDate, "PPPP")}`, ); } function goToLastMonth(container: HTMLElement) { const lastMonthButton = container.querySelector( ".react-datepicker__navigation-icon--previous", ); expect(lastMonthButton).toBeTruthy(); fireEvent.click(lastMonthButton!); } function goToNextMonth(container: HTMLElement) { const nextMonthButton = safeQuerySelector( container, ".react-datepicker__navigation-icon--next", ); fireEvent.click(nextMonthButton!); } function formatDayWithZeros(day: number) { const dayString = day.toString(); if (dayString.length === 1) { return `00${dayString}`; } if (dayString.length === 2) { return `0${dayString}`; } return dayString; } const hideDocument = (calendarInput: HTMLElement) => { jest.spyOn(document, "visibilityState", "get").mockReturnValue("hidden"); fireEvent(document, new Event("visibilitychange")); // Blur, To simulate the browser auto-blur the input before document hide fireEvent.blur(calendarInput); }; const showDocument = (calendarInput: HTMLElement) => { jest.spyOn(document, "visibilityState", "get").mockReturnValue("visible"); fireEvent(document, new Event("visibilitychange")); // Focus, To simulate the browser auto-refocus of the input fireEvent.focus(calendarInput); }; describe("DatePicker", () => { beforeEach(() => { setupMockResizeObserver(); }); afterEach(() => { 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"); if (!input) { throw new Error("Input element not found"); } fireEvent.click(input); expect(container.querySelector(".react-datepicker")).toBeTruthy(); hideDocument(input); showDocument(input); expect(container.querySelector(".react-datepicker")).toBeTruthy(); }); it("should retain the calendar close status when the document visibility change", () => { const { container } = render(); const input = safeQuerySelector(container, "input"); fireEvent.click(input); expect(container.querySelector(".react-datepicker")).toBeTruthy(); fireEvent.keyDown(input, getKey(KeyType.Escape)); expect(container.querySelector(".react-datepicker")).toBeFalsy(); hideDocument(input); showDocument(input); expect(container.querySelector(".react-datepicker")).toBeFalsy(); }); it("should be executed props.onFocus on input focus when the document visibility changes", () => { const onFocusSpy = jest.fn(); const { container } = render(); const input = safeQuerySelector(container, "input"); fireEvent.focus(input); expect(onFocusSpy).toHaveBeenCalled(); expect(container.querySelector(".react-datepicker")).toBeTruthy(); fireEvent.keyDown(input, getKey(KeyType.Escape)); fireEvent.blur(input); expect(container.querySelector(".react-datepicker")).toBeFalsy(); hideDocument(input); showDocument(input); expect(onFocusSpy).toHaveBeenCalled(); expect(container.querySelector(".react-datepicker")).toBeFalsy(); fireEvent.click(input); expect(onFocusSpy).toHaveBeenCalled(); expect(container.querySelector(".react-datepicker")).toBeTruthy(); }); it("should show the calendar when focusing on the date input", () => { const { container } = render(); const input = safeQuerySelector(container, "input"); fireEvent.focus(input); expect(container.querySelector(".react-datepicker")).not.toBeNull(); }); it("should allow the user to supply a wrapper component for the popper", () => { const { container } = render(); const input = safeQuerySelector(container, "input"); fireEvent.focus(input); expect(container.querySelectorAll(".test-wrapper").length).toBe(1); expect(container.querySelector(".react-datepicker")).not.toBeNull(); }); it("should allow the user to pass a wrapper component for the calendar", () => { const { container } = render( , ); const input = safeQuerySelector(container, "input"); fireEvent.focus(input); expect(container.querySelectorAll(".test-wrapper").length).toBe(1); expect(container.querySelector(".react-datepicker")).not.toBeNull(); }); it("should pass a custom class to the popper container", () => { const { container } = render( , ); const input = safeQuerySelector(container, "input"); fireEvent.focus(input); const popper = container.querySelectorAll(".react-datepicker-popper"); expect(popper.length).toBe(1); expect(popper[0]?.classList.contains("some-class-name")).toBe(true); }); it("should use popperTargetRef for positioning when provided", () => { const buttonRef = React.createRef(); /* eslint-disable react-hooks/refs -- passing ref object and callbacks as props, not accessing .current */ // Custom input component that exposes a button ref separately from the main input ref const CustomInputWithButton: React.FC<{ value?: string; onClick?: () => void; buttonRef?: React.RefObject; }> = (props) => { return (
); }; /* eslint-enable react-hooks/refs */ const { container } = render( } popperTargetRef={buttonRef} />, ); const button = safeQuerySelector( container, '[data-testid="custom-button"]', ); fireEvent.click(button); // Verify the popper is shown const popper = container.querySelector(".react-datepicker-popper"); expect(popper).not.toBeNull(); // Verify the button ref was properly set expect(buttonRef.current).toBe(button); }); it("should show the calendar when clicking on the date input", () => { const { container } = render(); const input = safeQuerySelector(container, "input"); fireEvent.click(input); expect(container.querySelector(".react-datepicker")).not.toBeNull(); }); it("should render the calendar in the portalHost prop when provided", () => { const root = document.createElement("div"); const shadow = root.attachShadow({ mode: "closed" }); const appHost = document.createElement("div"); shadow.appendChild(appHost); let instance: DatePicker | null = null; render( { instance = node; }} portalId="test-portal" portalHost={shadow} />, { container: appHost, }, ); expect(instance).toBeTruthy(); expect(instance!.input).toBeTruthy(); fireEvent.click(instance!.input!); expect(instance!.calendar).toBeDefined(); expect(shadow.getElementById("test-portal")).toBeDefined(); }); it("calendar should stay open when clicked within shadow dom and closed when clicked outside", async () => { let instance: DatePicker | null = null; render( { instance = node; }} /> , ); expect(instance).toBeTruthy(); expect(instance!.input).toBeTruthy(); await userEvent.click(instance!.input!); expect(instance!.isCalendarOpen()).toBe(true); expect(instance!.calendar).toBeTruthy(); expect(instance!.calendar!.containerRef.current).toBeTruthy(); await userEvent.click(instance!.calendar!.containerRef.current!); expect(instance!.isCalendarOpen()).toBe(true); await userEvent.click(document.body); expect(instance!.isCalendarOpen()).toBe(false); }); it("should not set open state when it is disabled and gets clicked", () => { const { container } = render(); const input = safeQuerySelector(container, "input"); fireEvent.click(input); expect(container.querySelector(".react-datepicker")).toBeNull(); }); it("should close the popper and return focus to the date input on Escape.", async () => { // https://www.w3.org/TR/wai-aria-practices/examples/dialog-modal/datepicker-dialog.html // Date Picker Dialog | Escape | Closes the dialog and returns focus to the Choose Date button. const div = document.createElement("div"); document.body.appendChild(div); let instance: DatePicker | null = null; render( { instance = node; }} />, { container: div, }, ); // user focuses the input field, the calendar opens expect(instance).toBeTruthy(); expect(instance!.input).toBeTruthy(); fireEvent.click(instance!.input!); // user may tab or arrow down to the current day (or some other element in the popper) const today = safeQuerySelector(div, ".react-datepicker__day--today"); act(() => { today?.focus(); }); // user hits Escape fireEvent.keyDown(today, getKey(KeyType.Escape)); expect(instance!.calendar).toBeFalsy(); await waitFor(() => { expect(instance!.state.preventFocus).toBe(false); expect(document.activeElement).toBe(div.querySelector("input")); }); }); it("should close the popper and return focus to the date input on Enter.", async () => { // https://www.w3.org/TR/wai-aria-practices/examples/dialog-modal/datepicker-dialog.html // Date Picker Dialog | Date Grid | Enter | Closes the dialog and returns focus to the Choose Date button. const div = document.createElement("div"); document.body.appendChild(div); let instance: DatePicker | null = null; render( { instance = node; }} />, { container: div, }, ); // user focuses the input field, the calendar opens expect(instance).toBeTruthy(); expect(instance!.input).toBeTruthy(); fireEvent.focus(instance!.input!); // user may tab or arrow down to the current day (or some other element in the popper) const today = safeQuerySelector(div, ".react-datepicker__day--today"); act(() => { today?.focus(); }); // user hits Enter fireEvent.keyDown(today, getKey(KeyType.Enter)); expect(instance!.calendar).toBeFalsy(); await waitFor(() => { expect(instance!.state.preventFocus).toBe(false); expect(document.activeElement).toBe(div.querySelector("input")); }); }); it("should not close the popper and keep focus on selected date if showTimeSelect is enabled.", async () => { const div = document.createElement("div"); document.body.appendChild(div); let instance: DatePicker | null = null; render( { instance = node; }} showTimeSelect />, { container: div, }, ); // user focuses the input field, the calendar opens expect(instance).toBeTruthy(); expect(instance!.input).toBeTruthy(); fireEvent.focus(instance!.input!); // user may tab or arrow down to the current day (or some other element in the popper) const today = safeQuerySelector(div, ".react-datepicker__day--today"); act(() => { today.focus(); }); // user hits Enter fireEvent.keyDown(today, getKey(KeyType.Enter)); expect(instance!.calendar).toBeTruthy(); await waitFor(() => { expect(document.activeElement).toBe(today); }); }); it("should not re-focus the date input when focusing the year dropdown", () => { const onBlurSpy = jest.fn(); const { container } = render( , ); const input = safeQuerySelector(container, "input"); const focusSpy = jest.spyOn(input, "focus"); fireEvent.focus(input); const yearSelect = safeQuerySelector( container, ".react-datepicker__year-select", ); fireEvent.blur(input); fireEvent.focus(yearSelect); expect(focusSpy).not.toHaveBeenCalled(); expect(onBlurSpy).not.toHaveBeenCalled(); }); it("should fire onYearChange when the year is selected", () => { const onYearChangeSpy = jest.fn(); const { container } = render( , ); const input = safeQuerySelector(container, "input"); fireEvent.click(input); const yearSelect = safeQuerySelector( container, ".react-datepicker__year-select", ); fireEvent.change(yearSelect, { target: { value: Array.from(yearSelect.querySelectorAll("option")).at(-2)?.value, }, }); expect(onYearChangeSpy).toHaveBeenCalled(); }); it("should keep the calendar shown when clicking the calendar", () => { const { container } = render(); const input = safeQuerySelector(container, "input"); fireEvent.focus(input); fireEvent.click(input); expect(container.querySelector(".react-datepicker")).not.toBeNull(); }); it("should not set open state when it is disabled and gets clicked.", () => { const { container } = render(); const input = safeQuerySelector(container, "input"); fireEvent.click(input); expect(container.querySelector(".react-datepicker")).toBeNull(); }); it("should not set open state when it is readOnly and gets clicked", () => { const { container } = render(); const input = safeQuerySelector(container, "input"); fireEvent.click(input); expect(container.querySelector(".react-datepicker")).toBeNull(); }); it("should hide the calendar when clicking a day on the calendar", () => { const { container } = render(); const input = safeQuerySelector(container, "input"); fireEvent.focus(input); const day = safeQuerySelector(container, ".react-datepicker__day"); fireEvent.click(day); expect(container.querySelector(".react-datepicker")).toBeNull(); }); it("should not hide the calendar when clicking a day on the calendar and shouldCloseOnSelect prop is false", () => { const { container } = render(); const input = safeQuerySelector(container, "input"); fireEvent.focus(input); const day = safeQuerySelector(container, ".react-datepicker__day"); fireEvent.click(day); expect(container.querySelector(".react-datepicker")).not.toBeNull(); }); it("should keep focus within calendar when clicking a day on the calendar and shouldCloseOnSelect prop is false", () => { const div = document.createElement("div"); document.body.appendChild(div); render(, { container: div, }); // user focuses the input field, the calendar opens const dateInput = safeQuerySelector(div, "input"); fireEvent.focus(dateInput); // user may tab or arrow down to the current day (or some other element in the popper) const today = safeQuerySelector(div, ".react-datepicker__day--today"); act(() => { today?.focus(); }); // user hits Enter fireEvent.keyDown(today, getKey(KeyType.Enter)); expect(document.activeElement).toBe(today); }); it("should set open to true if showTimeInput is true", () => { let instance: DatePicker | null = null; render( { instance = node; }} shouldCloseOnSelect={false} showTimeInput />, ); expect(instance).toBeTruthy(); const handleTimeChange = instance!.handleTimeChange; act(() => { handleTimeChange(newDate()); }); expect(instance!.state.open).toBe(true); }); it("should not hide the calendar when selecting a day in the calendar with Enter press, and shouldCloseOnSelect prop is false", () => { const data = getOnInputKeyDownStuff({ shouldCloseOnSelect: false }); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowUp)); fireEvent.keyDown(data.dateInput, getKey(KeyType.Enter)); expect(data.instance.state.open).toBe(true); }); it("should update the preSelection state when a day is selected with Enter press", () => { const data = getOnInputKeyDownStuff({ shouldCloseOnSelect: false, selected: "2024-06-06", }); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); const selectedDayNode = getSelectedDayNode(data.container); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.ArrowDown)); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.ArrowDown)); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.Enter)); data.copyM = addWeeks(data.copyM, 2); expect(data.instance.state.preSelection).toBeTruthy(); expect(formatDate(data.instance.state.preSelection!, data.testFormat)).toBe( formatDate(data.copyM, data.testFormat), ); }); it("should update the preSelection state when a day is selected with mouse click", () => { const data = getOnInputKeyDownStuff({ shouldCloseOnSelect: false, }); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); // put focus on current day const today = getSelectedDayNode(data.container); // store current day node const dayToClick = today?.nextElementSibling || today?.previousElementSibling; // choose next or previous day const copyM = today?.nextElementSibling ? addDays(data.copyM, 1) : subDays(data.copyM, 1); // update copyM to expected date expect(dayToClick).toBeTruthy(); fireEvent.click(dayToClick!); // will update the preSelection data.copyM = copyM; expect(data.instance.state.preSelection).toBeTruthy(); expect(formatDate(data.instance.state.preSelection!, data.testFormat)).toBe( formatDate(data.copyM, data.testFormat), ); }); it("should update the preSelection state when Today button is clicked after selecting a different day for inline mode", () => { let instance: DatePicker | null = null; const { container } = render( {}} ref={(node) => { instance = node; }} />, ); expect(instance).toBeTruthy(); const today = getSelectedDayNode(container!); const anyOtherDay = today?.nextElementSibling || today?.previousElementSibling; expect(anyOtherDay).toBeTruthy(); fireEvent.click(anyOtherDay!); // will update the preSelection to next or previous day const todayBtn = instance!.calendar?.containerRef.current?.querySelector( ".react-datepicker__today-button", ); expect(anyOtherDay).toBeTruthy(); fireEvent.click(todayBtn!); // will update the preSelection expect(instance!.state.preSelection).toBeTruthy(); expect(formatDate(instance!.state.preSelection!, "yyyy-MM-dd")).toBe( formatDate(newDate(), "yyyy-MM-dd"), ); }); it("should hide the calendar when pressing enter in the date input", () => { const { container } = render(); const input = safeQuerySelector(container, "input"); fireEvent.focus(input); fireEvent.keyDown(input, getKey(KeyType.Enter)); expect(container.querySelector(".react-datepicker")).toBeNull(); }); it("should hide the calendar when the pressing escape in the date input", () => { const { container } = render(); const input = safeQuerySelector(container, "input"); fireEvent.focus(input); fireEvent.keyDown(input, getKey(KeyType.Escape)); expect(container.querySelector(".react-datepicker")).toBeNull(); }); it("should hide the calendar and keep focus on input when pressing escape in the date input", async () => { const div = document.createElement("div"); document.body.appendChild(div); let instance: DatePicker | null = null; render( { instance = node; }} />, { container: div, }, ); // user focuses the input field, the calendar opens expect(instance).toBeTruthy(); expect(instance!.input).toBeTruthy(); fireEvent.focus(instance!.input!); fireEvent.keyDown(instance!.input!, getKey(KeyType.Escape)); expect(instance!.calendar).toBeFalsy(); await waitFor(() => { expect(document.activeElement).toBe(instance!.input); }); }); it("should auto-close the datepicker and lose focus when Tab key is pressed when the date input is focused", async () => { const { container } = render(); const input = safeQuerySelector(container, "input"); fireEvent.focus(input); let reactCalendar = container.querySelector("div.react-datepicker"); expect(reactCalendar).not.toBeNull(); fireEvent.keyDown(input, getKey(KeyType.Tab)); reactCalendar = container.querySelector("div.react-datepicker"); expect(reactCalendar).toBeNull(); await waitFor(() => { expect(document.activeElement).not.toBe(input); }); }); it("should hide the calendar when the pressing Shift + Tab in the date input", async () => { // eslint-disable-next-line prefer-const let onBlurSpy: ReturnType; const onBlur: React.FocusEventHandler = ( event: React.FocusEvent, ): void => { onBlurSpy(event); }; const { container } = render(); const input = safeQuerySelector(container, "input"); onBlurSpy = jest.spyOn(input, "blur"); fireEvent.focus(input); fireEvent.keyDown(input, getKey(KeyType.Tab, true)); expect(container.querySelector(".react-datepicker")).toBeNull(); await waitFor(() => { expect(onBlurSpy).toHaveBeenCalled(); }); }); it("should not apply the default outsideClickIgnoreClass class to the date input when closed", () => { const { container } = render(); const input = safeQuerySelector(container, "input"); expect(input?.classList.contains(OUTSIDE_CLICK_IGNORE_CLASS)).toBe(false); }); it("should apply the default outsideClickIgnoreClass class to date input when open", () => { const { container } = render(); const input = safeQuerySelector(container, "input"); fireEvent.focus(input); expect(input?.classList.contains(OUTSIDE_CLICK_IGNORE_CLASS)).toBe(true); }); it("should apply the outsideClickIgnoreClass class to date input when open", () => { const outsideClickIgnoreClass = "ignore-onclickoutside"; const { container } = render( , ); const input = safeQuerySelector(container, "input"); fireEvent.focus(input); expect(input?.classList.contains(outsideClickIgnoreClass)).toBe(true); }); it("should apply the default outsideClickIgnoreClass when prop is undefined", () => { const { container } = render( , ); const input = safeQuerySelector(container, "input"); fireEvent.focus(input); expect(input?.classList.contains(OUTSIDE_CLICK_IGNORE_CLASS)).toBe(true); }); it("should apply the default outsideClickIgnoreClass when prop is falsy", () => { const { container } = render( , ); const input = safeQuerySelector(container, "input"); fireEvent.focus(input); expect(input?.classList.contains(OUTSIDE_CLICK_IGNORE_CLASS)).toBe(true); }); it("should toggle the open status of calendar on click of the icon when toggleCalendarOnIconClick is set to true", () => { const { container } = render( , ); const calendarIcon = safeQuerySelector( container, "svg.react-datepicker__calendar-icon", ); fireEvent.click(calendarIcon); const reactCalendar = container.querySelector( "div.react-datepicker-popper .react-datepicker", ); expect(reactCalendar).not.toBeNull(); }); it("should not toggle the open status of calendar on click of the icon if toggleCalendarOnIconClick is set to false", () => { const { container } = render( , ); const calendarIcon = safeQuerySelector( container, "svg.react-datepicker__calendar-icon", ); fireEvent.click(calendarIcon); const reactCalendar = container.querySelector( "div.react-datepicker-popper .react-datepicker", ); expect(reactCalendar).toBeNull(); }); it("should not apply the react-datepicker-ignore-onclickoutside class to calendar icon when closed", () => { const { container } = render( , ); const calendarIcon = container.querySelector( ".react-datepicker__calendar-icon", ); expect( calendarIcon?.classList.contains( "react-datepicker-ignore-onclickoutside", ), ).toBe(false); }); it("should apply the react-datepicker-ignore-onclickoutside class to calendar icon when open", () => { const { container } = render( , ); let calendarIcon = safeQuerySelector( container, "svg.react-datepicker__calendar-icon", ); fireEvent.click(calendarIcon); calendarIcon = safeQuerySelector( container, "svg.react-datepicker__calendar-icon", ); expect( calendarIcon?.classList.contains( "react-datepicker-ignore-onclickoutside", ), ).toBe(true); }); it("should apply the calendarIconClassName to calendar icon", () => { const customClassName = "customClassName"; const { container } = render( , ); const calendarIcon = container.querySelector( "svg.react-datepicker__calendar-icon", ); expect(calendarIcon?.classList.contains(customClassName)).toBe(true); }); it("should not apply the calendarIconClassname to calendar icon with calendarIconClassName", () => { // Suppress expected deprecation warning const warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {}); const customClassName = "customClassName"; const customClassname = "customClassname"; const { container } = render( , ); const calendarIcon = container.querySelector( "svg.react-datepicker__calendar-icon", ); expect(calendarIcon?.classList.contains(customClassName)).toBe(true); expect(calendarIcon?.classList.contains(customClassname)).toBe(false); warnSpy.mockRestore(); }); it("should apply the calendarIconClassname to calendar icon without calendarIconClassName", () => { // Suppress expected deprecation warning const warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {}); const customClassname = "customClassName"; const { container } = render( , ); const calendarIcon = container.querySelector( "svg.react-datepicker__calendar-icon", ); expect(calendarIcon?.classList.contains(customClassname)).toBe(true); warnSpy.mockRestore(); }); it("should set the type attribute on the clear button to button", () => { const { container } = render( , ); const clearButton = container.querySelector( ".react-datepicker__close-icon", ); expect(clearButton?.type).toBe("button"); }); it("should allow clearing the date when isClearable is true", () => { let cleared = false; function handleChange(d: Date | null) { if (d === null) { cleared = true; } } const { container } = render( , ); const clearButton = safeQuerySelector( container, ".react-datepicker__close-icon", ); fireEvent.click(clearButton); expect(cleared).toBe(true); }); it("should clear input value in the local state", () => { let instance: DatePicker | null = null; const { container } = render( { instance = node; }} selected={newDate("2015-12-15")} isClearable />, ); expect(instance).toBeTruthy(); const clearButton = safeQuerySelector( container, ".react-datepicker__close-icon", ); fireEvent.click(clearButton); expect(instance!.state.inputValue).toBeNull(); }); it("should disable the clear button when the component is disabled", () => { const onChange = jest.fn(); const { getByLabelText } = render( , ); const clearButton = getByLabelText("clear"); expect(clearButton).toHaveProperty("disabled", true); fireEvent.click(clearButton); expect(onChange).not.toHaveBeenCalled(); }); it("should return focus to input when clear button is used", async () => { const div = document.createElement("div"); document.body.appendChild(div); const { container } = render( , { container: div, }, ); const clearButton = safeQuerySelector( container, ".react-datepicker__close-icon", ); fireEvent.click(clearButton); await waitFor(() => { expect(document.activeElement).toBe(div.querySelector("input")); }); }); it("should set the title attribute on the clear button if clearButtonTitle is supplied", () => { const { container } = render( , ); const clearButton = container.querySelector( ".react-datepicker__close-icon", ); expect(clearButton?.getAttribute("title")).toBe("clear button"); }); it("should customize the className attribute on the clear button if clearButtonClassName is supplied", () => { const className = "customized-close-icon"; const { container } = render( , ); expect( container .querySelector(".react-datepicker__close-icon") ?.classList.contains(className), ).toBeTruthy(); }); it("should save time from the selected date during day change", () => { const selected = newDate("2015-12-20 10:11:12"); let date: Date | null = null; const { container } = render( { date = d; }} />, ); const dayButton = safeQuerySelector(container, ".react-datepicker__day"); fireEvent.click(dayButton); expect(date).toBeTruthy(); expect(getHours(date!)).toBe(10); expect(getMinutes(date!)).toBe(11); expect(getSeconds(date!)).toBe(12); }); it("should save time from the selected date during date change", () => { const selected = newDate("2015-12-20 10:11:12"); let date: Date | null = null; const { container } = render( { date = d; }} />, ); const input = safeQuerySelector(container, "input"); fireEvent.change(input, { target: { value: "01/02/2014", }, }); expect(date).toBeTruthy(); expect(getHours(date!)).toBe(10); expect(getMinutes(date!)).toBe(11); expect(getSeconds(date!)).toBe(12); }); it("should mount and unmount properly", () => { type State = { mounted: boolean; }; class TestComponent extends React.Component { constructor(props: object) { super(props); this.state = { mounted: true }; } render() { return this.state.mounted ? : null; } } let instance: TestComponent | null = null; const { rerender } = render( { instance = node; }} />, ); expect(instance).toBeTruthy(); act(() => { (instance!.state as unknown as { mounted: boolean }).mounted = false; }); rerender( { instance = node; }} />, ); }); it("should render calendar inside PopperComponent when inline prop is not set", () => { const { container } = render(); expect(container.querySelector(".react-datepicker-wrapper")).not.toBeNull(); }); it("should render calendar directly without PopperComponent when inline prop is set", () => { const { container } = render(); expect(container.querySelector(".react-datepicker-wrapper")).toBeNull(); expect(container.querySelector(".react-datepicker")).not.toBeNull(); }); it("should ignore disable prop when inline prop is set", () => { const { container } = render(); expect(container.querySelector(".react-datepicker")).not.toBeNull(); }); it("should ignore withPortal prop when inline prop is set", () => { const { container } = render(); expect(container.querySelector(".react-datepicker__portal")).toBeNull(); }); it("should render Calendar in portal when withPortal is set and input has focus", () => { const { container } = render(); const input = safeQuerySelector(container, "input"); fireEvent.focus(input); expect( document.body.querySelector(".react-datepicker__portal"), ).not.toBeNull(); expect(container.querySelector(".react-datepicker")).not.toBeNull(); }); it("should render Calendar in portal when withPortal is set and should close on Escape key when focus is on header", () => { let instance: DatePicker | null = null; render( { instance = node; }} withPortal portalId="portal-id-dom-test" />, ); expect(instance).toBeTruthy(); expect(instance!.input).toBeTruthy(); fireEvent.focus(instance!.input!); expect( document.body.querySelector(".react-datepicker__portal"), ).not.toBeNull(); expect(instance!.calendar).toBeDefined(); const header = instance!.calendar?.containerRef.current?.querySelector( ".react-datepicker__current-month", ); expect(header).toBeTruthy(); fireEvent.click(header!); fireEvent.keyDown(header!, getKey(KeyType.Escape)); expect(instance!.calendar).toBeFalsy(); }); it("should not render Calendar when withPortal is set and no focus is given to input", () => { const { container } = render(); expect(document.body.querySelector(".react-datepicker__portal")).toBeNull(); expect(container.querySelector(".react-datepicker")).toBeNull(); }); it("should not render an extra wrapper div when withPortal is set", () => { // Fixes #5499 - withPortal should use React Fragment instead of a wrapper div const { container } = render(); // The container should directly contain the input container, not wrapped in an extra div // Before the fix:
...
// After the fix:
...
(no extra wrapper) const inputContainer = container.querySelector( ".react-datepicker__input-container", ); expect(inputContainer).not.toBeNull(); expect(inputContainer?.parentElement).toBe(container); }); it("should render Calendar in a ReactDOM portal when withPortal is set and portalId is set", () => { const { container } = render( , ); const input = safeQuerySelector(container, "input"); fireEvent.focus(input); expect(document.body.querySelector("#portal-id-dom-test")).not.toBeNull(); }); // eslint-disable-next-line @typescript-eslint/no-explicit-any function getOnInputKeyDownStuff(opts: { [key: string]: any } = {}) { const m = newDate(opts.selected); const copyM = newDate(m); const testFormat = "yyyy-MM-dd"; const exactishFormat = "yyyy-MM-dd hh: zzzz"; const callback = jest.fn(); const onInputErrorCallback = jest.fn(); let instance: DatePicker | null = null; const { container, rerender } = render( { instance = node; }} />, ); expect(instance).toBeTruthy(); expect(instance!.input).toBeTruthy(); const dateInput = instance!.input!; const dateCalendar = instance!.calendar; fireEvent.focus(dateInput); return { m, copyM, testFormat, exactishFormat, callback, onInputErrorCallback, datePicker: container.firstChild, dateInput, dateCalendar, rerender, instance: instance!, container, }; } it("should handle onDayKeyDown ArrowLeft", () => { const data = getOnInputKeyDownStuff(); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); const selectedDayNode = getSelectedDayNode(data.container); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.ArrowLeft)); data.copyM = subDays(data.copyM, 1); expect(data.instance.state.preSelection).toBeTruthy(); expect(formatDate(data.instance.state.preSelection!, data.testFormat)).toBe( formatDate(data.copyM, data.testFormat), ); }); it("should handle onDayKeyDown ArrowRight", () => { const data = getOnInputKeyDownStuff(); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); const selectedDayNode = getSelectedDayNode(data.container); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.ArrowRight)); data.copyM = addDays(data.copyM, 1); expect(data.instance.state.preSelection).toBeTruthy(); expect(formatDate(data.instance.state.preSelection!, data.testFormat)).toBe( formatDate(data.copyM, data.testFormat), ); }); it("should handle onDayKeyDown ArrowUp", () => { const data = getOnInputKeyDownStuff(); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); const selectedDayNode = getSelectedDayNode(data.container); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.ArrowUp)); data.copyM = subWeeks(data.copyM, 1); expect(data.instance.state.preSelection).toBeTruthy(); expect(formatDate(data.instance.state.preSelection!, data.testFormat)).toBe( formatDate(data.copyM, data.testFormat), ); }); it("should handle onDayKeyDown ArrowDown", () => { const data = getOnInputKeyDownStuff(); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); const selectedDayNode = getSelectedDayNode(data.container); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.ArrowDown)); data.copyM = addWeeks(data.copyM, 1); expect(data.instance.state.preSelection).toBeTruthy(); expect(formatDate(data.instance.state.preSelection!, data.testFormat)).toBe( formatDate(data.copyM, data.testFormat), ); }); it("should handle onDayKeyDown PageUp", () => { const data = getOnInputKeyDownStuff(); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); const selectedDayNode = getSelectedDayNode(data.container); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.PageUp)); data.copyM = subMonths(data.copyM, 1); expect(data.instance.state.preSelection).toBeTruthy(); expect(formatDate(data.instance.state.preSelection!, data.testFormat)).toBe( formatDate(data.copyM, data.testFormat), ); }); it("should handle onDayKeyDown Shift+PageUp", () => { const data = getOnInputKeyDownStuff(); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); const selectedDayNode = getSelectedDayNode(data.container); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.PageUp, true)); data.copyM = subYears(data.copyM, 1); expect(data.instance.state.preSelection).toBeTruthy(); expect(formatDate(data.instance.state.preSelection!, data.testFormat)).toBe( formatDate(data.copyM, data.testFormat), ); }); it("should handle onDayKeyDown PageDown", () => { const data = getOnInputKeyDownStuff(); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); const selectedDayNode = getSelectedDayNode(data.container); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.PageDown)); data.copyM = addMonths(data.copyM, 1); expect(data.instance.state.preSelection).toBeTruthy(); expect(formatDate(data.instance.state.preSelection!, data.testFormat)).toBe( formatDate(data.copyM, data.testFormat), ); }); it("should handle onDayKeyDown Shift+PageDown", () => { const data = getOnInputKeyDownStuff(); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); const selectedDayNode = getSelectedDayNode(data.container); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.PageDown, true)); data.copyM = addYears(data.copyM, 1); expect(data.instance.state.preSelection).toBeTruthy(); expect(formatDate(data.instance.state.preSelection!, data.testFormat)).toBe( formatDate(data.copyM, data.testFormat), ); }); it("should handle onDayKeyDown End", () => { const data = getOnInputKeyDownStuff(); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); const selectedDayNode = getSelectedDayNode(data.container); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.End)); data.copyM = getEndOfWeek(data.copyM); expect(data.instance.state.preSelection).toBeTruthy(); expect(formatDate(data.instance.state.preSelection!, data.testFormat)).toBe( formatDate(data.copyM, data.testFormat), ); }); it("should handle onDayKeyDown Home", () => { const data = getOnInputKeyDownStuff(); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); const selectedDayNode = getSelectedDayNode(data.container); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.Home)); data.copyM = getStartOfWeek(data.copyM); expect(data.instance.state.preSelection).toBeTruthy(); expect(formatDate(data.instance.state.preSelection!, data.testFormat)).toBe( formatDate(data.copyM, data.testFormat), ); }); it("should handle onDayKeyDown when the minDate is null", () => { const data = getOnInputKeyDownStuff({ minDate: null }); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); const selectedDayNode = getSelectedDayNode(data.container); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.ArrowLeft)); data.copyM = subDays(data.copyM, 1); expect(data.instance.state.preSelection).toBeTruthy(); expect(formatDate(data.instance.state.preSelection!, data.testFormat)).toBe( formatDate(data.copyM, data.testFormat), ); }); it("should handle onDayKeyDown when the maxDate is null", () => { const data = getOnInputKeyDownStuff({ minDate: null }); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); const selectedDayNode = getSelectedDayNode(data.container); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.ArrowRight)); data.copyM = addDays(data.copyM, 1); expect(data.instance.state.preSelection).toBeTruthy(); expect(formatDate(data.instance.state.preSelection!, data.testFormat)).toBe( formatDate(data.copyM, data.testFormat), ); }); it("should handle onDayKeyDown when both the minDate and the maxDate is null", () => { const data = getOnInputKeyDownStuff({ minDate: null }); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); const selectedDayNode = getSelectedDayNode(data.container); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.ArrowLeft)); data.copyM = subDays(data.copyM, 1); expect(data.instance.state.preSelection).toBeTruthy(); expect(formatDate(data.instance.state.preSelection!, data.testFormat)).toBe( formatDate(data.copyM, data.testFormat), ); (selectedDayNode as HTMLElement).focus(); fireEvent.keyDown( document.activeElement || document.body, getKey(KeyType.ArrowRight), ); data.copyM = addDays(data.copyM, 1); expect(data.instance.state.preSelection).toBeTruthy(); expect(formatDate(data.instance.state.preSelection!, data.testFormat)).toBe( formatDate(data.copyM, data.testFormat), ); }); // excluded stuff it("should skip over excluded date when ArrowRight is pressed", () => { const date = new Date("2024-05-01"); const data = getOnInputKeyDownStuff({ excludeDates: [new Date("2024-05-02")], selected: date, preSelection: date, }); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); const selectedDayNode = getSelectedDayNode(data.container); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.ArrowRight)); expect(data.instance.state.preSelection).toBeTruthy(); expect(formatDate(data.instance.state.preSelection!, data.testFormat)).toBe( formatDate(new Date("2024-05-03"), data.testFormat), ); }); it("should skip over excluded date when ArrowLeft is pressed", () => { const date = new Date("2024-05-01"); const data = getOnInputKeyDownStuff({ excludeDates: [new Date("2024-04-30")], selected: date, preSelection: date, }); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); const selectedDayNode = getSelectedDayNode(data.container); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.ArrowLeft)); expect(data.instance.state.preSelection).toBeTruthy(); expect(formatDate(data.instance.state.preSelection!, data.testFormat)).toBe( formatDate(new Date("2024-04-29"), data.testFormat), ); }); it("should skip over excluded date when ArrowUp is pressed", () => { const date = new Date("2024-05-22"); const data = getOnInputKeyDownStuff({ excludeDates: [new Date("2024-05-15")], selected: date, preSelection: date, }); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); const selectedDayNode = getSelectedDayNode(data.container); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.ArrowUp)); expect(data.instance.state.preSelection).toBeTruthy(); expect(formatDate(data.instance.state.preSelection!, data.testFormat)).toBe( formatDate(new Date("2024-05-08"), data.testFormat), ); }); it("should skip over excluded date when ArrowDown is pressed", () => { const date = new Date("2024-05-08"); const data = getOnInputKeyDownStuff({ excludeDates: [new Date("2024-05-15")], selected: date, preSelection: date, }); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); const selectedDayNode = getSelectedDayNode(data.container); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.ArrowDown)); expect(data.instance.state.preSelection).toBeTruthy(); expect(formatDate(data.instance.state.preSelection!, data.testFormat)).toBe( formatDate(new Date("2024-05-22"), data.testFormat), ); }); it("using keyboard, should not navigate to dates before minDate with ArrowLeft", () => { const date = new Date("2024-05-01"); const data = getOnInputKeyDownStuff({ minDate: date, selected: date, preSelection: date, }); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); const selectedDayNode = getSelectedDayNode(data.container); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.ArrowLeft)); expect(data.instance.state.preSelection).toBeTruthy(); expect(formatDate(data.instance.state.preSelection!, data.testFormat)).toBe( formatDate(date, data.testFormat), ); }); it("using keyboard, should not navigate to dates after maxDate with ArrowRight", () => { const date = new Date("2024-05-01"); const data = getOnInputKeyDownStuff({ maxDate: date, selected: date, preSelection: date, }); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); const selectedDayNode = getSelectedDayNode(data.container); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.ArrowRight)); expect(data.instance.state.preSelection).toBeTruthy(); expect(formatDate(data.instance.state.preSelection!, data.testFormat)).toBe( formatDate(date, data.testFormat), ); }); it("using keyboard, should not navigate to dates before minDate with PageUp and Home", () => { const date = new Date("2024-05-01"); const data = getOnInputKeyDownStuff({ minDate: date, selected: date, preSelection: date, }); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); const selectedDayNode = getSelectedDayNode(data.container); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.PageUp)); expect(data.instance.state.preSelection).toBeTruthy(); expect(formatDate(data.instance.state.preSelection!, data.testFormat)).toBe( formatDate(date, data.testFormat), ); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.Home)); expect(data.instance.state.preSelection).toBeTruthy(); expect(formatDate(data.instance.state.preSelection!, data.testFormat)).toBe( formatDate(date, data.testFormat), ); }); it("using keyboard, with PageUp pressed, should navigate to the date after the excluded date", () => { const date = new Date("2024-05-01"); const data = getOnInputKeyDownStuff({ excludeDates: [new Date("2024-04-01")], selected: date, preSelection: date, }); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); const selectedDayNode = getSelectedDayNode(data.container); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.PageUp)); expect(data.instance.state.preSelection).toBeTruthy(); expect(formatDate(data.instance.state.preSelection!, data.testFormat)).toBe( formatDate(new Date("2024-04-02"), data.testFormat), ); }); it("using keyboard, with Home pressed, should navigate to the date after the excluded date", () => { const date = new Date("2024-05-11"); const data = getOnInputKeyDownStuff({ excludeDates: [new Date("2024-05-05")], selected: date, preSelection: date, }); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); const selectedDayNode = getSelectedDayNode(data.container); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.Home)); expect(data.instance.state.preSelection).toBeTruthy(); expect(formatDate(data.instance.state.preSelection!, data.testFormat)).toBe( formatDate(new Date("2024-05-06"), data.testFormat), ); }); it("using keyboard, should not navigate to dates after maxDate with PageDown and End", () => { const date = new Date("2024-05-01"); const data = getOnInputKeyDownStuff({ maxDate: date, selected: date, preSelection: date, }); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); const selectedDayNode = getSelectedDayNode(data.container); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.PageDown)); expect(data.instance.state.preSelection).toBeTruthy(); expect(formatDate(data.instance.state.preSelection!, data.testFormat)).toBe( formatDate(date, data.testFormat), ); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.End)); expect(data.instance.state.preSelection).toBeTruthy(); expect(formatDate(data.instance.state.preSelection!, data.testFormat)).toBe( formatDate(date, data.testFormat), ); }); it("using keyboard, with PageDown pressed, should navigate to the date before the excluded date", () => { const date = new Date("2024-05-01"); const data = getOnInputKeyDownStuff({ excludeDates: [new Date("2024-06-01")], selected: date, preSelection: date, }); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); const selectedDayNode = getSelectedDayNode(data.container); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.PageDown)); expect(data.instance.state.preSelection).toBeTruthy(); expect(formatDate(data.instance.state.preSelection!, data.testFormat)).toBe( formatDate(new Date("2024-05-31"), data.testFormat), ); }); it("using keyboard, with End pressed, should navigate to the date before the excluded date", () => { const date = new Date("2024-05-05"); const data = getOnInputKeyDownStuff({ excludeDates: [new Date("2024-05-11")], selected: date, preSelection: date, }); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); const selectedDayNode = getSelectedDayNode(data.container); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.End)); expect(data.instance.state.preSelection).toBeTruthy(); expect(formatDate(data.instance.state.preSelection!, data.testFormat)).toBe( formatDate(new Date("2024-05-10"), data.testFormat), ); }); it("using keyboard, should not navigate to excluded date when excluded date and minDate are the same (edge case)", () => { const date = new Date("2024-05-03"); const data = getOnInputKeyDownStuff({ minDate: new Date("2024-05-02"), excludeDates: [new Date("2024-05-02")], selected: date, preSelection: date, }); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); const selectedDayNode = getSelectedDayNode(data.container); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.PageUp)); expect(data.instance.state.preSelection).toBeTruthy(); expect(formatDate(data.instance.state.preSelection!, data.testFormat)).toBe( formatDate(date, data.testFormat), ); }); it("using keyboard, should not navigate to excluded date when excluded date and maxDate are the same (edge case)", () => { const date = new Date("2024-05-03"); const data = getOnInputKeyDownStuff({ maxDate: new Date("2024-05-04"), excludeDates: [new Date("2024-05-04")], selected: date, preSelection: date, }); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); const selectedDayNode = getSelectedDayNode(data.container); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.PageDown)); expect(data.instance.state.preSelection).toBeTruthy(); expect(formatDate(data.instance.state.preSelection!, data.testFormat)).toBe( formatDate(date, data.testFormat), ); }); it("using keyboard, should not navigate to any date when there are no dates enabled (edge case)", () => { const date = new Date("2024-05-03"); const data = getOnInputKeyDownStuff({ minDate: date, maxDate: date, excludeDates: [date], selected: date, preSelection: date, }); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); const selectedDayNode = getSelectedDayNode(data.container); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.PageDown)); expect(data.instance.state.preSelection).toBeTruthy(); expect(formatDate(data.instance.state.preSelection!, data.testFormat)).toBe( formatDate(date, data.testFormat), ); }); it("should call onMonthChange when keyboard navigation moves preSelection to different month", () => { const onMonthChangeSpy = jest.fn(); const opts = { onMonthChange: onMonthChangeSpy }; const data = getOnInputKeyDownStuff(opts); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); const selectedDayNode = getSelectedDayNode(data.container); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.PageDown)); expect(onMonthChangeSpy).toHaveBeenCalledTimes(1); }); it("should call onSelect only once when keyboard navigation moves selection to different month", () => { const onSelectSpy = jest.fn(); const opts = { onSelect: onSelectSpy, adjustDateOnChange: true }; const data = getOnInputKeyDownStuff(opts); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); const selectedDayNode = getSelectedDayNode(data.container); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.PageDown)); expect(onSelectSpy).toHaveBeenCalledTimes(1); }); it("should not preSelect date if not between minDate and maxDate", () => { const data = getOnInputKeyDownStuff({ minDate: subDays(newDate(), 1), maxDate: addDays(newDate(), 1), }); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); expect(data.instance.state.preSelection).toBeTruthy(); expect(formatDate(data.instance.state.preSelection!, data.testFormat)).toBe( formatDate(data.copyM, data.testFormat), ); }); it("should not preSelect date if before minDate", () => { const data = getOnInputKeyDownStuff({ minDate: subDays(newDate(), 1), }); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowUp)); expect(data.instance.state.preSelection).toBeTruthy(); expect(formatDate(data.instance.state.preSelection!, data.testFormat)).toBe( formatDate(data.copyM, data.testFormat), ); }); it("should not preSelect date if after maxDate", () => { const data = getOnInputKeyDownStuff({ maxDate: addDays(newDate(), 1), }); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); expect(data.instance.state.preSelection).toBeTruthy(); expect(formatDate(data.instance.state.preSelection!, data.testFormat)).toBe( formatDate(data.copyM, data.testFormat), ); }); it("should be possible to preSelect minDate (no maxDate set)", () => { const minDate = new Date("2024-10-02"); const selected = minDate; const data = getOnInputKeyDownStuff({ selected, minDate, }); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); const selectedDayNode = getSelectedDayNode(data.container); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.ArrowRight)); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.ArrowLeft)); expect(data.instance.state.preSelection).toBeTruthy(); expect(data.instance.props.minDate).toBeTruthy(); expect(formatDate(data.instance.state.preSelection!, data.testFormat)).toBe( formatDate(data.instance.props.minDate!, data.testFormat), ); }); it("should be possible to preSelect minDate (maxDate set)", () => { const minDate = new Date("2024-10-02"); const maxDate = addDays(minDate, 20); const selected = minDate; const data = getOnInputKeyDownStuff({ selected, minDate, maxDate, }); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); const selectedDayNode = getSelectedDayNode(data.container); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.ArrowRight)); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.ArrowLeft)); expect(data.instance.state.preSelection).toBeTruthy(); expect(data.instance.props.minDate).toBeTruthy(); expect(formatDate(data.instance.state.preSelection!, data.testFormat)).toBe( formatDate(data.instance.props.minDate!, data.testFormat), ); }); it("should be possible to preSelect maxDate (no minDate set)", () => { const data = getOnInputKeyDownStuff({ maxDate: addDays(newDate(), 1), }); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); const selectedDayNode = getSelectedDayNode(data.container); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.ArrowRight)); expect(data.instance.state.preSelection).toBeTruthy(); expect(data.instance.props.maxDate).toBeTruthy(); expect(formatDate(data.instance.state.preSelection!, data.testFormat)).toBe( formatDate(data.instance.props.maxDate!, data.testFormat), ); }); it("should be possible to preSelect maxDate (minDate set)", () => { const data = getOnInputKeyDownStuff({ minDate: subDays(newDate(), 20), maxDate: addDays(newDate(), 1), }); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); const selectedDayNode = getSelectedDayNode(data.container); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.ArrowRight)); expect(data.instance.state.preSelection).toBeTruthy(); expect(data.instance.props.maxDate).toBeTruthy(); expect(formatDate(data.instance.state.preSelection!, data.testFormat)).toBe( formatDate(data.instance.props.maxDate!, data.testFormat), ); }); it("should not clear the preSelect date when a pressed key is not a navigation key", () => { const data = getOnInputKeyDownStuff(); fireEvent.keyDown(data.dateInput, getKey(KeyType.X)); expect(data.instance.state.preSelection).toBeTruthy(); expect(data.instance.state.preSelection!.valueOf()).toBe( data.copyM.valueOf(), ); }); describe("when update the datepicker input text while props.minDate is set", () => { const getCalendar = () => { return render( , ); }; it("should auto update calendar when the updated date text is after props.minDate", () => { const { container } = getCalendar(); const input = safeQuerySelector(container, "input"); fireEvent.change(input, { target: { value: "01/01/1801", }, }); expect(container.querySelector("input")?.value).toBe("01/01/1801"); expect( container.querySelector(".react-datepicker__current-month")?.innerHTML, ).toBe("January 1801"); }); it("should not auto update calendar when the updated date text is before props.minDate", () => { const { container } = getCalendar(); const input = safeQuerySelector(container, "input"); fireEvent.change(input, { target: { value: "1799/01/01", }, }); expect( container.querySelector(".react-datepicker__current-month")?.innerHTML, ).toBe("July 1993"); }); }); describe("when minDate is before 1800", () => { it("should allow manually typing dates before 1800 when minDate allows it", () => { const onChange = jest.fn(); const { container } = render( , ); const input = safeQuerySelector(container, "input"); fireEvent.change(input, { target: { value: "1350-03-20", }, }); expect(onChange).toHaveBeenCalled(); const calledWith = onChange.mock.calls[0][0]; expect(calledWith.getFullYear()).toBe(1350); expect(calledWith.getMonth()).toBe(2); // March is 0-indexed as 2 expect(calledWith.getDate()).toBe(20); }); it("should update calendar view when typing a date before 1800 with appropriate minDate", () => { const { container } = render( , ); const input = safeQuerySelector(container, "input"); fireEvent.change(input, { target: { value: "1350-03-20", }, }); expect( container.querySelector(".react-datepicker__current-month")?.innerHTML, ).toBe("March 1350"); }); }); it("should not manual select date if before minDate", () => { const minDate = subDays(newDate(), 1); const data = getOnInputKeyDownStuff({ minDate: minDate, }); fireEvent.change(data.dateInput, { target: { value: formatDate(subDays(minDate, 1), data.testFormat), }, }); fireEvent.keyDown(data.dateInput, getKey(KeyType.Enter)); expect(data.callback).not.toHaveBeenCalled(); }); it("should not manual select date if after maxDate", () => { const maxDate = addDays(newDate(), 1); const data = getOnInputKeyDownStuff({ maxDate: maxDate, }); fireEvent.change(data.dateInput, { target: { value: formatDate(addDays(maxDate, 1), data.testFormat), }, }); fireEvent.keyDown(data.dateInput, getKey(KeyType.Enter)); expect(data.callback).not.toHaveBeenCalled(); }); describe("onInputKeyDown Enter", () => { it("should update the selected date", () => { const data = getOnInputKeyDownStuff(); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); // puts focus on the calendar day const selectedDayNode = getSelectedDayNode(data.container); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.ArrowLeft)); expect(selectedDayNode).toBeTruthy(); (selectedDayNode as HTMLElement).focus(); fireEvent.keyDown( document.activeElement || document.body, getKey(KeyType.Enter), ); data.copyM = subDays(data.copyM, 1); expect(data.callback).toHaveBeenCalled(); const result = data.callback.mock.calls[0][0]; expect(formatDate(result, data.testFormat)).toBe( formatDate(data.copyM, data.testFormat), ); }); it("should update the selected date when spacebar is pressed", () => { const data = getOnInputKeyDownStuff(); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); // puts focus on the calendar day const selectedDayNode = getSelectedDayNode(data.container); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.ArrowLeft)); expect(selectedDayNode).toBeTruthy(); (selectedDayNode as HTMLElement).focus(); fireEvent.keyDown( document.activeElement || document.body, getKey(KeyType.Space), ); data.copyM = subDays(data.copyM, 1); expect(data.callback).toHaveBeenCalled(); const result = data.callback.mock.calls[0][0]; expect(formatDate(result, data.testFormat)).toBe( formatDate(data.copyM, data.testFormat), ); }); it("should update the selected date on manual input", () => { const data = getOnInputKeyDownStuff(); fireEvent.change(data.dateInput, { target: { value: "2017-02-02" }, }); fireEvent.keyDown(data.dateInput, getKey(KeyType.Enter)); data.copyM = newDate("2017-02-02"); expect(formatDate(data.callback.mock.calls[0][0], data.testFormat)).toBe( formatDate(data.copyM, data.testFormat), ); }); it("should not select excludeDates", () => { const data = getOnInputKeyDownStuff({ excludeDates: [subDays(newDate(), 1)], }); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowLeft)); fireEvent.keyDown(data.dateInput, getKey(KeyType.Enter)); expect(data.callback).not.toHaveBeenCalled(); }); describe("with excludeDateIntervals", () => { it("should not select the start date of the interval", () => { const data = getOnInputKeyDownStuff({ excludeDateIntervals: [ { start: subDays(newDate(), 1), end: addDays(newDate(), 1), }, ], }); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowLeft)); fireEvent.keyDown(data.dateInput, getKey(KeyType.Enter)); expect(data.callback).not.toHaveBeenCalled(); }); it("should not select a dates within the interval", () => { const data = getOnInputKeyDownStuff({ excludeDateIntervals: [ { start: subDays(newDate(), 1), end: addDays(newDate(), 1), }, ], }); fireEvent.keyDown(data.dateInput, getKey(KeyType.Enter)); expect(data.callback).not.toHaveBeenCalled(); }); it("should not select the end date of the interval", () => { const data = getOnInputKeyDownStuff({ excludeDateIntervals: [ { start: subDays(newDate(), 1), end: addDays(newDate(), 1), }, ], }); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowRight)); fireEvent.keyDown(data.dateInput, getKey(KeyType.Enter)); expect(data.callback).not.toHaveBeenCalled(); }); }); it("should not select dates excluded from filterDate", () => { const data = getOnInputKeyDownStuff({ filterDate: (date: Date) => getDay(date) !== getDay(subDays(newDate(), 1)), }); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowLeft)); fireEvent.keyDown(data.dateInput, getKey(KeyType.Enter)); expect(data.callback).not.toHaveBeenCalled(); }); }); describe("onInputKeyDown Escape", () => { it("should not update the selected date if the date input manually it has something wrong", () => { const data = getOnInputKeyDownStuff(); const preSelection = data.instance.state.preSelection; fireEvent.keyDown(data.dateInput, getKey(KeyType.Backspace)); fireEvent.keyDown(data.dateInput, getKey(KeyType.Escape)); expect(data.callback).not.toHaveBeenCalled(); // confirms that handleChange occurred expect(preSelection).toBe(data.instance.state.preSelection); // confirms the preSelection is still the same }); }); it("should reset the keyboard selection when closed", () => { const data = getOnInputKeyDownStuff(); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowLeft)); act(() => { data.instance.setOpen(false); }); expect(data.instance.state.preSelection).toBeTruthy(); expect(formatDate(data.instance.state.preSelection!, data.testFormat)).toBe( formatDate(data.copyM, data.testFormat), ); }); it("should retain the keyboard selection when already open", () => { const data = getOnInputKeyDownStuff(); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); const selectedDayNode = getSelectedDayNode(data.container); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.ArrowLeft)); data.copyM = subDays(data.copyM, 1); expect(data.instance.state.preSelection).toBeTruthy(); expect(formatDate(data.instance.state.preSelection!, data.testFormat)).toBe( formatDate(data.copyM, data.testFormat), ); }); it("should open the calendar when the down arrow key is pressed", () => { const data = getOnInputKeyDownStuff(); act(() => { data.instance.setOpen(false); }); expect(data.instance.state.open).toBe(false); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); expect(data.instance.state.open).toBe(true); }); it("should focus first month when the down arrow key is pressed", async () => { const div = document.createElement("div"); document.body.appendChild(div); render(, { container: div, }); // user focuses the input field, the calendar opens const dateInput = safeQuerySelector(div, "input"); fireEvent.focus(dateInput); fireEvent.keyDown(dateInput, getKey(KeyType.ArrowDown)); await waitFor(() => { const selectedMonth = div.querySelector( '.react-datepicker__month-text[tabindex="0"]', ); expect(document.activeElement).toBe(selectedMonth); }); }); it("should not open the calendar when the left arrow key is pressed", () => { const data = getOnInputKeyDownStuff(); act(() => { data.instance.setOpen(false); }); expect(data.instance.state.open).toBe(false); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowLeft)); expect(data.instance.state.open).toBe(false); }); it("should default to the current day on Enter", () => { const data = getOnInputKeyDownStuff({ selected: null }); fireEvent.keyDown(data.dateInput, getKey(KeyType.Enter)); expect(data.callback).toHaveBeenCalledTimes(1); const selected = data.callback.mock.calls[0][0]; expect(formatDate(selected, data.exactishFormat)).toBe( formatDate(data.copyM, data.exactishFormat), ); }); it("should autoFocus the input given the autoFocus prop", () => { const div = document.createElement("div"); document.body.appendChild(div); render(, { container: div, }); expect(div.querySelector("input")).toBe(document.activeElement); }); it("should autoFocus the input when calling the setFocus method", async () => { const div = document.createElement("div"); document.body.appendChild(div); let instance: DatePicker | null = null; render( { instance = node; }} />, { container: div, }, ); expect(instance).toBeTruthy(); act(() => { instance!.setFocus(); }); await waitFor(() => { expect(div.querySelector("input")).toBe(document.activeElement); }); }); it("should clear preventFocus timeout id when component is unmounted", () => { const div = document.createElement("div"); document.body.appendChild(div); let instance: DatePicker | null = null; const { unmount } = render( { instance = node; }} inline />, { container: div, }, ); expect(instance).toBeTruthy(); const clearPreventFocusTimeoutMock = jest.fn(); instance!.clearPreventFocusTimeout = clearPreventFocusTimeoutMock; unmount(); expect(clearPreventFocusTimeoutMock).toHaveBeenCalledTimes(1); }); function getOnInputKeyDownDisabledKeyboardNavigationStuff() { const m = newDate(); const copyM = newDate(m); const testFormat = "yyyy-MM-dd"; const callback = jest.fn(); let instance: DatePicker | null = null; const { container, rerender } = render( { instance = node; }} />, ); expect(instance).toBeTruthy(); expect(instance!.input).toBeTruthy(); const dateInput = instance!.input!; fireEvent.focus(dateInput); return { m, copyM, testFormat, callback, datePicker: container.firstChild, dateInput, rerender, instance, }; } it("should not handle onInputKeyDown ArrowLeft", () => { const data = getOnInputKeyDownDisabledKeyboardNavigationStuff(); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowLeft)); expect(data.callback).not.toHaveBeenCalled(); }); it("should not handle onInputKeyDown ArrowRight", () => { const data = getOnInputKeyDownDisabledKeyboardNavigationStuff(); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowRight)); expect(data.callback).not.toHaveBeenCalled(); }); it("should not handle onInputKeyDown ArrowUp", () => { const data = getOnInputKeyDownDisabledKeyboardNavigationStuff(); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowUp)); expect(data.callback).not.toHaveBeenCalled(); }); it("should not handle onInputKeyDown ArrowDown", () => { const data = getOnInputKeyDownDisabledKeyboardNavigationStuff(); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); expect(data.callback).not.toHaveBeenCalled(); }); it("should not handle onInputKeyDown PageUp", () => { const data = getOnInputKeyDownDisabledKeyboardNavigationStuff(); fireEvent.keyDown(data.dateInput, getKey(KeyType.PageUp)); expect(data.callback).not.toHaveBeenCalled(); }); it("should not handle onInputKeyDown PageDown", () => { const data = getOnInputKeyDownDisabledKeyboardNavigationStuff(); fireEvent.keyDown(data.dateInput, getKey(KeyType.PageDown)); expect(data.callback).not.toHaveBeenCalled(); }); it("should not handle onInputKeyDown Home", () => { const data = getOnInputKeyDownDisabledKeyboardNavigationStuff(); fireEvent.keyDown(data.dateInput, getKey(KeyType.Home)); expect(data.callback).not.toHaveBeenCalled(); }); it("should not handle onInputKeyDown End", () => { const data = getOnInputKeyDownDisabledKeyboardNavigationStuff(); fireEvent.keyDown(data.dateInput, getKey(KeyType.End)); expect(data.callback).not.toHaveBeenCalled(); }); it("should correctly clear date with empty input string", () => { let cleared = false; function handleChange(d: Date | null) { // Internally DateInput calls it's onChange prop with null // when the input value is an empty string if (d === null) { cleared = true; } } const { container } = render( , ); const input = safeQuerySelector(container, "input"); fireEvent.change(input, { target: { value: "", }, }); expect(cleared).toBe(true); }); it("should correctly clear date with empty input string (selectsRange)", () => { let instance: DatePicker | null = null; const onChangeSpy = jest.fn(); render( { instance = node; }} selectsRange startDate={newDate("2016-11-22")} endDate={undefined} onChange={onChangeSpy} isClearable />, ); expect(instance).toBeTruthy(); expect(instance!.input).toBeTruthy(); fireEvent.change(instance!.input!, { target: { value: "", }, }); expect(onChangeSpy.mock.calls.at(-1)[0]).toStrictEqual([null, null]); }); it("should keep input cleared after blur when using controlled component (issue #5814)", () => { function ControlledDatePicker() { const [selected, setSelected] = useState( newDate("2024-12-25"), ); return ( setSelected(date)} dateFormat="MM/dd/yyyy" /> ); } const { container } = render(); const input = safeQuerySelector(container, "input"); // Initial value should be the formatted date expect(input.value).toBe("12/25/2024"); // Clear the input fireEvent.change(input, { target: { value: "" } }); // Blur the input fireEvent.blur(input); // After blur, the input should still be empty (not revert to the date) expect(input.value).toBe(""); }); it("should handle mask pattern clearing (issue #5814)", () => { // Mask libraries often show "__/__/____" when cleared instead of empty string const onChangeSpy = jest.fn(); function MaskedDatePicker() { const [selected, setSelected] = useState( newDate("2024-12-25"), ); const handleChange = (date: Date | null) => { onChangeSpy(date); setSelected(date); }; return ( ); } const { container } = render(); const input = safeQuerySelector(container, "input"); // Initial value should be the formatted date expect(input.value).toBe("12/25/2024"); // Simulate mask library clearing - value becomes mask pattern, not empty fireEvent.change(input, { target: { value: "__/__/____" } }); // onChange should NOT be called with null because mask pattern isn't a valid date // But it also shouldn't be called with the old date // The key issue: what happens on blur? fireEvent.blur(input); // The input should show the mask pattern, not revert to the old date // This is where the bug manifests - in React 18, it might revert expect(input.value).not.toBe("12/25/2024"); }); it("should update preSelection when input changes for selectsRange", () => { let instance: DatePicker | null = null; const onChangeSpy = jest.fn(); render( { instance = node; }} selectsRange startDate={newDate("2024-01-15")} endDate={newDate("2024-01-20")} onChange={onChangeSpy} dateFormat="MM/dd/yyyy" />, ); expect(instance).toBeTruthy(); // Get initial preSelection const initialPreSelection = instance!.state.preSelection; // Change the date via input fireEvent.change(instance!.input!, { target: { value: "02/10/2024 - 02/15/2024", }, }); // preSelection should be updated to the new start date expect(instance!.state.preSelection).not.toEqual(initialPreSelection); expect(instance!.state.preSelection?.getMonth()).toBe(1); // February (0-indexed) expect(instance!.state.preSelection?.getDate()).toBe(10); }); it("should update calendar view when typing a partial date (year only)", () => { let instance: DatePicker | null = null; render( { instance = node; }} selected={newDate("2024-06-15")} onChange={jest.fn()} dateFormat="MM/dd/yyyy" open />, ); expect(instance).toBeTruthy(); // Verify initial calendar shows June 2024 expect(instance!.state.preSelection?.getFullYear()).toBe(2024); expect(instance!.state.preSelection?.getMonth()).toBe(5); // June // Type a partial date - just a year (use 2000 which won't match as a month) fireEvent.change(instance!.input!, { target: { value: "2000", }, }); // Calendar should navigate to 2000 with the same month (June) from the selected date expect(instance!.state.preSelection?.getFullYear()).toBe(2000); expect(instance!.state.preSelection?.getMonth()).toBe(5); // June preserved from refDate }); it("should update calendar view when typing a partial date (month and year)", () => { let instance: DatePicker | null = null; render( { instance = node; }} selected={newDate("2024-06-15")} onChange={jest.fn()} dateFormat="MM/dd/yyyy" open />, ); expect(instance).toBeTruthy(); // Type a partial date - month and year fireEvent.change(instance!.input!, { target: { value: "03/2014", }, }); // Calendar should navigate to March 2014 expect(instance!.state.preSelection?.getFullYear()).toBe(2014); expect(instance!.state.preSelection?.getMonth()).toBe(2); // March (0-indexed) }); it("should not update calendar view when typing text without a valid year", () => { let instance: DatePicker | null = null; render( { instance = node; }} selected={newDate("2024-06-15")} onChange={jest.fn()} dateFormat="MM/dd/yyyy" open />, ); expect(instance).toBeTruthy(); // Verify initial calendar shows 2024 expect(instance!.state.preSelection?.getFullYear()).toBe(2024); expect(instance!.state.preSelection?.getMonth()).toBe(5); // June (0-indexed) // Type text without a valid 4-digit year fireEvent.change(instance!.input!, { target: { value: "03/", }, }); // Calendar should remain on the original date since no valid year was found expect(instance!.state.preSelection?.getFullYear()).toBe(2024); expect(instance!.state.preSelection?.getMonth()).toBe(5); // June (0-indexed) }); it("should correctly update the input when the value prop changes", () => { let instance: DatePicker | null = null; const { rerender } = render( { instance = node; }} />, ); expect(instance).toBeTruthy(); expect(instance!.input).toBeTruthy(); expect((instance!.input! as HTMLInputElement).value).toBe(""); rerender( { instance = node; }} value="foo" />, ); expect((instance!.input! as HTMLInputElement).value).toBe("foo"); }); it("should preserve user input as they are typing", () => { let instance: DatePicker | null = null; let selected: Date | null = null; const onChange = (date: Date | null) => { selected = date; }; const { rerender } = render( { instance = node; }} dateFormat={["yyyy-MM-dd", "MM/dd/yyyy", "MM/dd/yy"]} onChange={onChange} />, ); expect(instance).toBeTruthy(); expect(instance!.input).toBeTruthy(); expect((instance!.input! as HTMLInputElement).value).toBe(""); const str = "12/30/1982"; fireEvent.focus(instance!.input!); str.split("").forEach((c, i) => { fireEvent.change(instance!.input!, { target: { value: (instance!.input! as HTMLInputElement).value + c }, }); rerender( { instance = node; }} dateFormat={["yyyy-MM-dd", "MM/dd/yyyy", "MM/dd/yy"]} onChange={onChange} selected={selected} />, ); expect((instance!.input! as HTMLInputElement).value).toBe( str.substring(0, i + 1), ); }); expect(selected).not.toBeNull(); expect(formatDate(selected as unknown as Date, "yyyy-MM-dd")).toBe( "1982-12-30", ); }); it("should invoke provided onChangeRaw function and should not invoke provided onSelect function on manual input change", () => { const inputValue = "test"; const onChangeRawSpy = jest.fn(); const onSelectSpy = jest.fn(); const { container } = render( , ); expect(onChangeRawSpy).not.toHaveBeenCalled(); expect(onSelectSpy).not.toHaveBeenCalled(); const input = safeQuerySelector(container, "input"); fireEvent.change(input, { target: { value: inputValue, }, }); expect(onChangeRawSpy).toHaveBeenCalledTimes(1); expect(onChangeRawSpy.mock.calls[0][0].target.value).toBe(inputValue); expect(onSelectSpy).not.toHaveBeenCalled(); }); it("should invoke provided onChangeRaw and onSelect functions when clicking a day on the calendar", () => { const onChangeRawSpy = jest.fn(); const onSelectSpy = jest.fn(); const { container } = render( , ); expect(onChangeRawSpy).not.toHaveBeenCalled(); expect(onSelectSpy).not.toHaveBeenCalled(); const input = safeQuerySelector(container, "input"); fireEvent.focus(input); const day = safeQuerySelector(container, ".react-datepicker__day"); fireEvent.click(day); expect(onChangeRawSpy).toHaveBeenCalledTimes(1); expect(onSelectSpy).toHaveBeenCalledTimes(1); }); it("should allow onChangeRaw to prevent a change", () => { const onChangeRaw = ( event?: React.MouseEvent | React.KeyboardEvent, ) => event && (event.target as unknown as HTMLInputElement).value > "2" && event.preventDefault(); const { container } = render(); const input = safeQuerySelector(container, "input"); expect(input.value).toBe(""); fireEvent.change(input, { target: { value: "3", }, }); expect(input.value).toBe(""); fireEvent.change(input, { target: { value: "1", }, }); expect(input.value).toBe("1"); }); it("should call onChangeRaw with all arguments", () => { const inputValue = "test"; const onChangeRawSpy = jest.fn(); const { container } = render( } onChangeRaw={onChangeRawSpy} />, ); expect(onChangeRawSpy).not.toHaveBeenCalled(); const input = safeQuerySelector(container, "input"); fireEvent.change(input, { target: { value: inputValue, }, }); expect(onChangeRawSpy).toHaveBeenCalledTimes(1); expect(onChangeRawSpy.mock.calls[0][0].target.value).toBe(inputValue); expect(onChangeRawSpy.mock.calls[0][1]).toBe("test"); }); it("should handle the lack of an 'event' object as the first argument to handleChange analogously to 'preventDefault' being called", () => { const inputValue = "test"; const onChangeRawSpy = jest.fn(); const customInput = ( [event, event.target.value]} /> ); const { container } = render( , ); expect(onChangeRawSpy).not.toHaveBeenCalled(); const input = safeQuerySelector(container, "input"); fireEvent.change(input, { target: { value: inputValue, }, }); expect(onChangeRawSpy).toHaveBeenCalled(); expect(onChangeRawSpy.mock.calls[0][1]).toBe("test"); }); it("should handle a click outside of the calendar", () => { let instance: DatePicker | null = null; render( { instance = node; }} selected={newDate()} withPortal />, ); expect(instance).toBeTruthy(); const openSpy = jest.spyOn(instance!, "setOpen"); act(() => { instance!.handleCalendarClickOutside({ preventDefault: jest.fn(), } as unknown as MouseEvent); }); expect(openSpy).toHaveBeenCalledWith(false); }); it("should close date picker on outside click", () => { const onClickOutsideSpy = jest.fn(); const { container } = render(
test text
, ); expect(container.querySelector(".react-datepicker")).toBeNull(); const input = safeQuerySelector(container, "input"); fireEvent.focus(input); expect(container.querySelector(".react-datepicker")).not.toBeNull(); const testText = safeQuerySelector(container, ".testText"); fireEvent.mouseDown(testText); expect(container.querySelector(".react-datepicker")).toBeNull(); expect(onClickOutsideSpy).toHaveBeenCalledTimes(1); }); it("should not close date picker when onClickOutside calls preventDefault", () => { const onClickOutside = (event: MouseEvent) => { event.preventDefault(); }; const { container } = render(
outside
, ); const input = safeQuerySelector(container, "input"); fireEvent.focus(input); expect(container.querySelector(".react-datepicker")).not.toBeNull(); const outsideElement = safeQuerySelector(container, ".outsideElement"); fireEvent.mouseDown(outsideElement); // Calendar should remain open because preventDefault was called expect(container.querySelector(".react-datepicker")).not.toBeNull(); }); it("should not close date picker on input click", () => { const onClickOutsideSpy = jest.fn(); const { container } = render( , ); expect(container.querySelector(".react-datepicker")).toBeNull(); const input = safeQuerySelector(container, "input"); fireEvent.focus(input); expect(container.querySelector(".react-datepicker")).not.toBeNull(); fireEvent.mouseDown(input); expect(container.querySelector(".react-datepicker")).not.toBeNull(); expect(onClickOutsideSpy).not.toHaveBeenCalled(); }); it("should default to the currently selected date", () => { let instance: DatePicker | null = null; render( { instance = node; }} selected={newDate("1988-12-30")} />, ); expect(instance).toBeTruthy(); expect(instance!.state.preSelection).toBeTruthy(); expect(formatDate(instance!.state.preSelection!, "yyyy-MM-dd")).toBe( "1988-12-30", ); }); it("should default to the start date when selecting an end date", () => { let instance: DatePicker | null = null; render( { instance = node; }} startDate={newDate("1988-11-30")} selectsEnd />, ); expect(instance).toBeTruthy(); expect(instance!.state.preSelection).toBeTruthy(); expect(formatDate(instance!.state.preSelection!, "yyyy-MM-dd")).toBe( "1988-11-30", ); }); it("should default to the end date when selecting a start date", () => { let instance: DatePicker | null = null; render( { instance = node; }} endDate={newDate("1988-12-31")} selectsStart />, ); expect(instance).toBeTruthy(); expect(instance!.state.preSelection).toBeTruthy(); expect(formatDate(instance!.state.preSelection!, "yyyy-MM-dd")).toBe( "1988-12-31", ); }); it("should default to a date <= maxDate", () => { let instance: DatePicker | null = null; render( { instance = node; }} maxDate={newDate("1982-01-01")} />, ); expect(instance).toBeTruthy(); expect(instance!.state.preSelection).toBeTruthy(); expect(formatDate(instance!.state.preSelection!, "yyyy-MM-dd")).toBe( "1982-01-01", ); }); it("should default to a date >= minDate", () => { let instance: DatePicker | null = null; render( { instance = node; }} minDate={newDate("2063-04-05")} />, ); expect(instance).toBeTruthy(); expect(instance!.state.preSelection).toBeTruthy(); expect(formatDate(instance!.state.preSelection!, "yyyy-MM-dd")).toBe( "2063-04-05", ); }); it("should default to the openToDate if there is one", () => { let instance: DatePicker | null = null; render( { instance = node; }} openToDate={newDate("2020-01-23")} />, ); expect(instance).toBeTruthy(); expect(instance!.state.preSelection).toBeTruthy(); expect(formatDate(instance!.state.preSelection!, "yyyy-MM-dd")).toBe( "2020-01-23", ); }); it("should otherwise default to the current date", () => { let instance: DatePicker | null = null; render( { instance = node; }} />, ); expect(instance).toBeTruthy(); expect(instance!.state.preSelection).toBeTruthy(); expect(formatDate(instance!.state.preSelection!, "yyyy-MM-dd")).toBe( formatDate(newDate(), "yyyy-MM-dd"), ); }); it("should support an initial null `selected` value in inline mode", () => { const { rerender } = render(); expect(() => rerender(), ).not.toThrow(); }); it("should switch month in inline mode immediately", () => { let instance: DatePicker | null = null; const selected = newDate(); const future = addDays(newDate(), 100); const { rerender } = render( { instance = node; }} inline selected={selected} />, ); expect(instance).toBeTruthy(); expect(instance!.state.preSelection).toBeTruthy(); expect(formatDate(instance!.state.preSelection!, "yyyy-MM-dd")).toBe( formatDate(selected, "yyyy-MM-dd"), ); rerender( { instance = node; }} inline selected={future} />, ); expect(instance).toBeTruthy(); expect(instance!.state.preSelection).toBeTruthy(); expect(formatDate(instance!.state.preSelection!, "yyyy-MM-dd")).toBe( formatDate(future, "yyyy-MM-dd"), ); }); it("should switch month in inline mode immediately, when year is updated", () => { let instance: DatePicker | null = null; const selected = newDate(); const future = addYears(newDate(), 1); const { rerender } = render( { instance = node; }} inline selected={selected} />, ); expect(instance).toBeTruthy(); expect(instance!.state.preSelection).toBeTruthy(); expect(formatDate(instance!.state.preSelection!, "yyyy-MM-dd")).toBe( formatDate(selected, "yyyy-MM-dd"), ); rerender( { instance = node; }} inline selected={future} />, ); expect(instance).toBeTruthy(); expect(instance!.state.preSelection).toBeTruthy(); expect(formatDate(instance!.state.preSelection!, "yyyy-MM-dd")).toBe( formatDate(future, "yyyy-MM-dd"), ); }); it("should update calendar view when selected prop changes to a different month (issue #3367)", () => { // This test verifies that when a user programmatically sets selected date // (e.g., via a "Today" button), the calendar view updates to show // the month containing the new date, even if user was viewing a different month. const initialDate = new Date("2024-01-15"); let instance: DatePicker | null = null; const { container, rerender } = render( { instance = node; }} selected={initialDate} onChange={() => {}} />, ); expect(instance).toBeTruthy(); // Open the calendar const input = safeQuerySelector(container, "input"); fireEvent.click(input); // Verify initial preSelection is set to January 2024 expect(instance!.state.preSelection?.getMonth()).toBe(0); // January expect(instance!.state.preSelection?.getFullYear()).toBe(2024); // Close calendar fireEvent.click(input); // Simulate programmatic update to a different month (e.g., user clicks "Jump to March" button) const newDate = new Date("2024-03-10"); rerender( { instance = node; }} selected={newDate} onChange={() => {}} />, ); // Open the calendar again fireEvent.click(input); // The calendar should now show March 2024, not January 2024 expect(instance!.state.preSelection?.getMonth()).toBe(2); // March expect(instance!.state.preSelection?.getFullYear()).toBe(2024); }); it("should not set open state when focusing on the date input and the preventOpenOnFocus prop is set", () => { const { container } = render(); const input = safeQuerySelector(container, "input"); fireEvent.focus(input); expect(container.querySelector(".react-datepicker")).toBeNull(); }); it("should not set open state onInputKeyDown when preventOpenOnFocus prop is set", () => { const { container } = render(); const input = safeQuerySelector(container, "input"); fireEvent.keyDown(input, getKey(KeyType.ArrowLeft)); expect(container.querySelector(".react-datepicker")).toBeNull(); }); it("should clear the input when clear() member function is called", () => { let instance: DatePicker | null = null; render( { instance = node; }} selected={newDate("2015-12-15")} />, ); expect(instance).toBeTruthy(); act(() => { instance!.clear(); }); expect(instance!.state.inputValue).toBeNull(); }); it("should not open when open is false and input is focused", () => { const { container } = render(); const input = safeQuerySelector(container, "input"); fireEvent.focus(input); expect(container.querySelector(".react-datepicker")).toBeNull(); }); it("should open when open is true", () => { const { container } = render(); expect(container.querySelector(".react-datepicker")).not.toBeNull(); }); it("should fire onInputClick when input is clicked", () => { const onInputClickSpy = jest.fn(); const { container } = render(); const input = safeQuerySelector(container, "input"); fireEvent.click(input); expect(onInputClickSpy).toHaveBeenCalledTimes(1); }); it("should set monthSelectedIn to 0 if monthsShown prop changes", () => { let instance: DatePicker | null = null; const { rerender } = render( { instance = node; }} monthsShown={2} inline />, ); expect(instance).toBeTruthy(); act(() => { ( instance!.state as unknown as { monthSelectedIn: number } ).monthSelectedIn = 1; }); expect(instance!.state.monthSelectedIn).toEqual(1); rerender( { instance = node; }} monthsShown={1} inline />, ); expect(instance!.state.monthSelectedIn).toEqual(0); }); it("should set monthSelectedIn to 0 when calendar opens with selectsRange", () => { let instance: DatePicker | null = null; const { container } = render( { instance = node; }} monthsShown={2} selectsRange />, ); expect(instance).toBeTruthy(); // Manually set monthSelectedIn to 1 (simulating previous user interaction) act(() => { ( instance!.state as unknown as { monthSelectedIn: number } ).monthSelectedIn = 1; }); expect(instance!.state.monthSelectedIn).toEqual(1); // Open the calendar by clicking the input const input = container.querySelector("input"); expect(input).not.toBeNull(); fireEvent.click(input!); // monthSelectedIn should be reset to 0 when the calendar opens expect(instance!.state.monthSelectedIn).toEqual(0); }); it("should NOT reset monthSelectedIn when selecting a date from second calendar with selectsRange", () => { let instance: DatePicker | null = null; const onChange = jest.fn(); const { container } = render( { instance = node; }} monthsShown={2} selectsRange inline onChange={onChange} />, ); expect(instance).toBeTruthy(); // Find a day in the second month container const secondMonthContainer = container.querySelectorAll( ".react-datepicker__month-container", )[1]; expect(secondMonthContainer).toBeTruthy(); const secondMonthDays = secondMonthContainer?.querySelectorAll( ".react-datepicker__day:not(.react-datepicker__day--outside-month)", ); expect(secondMonthDays?.length).toBeGreaterThan(0); // Click a day in the second month fireEvent.click(secondMonthDays![10]!); // monthSelectedIn should be 1 (second calendar), not reset to 0 expect(instance!.state.monthSelectedIn).toEqual(1); }); it("should disable non-jumping if prop focusSelectedMonth is true", () => { let instance: DatePicker | null = null; const { container } = render( { instance = node; }} inline monthsShown={2} focusSelectedMonth />, ); const dayButtonInline = container.querySelectorAll( ".react-datepicker__day", )[40]; expect(dayButtonInline).toBeTruthy(); fireEvent.click(dayButtonInline!); expect(instance).toBeTruthy(); expect(instance!.state.monthSelectedIn).toEqual(undefined); }); it("should reset monthSelectedIn to 0 when changeMonth is called from custom header", () => { let instance: DatePicker | null = null; let changeMonthFn: ((month: number) => void) | null = null; const { container } = render( { instance = node; }} inline monthsShown={2} selected={newDate("2024-06-15")} renderCustomHeader={({ changeMonth }) => { changeMonthFn = changeMonth; return (
); }} />, ); expect(instance).toBeTruthy(); // First, select a day in the second month panel to set monthSelectedIn to 1 const dayButtonsInSecondMonth = container .querySelectorAll(".react-datepicker__month-container")[1] ?.querySelectorAll( ".react-datepicker__day:not(.react-datepicker__day--outside-month)", ); expect(dayButtonsInSecondMonth).toBeTruthy(); expect(dayButtonsInSecondMonth!.length).toBeGreaterThan(0); // Click a day in the second month to set monthSelectedIn to 1 fireEvent.click(dayButtonsInSecondMonth![10]!); expect(instance!.state.monthSelectedIn).toEqual(1); // Now call changeMonth from the custom header expect(changeMonthFn).toBeTruthy(); act(() => { changeMonthFn!(0); // Change to January }); // monthSelectedIn should be reset to 0 expect(instance!.state.monthSelectedIn).toEqual(0); }); it("should show the popper arrow when showPopperArrow is true", () => { const { container } = render(); const input = safeQuerySelector(container, "input"); fireEvent.click(input); const arrow = container.querySelector(".react-datepicker__triangle"); expect(arrow).not.toBeNull(); }); it("should not show the popper arrow when showPopperArrow is false", () => { const { container } = render(); const input = safeQuerySelector(container, "input"); fireEvent.click(input); const arrows = container.querySelectorAll(".react-datepicker__triangle"); expect(arrows).toHaveLength(0); }); it("should pass chooseDayAriaLabelPrefix prop to the correct child component", () => { const chooseDayAriaLabelPrefix = "My choose-day-prefix"; const { container } = render( , ); expect( container .querySelector(".react-datepicker__day") ?.getAttribute("aria-label"), ).toContain(chooseDayAriaLabelPrefix); }); it("should pass disabledDayAriaLabelPrefix prop to the correct child component", () => { const disabledDayAriaLabelPrefix = "My disabled-day-prefix"; const day = new Date(); const { container } = render( , ); expect( container .querySelector(".react-datepicker__day--today") ?.getAttribute("aria-label"), ).toContain(disabledDayAriaLabelPrefix); }); it("should pass weekAriaLabelPrefix prop to the correct child component", () => { const weekAriaLabelPrefix = "My week-prefix"; const { container } = render( , ); expect( container .querySelector(".react-datepicker__week-number") ?.getAttribute("aria-label"), ).toContain(weekAriaLabelPrefix); }); it("should pass monthAriaLabelPrefix prop to the correct child component", () => { const monthAriaLabelPrefix = "My month-prefix"; const { container } = render( , ); expect( container .querySelector(".react-datepicker__month") ?.getAttribute("aria-label"), ).toContain(monthAriaLabelPrefix); }); it("should close the calendar after scrolling", () => { const { container } = render(); const input = safeQuerySelector(container, "input"); fireEvent.focus(input); expect(container.querySelector(".react-datepicker")).not.toBeNull(); fireEvent.scroll(document); expect(container.querySelector(".react-datepicker")).toBeNull(); }); it("should not close the calendar after scrolling", () => { const div = document.createElement("div"); document.body.appendChild(div); const { container } = render(, { container: div, }); const input = safeQuerySelector(container, "input"); fireEvent.focus(input); fireEvent.scroll(div); expect(container.querySelector(".react-datepicker")).not.toBeNull(); }); it("should close the calendar after scrolling.", () => { const { container } = render( true} />); const input = safeQuerySelector(container, "input"); fireEvent.focus(input); expect(container.querySelector(".react-datepicker")).not.toBeNull(); fireEvent.scroll(document); expect(container.querySelector(".react-datepicker")).toBeNull(); }); it("should not close the calendar after scrolling.", () => { const { container } = render( false} />); const input = safeQuerySelector(container, "input"); fireEvent.focus(input); fireEvent.scroll(document); expect(container.querySelector(".react-datepicker")).not.toBeNull(); }); describe("multiSelecting non-consecutive dates", () => { const testDate = new Date("2024-02-02 00:00:00"); beforeAll(() => { jest.useFakeTimers(); jest.setSystemTime(testDate); }); afterAll(() => { jest.useRealTimers(); }); it("should return array of dates to onChange callback when day is selected in multiSelect mode", () => { const onChange = jest.fn(); const { container } = render( , ); const input = safeQuerySelector(container, "input"); fireEvent.focus(input); const days = container.querySelectorAll(".react-datepicker__day"); expect(days[5]).toBeTruthy(); // Might seem odd, but the first couple of days of the calendar is in january, so days[5] is february 2nd fireEvent.click(days[5]!); expect(onChange).toHaveBeenCalled(); expect(onChange).toHaveBeenCalledWith([testDate], expect.anything()); }); it("should remove previously selected date from array if date is selected again", () => { const onChange = jest.fn(); const anotherDate = new Date("2024-01-01"); const { container } = render( , ); const input = safeQuerySelector(container, "input"); fireEvent.focus(input); const days = container.querySelectorAll(".react-datepicker__day"); expect(days[5]).toBeTruthy(); // Might seem odd, but the first couple of days of the calendar is in january, so days[5] is february 2nd fireEvent.click(days[5]!); expect(onChange).toHaveBeenCalled(); expect(onChange).toHaveBeenCalledWith([anotherDate], expect.anything()); }); it("should add newly selected date to array of selected dates", () => { const onChange = jest.fn(); const previouslyAddedDate = new Date("2024-01-01"); const { container } = render( , ); const input = safeQuerySelector(container, "input"); fireEvent.focus(input); const days = container.querySelectorAll(".react-datepicker__day"); expect(days[5]).toBeTruthy(); // Might seem odd, but the first couple of days of the calendar is in january, so days[5] is february 2nd fireEvent.click(days[5]!); expect(onChange).toHaveBeenCalled(); expect(onChange).toHaveBeenCalledWith( [previouslyAddedDate, testDate], expect.anything(), ); }); }); describe("selectsRange with inline", () => { it("should change dates of range when dates are empty", () => { const selected = newDate(); let startDate, endDate; const onChange = (dates: [Date | null, Date | null]) => { startDate = dates[0] ?? undefined; endDate = dates[1] ?? undefined; }; const { container } = render( , ); const selectedDay = safeQuerySelector( container, ".react-datepicker__day--selected", ); fireEvent.click(selectedDay); expect(startDate).toBeTruthy(); expect(formatDate(startDate!, "yyyy-MM-dd")).toBe( formatDate(selected, "yyyy-MM-dd"), ); expect(endDate).toBeUndefined(); }); it("should change dates of range set endDate when startDate is set", () => { let startDate: Date | undefined = newDate("2024-03-08"); const nextDay = addDays(startDate, 1); let endDate: Date | undefined = undefined; const onChange = (dates: [Date | null, Date | null]) => { startDate = dates[0] ?? undefined; endDate = dates[1] ?? undefined; }; const { container } = render( , ); const day = safeQuerySelector( container, ".react-datepicker__day--selected + .react-datepicker__day", ); fireEvent.click(day); expect(formatDate(startDate, "yyyy-MM-dd")).toBe( formatDate(startDate, "yyyy-MM-dd"), ); expect(endDate).not.toBeUndefined(); expect(formatDate(endDate as unknown as Date, "yyyy-MM-dd")).toBe( formatDate(nextDay, "yyyy-MM-dd"), ); }); it("should change dates of range set endDate null when range is filled", () => { const selected = newDate(); let [startDate, endDate]: [Date | undefined, Date | undefined] = [ selected, selected, ]; const onChange = (dates: [Date | null, Date | null]) => { startDate = dates[0] ?? undefined; endDate = dates[1] ?? undefined; }; const { container } = render( , ); const day = safeQuerySelector( container, ".react-datepicker__day--selected", ); fireEvent.click(day); expect(formatDate(startDate, "yyyy-MM-dd")).toBe( formatDate(selected, "yyyy-MM-dd"), ); expect(endDate).toBeUndefined(); }); it("should change dates of range change startDate when endDate set before startDate", () => { const selected = newDate(); const selectedPrevious = subDays(newDate(), 3); let [startDate, endDate]: [Date | undefined, Date | undefined] = [ selected, undefined, ]; const onChange = (dates: [Date | null, Date | null]) => { startDate = dates[0] ?? undefined; endDate = dates[1] ?? undefined; }; const { container } = render( , ); let selectedDay = findSelectedDay(container, selectedPrevious); // Ensure that we're dealing with a date at the beginning of the month if (!selectedDay) { // If it's the beginning of the month & if the selectedPrevious is not being displayed, navigate to the previous month and reselect the selectedPrevious goToLastMonth(container); selectedDay = findSelectedDay(container, selectedPrevious); } fireEvent.click(selectedDay ?? new HTMLElement()); expect(formatDate(startDate, "yyyy-MM-dd")).toBe( formatDate(selectedPrevious, "yyyy-MM-dd"), ); expect(endDate).toBeUndefined(); }); it("should swap dates of range when endDate set before startDate", () => { const selected = newDate("2024-04-03"); const selectedPrevious = subDays(selected, 3); let [startDate, endDate]: [Date | undefined, Date | undefined] = [ selected, undefined, ]; const onChange = (dates: [Date | null, Date | null]) => { startDate = dates[0] ?? undefined; endDate = dates[1] ?? undefined; }; const { container } = render( , ); let selectedDay = findSelectedDay(container, selectedPrevious); // Ensure that we're dealing with a date at the beginning of the month if (!selectedDay) { // If it's the beginning of the month & if the selectedPrevious is not being displayed, navigate to the previous month and reselect the selectedPrevious goToLastMonth(container); selectedDay = findSelectedDay(container, selectedPrevious); } fireEvent.click(selectedDay ?? new HTMLElement()); expect(formatDate(startDate, "yyyy-MM-dd")).toBe( formatDate(selectedPrevious, "yyyy-MM-dd"), ); expect(endDate).not.toBeUndefined(); expect(formatDate(endDate as unknown as Date, "yyyy-MM-dd")).toBe( formatDate(selected, "yyyy-MM-dd"), ); }); }); describe("is-selecting-range", () => { const IN_RANGE_DAY_CLASS_NAME = "react-datepicker__day--in-selecting-range"; it("should apply '--in-selecting-range' class to the days till the preselected keyboard date on navigating to the next month without selecting endDate in the endDatePicker", () => { const preselectedDay = 5; const startDate = new Date(`2025/02/${preselectedDay}`); const { container } = render( , ); goToNextMonth(container); for (let i = 1; i <= preselectedDay; i++) { const inSelectionRangeDay = safeQuerySelector( container, `.react-datepicker__day--00${i}`, ); expect( inSelectionRangeDay.classList.contains(IN_RANGE_DAY_CLASS_NAME), ).toBe(true); } }); }); describe("selectsRange without inline", () => { it("should have preSelection set to startDate upon opening", () => { const startDate = new Date("2021-04-20 00:00:00"); const endDate = undefined; let instance: DatePicker | null = null; render( { instance = node; }} selectsRange startDate={startDate} endDate={endDate} />, ); expect(instance).toBeTruthy(); expect(instance!.input).toBeTruthy(); // Click to open fireEvent.click(instance!.input!); expect(instance!.state.preSelection).toBe(startDate); }); it("should remain open after clicking day when startDate is null", () => { const startDate = undefined; const endDate = undefined; const { container } = render( , ); const input = safeQuerySelector(container, "input"); fireEvent.click(input); const datePickerDay = safeQuerySelector( container, ".react-datepicker__day", ); fireEvent.click(datePickerDay); expect(container.querySelector(".react-datepicker")).not.toBeNull(); }); it("should be closed after clicking day when startDate has a value (endDate is being selected)", () => { const startDate = new Date("2021-01-01 00:00:00"); const endDate = undefined; const { container } = render( , ); const input = safeQuerySelector(container, "input"); fireEvent.click(input); const days = container.querySelectorAll(".react-datepicker__day"); const day = days[Math.floor(days.length / 2)]; expect(day).toBeTruthy(); fireEvent.click(day!); expect(container.querySelector(".react-datepicker")).toBeNull(); }); it("should be closed after clicking day when startDate has a value (endDate is being selected) and swapRange prop was passed", () => { const startDate = new Date("2021-01-01 00:00:00"); const endDate = undefined; const { container } = render( , ); const input = safeQuerySelector(container, "input"); fireEvent.click(input); const days = container.querySelectorAll(".react-datepicker__day"); const day = days[Math.floor(days.length / 2)]; expect(day).toBeTruthy(); fireEvent.click(day!); expect(container.querySelector(".react-datepicker")).toBeNull(); }); it("has clear button rendered when isClearable is true and startDate has value", () => { const startDate = new Date("2021-01-01 00:00:00"); const endDate = new Date("2021-01-21 00:00:00"); const { container } = render( , ); const clearButton = container.querySelector( ".react-datepicker__close-icon", ); expect(clearButton).not.toBeNull(); }); it("clearing calls onChange with [null, null] in first argument making it consistent with the onChange behaviour for selecting days for selectsRange", () => { const onChangeSpy = jest.fn(); let instance: DatePicker | null = null; render( { instance = node; }} />, ); expect(instance).toBeTruthy(); act(() => { instance!.clear(); }); expect(onChangeSpy).toHaveBeenCalled(); expect(Array.isArray(onChangeSpy.mock.calls[0][0])).toBe(true); expect(onChangeSpy.mock.calls[0][0][0]).toBeNull(); expect(onChangeSpy.mock.calls[0][0][1]).toBeNull(); }); it("should call the onChange even when the startDate and the endDate is same in the range (case when we programmatically set the startDate, but set the same endDate through UI)", () => { const startDate = new Date(); const endDate = undefined; const onChangeSpy = jest.fn(); const { container } = render( , ); const input = safeQuerySelector(container, "input"); expect(input).toBeTruthy(); fireEvent.click(input); const calendar = container.querySelector(".react-datepicker"); expect(calendar).toBeTruthy(); // Select the same date as the start date const startDatePrefixedWithZeros = formatDayWithZeros( startDate.getDate(), ); const endDateElement = safeQuerySelector( container, `.react-datepicker__day--${startDatePrefixedWithZeros}`, ); fireEvent.click(endDateElement); expect(onChangeSpy).toHaveBeenCalled(); }); it("should hide the calendar even when the startDate and the endDate is same in the range", () => { let startDate: Date | null = new Date("2021-01-21 00:00:00"); let endDate: Date | null = null; const onCalendarCloseSpy = jest.fn(); const onChange = (dates: [Date | null, Date | null]) => { const [start, end] = dates; startDate = start; endDate = end; }; const { container } = render( , ); const input = safeQuerySelector(container, "input"); expect(input).toBeTruthy(); fireEvent.click(input!); let calendar = container.querySelector(".react-datepicker"); expect(calendar).toBeTruthy(); // Select the same date as the start date const startDatePrefixedWithZeros = formatDayWithZeros( startDate.getDate(), ); const endDateElement = safeQuerySelector( container, `.react-datepicker__day--${startDatePrefixedWithZeros}`, ); fireEvent.click(endDateElement); calendar = container.querySelector(".react-datepicker"); expect(calendar).toBeFalsy(); expect(onCalendarCloseSpy).toHaveBeenCalled(); }); it("should select start date and end date if user inputs the range manually in the input box", () => { const onChangeSpy = jest.fn(); let instance: DatePicker | null = null; const { container } = render( { instance = node; }} />, ); expect(instance).toBeTruthy(); const input = safeQuerySelector(container, "input"); fireEvent.change(input, { target: { value: "03/04/2024 - 05/06/2024", }, }); // cover `if (startDateNew && isDayDisabled(startDateNew, this.props))` block fireEvent.change(input, { target: { value: "01/01/2024-05/06/2024", }, }); // cover `if (endDateNew && isDayDisabled(endDateNew, this.props))` block fireEvent.change(input, { target: { value: "03/04/2023-01/01/2024", }, }); expect(onChangeSpy).toHaveBeenCalledTimes(1); expect(Array.isArray(onChangeSpy.mock.calls[0][0])).toBe(true); expect(onChangeSpy.mock.calls[0][0][0]).toBeTruthy(); expect(onChangeSpy.mock.calls[0][0][1]).toBeTruthy(); expect(formatDate(onChangeSpy.mock.calls[0][0][0], "MM/dd/yyyy")).toBe( "03/04/2024", ); expect(formatDate(onChangeSpy.mock.calls[0][0][1], "MM/dd/yyyy")).toBe( "05/06/2024", ); }); it("should parses date range with dashes correctly", () => { const onChangeSpy = jest.fn(); const dateFormat = "yyyy-MM-dd"; const { container } = render( , ); const input = safeQuerySelector(container, "input"); fireEvent.change(input, { target: { value: "2024-03-04 - 2024-05-06", }, }); expect(onChangeSpy).toHaveBeenCalledTimes(1); expect(Array.isArray(onChangeSpy.mock.calls[0][0])).toBe(true); const [startDate, endDate] = onChangeSpy.mock.calls[0][0]; expect(startDate).toBeTruthy(); expect(endDate).toBeTruthy(); expect(formatDate(startDate, dateFormat)).toBe("2024-03-04"); expect(formatDate(endDate, dateFormat)).toBe("2024-05-06"); }); it("should not fire onChange a second time if user edits text box without the parsing result changing", () => { const onChangeSpy = jest.fn(); let instance: DatePicker | null = null; const { container } = render( { instance = node; }} />, ); expect(instance).toBeTruthy(); const input = safeQuerySelector(container, "input"); // cover `if (!startChanged && !endChanged)` block fireEvent.change(input, { target: { value: "03/04/2024-05/06/2024", }, }); expect(onChangeSpy).not.toHaveBeenCalled(); }); it("should render custom separator when `rangeSeparator` is provided", () => { const onChangeSpy = jest.fn(); const { container } = render( , ); const input = safeQuerySelector(container, "input"); expect(input.value).toBe("2025/01/01 to 2025/01/03"); }); it("should update calendar view when startDate prop changes to a different month (issue #3367)", () => { // This test verifies that when a user programmatically sets startDate // (e.g., via a "This Week" button), the calendar view updates to show // the month containing the new startDate, even if user was viewing a different month. const initialStartDate = new Date("2024-01-15"); const initialEndDate = new Date("2024-01-20"); let instance: DatePicker | null = null; const { container, rerender } = render( { instance = node; }} selectsRange startDate={initialStartDate} endDate={initialEndDate} onChange={() => {}} />, ); expect(instance).toBeTruthy(); // Open the calendar const input = safeQuerySelector(container, "input"); fireEvent.click(input); // Verify initial preSelection is set to January 2024 expect(instance!.state.preSelection?.getMonth()).toBe(0); // January expect(instance!.state.preSelection?.getFullYear()).toBe(2024); // Close calendar fireEvent.click(input); // Simulate programmatic update to a different month (e.g., user clicks "This Week" button in March) const newStartDate = new Date("2024-03-10"); const newEndDate = new Date("2024-03-15"); rerender( { instance = node; }} selectsRange startDate={newStartDate} endDate={newEndDate} onChange={() => {}} />, ); // Open the calendar again fireEvent.click(input); // The calendar should now show March 2024, not January 2024 expect(instance!.state.preSelection?.getMonth()).toBe(2); // March expect(instance!.state.preSelection?.getFullYear()).toBe(2024); }); }); describe("duplicate dates when multiple months", () => { const selected = newDate("2023-05-15"); it("should find duplicates at end on all months except last month", () => { // twoMonths const { container, rerender } = render( , ); const input = safeQuerySelector(container, "input"); fireEvent.click(input); const months = container.querySelectorAll(".react-datepicker__month"); expect(months).toHaveLength(2); // 2023-05 monthShowsDuplicateDaysEnd:true // 2023-06-03 expect( Array.from( months[0]?.querySelectorAll(".react-datepicker__day") ?? [], ).at(-1)?.textContent, ).toBe(""); // 2023-06 monthShowsDuplicateDaysEnd: false // 2023-07-01 expect( Array.from( months[1]?.querySelectorAll(".react-datepicker__day") ?? [], ).at(-1)?.textContent, ).toBe("1"); // moreThanTwoMonths rerender(); fireEvent.click(input); const monthsMore = container.querySelectorAll(".react-datepicker__month"); expect(monthsMore).toHaveLength(4); // 2023-05 monthShowsDuplicateDaysEnd:true // 2023-06-03 expect( Array.from( monthsMore[0]?.querySelectorAll(".react-datepicker__day") ?? [], ).at(-1)?.textContent, ).toBe(""); // 2023-06 monthShowsDuplicateDaysEnd:true // 2023-07-01 expect( Array.from( monthsMore[1]?.querySelectorAll(".react-datepicker__day") ?? [], ).at(-1)?.textContent, ).toBe(""); // 2023-07 monthShowsDuplicateDaysEnd:true // 2023-08-05 expect( Array.from( monthsMore[2]?.querySelectorAll(".react-datepicker__day") ?? [], ).at(-1)?.textContent, ).toBe(""); // 2023-08 monthShowsDuplicateDaysEnd:false // 2023-09-02 expect( Array.from( monthsMore[3]?.querySelectorAll(".react-datepicker__day") ?? [], ).at(-1)?.textContent, ).toBe("2"); }); it("should find duplicates at start on all months except first month", () => { // twoMonths const { container, rerender } = render( , ); const input = safeQuerySelector(container, "input"); fireEvent.click(input); const months = container.querySelectorAll(".react-datepicker__month"); expect(months).toHaveLength(2); // 2023-05 monthShowsDuplicateDaysStart:false // 2023-04-30 expect( months[0]?.querySelector(".react-datepicker__day")?.textContent, ).toBe("30"); // 2023-06 monthShowsDuplicateDaysStart:true // 2023-05-28 expect( months[1]?.querySelector(".react-datepicker__day")?.textContent, ).toBe(""); // moreThanTwoMonths rerender(); fireEvent.click(input); const monthsMore = container.querySelectorAll(".react-datepicker__month"); expect(monthsMore).toHaveLength(4); // 2023-05 monthShowsDuplicateDaysStart:false // 2023-04-30 expect( monthsMore[0]?.querySelector(".react-datepicker__day")?.textContent, ).toBe("30"); // 2023-06 monthShowsDuplicateDaysStart:true // 2023-05-28 expect( monthsMore[1]?.querySelector(".react-datepicker__day")?.textContent, ).toBe(""); // 2023-07 monthShowsDuplicateDaysStart:true // 2023-06-25 expect( monthsMore[2]?.querySelector(".react-datepicker__day")?.textContent, ).toBe(""); // 2023-08 monthShowsDuplicateDaysStart:true // 2023-08-30 expect( monthsMore[3]?.querySelector(".react-datepicker__day")?.textContent, ).toBe(""); }); it("should not find duplicates when single month displayed", () => { const { container } = render(); const input = safeQuerySelector(container, "input"); fireEvent.click(input); const months = container.querySelectorAll(".react-datepicker__month"); expect(months).toHaveLength(1); // 2023-05 monthShowsDuplicateDaysEnd:false // 2023-06-03 expect( Array.from( months[0]?.querySelectorAll(".react-datepicker__day") ?? [], ).at(-1)?.textContent, ).toBe("3"); // 2023-05 monthShowsDuplicateDaysStart:false // 2023-04-30 expect( months[0]?.querySelector(".react-datepicker__day")?.textContent, ).toBe("30"); }); }); describe("multiple MonthYearPicker", () => { const selected = newDate("2023-05-15"); it("should contain a different year all headers.", () => { let instance: DatePicker | null = null; // 2 Years const { rerender } = render( { instance = node; }} monthsShown={2} selected={selected} showMonthYearPicker />, ); expect(instance).toBeTruthy(); expect(instance!.input).toBeTruthy(); fireEvent.click(instance!.input!); const headers = instance!.calendar?.containerRef.current?.querySelectorAll( ".react-datepicker__header", ) ?? []; expect(headers).toHaveLength(2); expect(headers[0]!.textContent).toBe("2023"); expect(headers[1]!.textContent).toBe("2024"); // 4 Years rerender( { instance = node; }} monthsShown={4} selected={selected} showMonthYearPicker />, ); expect(instance!.input).toBeTruthy(); fireEvent.click(instance!.input!); const headersMore = instance!.calendar?.containerRef.current?.querySelectorAll( ".react-datepicker__header", ) ?? []; expect(headersMore).toHaveLength(4); expect(headersMore[0]!.textContent).toBe("2023"); expect(headersMore[1]!.textContent).toBe("2024"); expect(headersMore[2]!.textContent).toBe("2025"); expect(headersMore[3]!.textContent).toBe("2026"); }); }); describe("multiple QuarterYearPicker", () => { const selected = newDate("2023-05-15"); it("should contain a different year all headers.", () => { let instance: DatePicker | null = null; // 2 Years const { rerender } = render( { instance = node; }} monthsShown={2} selected={selected} showQuarterYearPicker />, ); expect(instance).toBeTruthy(); expect(instance!.input).toBeTruthy(); fireEvent.click(instance!.input!); const headers = instance!.calendar?.containerRef.current?.querySelectorAll( ".react-datepicker__header", ) ?? []; expect(headers).toHaveLength(2); expect(headers[0]!.textContent).toBe("2023"); expect(headers[1]!.textContent).toBe("2024"); // 4 Years rerender( { instance = node; }} monthsShown={4} selected={selected} showQuarterYearPicker />, ); expect(instance!.input).toBeTruthy(); fireEvent.click(instance!.input!); const headersMore = instance!.calendar?.containerRef.current?.querySelectorAll( ".react-datepicker__header", ) ?? []; expect(headersMore).toHaveLength(4); expect(headersMore[0]!.textContent).toBe("2023"); expect(headersMore[1]!.textContent).toBe("2024"); expect(headersMore[2]!.textContent).toBe("2025"); expect(headersMore[3]!.textContent).toBe("2026"); }); }); describe("shouldFocusDayInline state", () => { const dateFormat = "yyyy-MM-dd"; it("should be set to true when navigating with ArrowRight key without changing displayed month", () => { let instance: DatePicker | null = null; const { container } = render( { instance = node; }} />, ); const selectedDayNode = getSelectedDayNode(container); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.ArrowRight)); expect(instance).toBeTruthy(); // Always set to true for keyboard navigation to ensure focus transfers correctly expect(instance!.state.shouldFocusDayInline).toBe(true); }); it("should be set to true when changing displayed month with ArrowRight key", () => { let instance; const { container } = render( { instance = node; }} />, ); const selectedDayNode = getSelectedDayNode(container); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.ArrowRight)); expect(instance).toBeTruthy(); expect(instance!.state.shouldFocusDayInline).toBe(true); }); it("should be set to true when changing displayed month with PageDown key", () => { let instance; const { container } = render( { instance = node; }} />, ); const selectedDayNode = getSelectedDayNode(container); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.PageDown)); expect(instance).toBeTruthy(); expect(instance!.state.shouldFocusDayInline).toBe(true); }); it("should maintain keyboard focus when navigating within same month in inline selectsRange mode with showPreviousMonths", () => { // This test verifies the fix for GitHub issue #5750 // Focus was being lost when navigating within the same month in inline mode // with selectsRange and showPreviousMonths enabled const startDate = newDate("2025-06-01"); const endDate = newDate("2025-07-01"); const div = document.createElement("div"); document.body.appendChild(div); const { container } = render( , { container: div }, ); // Find the start date (June 1) and focus it const startDateNode = container.querySelector( '.react-datepicker__day--range-start[tabindex="0"]', ); expect(startDateNode).toBeTruthy(); act(() => { (startDateNode as HTMLElement)?.focus(); }); expect(document.activeElement).toBe(startDateNode); // Navigate right (to June 2, same month) fireEvent.keyDown(startDateNode!, getKey(KeyType.ArrowRight)); // After navigation, focus should be on June 2, not lost to body const newFocusedDay = container.querySelector( '.react-datepicker__day[tabindex="0"]', ); expect(newFocusedDay).toBeTruthy(); expect(document.activeElement).not.toBe(document.body); expect( document.activeElement?.classList.contains("react-datepicker__day"), ).toBe(true); document.body.removeChild(div); }); }); describe("Calendar Header Accessibility", () => { it("renders day names with react-datepicker__sr-only full weekday and visible short name", () => { const { container } = render(); const input = safeQuerySelector(container, "input"); fireEvent.focus(input); const headers = container.querySelectorAll( '.react-datepicker__day-names > [role="columnheader"]', ); expect(headers.length).toBe(7); headers.forEach((header) => { // Should have a visually hidden span with the full weekday name const srOnly = header.querySelector(".react-datepicker__sr-only"); expect(srOnly).toBeTruthy(); expect(srOnly?.textContent?.length).toBeGreaterThan(2); // Should have a visible short name const visible = header.querySelector('span[aria-hidden="true"]'); expect(visible).toBeTruthy(); expect(visible?.textContent?.length).toBeLessThanOrEqual(3); }); }); it("renders week number column header with react-datepicker__sr-only label and visible #", () => { const { container } = render(); const input = safeQuerySelector(container, "input"); fireEvent.focus(input); const headers = container.querySelectorAll( '.react-datepicker__day-names > [role="columnheader"]', ); expect(headers.length).toBe(8); const weekNumberHeader = headers[0] as Element; const srOnly = weekNumberHeader.querySelector( ".react-datepicker__sr-only", ); expect(srOnly).toBeTruthy(); expect(srOnly?.textContent?.trim()?.toLowerCase()).toEqual("week number"); // Should have a visible short name const visible = weekNumberHeader.querySelector( 'span[aria-hidden="true"]', ); expect(visible).toBeTruthy(); expect(visible?.textContent?.trim()?.toLowerCase()).toEqual("#"); }); }); it("should show the correct start of week for GB locale", () => { registerLocale("en-GB", enGB); const { container } = render(); const input = safeQuerySelector(container, "input"); jest.spyOn(input, "focus"); fireEvent.focus(input); const firstDay = container.querySelector( ".react-datepicker__day-names > div[role='columnheader'] > span[aria-hidden='true']", ); expect(firstDay?.textContent).toBe("Mo"); }); it("should show the correct start of week for US locale", () => { registerLocale("en-US", enUS); const { container } = render(); const input = safeQuerySelector(container, "input"); jest.spyOn(input, "focus"); fireEvent.focus(input); const firstDay = container.querySelector( ".react-datepicker__day-names > div[role='columnheader'] > span[aria-hidden='true']", ); expect(firstDay?.textContent).toBe("Su"); }); it("should correctly format weekday using formatWeekDay prop", () => { const { container } = render( day.charAt(0)} />, ); const input = safeQuerySelector(container, "input"); fireEvent.focus(input); const dayNames = container.querySelectorAll( ".react-datepicker__day-names > div[role='columnheader'] > span[aria-hidden='true']", ); dayNames.forEach((dayName) => { expect(dayName.textContent).toHaveLength(1); }); }); describe("when update the datepicker input text while props.showTimeSelectOnly is set and dateFormat has only time related format", () => { const format = "h:mm aa"; it("should keep selected date in state except new time", () => { const selected = newDate("2022-02-24 10:00:00"); let date; const { container } = render( { date = d; }} showTimeSelect showTimeSelectOnly dateFormat={format} timeFormat={format} />, ); const input = safeQuerySelector(container, "input"); fireEvent.change(input, { target: { value: "8:22 AM", }, }); expect(date).toBeTruthy(); expect(isSameDay(date, selected)).toBe(true); expect(getHours(date!)).toBe(8); expect(getMinutes(date!)).toBe(22); }); }); it("clears the selected date on empty date input", () => { let date: Date | string | null = "2023-10-23 10:00:00"; const selected: Date = newDate(date); const { container: datepicker } = render( { date = d; }} showTimeSelect dateFormat="MMMM d, yyyy h:mm aa" />, ); const input = safeQuerySelector( datepicker, ".react-datepicker__input-container > input", ); fireEvent.change(input, { target: { value: "" } }); expect(date).toBe(null); }); it("clears the selected date on empty date input with showTimeSelectOnly", () => { const format = "h:mm aa"; let date: Date | string | null = "2022-02-24 10:00:00"; const selected = newDate(date); const { container: datepicker } = render( { date = d; }} showTimeSelectOnly dateFormat={format} timeFormat={format} />, ); const input = safeQuerySelector( datepicker, ".react-datepicker__input-container > input", ); fireEvent.change(input, { target: { value: "" } }); expect(date).toBe(null); }); it("should selected month when specified minDate same month", () => { const selected = newDate("2023-01-09"); let date: Date | null = null; const { container } = render( (date = d)} dateFormat="MM/yyyy" minDate={newDate("2022-12-31")} showMonthYearPicker />, ); const input = safeQuerySelector(container, "input"); fireEvent.change(input, { target: { value: "11/2022", }, }); expect(date).toBeNull(); fireEvent.change(input, { target: { value: "12/2022", }, }); expect(`${date}`).toBe(`${newDate("2022-12-01")}`); }); it("should selected year when specified minDate same year", () => { const selected = newDate("2023-01-09"); let date: Date | null = null; const { container } = render( (date = d)} dateFormat="yyyy" minDate={newDate("2022-12-31")} showYearPicker />, ); const input = safeQuerySelector(container, "input"); fireEvent.change(input, { target: { value: "2021", }, }); expect(date).toBeNull(); fireEvent.change(input, { target: { value: "2022", }, }); expect(`${date}`).toBe(`${newDate("2022-01-01")}`); }); describe("should render aria live region after date selection", () => { it("should have correct format if datepicker does not contain time", () => { let instance: DatePicker | null = null; const { container } = render( { instance = node; }} showDateSelect selected={newDate()} />, ); expect(instance).toBeTruthy(); expect(instance!.input).toBeTruthy(); fireEvent.focus(instance!.input!); fireEvent.keyDown(instance!.input!, getKey(KeyType.Enter)); const ariaLiveMessage = container.querySelector( ".react-datepicker__aria-live", )?.textContent; expect(ariaLiveMessage).toBe( `Selected date: ${safeDateFormat(instance!.props.selected, { dateFormat: "PPPP", locale: instance!.props.locale, })}`, ); }); it("should have correct format if datepicker contains time", () => { let instance: DatePicker | null = null; const { container } = render( { instance = node; }} showTimeInput showDateSelect selected={newDate()} />, ); expect(instance).toBeTruthy(); expect(instance!.input).toBeTruthy(); fireEvent.focus(instance!.input!); fireEvent.keyDown(instance!.input!, getKey(KeyType.Enter)); const ariaLiveMessage = container.querySelector( ".react-datepicker__aria-live", )?.textContent; expect(ariaLiveMessage).toBe( `Selected date: ${safeDateFormat(instance!.props.selected, { dateFormat: "PPPPp", locale: instance!.props.locale, })}`, ); }); }); describe("aria attributes on input", () => { it("should pass aria-describedby to the input using standard HTML attribute name", () => { const { container } = render( , ); const input = safeQuerySelector(container, "input"); expect(input.getAttribute("aria-describedby")).toBe("description-id"); }); it("should pass aria-describedby to the input using camelCase prop name", () => { const { container } = render( , ); const input = safeQuerySelector(container, "input"); expect(input.getAttribute("aria-describedby")).toBe("description-id"); }); it("should prefer standard HTML attribute name over camelCase for aria-describedby", () => { const { container } = render( , ); const input = safeQuerySelector(container, "input"); expect(input.getAttribute("aria-describedby")).toBe("standard-id"); }); it("should pass aria-invalid to the input using standard HTML attribute name", () => { const { container } = render( , ); const input = safeQuerySelector(container, "input"); expect(input.getAttribute("aria-invalid")).toBe("true"); }); it("should pass aria-invalid to the input using camelCase prop name", () => { const { container } = render( , ); const input = safeQuerySelector(container, "input"); expect(input.getAttribute("aria-invalid")).toBe("true"); }); it("should pass aria-labelledby to the input using standard HTML attribute name", () => { const { container } = render( , ); const input = safeQuerySelector(container, "input"); expect(input.getAttribute("aria-labelledby")).toBe("label-id"); }); it("should pass aria-labelledby to the input using camelCase prop name", () => { const { container } = render( , ); const input = safeQuerySelector(container, "input"); expect(input.getAttribute("aria-labelledby")).toBe("label-id"); }); it("should pass aria-required to the input using standard HTML attribute name", () => { const { container } = render( , ); const input = safeQuerySelector(container, "input"); expect(input.getAttribute("aria-required")).toBe("true"); }); it("should pass aria-required to the input using camelCase prop name", () => { const { container } = render( , ); const input = safeQuerySelector(container, "input"); expect(input.getAttribute("aria-required")).toBe("true"); }); it("should pass aria-label to the input using standard HTML attribute name", () => { const { container } = render( , ); const input = safeQuerySelector(container, "input"); expect(input.getAttribute("aria-label")).toBe("Select a date"); }); it("should pass aria-label to the input using camelCase prop name", () => { const { container } = render( , ); const input = safeQuerySelector(container, "input"); expect(input.getAttribute("aria-label")).toBe("Select a date"); }); it("should prefer standard HTML attribute name over camelCase for aria-label", () => { const { container } = render( , ); const input = safeQuerySelector(container, "input"); expect(input.getAttribute("aria-label")).toBe("standard-label"); }); it("should pass aria attributes to custom input using standard HTML attribute names", () => { const { container } = render( } aria-describedby="desc-id" aria-invalid="true" aria-label="date-label" aria-labelledby="label-id" aria-required="true" />, ); const input = safeQuerySelector(container, "input"); expect(input.getAttribute("aria-describedby")).toBe("desc-id"); expect(input.getAttribute("aria-invalid")).toBe("true"); expect(input.getAttribute("aria-label")).toBe("date-label"); expect(input.getAttribute("aria-labelledby")).toBe("label-id"); expect(input.getAttribute("aria-required")).toBe("true"); }); it("should preserve custom input's own aria attributes when DatePicker does not specify them", () => { // Custom input with its own aria attributes const CustomInputWithAria = React.forwardRef< HTMLInputElement, React.InputHTMLAttributes >((props, ref) => ( )); CustomInputWithAria.displayName = "CustomInputWithAria"; const { container } = render( } />, ); const input = safeQuerySelector(container, "input"); // Should preserve the custom input's aria attributes since DatePicker didn't specify any expect(input.getAttribute("aria-describedby")).toBe("custom-desc"); expect(input.getAttribute("aria-invalid")).toBe("false"); }); }); it("should not customize the className attribute if showIcon is set to false", () => { const { container } = render( , ); const showIconClass = container .querySelector(".react-datepicker__input-container") ?.getAttribute("class"); expect(showIconClass).toBe("react-datepicker__input-container"); }); it("should display the Calendar icon if showIcon is set to true", () => { const { container, rerender } = render( , ); let showIconClass = container .querySelector(".react-datepicker__input-container") ?.getAttribute("class"); expect(showIconClass).toBe( "react-datepicker__input-container react-datepicker__view-calendar-icon", ); rerender(); showIconClass = container .querySelector(".react-datepicker__calendar-icon") ?.getAttribute("class"); expect(showIconClass).toContain("react-datepicker__calendar-icon"); }); describe("Year picker", () => { it("should call onYearMouseEnter and onYearMouseEnter (Mouse Event)", () => { const onYearMouseEnterSpy = jest.fn(); const onYearMouseLeaveSpy = jest.fn(); const { container } = render( , ); const dateInput = safeQuerySelector(container, "input"); fireEvent.focus(dateInput); const selectedYear = safeQuerySelector( container, ".react-datepicker__year-text--selected", ); fireEvent.mouseEnter(selectedYear); fireEvent.mouseLeave(selectedYear); expect(onYearMouseEnterSpy).toHaveBeenCalled(); expect(onYearMouseLeaveSpy).toHaveBeenCalled(); }); it("should call onYearMouseEnter and onYearMouseEnter (Pointer Event)", () => { const onYearMouseEnterSpy = jest.fn(); const onYearMouseLeaveSpy = jest.fn(); const { container } = render( , ); const dateInput = safeQuerySelector(container, "input"); fireEvent.focus(dateInput); const selectedYear = safeQuerySelector( container, ".react-datepicker__year-text--selected", ); fireEvent.pointerEnter(selectedYear); fireEvent.pointerLeave(selectedYear); expect(onYearMouseEnterSpy).toHaveBeenCalled(); expect(onYearMouseLeaveSpy).toHaveBeenCalled(); }); }); describe("Week picker", () => { describe("Keyboard navigation", () => { it("should select the week when pressing Enter", () => { const date = new Date("2021-02-08"); let selected: Date | null = date; const onChange = (d: Date | null) => { selected = d; }; const data = getOnInputKeyDownStuff({ showWeekPicker: true, selected: date, preSelection: date, onChange, }); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); // open const selectedDayNode = getSelectedDayNode(data.container); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.ArrowDown)); // navigate to week fireEvent.keyDown(selectedDayNode!, getKey(KeyType.Enter)); expect(formatDate(selected, data.testFormat)).toBe( formatDate(new Date("2021-02-15"), data.testFormat), ); }); it("should select the week when pressing Space", () => { const date = new Date("2021-02-08"); let selected: Date | null = date; const onChange = (d: Date | null) => { selected = d; }; const data = getOnInputKeyDownStuff({ showWeekPicker: true, selected: date, preSelection: date, onChange, }); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); // open const selectedDayNode = getSelectedDayNode(data.container); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.ArrowDown)); // navigate to week fireEvent.keyDown(selectedDayNode!, getKey(KeyType.Space)); expect(formatDate(selected, data.testFormat)).toBe( formatDate(new Date("2021-02-15"), data.testFormat), ); }); it("should navigate to the previous week when pressing ArrowUp", () => { const date = new Date("2021-02-08"); const data = getOnInputKeyDownStuff({ showWeekPicker: true, selected: date, preSelection: date, }); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); const selectedDayNode = getSelectedDayNode(data.container); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.ArrowUp)); expect(data.instance.state.preSelection).toBeTruthy(); expect( formatDate(data.instance.state.preSelection!, data.testFormat), ).toBe(formatDate(new Date("2021-02-01"), data.testFormat)); }); it("should navigate to the previous week when pressing ArrowLeft", () => { const date = new Date("2021-02-08"); const data = getOnInputKeyDownStuff({ showWeekPicker: true, selected: date, preSelection: date, }); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); const selectedDayNode = getSelectedDayNode(data.container); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.ArrowLeft)); expect(data.instance.state.preSelection).toBeTruthy(); expect( formatDate(data.instance.state.preSelection!, data.testFormat), ).toBe(formatDate(new Date("2021-02-01"), data.testFormat)); }); it("should navigate to the next week when pressing ArrowDown", () => { const date = new Date("2021-02-08"); const data = getOnInputKeyDownStuff({ showWeekPicker: true, selected: date, preSelection: date, }); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); const selectedDayNode = getSelectedDayNode(data.container); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.ArrowDown)); expect(data.instance.state.preSelection).toBeTruthy(); expect( formatDate(data.instance.state.preSelection!, data.testFormat), ).toBe(formatDate(new Date("2021-02-15"), data.testFormat)); }); it("should navigate to the next week when pressing ArrowRight", () => { const date = new Date("2021-02-08"); const data = getOnInputKeyDownStuff({ showWeekPicker: true, selected: date, preSelection: date, }); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); const selectedDayNode = getSelectedDayNode(data.container); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.ArrowRight)); expect(data.instance.state.preSelection).toBeTruthy(); expect( formatDate(data.instance.state.preSelection!, data.testFormat), ).toBe(formatDate(new Date("2021-02-15"), data.testFormat)); }); it("should skip excluded week when pressing ArrowUp", () => { const date = new Date("2021-02-08"); const data = getOnInputKeyDownStuff({ showWeekPicker: true, selected: date, preSelection: date, excludeDateIntervals: [ { start: new Date("2021-02-01"), end: new Date("2021-02-07") }, ], }); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); const selectedDayNode = getSelectedDayNode(data.container); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.ArrowUp)); expect(data.instance.state.preSelection).toBeTruthy(); expect( formatDate(data.instance.state.preSelection!, data.testFormat), ).toBe(formatDate(new Date("2021-01-25"), data.testFormat)); }); it("should skip excluded week when pressing ArrowLeft", () => { const date = new Date("2021-02-08"); const data = getOnInputKeyDownStuff({ showWeekPicker: true, selected: date, preSelection: date, excludeDateIntervals: [ { start: new Date("2021-02-01"), end: new Date("2021-02-07") }, ], }); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); const selectedDayNode = getSelectedDayNode(data.container); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.ArrowLeft)); expect(data.instance.state.preSelection).toBeTruthy(); expect( formatDate(data.instance.state.preSelection!, data.testFormat), ).toBe(formatDate(new Date("2021-01-25"), data.testFormat)); }); it("should skip excluded week when pressing ArrowDown", () => { const date = new Date("2021-02-08"); const data = getOnInputKeyDownStuff({ showWeekPicker: true, selected: date, preSelection: date, excludeDateIntervals: [ { start: new Date("2021-02-15"), end: new Date("2021-02-21") }, ], }); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); const selectedDayNode = getSelectedDayNode(data.container); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.ArrowDown)); expect(data.instance.state.preSelection).toBeTruthy(); expect( formatDate(data.instance.state.preSelection!, data.testFormat), ).toBe(formatDate(new Date("2021-02-22"), data.testFormat)); }); it("should skip excluded week when pressing ArrowRight", () => { const date = new Date("2021-02-08"); const data = getOnInputKeyDownStuff({ showWeekPicker: true, selected: date, preSelection: date, excludeDateIntervals: [ { start: new Date("2021-02-15"), end: new Date("2021-02-21") }, ], }); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); const selectedDayNode = getSelectedDayNode(data.container); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.ArrowRight)); expect(data.instance.state.preSelection).toBeTruthy(); expect( formatDate(data.instance.state.preSelection!, data.testFormat), ).toBe(formatDate(new Date("2021-02-22"), data.testFormat)); }); it("should move to the next available week after pressing PageUp to a disabled date", () => { const date = new Date("2021-02-08"); const data = getOnInputKeyDownStuff({ showWeekPicker: true, selected: date, preSelection: date, excludeDateIntervals: [ { start: new Date("2021-01-04"), end: new Date("2021-01-10") }, ], }); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); const selectedDayNode = getSelectedDayNode(data.container); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.PageUp)); expect(data.instance.state.preSelection).toBeTruthy(); expect( formatDate(data.instance.state.preSelection!, data.testFormat), ).toBe(formatDate(new Date("2021-01-15"), data.testFormat)); }); it("should move to the previous available week after pressing PageDown to a disabled date", () => { const date = new Date("2021-02-08"); const data = getOnInputKeyDownStuff({ showWeekPicker: true, selected: date, preSelection: date, excludeDateIntervals: [ { start: new Date("2021-03-08"), end: new Date("2021-03-14") }, ], }); fireEvent.keyDown(data.dateInput, getKey(KeyType.ArrowDown)); const selectedDayNode = getSelectedDayNode(data.container); expect(selectedDayNode).toBeTruthy(); fireEvent.keyDown(selectedDayNode!, getKey(KeyType.PageDown)); expect(data.instance.state.preSelection).toBeTruthy(); expect( formatDate(data.instance.state.preSelection!, data.testFormat), ).toBe(formatDate(new Date("2021-03-01"), data.testFormat)); }); }); }); describe("input reset", () => { const renderDatePickerInput = (open: boolean | null = null) => { const WrapperComponent = ({ open }: { open: boolean | null }) => { const [date, setDate] = useState(new Date()); return ( ); }; return render(); }; it("should reset the date input element with the previously entered value element on blur even when the calendar open is false", () => { const { container } = renderDatePickerInput(false); const input = safeQuerySelector(container, "input") as HTMLInputElement; if (!input) { throw new Error("Input element not found"); } fireEvent.click(input); const DATE_VALUE = "02/22/2025"; fireEvent.change(input, { target: { value: DATE_VALUE, }, }); fireEvent.blur(input); expect(input.value).toBe(DATE_VALUE); fireEvent.click(input); const INVALID_DATE_VALUE = "2025-02-45"; fireEvent.change(input, { target: { value: INVALID_DATE_VALUE, }, }); fireEvent.blur(input); expect(input.value).toBe(DATE_VALUE); }); it("should reset the date input element with the previously entered value on blur even when the calendar is not open", () => { const { container } = renderDatePickerInput(); const input = safeQuerySelector(container, "input") as HTMLInputElement; fireEvent.click(input); const VALID_DATE_VALUE = "06/23/2025"; fireEvent.change(input, { target: { value: VALID_DATE_VALUE, }, }); fireEvent.blur(input); expect(input.value).toBe(VALID_DATE_VALUE); fireEvent.click(input); fireEvent.keyDown(input, getKey(KeyType.Escape)); // Make sure the calendar is hidden expect(container.querySelector(".react-datepicker")).toBeFalsy(); const INVALID_DATE_VALUE = "2025-02-45"; fireEvent.change(input, { target: { value: INVALID_DATE_VALUE, }, }); fireEvent.blur(input); expect(input.value).toBe(VALID_DATE_VALUE); }); }); describe("Close on ESC Key", () => { it("should close DatePicker on ESC key press", () => { const { container } = render(); const input = safeQuerySelector(container, "input"); fireEvent.click(input); const calendar = safeQuerySelector(container, ".react-datepicker"); fireEvent.keyDown(calendar, getKey(KeyType.Escape)); const calendarAfterEsc = container.querySelector(".react-datepicker"); expect(calendarAfterEsc).toBeFalsy(); }); it("should close DatePicker on ESC key press - even when the focus is at Calendar header buttons", () => { const { container } = render(); const input = safeQuerySelector(container, "input"); fireEvent.click(input); const calendar = safeQuerySelector(container, ".react-datepicker"); const nextMontButton = safeQuerySelector( calendar, "button.react-datepicker__navigation--next", ); fireEvent.click(nextMontButton); fireEvent.click(nextMontButton); fireEvent.keyDown(nextMontButton, getKey(KeyType.Escape)); const calendarAfterEsc = container.querySelector(".react-datepicker"); expect(calendarAfterEsc).toBeFalsy(); }); }); describe("dateFormat", () => { it("should use the default dateFormat if dateFormat prop is not provided", () => { const { container } = render( , ); const input = safeQuerySelector(container, "input") as HTMLInputElement; expect(input?.value).toBe("07/17/2025"); }); }); describe("Date Range - Handle null start date", () => { it("should display the endDate when the startDate is not available", () => { const endDateLabel = "2025-11-22"; const { container } = render( {}} isClearable />, ); const input = safeQuerySelector(container, "input"); expect(input.value).toBe(` - ${endDateLabel}`); }); it("should clear the input when the startDate alone is cleared while the endDate is still available", () => { const startDateLabel = "2025-11-17"; const endDateLabel = "2025-11-22"; const onChangeSpy = jest.fn(); const { container } = render( , ); const input = safeQuerySelector(container, "input"); expect(input.value).toBe(`${startDateLabel} - ${endDateLabel}`); fireEvent.change(input, { target: { value: ` - ${endDateLabel}` } }); expect(onChangeSpy).toHaveBeenCalledTimes(1); expect(onChangeSpy).toHaveBeenCalledWith([null, null], expect.anything()); }); it("should clear the endDate and set the startDate when the endDate is alone available and the newly selected startDate is greater than the endDate", () => { const endDateLabel = "2025-11-20"; const onChangeSpy = jest.fn(); const { container } = render( , ); const input = safeQuerySelector(container, "input"); fireEvent.focus(input); expect(container.querySelector(".react-datepicker")).toBeTruthy(); const newStartDateEl = safeQuerySelector( container, ".react-datepicker__day--021", ); fireEvent.click(newStartDateEl); expect(onChangeSpy).toHaveBeenCalledTimes(1); const changedDateRange = onChangeSpy.mock.calls[0][0]; const [changedStartDate, changedEndDate] = changedDateRange; expect(changedEndDate).toBe(null); expect(changedStartDate.toISOString()).toBe( newDate("2025-11-21").toISOString(), ); }); it("should set the startDate when the endDate is alone available and the newly selected startDate is less than the endDate", () => { const endDateLabel = "2025-11-20"; const onChangeSpy = jest.fn(); const { container } = render( , ); const input = safeQuerySelector(container, "input"); fireEvent.focus(input); expect(container.querySelector(".react-datepicker")).toBeTruthy(); const newStartDateEl = safeQuerySelector( container, ".react-datepicker__day--019", ); fireEvent.click(newStartDateEl); expect(onChangeSpy).toHaveBeenCalledTimes(1); const changedDateRange = onChangeSpy.mock.calls[0][0]; const [changedStartDate, changedEndDate] = changedDateRange; expect(changedEndDate.toISOString()).toBe( newDate(endDateLabel).toISOString(), ); expect(changedStartDate.toISOString()).toBe( newDate("2025-11-19").toISOString(), ); }); }); describe("Refocus Input", () => { it("should refocus the date input when a date is selected", async () => { const selectedDate = newDate("2025-11-01"); const onChangeSpy = jest.fn(); const { container } = render( , ); const input = safeQuerySelector(container, "input"); fireEvent.focus(input); expect(container.querySelector(".react-datepicker")).toBeTruthy(); const newSelectedDateEl = safeQuerySelector( container, ".react-datepicker__day--002", ); fireEvent.click(newSelectedDateEl); await waitFor(() => { expect(document.activeElement).not.toBe(newSelectedDateEl); expect(document.activeElement).toBe(input); }); }); describe("Date Range", () => { it("should not refocus the input when the endDate is not selected in the Date Range", async () => { const selectedDate = newDate("2025-11-01"); let startDate, endDate; const onChangeSpy = jest.fn((dates) => { [startDate, endDate] = dates; }); const { container } = render( , ); const input = safeQuerySelector(container, "input"); fireEvent.focus(input); expect(container.querySelector(".react-datepicker")).toBeTruthy(); const newStartDateEl = safeQuerySelector( container, ".react-datepicker__day--002", ); fireEvent.click(newStartDateEl); expect(onChangeSpy).toHaveBeenCalledTimes(1); await waitFor(() => { expect(document.activeElement).not.toBe(input); expect(document.activeElement).toBe(newStartDateEl); }); }); it("should refocus the input when the endDate is selected in the Date Range (if the end date is after the start date)", async () => { const selectedDate = newDate("2025-11-01"); let startDate = selectedDate, endDate; const onChangeSpy = jest.fn((dates) => { [startDate, endDate] = dates; }); const { container } = render( , ); const input = safeQuerySelector(container, "input"); fireEvent.focus(input); expect(container.querySelector(".react-datepicker")).toBeTruthy(); const endDateEl = safeQuerySelector( container, ".react-datepicker__day--005", ); fireEvent.click(endDateEl); expect(onChangeSpy).toHaveBeenCalledTimes(1); await waitFor(() => { expect(document.activeElement).toBe(input); }); }); it("should not refocus the input when the selected endDate is before the startDate", async () => { const selectedDate = newDate("2025-11-05"); let startDate = selectedDate, endDate; const onChangeSpy = jest.fn((dates) => { [startDate, endDate] = dates; }); const { container } = render( , ); const input = safeQuerySelector(container, "input"); fireEvent.focus(input); expect(container.querySelector(".react-datepicker")).toBeTruthy(); const endDateEl = safeQuerySelector( container, ".react-datepicker__day--002", ); fireEvent.click(endDateEl); expect(onChangeSpy).toHaveBeenCalledTimes(1); await waitFor(() => { expect(document.activeElement).not.toBe(input); expect(document.activeElement).toBe(endDateEl); }); }); it('should refocus the input when the selected endDate is before the startDate when the "swapRange" prop is set', async () => { const selectedDate = newDate("2025-11-05"); let startDate = selectedDate, endDate; const onChangeSpy = jest.fn((dates) => { [startDate, endDate] = dates; }); const { container } = render( , ); const input = safeQuerySelector(container, "input"); fireEvent.focus(input); expect(container.querySelector(".react-datepicker")).toBeTruthy(); const endDateEl = safeQuerySelector( container, ".react-datepicker__day--002", ); fireEvent.click(endDateEl); expect(onChangeSpy).toHaveBeenCalledTimes(1); await waitFor(() => { expect(document.activeElement).not.toBe(endDateEl); expect(document.activeElement).toBe(input); }); }); }); }); describe("onChangeRaw - selectionMeta", () => { it("should include selectionMeta as a second param to the onChangeRaw when user selected a date to provide extra meta about the selected date element", () => { const selectedDate = newDate("2025-11-05"); const onChangeRawSpy = jest.fn(); const { container } = render( , ); expect(onChangeRawSpy).not.toHaveBeenCalled(); const input = safeQuerySelector(container, "input"); fireEvent.focus(input); const day = safeQuerySelector(container, ".react-datepicker__day--002"); fireEvent.click(day); expect(onChangeRawSpy).toHaveBeenCalledTimes(1); const params = onChangeRawSpy.mock.calls[0]; expect(params.length).toBe(2); const eventObject = params[0]; expect(typeof eventObject).toBe("object"); const selectionMeta = params[1]; expect(typeof selectionMeta).toBe("object"); expect(selectionMeta).toHaveProperty("date"); expect(selectionMeta).toHaveProperty("formattedDate"); expect(selectionMeta.formattedDate).toBe("11/02/2025"); expect(selectionMeta.date).toBeInstanceOf(Date); }); it("should not include selectionMeta as a second param to the onChangeRaw when user updated date using date input", () => { const selectedDate = newDate("2025-11-05"); const onChangeRawSpy = jest.fn(); const { container } = render( , ); expect(onChangeRawSpy).not.toHaveBeenCalled(); const input = safeQuerySelector(container, "input"); const inputValue = "11/02/2025"; fireEvent.change(input, { target: { value: inputValue, }, }); expect(onChangeRawSpy).toHaveBeenCalledTimes(1); const params = onChangeRawSpy.mock.calls[0]; expect(params.length).toBe(1); const eventObject = params[0]; expect(typeof eventObject).toBe("object"); expect(eventObject.target?.value).toBe(inputValue); }); }); describe("disabled", () => { const validateAllDisabled = (container: HTMLElement, className: string) => { const allDays = Array.from(container.querySelectorAll(`.${className}`)); expect(allDays.length).toBeGreaterThan(0); expect( allDays.every((day) => day.classList.contains(`${className}--disabled`), ), ).toBe(true); }; const validateNonExistence = ( container: HTMLElement, querySelector: string, ) => { const element = container.querySelector(querySelector); expect(element).toBeFalsy(); }; it("should disable all days and headers in DatePicker when disabled prop is true", () => { const { container } = render(); validateAllDisabled(container, "react-datepicker__day"); validateAllDisabled(container, "react-datepicker__day-name"); validateNonExistence(container, "react-datepicker__navigation"); }); it("should disable all days and headers in DatePicker Range Selector when disabled prop is true", () => { const { container } = render(); validateAllDisabled(container, "react-datepicker__day"); validateAllDisabled(container, "react-datepicker__day-name"); validateNonExistence(container, "react-datepicker__navigation"); }); it("should disable all days and headers in MonthPicker when disabled prop is true", () => { const { container } = render( , ); validateAllDisabled(container, "react-datepicker__month-text"); validateNonExistence(container, "react-datepicker__navigation"); }); it("should disable all days and headers in YearPicker when disabled prop is true", () => { const { container } = render( , ); validateAllDisabled(container, "react-datepicker__year-text"); validateNonExistence(container, "react-datepicker__navigation"); }); it("should disable all days and headers in WeekPicker when disabled prop is true", () => { const { container } = render( , ); validateAllDisabled(container, "react-datepicker__day"); validateNonExistence(container, "react-datepicker__navigation"); }); it("should disable all days and headers in QuarterPicker when disabled prop is true", () => { const { container } = render( , ); validateAllDisabled(container, "react-datepicker__quarter-text"); validateNonExistence(container, "react-datepicker__navigation"); }); }); describe("Coverage improvements for index.tsx", () => { it("should handle highlightDates prop changes in componentDidUpdate", () => { const { rerender } = render( {}} />, ); const highlightDates = [newDate()]; rerender( {}} highlightDates={highlightDates} />, ); // Line 340: highlightDates state update in componentDidUpdate expect(true).toBe(true); }); it("should handle invalid holiday dates in modifyHolidays", () => { const invalidHolidays = [ { date: "invalid-date", holidayName: "Invalid Holiday" }, { date: "2024-01-01", holidayName: "Valid Holiday" }, ]; const { container } = render( {}} holidays={invalidHolidays} inline />, ); // Lines 391-396: modifyHolidays filters out invalid dates expect(container.querySelector(".react-datepicker")).not.toBeNull(); }); it("should apply holiday class to correct date regardless of timezone (issue #6105)", () => { // This test verifies that holidays specified as ISO date strings (YYYY-MM-DD) // are displayed on the correct date. The bug was that new Date("YYYY-MM-DD") // parses as UTC midnight, causing dates to shift in western timezones. const holidays = [{ date: "2024-01-15", holidayName: "Test Holiday" }]; const { container } = render( {}} holidays={holidays} inline />, ); // Find the day element for January 15th and verify it has the holiday class const jan15 = container.querySelector( ".react-datepicker__day--015:not(.react-datepicker__day--outside-month)", ); expect(jan15).not.toBeNull(); expect(jan15?.classList.contains("react-datepicker__day--holidays")).toBe( true, ); // Verify January 14th does NOT have the holiday class (the bug would show it here) const jan14 = container.querySelector( ".react-datepicker__day--014:not(.react-datepicker__day--outside-month)", ); expect(jan14).not.toBeNull(); expect(jan14?.classList.contains("react-datepicker__day--holidays")).toBe( false, ); }); it("should handle deferFocusInput and cancelFocusInput", () => { jest.useFakeTimers(); const { container } = render( {}} />, ); const input = container.querySelector("input") as HTMLInputElement; // Lines 588-589: deferFocusInput uses setTimeout fireEvent.blur(input); fireEvent.focus(input); jest.advanceTimersByTime(1); expect(input).not.toBeNull(); jest.useRealTimers(); }); it("deferFocusInput cancels pending timeouts before focusing input", () => { jest.useFakeTimers(); const { instance } = renderDatePickerWithRef({ selected: newDate(), onChange: () => {}, }); expect(instance).not.toBeNull(); const setFocusSpy = jest .spyOn(instance as DatePicker, "setFocus") .mockImplementation(() => undefined); act(() => { instance?.deferFocusInput(); instance?.deferFocusInput(); }); jest.advanceTimersByTime(1); expect(setFocusSpy).toHaveBeenCalledTimes(1); setFocusSpy.mockRestore(); jest.useRealTimers(); }); it("clears ranges when changed date is null and start date exists", () => { const onChange = jest.fn(); const startDate = new Date("2024-01-15T00:00:00"); const { instance } = renderDatePickerWithRef({ inline: true, selectsRange: true, startDate, endDate: null, selected: null, onChange, }); act(() => { instance?.setSelected(null); }); expect(onChange).toHaveBeenCalledWith([null, null], undefined); }); it("reports input errors when escaping with invalid preSelection", () => { const onInputError = jest.fn(); const { instance } = renderDatePickerWithRef({ selected: null, onChange: () => {}, onInputError, }); act(() => { instance?.setState({ preSelection: "invalid-date" as unknown as Date, open: true, }); }); act(() => { instance?.onInputKeyDown({ key: "Escape", preventDefault: jest.fn(), target: document.createElement("input"), } as unknown as React.KeyboardEvent); }); expect(onInputError).toHaveBeenCalled(); }); it("reports input errors when input key down completes with invalid preSelection", () => { const onInputError = jest.fn(); const { instance } = renderDatePickerWithRef({ selected: null, onChange: () => {}, onInputError, }); act(() => { instance?.setState({ preSelection: "invalid-date" as unknown as Date, open: true, }); }); act(() => { instance?.onInputKeyDown({ key: "Tab", preventDefault: jest.fn(), target: document.createElement("input"), } as unknown as React.KeyboardEvent); }); expect(onInputError).toHaveBeenCalled(); }); it("reports input errors when unsupported key is pressed in calendar grid", () => { const onInputError = jest.fn(); const { instance } = renderDatePickerWithRef({ selected: newDate(), onChange: () => {}, onInputError, inline: true, }); act(() => { instance?.setState({ preSelection: newDate() }); }); act(() => { instance?.onDayKeyDown({ key: "A", shiftKey: false, preventDefault: jest.fn(), target: document.createElement("div"), } as unknown as React.KeyboardEvent); }); expect(onInputError).toHaveBeenCalled(); }); it("reports input errors when escape is pressed within the calendar grid", () => { const onInputError = jest.fn(); const { instance } = renderDatePickerWithRef({ selected: null, onChange: () => {}, onInputError, inline: true, }); act(() => { instance?.setState({ preSelection: "invalid-date" as unknown as Date }); }); act(() => { instance?.onDayKeyDown({ key: "Escape", shiftKey: false, preventDefault: jest.fn(), target: document.createElement("div"), } as unknown as React.KeyboardEvent); }); expect(onInputError).toHaveBeenCalled(); }); describe("aria-live messaging", () => { it("describes range selections", () => { const startDate = new Date("2024-01-01T00:00:00"); const endDate = new Date("2024-01-02T00:00:00"); const { instance } = renderDatePickerWithRef({ selectsRange: true, startDate, endDate, selected: endDate, }); const message = instance?.renderAriaLiveRegion(); expect(message?.props.children).toContain("Selected start date"); }); it("describes time-only selections", () => { const { instance } = renderDatePickerWithRef({ showTimeSelectOnly: true, selected: new Date("2024-01-01T12:00:00"), }); const message = instance?.renderAriaLiveRegion(); expect(message?.props.children).toContain("Selected time:"); }); it("describes year picker selections", () => { const { instance } = renderDatePickerWithRef({ showYearPicker: true, selected: new Date("2024-01-01T00:00:00"), }); const message = instance?.renderAriaLiveRegion(); expect(message?.props.children).toContain("Selected year:"); }); it("describes month-year picker selections", () => { const { instance } = renderDatePickerWithRef({ showMonthYearPicker: true, selected: new Date("2024-03-01T00:00:00"), }); const message = instance?.renderAriaLiveRegion(); expect(message?.props.children).toContain("Selected month:"); }); it("describes quarter picker selections", () => { const { instance } = renderDatePickerWithRef({ showQuarterYearPicker: true, selected: new Date("2024-06-01T00:00:00"), }); const message = instance?.renderAriaLiveRegion(); expect(message?.props.children).toContain("Selected quarter:"); }); }); it("should handle focus on year dropdown", () => { const { container } = render( {}} showYearDropdown dropdownMode="select" />, ); const input = container.querySelector("input") as HTMLInputElement; fireEvent.focus(input); const yearDropdown = container.querySelector( ".react-datepicker__year-select", ) as HTMLSelectElement; // Line 851: handleDropdownFocus if (yearDropdown) { fireEvent.focus(yearDropdown); } expect(container.querySelector(".react-datepicker")).not.toBeNull(); }); it("should handle setSelected with adjustDateOnChange", () => { const onChange = jest.fn(); const { container } = render( , ); const input = container.querySelector("input") as HTMLInputElement; fireEvent.focus(input); const calendar = container.querySelector(".react-datepicker"); const dayElement = calendar?.querySelector( ".react-datepicker__day:not(.react-datepicker__day--disabled)", ) as HTMLElement; if (dayElement) { fireEvent.click(dayElement); } // Line 1044: adjustDateOnChange logic expect(calendar).not.toBeNull(); }); it("should handle onInputKeyDown with date range and Tab key", () => { const onChange = jest.fn(); const { container } = render( , ); const input = container.querySelector("input") as HTMLInputElement; fireEvent.focus(input); // Line 1191: Tab key handling in range mode fireEvent.keyDown(input, { key: "Tab", code: 9 }); expect(input).not.toBeNull(); }); it("should handle onDayMouseEnter with selectsRange and keyboard selection", () => { const { container } = render( {}} startDate={newDate()} endDate={null} selectsRange inline />, ); const days = container.querySelectorAll( ".react-datepicker__day:not(.react-datepicker__day--disabled)", ); if (days.length > 1) { const secondDay = days[1] as HTMLElement; // Lines 1210-1211: onDayMouseEnter with selectsRange fireEvent.mouseEnter(secondDay); } expect(container.querySelector(".react-datepicker")).not.toBeNull(); }); it("should handle ariaLiveMessage with selectsRange", () => { const { container } = render( {}} startDate={newDate()} endDate={newDate()} selectsRange inline />, ); // Line 1336: ariaLiveMessage for selectsRange is constructed const datepicker = container.querySelector(".react-datepicker"); expect(datepicker).not.toBeNull(); }); it("should handle onYearMouseEnter with selectsRange", () => { const { container } = render( {}} startDate={newDate()} endDate={null} selectsRange showYearPicker inline />, ); const yearElement = container.querySelector( ".react-datepicker__year-text", ) as HTMLElement; // Line 1353: onYearMouseEnter with selectsRange if (yearElement) { fireEvent.mouseEnter(yearElement); } expect(yearElement).not.toBeNull(); }); it("should handle onMonthMouseLeave with selectsRange", () => { const { container } = render( {}} startDate={newDate()} endDate={null} selectsRange showMonthYearPicker inline />, ); const monthElement = container.querySelector( ".react-datepicker__month-text", ) as HTMLElement; // Line 1358: onMonthMouseLeave if (monthElement) { fireEvent.mouseLeave(monthElement); } expect(container.querySelector(".react-datepicker")).not.toBeNull(); }); it("should handle onQuarterMouseLeave with selectsRange", () => { const { container } = render( {}} startDate={newDate()} endDate={null} selectsRange showQuarterYearPicker inline />, ); const quarterElement = container.querySelector( ".react-datepicker__quarter-text", ) as HTMLElement; // Line 1363: onQuarterMouseLeave if (quarterElement) { fireEvent.mouseLeave(quarterElement); } expect(container.querySelector(".react-datepicker")).not.toBeNull(); }); it("should handle onYearMouseLeave with selectsRange", () => { const { container } = render( {}} startDate={newDate()} endDate={null} selectsRange showYearPicker inline />, ); const yearElement = container.querySelector( ".react-datepicker__year-text", ) as HTMLElement; // Line 1368: onYearMouseLeave if (yearElement) { fireEvent.mouseLeave(yearElement); } expect(yearElement).not.toBeNull(); }); }); describe("showTimeSelect with selectsRange", () => { it("should apply time to startDate when only startDate is selected", () => { const startDate = newDate("2024-01-15 00:00:00"); const onChange = jest.fn(); const { container } = render( , ); const timeElements = container.querySelectorAll( ".react-datepicker__time-list-item", ); expect(timeElements.length).toBeGreaterThan(0); // Find a time element (e.g., 10:00 AM) const timeElement = Array.from(timeElements).find( (el) => el.textContent === "10:00 AM", ); expect(timeElement).toBeTruthy(); fireEvent.click(timeElement!); expect(onChange).toHaveBeenCalledTimes(1); const [changedStartDate, changedEndDate] = onChange.mock.calls[0][0]; // startDate should have the new time applied expect(changedStartDate).toBeTruthy(); expect(getHours(changedStartDate)).toBe(10); expect(getMinutes(changedStartDate)).toBe(0); // endDate should still be null expect(changedEndDate).toBeNull(); }); it("should apply time to endDate when both startDate and endDate are selected", () => { const startDate = newDate("2024-01-15 09:00:00"); const endDate = newDate("2024-01-20 00:00:00"); const onChange = jest.fn(); const { container } = render( , ); const timeElements = container.querySelectorAll( ".react-datepicker__time-list-item", ); expect(timeElements.length).toBeGreaterThan(0); // Find a time element (e.g., 2:30 PM) const timeElement = Array.from(timeElements).find( (el) => el.textContent === "2:30 PM", ); expect(timeElement).toBeTruthy(); fireEvent.click(timeElement!); expect(onChange).toHaveBeenCalledTimes(1); const [changedStartDate, changedEndDate] = onChange.mock.calls[0][0]; // startDate should remain unchanged expect(changedStartDate).toBeTruthy(); expect(getHours(changedStartDate)).toBe(9); expect(getMinutes(changedStartDate)).toBe(0); // endDate should have the new time applied expect(changedEndDate).toBeTruthy(); expect(getHours(changedEndDate)).toBe(14); expect(getMinutes(changedEndDate)).toBe(30); }); it("should not call onChange when no dates are selected in range mode", () => { const onChange = jest.fn(); const { container } = render( , ); const timeElements = container.querySelectorAll( ".react-datepicker__time-list-item", ); expect(timeElements.length).toBeGreaterThan(0); // Click a time when no dates are selected fireEvent.click(timeElements[0]!); // onChange should not be called when no dates are selected // because we need a date to apply the time to expect(onChange).not.toHaveBeenCalled(); }); it("should call onChange with tuple format [Date, Date] when time is selected in range mode", () => { const startDate = newDate("2024-01-15 00:00:00"); const endDate = newDate("2024-01-20 00:00:00"); const onChange = jest.fn(); const { container } = render( , ); const timeElements = container.querySelectorAll( ".react-datepicker__time-list-item", ); const timeElement = timeElements[0]; expect(timeElement).toBeTruthy(); fireEvent.click(timeElement!); expect(onChange).toHaveBeenCalledTimes(1); // Verify the argument is an array (tuple) const callArg = onChange.mock.calls[0][0]; expect(Array.isArray(callArg)).toBe(true); expect(callArg.length).toBe(2); }); it("should not throw TypeError when clicking time with selectsRange enabled", () => { const startDate = newDate("2024-01-15 00:00:00"); const onChange = jest.fn(); const { container } = render( , ); const timeElements = container.querySelectorAll( ".react-datepicker__time-list-item", ); expect(timeElements.length).toBeGreaterThan(0); // This should not throw "Uncaught TypeError: Invalid attempt to destructure non-iterable instance" expect(() => { fireEvent.click(timeElements[0]!); }).not.toThrow(); expect(onChange).toHaveBeenCalled(); }); it("should close calendar after time selection when shouldCloseOnSelect is true", () => { const startDate = newDate("2024-01-15 00:00:00"); const endDate = newDate("2024-01-20 00:00:00"); let instance: DatePicker | null = null; const { container } = render( { instance = node; }} selectsRange startDate={startDate} endDate={endDate} onChange={jest.fn()} showTimeSelect shouldCloseOnSelect />, ); expect(instance).toBeTruthy(); // Open the calendar const input = safeQuerySelector(container, "input"); fireEvent.focus(input); expect(instance!.state.open).toBe(true); const timeElements = container.querySelectorAll( ".react-datepicker__time-list-item", ); expect(timeElements.length).toBeGreaterThan(0); fireEvent.click(timeElements[0]!); // Calendar should close after time selection expect(instance!.state.open).toBe(false); }); }); describe("showTimeInput with selectsRange", () => { it("should render two time inputs when selectsRange is true", () => { const startDate = newDate("2024-01-15 09:00:00"); const endDate = newDate("2024-01-20 14:30:00"); const { container } = render( , ); const timeInputs = container.querySelectorAll( ".react-datepicker__input-time-container", ); // Should render two time input containers (one for start, one for end) expect(timeInputs.length).toBe(2); // Check labels const labels = container.querySelectorAll( ".react-datepicker-time__caption", ); expect(labels.length).toBe(2); expect(labels[0]?.textContent).toContain("Start"); expect(labels[1]?.textContent).toContain("End"); }); it("should apply time to startDate when start time input is changed", () => { const startDate = newDate("2024-01-15 09:00:00"); const endDate = newDate("2024-01-20 14:30:00"); const onChange = jest.fn(); const { container } = render( , ); const timeInputs = container.querySelectorAll( ".react-datepicker-time__input input", ); expect(timeInputs.length).toBe(2); // Change the start time input fireEvent.change(timeInputs[0]!, { target: { value: "11:30" } }); expect(onChange).toHaveBeenCalledTimes(1); const [changedStartDate, changedEndDate] = onChange.mock.calls[0][0]; // startDate should have the new time applied expect(changedStartDate).toBeTruthy(); expect(getHours(changedStartDate)).toBe(11); expect(getMinutes(changedStartDate)).toBe(30); // endDate should remain unchanged expect(changedEndDate).toBeTruthy(); expect(getHours(changedEndDate)).toBe(14); expect(getMinutes(changedEndDate)).toBe(30); }); it("should apply time to endDate when end time input is changed", () => { const startDate = newDate("2024-01-15 09:00:00"); const endDate = newDate("2024-01-20 14:30:00"); const onChange = jest.fn(); const { container } = render( , ); const timeInputs = container.querySelectorAll( ".react-datepicker-time__input input", ); expect(timeInputs.length).toBe(2); // Change the end time input fireEvent.change(timeInputs[1]!, { target: { value: "16:45" } }); expect(onChange).toHaveBeenCalledTimes(1); const [changedStartDate, changedEndDate] = onChange.mock.calls[0][0]; // startDate should remain unchanged expect(changedStartDate).toBeTruthy(); expect(getHours(changedStartDate)).toBe(9); expect(getMinutes(changedStartDate)).toBe(0); // endDate should have the new time applied expect(changedEndDate).toBeTruthy(); expect(getHours(changedEndDate)).toBe(16); expect(getMinutes(changedEndDate)).toBe(45); }); it("should render only one time input when selectsRange is false", () => { const selected = newDate("2024-01-15 09:00:00"); const { container } = render( , ); const timeInputs = container.querySelectorAll( ".react-datepicker__input-time-container", ); // Should render only one time input container expect(timeInputs.length).toBe(1); }); it("should show correct initial time values in both inputs", () => { const startDate = newDate("2024-01-15 09:30:00"); const endDate = newDate("2024-01-20 14:45:00"); const { container } = render( , ); const timeInputs = container.querySelectorAll( ".react-datepicker-time__input input", ); expect(timeInputs.length).toBe(2); // Check start time input value expect(timeInputs[0]?.value).toBe("09:30"); // Check end time input value expect(timeInputs[1]?.value).toBe("14:45"); }); it("should handle case when only startDate is selected", () => { const startDate = newDate("2024-01-15 09:00:00"); const onChange = jest.fn(); const { container } = render( , ); const timeInputs = container.querySelectorAll( ".react-datepicker-time__input input", ); expect(timeInputs.length).toBe(2); // Change the start time input fireEvent.change(timeInputs[0]!, { target: { value: "11:30" } }); expect(onChange).toHaveBeenCalledTimes(1); const [changedStartDate, changedEndDate] = onChange.mock.calls[0][0]; // startDate should have the new time applied expect(changedStartDate).toBeTruthy(); expect(getHours(changedStartDate)).toBe(11); expect(getMinutes(changedStartDate)).toBe(30); // endDate should still be null expect(changedEndDate).toBeNull(); }); it("should not throw TypeError when changing time with selectsRange enabled", () => { const startDate = newDate("2024-01-15 09:00:00"); const endDate = newDate("2024-01-20 14:30:00"); const onChange = jest.fn(); const { container } = render( , ); const timeInputs = container.querySelectorAll( ".react-datepicker-time__input input", ); // This should not throw any errors expect(() => { fireEvent.change(timeInputs[0]!, { target: { value: "11:30" } }); }).not.toThrow(); expect(() => { fireEvent.change(timeInputs[1]!, { target: { value: "16:45" } }); }).not.toThrow(); }); it("should apply time change in single date mode (non-range)", () => { const selected = newDate("2024-01-15 09:00:00"); const onChange = jest.fn(); const { container } = render( , ); const timeInputs = container.querySelectorAll( ".react-datepicker-time__input input", ); expect(timeInputs.length).toBe(1); // Change the time input fireEvent.change(timeInputs[0]!, { target: { value: "14:30" } }); expect(onChange).toHaveBeenCalledTimes(1); const changedDate = onChange.mock.calls[0][0]; // Date should have the new time applied expect(changedDate).toBeTruthy(); expect(getHours(changedDate)).toBe(14); expect(getMinutes(changedDate)).toBe(30); }); it("should render two time inputs with empty values when no dates are selected in range mode", () => { const { container } = render( , ); const timeInputs = container.querySelectorAll( ".react-datepicker-time__input input", ); // Should render two time input containers expect(timeInputs.length).toBe(2); // Both inputs should have empty values since no dates are selected expect(timeInputs[0]?.value).toBe(""); expect(timeInputs[1]?.value).toBe(""); }); }); describe("Critical functions coverage - best in class", () => { it("should handle handleTimeChange with selectsMultiple (line 942)", () => { const onChange = jest.fn(); const { container } = render( , ); const timeElements = container.querySelectorAll( ".react-datepicker__time-list-item", ); expect(timeElements.length).toBeGreaterThan(0); const firstTimeElement = timeElements[0] as HTMLElement; // Line 942: handleTimeChange early return for selectsMultiple fireEvent.click(firstTimeElement); expect(container.querySelector(".react-datepicker")).not.toBeNull(); }); it("should handle handleTimeChange with selectsMultiple - duplicate test (line 942)", () => { const onChange = jest.fn(); const { container } = render( , ); const timeElements = container.querySelectorAll( ".react-datepicker__time-list-item", ); expect(timeElements.length).toBeGreaterThan(0); const firstTimeElement = timeElements[0] as HTMLElement; // Line 942: handleTimeChange early return for selectsMultiple fireEvent.click(firstTimeElement); expect(container.querySelector(".react-datepicker")).not.toBeNull(); }); it("should handle adjustDateOnChange in setSelected (line 1044)", () => { const onChange = jest.fn(); const minDate = newDate("2024-01-10"); const { container } = render( , ); // Click a date before minDate const calendar = container.querySelector(".react-datepicker"); const days = calendar?.querySelectorAll(".react-datepicker__day"); expect(days).toBeDefined(); expect(days!.length).toBeGreaterThan(0); const firstDay = days![0] as HTMLElement; fireEvent.click(firstDay); // Line 1044: adjustDateOnChange should adjust the date to minDate expect(onChange).toHaveBeenCalled(); }); it("should handle onDayMouseEnter with selectsRange and keyboard (lines 1210-1211)", () => { const { container } = render( {}} startDate={newDate()} endDate={null} selectsRange inline />, ); const days = container.querySelectorAll( ".react-datepicker__day:not(.react-datepicker__day--disabled)", ); expect(days.length).toBeGreaterThan(2); const firstDay = days[0] as HTMLElement; const secondDay = days[1] as HTMLElement; // Simulate keyboard selection start fireEvent.keyDown(firstDay, { key: "Enter" }); // Lines 1210-1211: onDayMouseEnter with keyboard selection fireEvent.mouseEnter(secondDay); expect(secondDay).not.toBeNull(); }); it("should handle onYearMouseEnter with selectsRange (line 1353)", () => { const { container } = render( {}} startDate={newDate()} endDate={null} selectsRange showYearPicker inline />, ); const yearElements = container.querySelectorAll( ".react-datepicker__year-text", ); expect(yearElements.length).toBeGreaterThan(1); const firstYear = yearElements[0] as HTMLElement; const secondYear = yearElements[1] as HTMLElement; // Start range selection fireEvent.click(firstYear); // Line 1353: onYearMouseEnter with selectsRange fireEvent.mouseEnter(secondYear); expect(secondYear).not.toBeNull(); }); it("should handle onMonthMouseLeave with selectsRange (line 1358)", () => { const { container } = render( {}} startDate={newDate()} endDate={null} selectsRange showMonthYearPicker inline />, ); const monthElements = container.querySelectorAll( ".react-datepicker__month-text", ); expect(monthElements.length).toBeGreaterThan(0); const firstMonth = monthElements[0] as HTMLElement; // Line 1358: onMonthMouseLeave with selectsRange fireEvent.mouseEnter(firstMonth); fireEvent.mouseLeave(firstMonth); expect(firstMonth).not.toBeNull(); }); it("should handle onQuarterMouseLeave with selectsRange (line 1363)", () => { const { container } = render( {}} startDate={newDate()} endDate={null} selectsRange showQuarterYearPicker inline />, ); const quarterElements = container.querySelectorAll( ".react-datepicker__quarter-text", ); expect(quarterElements.length).toBeGreaterThan(0); const firstQuarter = quarterElements[0] as HTMLElement; // Line 1363: onQuarterMouseLeave with selectsRange fireEvent.mouseEnter(firstQuarter); fireEvent.mouseLeave(firstQuarter); expect(firstQuarter).not.toBeNull(); }); it("should handle onYearMouseLeave with selectsRange (line 1368)", () => { const { container } = render( {}} startDate={newDate()} endDate={null} selectsRange showYearPicker inline />, ); const yearElements = container.querySelectorAll( ".react-datepicker__year-text", ); expect(yearElements.length).toBeGreaterThan(0); const firstYear = yearElements[0] as HTMLElement; // Line 1368: onYearMouseLeave with selectsRange fireEvent.mouseEnter(firstYear); fireEvent.mouseLeave(firstYear); expect(firstYear).not.toBeNull(); }); it("should handle Tab key in date range mode (line 1191)", () => { const onChange = jest.fn(); const { container } = render( , ); const input = container.querySelector("input") as HTMLInputElement; fireEvent.focus(input); // Line 1191: Tab key handling in range mode fireEvent.keyDown(input, { key: "Tab", code: 9, which: 9 }); expect(input).not.toBeNull(); }); it("should handle ariaLiveMessage construction for selectsRange (line 1336)", () => { const { container } = render( {}} startDate={newDate("2024-01-15")} endDate={newDate("2024-01-20")} selectsRange inline />, ); // Line 1336: ariaLiveMessage is constructed for screen readers const datepicker = container.querySelector(".react-datepicker"); expect(datepicker).not.toBeNull(); }); }); });