/** * @jest-environment jsdom */ import { render, fireEvent, waitFor } from "@testing-library/react"; import { act } from "react"; import { setDate, startOfMonth, eachDayOfInterval, endOfMonth, endOfYear, isSunday, } from "date-fns"; import { eo } from "date-fns/locale/eo"; import { fi } from "date-fns/locale/fi"; import React from "react"; import Calendar from "../calendar"; import { KeyType, getMonthInLocale, getStartOfMonth, getStartOfWeek, getWeekdayMinInLocale, newDate, parseDate, registerLocale, setDefaultLocale, formatDate, addMonths, addYears, getMonth, getYear, isSameDay, subMonths, subYears, addDays, getDate, } from "../date_utils"; import DatePicker from "../index"; import { getKey, getRandomMonthExcludingCurrent, SafeElementWrapper, safeQuerySelector, safeQuerySelectorAll, setupMockResizeObserver, } from "./test_utils"; import type { ReactDatePickerCustomHeaderProps } from "../calendar"; import type { Locale } from "../date_utils"; import type { Day } from "date-fns"; import type Month from "month"; import type MonthYearDropdown from "month_year_dropdown"; import type Year from "year"; import type YearDropdown from "year_dropdown"; const DATE_FORMAT = "MM/dd/yyyy"; type CalendarProps = React.ComponentProps; interface YearDropdownProps extends React.ComponentPropsWithoutRef< typeof YearDropdown > {} interface MonthYearDropdownProps extends React.ComponentPropsWithoutRef< typeof MonthYearDropdown > {} interface YearProps extends React.ComponentPropsWithoutRef {} interface MonthProps extends React.ComponentPropsWithoutRef {} describe("Calendar", () => { const dateFormat = "MMMM yyyy"; registerLocale("fi", fi); function getCalendar( extraProps?: Partial< Pick< CalendarProps, "dateFormat" | "onSelect" | "onClickOutside" | "dropdownMode" > > & Omit< CalendarProps, | "dateFormat" | "onSelect" | "onClickOutside" | "dropdownMode" | "showMonthYearDropdown" > & ( | ({ showMonthYearDropdown: true; } & Pick) | ({ showMonthYearDropdown?: never; } & Pick & Pick & Pick) ), ) { let instance: Calendar | null = null; const { container, rerender } = render( { instance = node; }} dateFormat={dateFormat} onSelect={() => {}} onClickOutside={() => {}} dropdownMode="scroll" {...extraProps} />, ); const rerenderFunc = ( props?: Partial>, ) => { return rerender( { instance = node; }} dateFormat={dateFormat} onSelect={() => {}} onClickOutside={() => {}} dropdownMode="scroll" {...extraProps} {...props} />, ); }; return { calendar: container, instance: instance as unknown as Calendar | null, rerender: rerenderFunc, }; } beforeAll(() => { setupMockResizeObserver(); }); it("should start with the current date in view if no date range", () => { const now = newDate(); const { instance } = getCalendar(); expect(isSameDay(instance?.state.date, now)).toBeTruthy(); }); it("should start with the selected date in view if provided", () => { const selected = addYears(newDate(), 1); const { instance } = getCalendar({ selected }); expect(isSameDay(instance?.state.date, selected)).toBeTruthy(); }); it("should start with the current date in view if in date range", () => { const now = newDate(); const minDate = subYears(now, 1); const maxDate = addYears(now, 1); const { instance } = getCalendar({ minDate, maxDate }); expect(isSameDay(instance?.state.date, now)).toBeTruthy(); }); it("should start with the min date in view if after the current date", () => { const minDate = addYears(newDate(), 1); const { instance } = getCalendar({ minDate }); expect(isSameDay(instance?.state.date, minDate)).toBeTruthy(); }); it("should start with the min include date in view if after the current date", () => { const minDate = addYears(newDate(), 1); const { instance } = getCalendar({ includeDates: [minDate] }); expect(isSameDay(instance?.state.date, minDate)).toBeTruthy(); }); it("should start with the max date in view if before the current date", () => { const maxDate = subYears(newDate(), 1); const { instance } = getCalendar({ maxDate }); expect(isSameDay(instance?.state.date, maxDate)).toBeTruthy(); }); it("should start with the max include date in view if before the current date", () => { const maxDate = subYears(newDate(), 1); const { instance } = getCalendar({ includeDates: [maxDate] }); expect(isSameDay(instance?.state.date, maxDate)).toBeTruthy(); }); it("should start with the open to date in view if given and no selected/min/max dates given", () => { const openToDate = parseDate("09/28/1993", DATE_FORMAT, undefined, false) ?? undefined; const { instance } = getCalendar({ openToDate }); expect(isSameDay(instance?.state.date, openToDate)).toBeTruthy(); }); it("should start with the open to date in view if given and after a min date", () => { const openToDate = parseDate("09/28/1993", DATE_FORMAT, undefined, false) ?? undefined; const minDate = parseDate("01/01/1993", DATE_FORMAT, undefined, false) ?? undefined; const { instance } = getCalendar({ openToDate, minDate }); expect(isSameDay(instance?.state.date, openToDate)).toBeTruthy(); }); it("should start with the open to date in view if given and before a max date", () => { const openToDate = parseDate("09/28/1993", DATE_FORMAT, undefined, false) ?? undefined; const maxDate = parseDate("12/31/1993", DATE_FORMAT, undefined, false) ?? undefined; const { instance } = getCalendar({ openToDate, maxDate }); expect(isSameDay(instance?.state.date, openToDate)).toBeTruthy(); }); it("should start with the open to date in view if given and in range of the min/max dates", () => { const openToDate = parseDate("09/28/1993", DATE_FORMAT, undefined, false) ?? undefined; const minDate = parseDate("01/01/1993", DATE_FORMAT, undefined, false) ?? undefined; const maxDate = parseDate("12/31/1993", DATE_FORMAT, undefined, false) ?? undefined; const { instance } = getCalendar({ openToDate, minDate, maxDate }); expect(isSameDay(instance?.state.date, openToDate)).toBeTruthy(); }); it("should move pre-selection to first enabled day when month changes", () => { const onSelect = jest.fn(); const setOpen = jest.fn(); const setPreSelection = jest.fn(); const filterDate = (date: Date) => date.getDate() >= 3; const { instance } = getCalendar({ adjustDateOnChange: true, onSelect, setOpen, setPreSelection, filterDate, selected: new Date("2024-01-15T00:00:00"), }); const targetMonth = new Date("2024-02-01T00:00:00"); act(() => { instance?.handleMonthChange(targetMonth); }); const expectedDate = new Date("2024-02-03T00:00:00"); const [selectedDate] = onSelect.mock.calls[0]; expect(isSameDay(selectedDate, expectedDate)).toBe(true); expect(setOpen).toHaveBeenCalledWith(true); const [preSelectionDate] = setPreSelection.mock.calls[0]; expect(isSameDay(preSelectionDate, expectedDate)).toBe(true); expect(instance?.state.isRenderAriaLiveMessage).toBe(true); }); it("should fall back to provided month date when no enabled days exist", () => { const onSelect = jest.fn(); const setPreSelection = jest.fn(); const filterDate = () => false; const { instance } = getCalendar({ adjustDateOnChange: true, onSelect, setPreSelection, filterDate, }); const targetDate = new Date("2024-03-10T00:00:00"); act(() => { instance?.handleMonthChange(targetDate); }); const [fallbackSelected] = onSelect.mock.calls[0]; expect(isSameDay(fallbackSelected, targetDate)).toBe(true); const [fallbackPreSelection] = setPreSelection.mock.calls[0]; expect(isSameDay(fallbackPreSelection, targetDate)).toBe(true); }); it("should open on openToDate date rather than selected date when both are specified", () => { const openToDate = parseDate("09/28/1993", DATE_FORMAT, undefined, false) ?? undefined; const selected = parseDate("09/28/1995", DATE_FORMAT, undefined, false) ?? undefined; const { instance } = getCalendar({ openToDate, selected }); expect(isSameDay(instance?.state.date, openToDate)).toBeTruthy(); }); it("should trigger date change when openToDate prop is set after calcInitialState()", () => { const openToDate = parseDate("09/28/1993", DATE_FORMAT, undefined, false) ?? undefined; const oneMonthFromOpenToDate = parseDate("10/28/1993", DATE_FORMAT, undefined, false) ?? undefined; const { instance, rerender } = getCalendar({ openToDate }); expect(isSameDay(instance?.state.date, openToDate)).toBeTruthy(); rerender({ openToDate: oneMonthFromOpenToDate, }); expect( isSameDay(instance?.state.date, oneMonthFromOpenToDate), ).toBeTruthy(); }); it("should render month and year as a header of datepicker by default", () => { const { calendar } = getCalendar(); expect(() => calendar.querySelector("h2.react-datepicker__current-month"), ).not.toThrow(); }); it("should not show the year dropdown menu by default", () => { const { calendar } = getCalendar(); const yearReadView = calendar.querySelectorAll( ".react-datepicker__year-dropdown-container", ); expect(yearReadView).toHaveLength(0); }); it("should show the year dropdown menu if toggled on", () => { const { calendar } = getCalendar({ showYearDropdown: true }); const yearReadView = calendar.querySelectorAll( ".react-datepicker__year-dropdown-container", ); expect(yearReadView).toHaveLength(1); }); it("should show only one year dropdown menu if toggled on and multiple month mode on", () => { const { calendar } = getCalendar({ showYearDropdown: true, monthsShown: 2, }); const monthReadView = calendar.querySelectorAll( ".react-datepicker__year-dropdown-container", ); expect(monthReadView).toHaveLength(1); }); it("should show month navigation if toggled on", () => { const { calendar } = getCalendar({ includeDates: [newDate()], forceShowMonthNavigation: true, }); const nextNavigationButton = calendar.querySelectorAll( ".react-datepicker__navigation--next", ); expect(nextNavigationButton).toHaveLength(1); }); it("should correctly format weekday using formatWeekDay prop", () => { const { calendar } = getCalendar({ formatWeekDay: (day) => day.charAt(0) }); calendar .querySelectorAll( ".react-datepicker__day-name > span[aria-hidden='true']", ) .forEach((dayName) => expect(dayName.textContent).toHaveLength(1)); }); it("should contain the correct class when using the weekDayClassName prop", () => { const func = (date: Date) => (isSunday(date) ? "sunday" : ""); const { container } = render( {}} onSelect={() => {}} weekDayClassName={func} />, ); const sunday = container.querySelectorAll( ".react-datepicker__day-name.sunday", ); expect(sunday).toHaveLength(1); }); it("should render the months correctly adjusted by monthSelectedIn", () => { const selected = newDate("2018-11-19"); const { calendar, rerender } = getCalendar({ inline: true, monthsShown: 2, selected, }); rerender(); const renderedMonths = calendar.querySelectorAll( ".react-datepicker__month", ); expect( getMonth(renderedMonths[0]?.getAttribute("aria-label") ?? 0), ).toEqual(10); }); it("should render the months correctly adjusted by monthSelectedIn for showPreviousMonths", () => { const selected = newDate("2018-11-19"); const { calendar, rerender } = getCalendar({ inline: true, monthsShown: 2, selected, showPreviousMonths: true, }); rerender(); const renderedMonths = calendar.querySelectorAll( ".react-datepicker__month", ); expect( getMonth(renderedMonths[0]?.getAttribute("aria-label") ?? 0), ).toEqual(9); }); it("should render the correct default aria labels for next and prev months buttons", () => { const { calendar } = getCalendar(); const previousButtonAriaLabel = calendar .querySelector(".react-datepicker__navigation--previous") ?.getAttribute("aria-label"); const nextButtonAriaLabel = calendar .querySelector(".react-datepicker__navigation--next") ?.getAttribute("aria-label"); expect(previousButtonAriaLabel).toBe("Previous Month"); expect(nextButtonAriaLabel).toBe("Next Month"); }); it("should render by default aria labels for next and prev months button equal to the next and prev buttons text", () => { const previousMonthButtonLabel = "Go to previous month"; const nextMonthButtonLabel = "Go to next month"; const { calendar } = getCalendar({ previousMonthButtonLabel, nextMonthButtonLabel, }); const previousButtonAriaLabel = calendar .querySelector(".react-datepicker__navigation--previous") ?.getAttribute("aria-label"); const nextButtonAriaLabel = calendar .querySelector(".react-datepicker__navigation--next") ?.getAttribute("aria-label"); expect(previousButtonAriaLabel).toBe(previousMonthButtonLabel); expect(nextButtonAriaLabel).toBe(nextMonthButtonLabel); }); it("should allow user to pass a custom aria label for next and/or previous month button", () => { const previousMonthAriaLabel = "Go to the previous month of the year"; const nextMonthAriaLabel = "Go to the next month of the year"; const { calendar } = getCalendar({ previousMonthButtonLabel: "Go to previous month", nextMonthButtonLabel: "Go to next month", previousMonthAriaLabel, nextMonthAriaLabel, }); const previousButtonAriaLabel = calendar .querySelector(".react-datepicker__navigation--previous") ?.getAttribute("aria-label"); const nextButtonAriaLabel = calendar .querySelector(".react-datepicker__navigation--next") ?.getAttribute("aria-label"); expect(previousButtonAriaLabel).toBe(previousButtonAriaLabel); expect(nextButtonAriaLabel).toBe(nextButtonAriaLabel); }); it("should render by default aria labels for next and prev months buttons when providing a react node", () => { const previousMonthButtonLabel = Custom react previous month; const nextMonthButtonLabel = Custom react next month; const { calendar } = getCalendar({ previousMonthButtonLabel, nextMonthButtonLabel, }); const previousButtonAriaLabel = calendar .querySelector(".react-datepicker__navigation--previous") ?.getAttribute("aria-label"); const nextButtonAriaLabel = calendar .querySelector(".react-datepicker__navigation--next") ?.getAttribute("aria-label"); expect(previousButtonAriaLabel).toBe("Previous Month"); expect(nextButtonAriaLabel).toBe("Next Month"); }); it("should render the correct default aria labels for next and prev year buttons", () => { const { calendar } = getCalendar({ showYearPicker: true }); const previousButtonAriaLabel = calendar .querySelector(".react-datepicker__navigation--previous") ?.getAttribute("aria-label"); const nextButtonAriaLabel = calendar .querySelector(".react-datepicker__navigation--next") ?.getAttribute("aria-label"); expect(previousButtonAriaLabel).toBe("Previous Year"); expect(nextButtonAriaLabel).toBe("Next Year"); }); it("should render by default aria labels for next and prev year buttons equal to the next and prev buttons text", () => { const previousYearButtonLabel = "Go to previous year"; const nextYearButtonLabel = "Go to next year"; const { calendar } = getCalendar({ showYearPicker: true, previousYearButtonLabel, nextYearButtonLabel, }); const previousButtonAriaLabel = calendar .querySelector(".react-datepicker__navigation--previous") ?.getAttribute("aria-label"); const nextButtonAriaLabel = calendar .querySelector(".react-datepicker__navigation--next") ?.getAttribute("aria-label"); expect(previousButtonAriaLabel).toBe(previousYearButtonLabel); expect(nextButtonAriaLabel).toBe(nextYearButtonLabel); }); it("should allow user to pass a custom aria label for next and/or previous year button", () => { const previousYearAriaLabel = "Go to the previous year"; const nextYearAriaLabel = "Go to the next year"; const { calendar } = getCalendar({ showYearPicker: true, previousYearButtonLabel: "Go to prev year", nextYearButtonLabel: "Go to next year", previousYearAriaLabel, nextYearAriaLabel, }); const previousButtonAriaLabel = calendar .querySelector(".react-datepicker__navigation--previous") ?.getAttribute("aria-label"); const nextButtonAriaLabel = calendar .querySelector(".react-datepicker__navigation--next") ?.getAttribute("aria-label"); expect(previousButtonAriaLabel).toBe(previousYearAriaLabel); expect(nextButtonAriaLabel).toBe(nextYearAriaLabel); }); it("should render by default aria labels for next and prev year buttons when providing a react node", () => { const previousYearButtonLabel = Custom react previous year; const nextYearButtonLabel = Custom react next year; const { calendar } = getCalendar({ showYearPicker: true, previousYearButtonLabel, nextYearButtonLabel, }); const previousButtonAriaLabel = calendar .querySelector(".react-datepicker__navigation--previous") ?.getAttribute("aria-label"); const nextButtonAriaLabel = calendar .querySelector(".react-datepicker__navigation--next") ?.getAttribute("aria-label"); expect(previousButtonAriaLabel).toBe("Previous Year"); expect(nextButtonAriaLabel).toBe("Next Year"); }); it("should not have previous month button when selecting a date in the second month, when min date is specified", () => { const minDate = new Date("2024-11-06"); const maxDate = new Date("2025-01-01"); const selectedDate = minDate; const { container } = render( , ); expect( container.querySelector(".react-datepicker__navigation--previous"), ).toBe(null); const secondMonthDate = safeQuerySelectorAll( container, ".react-datepicker__day--009", )[1]; if (!secondMonthDate) { throw new Error("second month date is not found"); } fireEvent.click(secondMonthDate); expect( container.querySelector(".react-datepicker__navigation--previous"), ).toBe(null); }); describe("custom header", () => { const months = [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December", ]; const renderCustomHeader = (params: ReactDatePickerCustomHeaderProps) => { const { date, visibleYearsRange, changeYear, changeMonth, decreaseMonth, increaseMonth, prevMonthButtonDisabled, nextMonthButtonDisabled, } = params; return (
{visibleYearsRange && (
{visibleYearsRange.startYear} to {visibleYearsRange.endYear}
)}
); }; it("should call render custom header function and returns parameters", () => { const renderCustomHeader = jest.fn(); getCalendar({ renderCustomHeader }); const match = { customHeaderCount: expect.any(Number), changeMonth: expect.any(Function), changeYear: expect.any(Function), date: expect.any(Date), decreaseMonth: expect.any(Function), increaseMonth: expect.any(Function), decreaseYear: expect.any(Function), increaseYear: expect.any(Function), nextMonthButtonDisabled: expect.any(Boolean), prevMonthButtonDisabled: expect.any(Boolean), nextYearButtonDisabled: expect.any(Boolean), prevYearButtonDisabled: expect.any(Boolean), isRenderAriaLiveMessage: expect.any(Boolean), monthContainer: undefined, monthDate: expect.any(Date), selectingDate: undefined, }; expect(renderCustomHeader).toHaveBeenCalledWith(match); }); it("should render only custom header", () => { const { calendar } = getCalendar({ renderCustomHeader }); const nextMontButton = calendar.querySelectorAll( ".react-datepicker__navigation--next", ); const prevMontButton = calendar.querySelectorAll( ".react-datepicker__navigation--previous", ); expect(nextMontButton).toHaveLength(0); expect(prevMontButton).toHaveLength(0); }); it("should render custom header with selects and buttons", () => { const { calendar } = getCalendar({ renderCustomHeader, }); expect( calendar.querySelectorAll(".react-datepicker__header--custom"), ).toHaveLength(1); expect(calendar.querySelectorAll(".custom-header")).toHaveLength(1); const yearSelect = calendar.querySelectorAll(".year-select"); const monthSelect = calendar.querySelectorAll(".month-select"); const prevMonth = calendar.querySelectorAll(".prevMonth"); const nextMonth = calendar.querySelectorAll(".nextMonth"); expect(yearSelect).toHaveLength(1); expect(monthSelect).toHaveLength(1); expect(prevMonth).toHaveLength(1); expect(nextMonth).toHaveLength(1); }); it("should render custom header when showing year picker", () => { const { calendar } = getCalendar({ renderCustomHeader, showYearPicker: true, }); expect( calendar.querySelectorAll(".react-datepicker__header--custom"), ).toHaveLength(1); expect( calendar.querySelectorAll(".react-datepicker__year--container"), ).toHaveLength(1); }); it("should render day names with renderCustomHeader", () => { const { calendar } = getCalendar({ renderCustomHeader, }); expect( calendar.querySelectorAll(".react-datepicker__header--custom"), ).toHaveLength(1); expect( calendar.querySelectorAll(".react-datepicker__day-names"), ).toHaveLength(1); }); it("should render custom header with visible year range for YearPicker", () => { const { calendar } = getCalendar({ renderCustomHeader, showYearPicker: true, }); expect( calendar.querySelector( ".react-datepicker__header--custom .visible-years-range", ), ).not.toBeNull(); }); it("should not render visible year range for non-YearPicker views", () => { const { calendar } = getCalendar({ renderCustomHeader, }); expect( calendar.querySelector( ".react-datepicker__header--custom .visible-years-range", ), ).toBeNull(); }); it("should not render day names with renderCustomHeader & showMonthYearPicker", () => { const { calendar } = getCalendar({ renderCustomHeader, showMonthYearPicker: true, }); expect( calendar.querySelectorAll(".react-datepicker__header--custom"), ).toHaveLength(1); expect( calendar.querySelectorAll(".react-datepicker__day-names"), ).toHaveLength(0); }); it("should not render day names with renderCustomHeader & showYearPicker", () => { const { calendar } = getCalendar({ renderCustomHeader, showYearPicker: true, }); expect( calendar.querySelectorAll(".react-datepicker__header--custom"), ).toHaveLength(1); expect( calendar.querySelectorAll(".react-datepicker__day-names"), ).toHaveLength(0); }); it("should not render day names with renderCustomHeader & showQuarterYearPicker", () => { const { calendar } = getCalendar({ renderCustomHeader, showQuarterYearPicker: true, }); expect( calendar.querySelectorAll(".react-datepicker__header--custom"), ).toHaveLength(1); expect( calendar.querySelectorAll(".react-datepicker__day-names"), ).toHaveLength(0); }); it("should go to previous month", () => { const { calendar, instance } = getCalendar({ renderCustomHeader, }); const selected = newDate(instance?.state.date); const prevMonth = safeQuerySelector(calendar, ".prevMonth"); fireEvent.click(prevMonth); expect(getMonth(selected)).toBe( (getMonth(instance!.state.date) + 1) % 12, ); }); it("should go to next month", () => { const { calendar, instance } = getCalendar({ renderCustomHeader, }); const selected = newDate(instance?.state.date); const nextMonth = safeQuerySelector(calendar, ".nextMonth"); fireEvent.click(nextMonth); const newMonth = getMonth(instance!.state.date) - 1; const resultMonth = newMonth === -1 ? 11 : newMonth; expect(getMonth(selected)).toBe(resultMonth); }); it("nextMonthButtonDisabled flag should be true", () => { const renderCustomHeader = jest.fn(); getCalendar({ renderCustomHeader, minDate: subMonths(newDate(), 1), maxDate: newDate(), }); const { prevMonthButtonDisabled, nextMonthButtonDisabled } = renderCustomHeader.mock.calls[0][0]; expect(prevMonthButtonDisabled).toBe(false); expect(nextMonthButtonDisabled).toBe(true); }); it("prevMonthButtonDisabled flag should be true", () => { const renderCustomHeader = jest.fn(); getCalendar({ renderCustomHeader, minDate: newDate(), maxDate: addMonths(newDate(), 1), }); const { prevMonthButtonDisabled, nextMonthButtonDisabled } = renderCustomHeader.mock.calls[0][0]; expect(prevMonthButtonDisabled).toBe(true); expect(nextMonthButtonDisabled).toBe(false); }); it("should select april from month select", async () => { const { calendar, instance } = getCalendar({ renderCustomHeader, }); const monthSelect = safeQuerySelector(calendar, ".month-select"); fireEvent.change(monthSelect, { target: { value: 4 } }); const selected = newDate(instance?.state.date); expect(getMonth(selected)).toBe(4); }); it("should select 2017 from month select", () => { const { calendar, instance } = getCalendar({ renderCustomHeader, }); const yearSelect = safeQuerySelector(calendar, ".year-select"); fireEvent.change(yearSelect, { target: { value: 2017 } }); const selected = newDate(instance?.state.date); expect(getYear(selected)).toBe(2017); }); it("should render custom headers according to monthsShown prop", () => { const { calendar: twoMonthsCalendar } = getCalendar({ renderCustomHeader, monthsShown: 2, }); expect( twoMonthsCalendar.querySelectorAll(".react-datepicker__header--custom"), ).toHaveLength(2); const { calendar: fourMonthsCalendar } = getCalendar({ renderCustomHeader, monthsShown: 4, }); expect( fourMonthsCalendar.querySelectorAll( ".react-datepicker__header--custom", ), ).toHaveLength(4); }); it("should set monthDate prop correctly when rendering custom headers", () => { const renderMonthDateInCustomHeader = ({ monthDate, }: { monthDate: Date; }) => (
{`${monthDate.getFullYear()}-${monthDate.getMonth()}-${monthDate.getDate()}`}
); const { calendar: twoMonthsCalendar } = getCalendar({ renderCustomHeader: renderMonthDateInCustomHeader, monthsShown: 2, }); const firstDate = new Date(); const secondDate = addMonths(new Date(), 1); const firstDateInCustomHeader = twoMonthsCalendar.querySelectorAll( ".customheader-monthdate", )[0]?.textContent; const secondDateInCustomHeader = twoMonthsCalendar.querySelectorAll( ".customheader-monthdate", )[1]?.textContent; expect(firstDateInCustomHeader).toBe( `${firstDate.getFullYear()}-${firstDate.getMonth()}-${firstDate.getDate()}`, ); expect(secondDateInCustomHeader).toBe( `${secondDate.getFullYear()}-${secondDate.getMonth()}-${secondDate.getDate()}`, ); }); it("should render custom header with show time select", () => { const { container } = render( {}} onSelect={() => {}} />, ); const header = container.querySelectorAll( ".react-datepicker__header--custom", ); const time = container.querySelectorAll(".react-datepicker__time"); expect(header).toHaveLength(1); expect(time).toHaveLength(1); }); it("should display the target month in the leftmost position when changeMonth is called with monthsShown >= 2", () => { // This test verifies the fix for issue #3829 // When using changeMonth in a custom header with monthsShown >= 2, // the target month should always appear in the leftmost position // regardless of which calendar panel the user last selected a date in const onMonthSelectedInChangeSpy = jest.fn(); const renderCustomHeaderWithMonthSelect = ({ changeMonth, }: { changeMonth: (month: number) => void; }) => (
); const { calendar } = getCalendar({ renderCustomHeader: renderCustomHeaderWithMonthSelect, monthsShown: 2, onMonthSelectedInChange: onMonthSelectedInChangeSpy, }); // Select June (month index 5) from the month dropdown const monthSelect = safeQuerySelector(calendar, ".month-select"); fireEvent.change(monthSelect, { target: { value: 5 } }); // Verify that onMonthSelectedInChange was called with 0 // This ensures the target month appears in the leftmost position expect(onMonthSelectedInChangeSpy).toHaveBeenCalledWith(0); }); }); describe("when showDisabledMonthNavigation is enabled", () => { let onMonthChangeSpy = jest.fn(); beforeEach(() => { onMonthChangeSpy = jest.fn(); }); it("should show disabled previous month navigation", () => { const { calendar } = getCalendar({ minDate: newDate(), maxDate: addMonths(newDate(), 3), showDisabledMonthNavigation: true, }); const prevDisabledNavigationButton = calendar.querySelectorAll( ".react-datepicker__navigation--previous--disabled", ); const nextDisabledNavigationButton = calendar.querySelectorAll( ".react-datepicker__navigation--next--disabled", ); expect(prevDisabledNavigationButton).toHaveLength(1); expect(nextDisabledNavigationButton).toHaveLength(0); }); it("should show disabled next month navigation", () => { const { calendar } = getCalendar({ minDate: subMonths(newDate(), 3), maxDate: newDate(), showDisabledMonthNavigation: true, }); const prevDisabledNavigationButton = calendar.querySelectorAll( ".react-datepicker__navigation--previous--disabled", ); const nextDisabledNavigationButton = calendar.querySelectorAll( ".react-datepicker__navigation--next--disabled", ); expect(prevDisabledNavigationButton).toHaveLength(0); expect(nextDisabledNavigationButton).toHaveLength(1); }); it("should not show disabled previous/next month navigation when next/previous month available", () => { const { calendar } = getCalendar({ minDate: subMonths(newDate(), 3), maxDate: addMonths(newDate(), 3), showDisabledMonthNavigation: true, }); const prevDisabledNavigationButton = calendar.querySelectorAll( ".react-datepicker__navigation--previous--disabled", ); const nextDisabledNavigationButton = calendar.querySelectorAll( ".react-datepicker__navigation--next--disabled", ); expect(prevDisabledNavigationButton).toHaveLength(0); expect(nextDisabledNavigationButton).toHaveLength(0); }); it("when clicking disabled month navigation, should not change month", () => { const { calendar } = getCalendar({ minDate: newDate(), maxDate: newDate(), showDisabledMonthNavigation: true, onMonthChange: onMonthChangeSpy, }); const prevNavigationButton = safeQuerySelector( calendar, ".react-datepicker__navigation--previous", ); const nextNavigationButton = safeQuerySelector( calendar, ".react-datepicker__navigation--next", ); fireEvent.click(prevNavigationButton); expect(onMonthChangeSpy).toHaveBeenCalledTimes(0); fireEvent.click(nextNavigationButton); expect(onMonthChangeSpy).toHaveBeenCalledTimes(0); }); it("when clicking non-disabled month navigation, should change month", () => { const { calendar } = getCalendar({ selected: newDate(), minDate: subMonths(newDate(), 3), maxDate: addMonths(newDate(), 3), showDisabledMonthNavigation: true, onMonthChange: onMonthChangeSpy, }); const prevNavigationButton = safeQuerySelector( calendar, ".react-datepicker__navigation--previous", ); const nextNavigationButton = safeQuerySelector( calendar, ".react-datepicker__navigation--next", ); fireEvent.click(prevNavigationButton); fireEvent.click(nextNavigationButton); expect(onMonthChangeSpy).toHaveBeenCalledTimes(2); }); }); it("should not show the month dropdown menu by default", () => { const { calendar } = getCalendar(); const monthReadView = calendar.querySelectorAll( ".react-datepicker__month-dropdown-container", ); expect(monthReadView).toHaveLength(0); }); it("should show the month dropdown menu if toggled on", () => { const { calendar } = getCalendar({ showMonthDropdown: true }); const monthReadView = calendar.querySelectorAll( ".react-datepicker__month-dropdown-container", ); expect(monthReadView).toHaveLength(1); }); it("should show only one month dropdown menu if toggled on and multiple month mode on", () => { const { calendar } = getCalendar({ showMonthDropdown: true, monthsShown: 2, }); const monthReadView = calendar.querySelectorAll( ".react-datepicker__month-dropdown-container", ); expect(monthReadView).toHaveLength(1); }); it("should not show the month-year dropdown menu by default", () => { const { calendar } = getCalendar(); const monthYearReadView = calendar.querySelectorAll( ".react-datepicker__month-year-dropdown-container", ); expect(monthYearReadView).toHaveLength(0); }); it("should show the month-year dropdown menu if toggled on", () => { const { calendar } = getCalendar({ showMonthYearDropdown: true, minDate: subYears(newDate(), 1), maxDate: addYears(newDate(), 1), }); const monthYearReadView = calendar.querySelectorAll( ".react-datepicker__month-year-dropdown-container", ); expect(monthYearReadView).toHaveLength(1); }); it("should show only one month-year dropdown menu if toggled on and multiple month mode on", () => { const { calendar } = getCalendar({ showMonthYearDropdown: true, minDate: subYears(newDate(), 1), maxDate: addYears(newDate(), 1), monthsShown: 2, }); const monthReadView = calendar.querySelectorAll( ".react-datepicker__month-year-dropdown-container", ); expect(monthReadView).toHaveLength(1); }); it("should not show the today button by default", () => { const { calendar } = getCalendar(); const todayButton = calendar.querySelectorAll( ".react-datepicker__today-button", ); expect(todayButton).toHaveLength(0); }); it("should show the today button if toggled on", () => { const { calendar } = getCalendar({ todayButton: "Vandaag" }); const todayButton = calendar.querySelectorAll( ".react-datepicker__today-button", ); expect(todayButton).toHaveLength(1); expect(todayButton[0]?.textContent).toBe("Vandaag"); }); it("should set the date when pressing todayButton", () => { const { calendar, instance } = getCalendar({ todayButton: "Vandaag" }); const todayButton = safeQuerySelector( calendar, ".react-datepicker__today-button", ); fireEvent.click(todayButton); expect(isSameDay(instance?.state.date, newDate())).toBeTruthy(); }); it("should use a hash for week label if weekLabel is NOT provided", () => { const { calendar } = getCalendar({ showWeekNumbers: true }); const weekLabel = calendar.querySelectorAll( ".react-datepicker__day-name > span[aria-hidden='true']", ); expect(weekLabel[0]?.textContent).toBe("#"); }); it("should set custom week label if weekLabel is provided", () => { const { calendar } = getCalendar({ showWeekNumbers: true, weekLabel: "Foo", }); const weekLabel = calendar.querySelectorAll( ".react-datepicker__day-name > span[aria-hidden='true']", ); expect(weekLabel[0]?.textContent).toBe("Foo"); }); it("should track the currently hovered day (Mouse Event)", () => { const onDayMouseEnterSpy = jest.fn(); const { container } = render( {}} onSelect={() => {}} onDayMouseEnter={onDayMouseEnterSpy} />, ); const day = safeQuerySelector(container, ".react-datepicker__day"); fireEvent.mouseEnter(day); expect(onDayMouseEnterSpy).toHaveBeenLastCalledWith( getStartOfWeek(getStartOfMonth(newDate())), ); }); it("should track the currently hovered day (Pointer Event)", () => { const onDayMouseEnterSpy = jest.fn(); const { container } = render( {}} onSelect={() => {}} onDayMouseEnter={onDayMouseEnterSpy} usePointerEvent />, ); const day = safeQuerySelector(container, ".react-datepicker__day"); fireEvent.pointerEnter(day); expect(onDayMouseEnterSpy).toHaveBeenLastCalledWith( getStartOfWeek(getStartOfMonth(newDate())), ); }); it("should clear the hovered day when the mouse leaves", () => { let instance: Calendar | null = null; const { container, rerender } = render( { instance = node; }} selectsStart dateFormat={dateFormat} dropdownMode="scroll" onClickOutside={() => {}} onSelect={() => {}} />, ); expect(instance).not.toBeFalsy(); act(() => { (instance!.state as Pick).selectingDate = newDate(); }); rerender( { instance = node; }} selectsStart dateFormat={dateFormat} dropdownMode="scroll" onClickOutside={() => {}} onSelect={() => {}} />, ); const month = safeQuerySelector(container, ".react-datepicker__month"); expect( month?.classList.contains("react-datepicker__month--selecting-range"), ).toBeTruthy(); fireEvent.mouseLeave(month); expect( container .querySelector(".react-datepicker__month") ?.classList.contains("react-datepicker__month--selecting-range"), ).toBeFalsy(); }); it("uses weekdaysShort instead of weekdaysMin provided useWeekdaysShort prop is present", () => { const calendarShort = render( {}} onSelect={() => {}} useWeekdaysShort dropdownMode="scroll" />, ).container; const calendarMin = render( {}} onSelect={() => {}} dropdownMode="scroll" />, ).container; const daysNamesShort = calendarShort.querySelectorAll( ".react-datepicker__day-name > span[aria-hidden='true']", ); expect(daysNamesShort[0]?.textContent).toBe("Sun"); expect(daysNamesShort[6]?.textContent).toBe("Sat"); const daysNamesMin = calendarMin.querySelectorAll( ".react-datepicker__day-name > span[aria-hidden='true']", ); expect(daysNamesMin[0]?.textContent).toBe("Su"); expect(daysNamesMin[6]?.textContent).toBe("Sa"); }); it("should set the date to the selected day of the previous month when previous button clicked", () => { let date: Date | null = null; const expectedDate = "28.06.2017"; const { container } = render( { date = d; }} />, ); const input = safeQuerySelector(container, "input"); fireEvent.focus(input); const previousButton = safeQuerySelector( container, ".react-datepicker__navigation--previous", ); fireEvent.click(previousButton); expect(date).not.toBeNull(); expect(formatDate(date!, "dd.MM.yyyy")).toBe(expectedDate); }); it("should set the date to the selected day of the next when next button clicked", () => { let date: Date | null = null; const expectedDate = "28.08.2017"; const { container } = render( { date = d; }} />, ); const input = safeQuerySelector(container, "input"); fireEvent.focus(input); const nextButton = safeQuerySelector( container, ".react-datepicker__navigation--next", ); fireEvent.click(nextButton); expect(date).not.toBeNull(); expect(formatDate(date!, "dd.MM.yyyy")).toBe(expectedDate); }); it("should set the date to the last possible day of the previous month when previous button clicked", () => { let date: Date | null = null; const expectedDate = "30.11.2017"; const { container } = render( { date = d; }} />, ); const input = safeQuerySelector(container, "input"); fireEvent.focus(input); const previousButton = safeQuerySelector( container, ".react-datepicker__navigation--previous", ); fireEvent.click(previousButton); expect(date).not.toBeNull(); expect(formatDate(date!, "dd.MM.yyyy")).toBe(expectedDate); }); it("should trigger onCalendarOpen and onCalendarClose", () => { const onCalendarOpen = jest.fn(); const onCalendarClose = jest.fn(); const { container } = render( , ); const input = safeQuerySelector(container, "input"); fireEvent.focus(input); expect(onCalendarOpen).toHaveBeenCalled(); fireEvent.blur(input); expect(onCalendarOpen).toHaveBeenCalled(); }); describe("onMonthChange", () => { let onMonthChangeSpy = jest.fn(); let calendar: HTMLElement; beforeEach(() => { onMonthChangeSpy = jest.fn(); calendar = render( {}} onClickOutside={() => {}} dropdownMode="select" showYearDropdown showMonthDropdown forceShowMonthNavigation onMonthChange={onMonthChangeSpy} />, ).container; }); it("calls onMonthChange prop when previous month button clicked", () => { const select = safeQuerySelector( calendar, ".react-datepicker__navigation--previous", ); fireEvent.click(select); expect(onMonthChangeSpy).toHaveBeenCalled(); }); it("calls onMonthChange prop when next month button clicked", () => { const select = safeQuerySelector( calendar, ".react-datepicker__navigation--next", ); fireEvent.click(select); expect(onMonthChangeSpy).toHaveBeenCalled(); }); it("calls onMonthChange prop when month changed from month dropdown", () => { const select = new SafeElementWrapper(calendar) .safeQuerySelector(".react-datepicker__month-dropdown-container") .safeQuerySelector("select") .getElement(); const month = getRandomMonthExcludingCurrent(); fireEvent.change(select, { target: { value: month, }, }); expect(onMonthChangeSpy).toHaveBeenCalled(); }); }); describe("onYearChange", () => { let onYearChangeSpy = jest.fn(); let calendar: HTMLElement; beforeEach(() => { onYearChangeSpy = jest.fn(); calendar = render( {}} onClickOutside={() => {}} dropdownMode="select" showYearDropdown onYearChange={onYearChangeSpy} />, ).container; }); it("calls onYearChange prop when year changed from year dropdown", () => { const yearDropdownContainer = safeQuerySelector( calendar, ".react-datepicker__year-dropdown-container", ); const select = safeQuerySelector(yearDropdownContainer, "select"); fireEvent.change(select, { target: { value: Array.from( select.querySelectorAll("option"), ).at(-2)?.value, }, }); expect(onYearChangeSpy).toHaveBeenCalled(); }); }); describe("monthYearDropdown change", () => { let onYearChangeSpy = jest.fn(); let onMonthChangeSpy = jest.fn(); beforeEach(() => { onYearChangeSpy = jest.fn(); onMonthChangeSpy = jest.fn(); }); const renderCalendar = () => { let instance: Calendar | null = null; const { container } = render( { instance = node; }} dateFormat={dateFormat} onSelect={() => {}} onClickOutside={() => {}} dropdownMode="select" showMonthYearDropdown minDate={subYears(newDate(), 1)} maxDate={addYears(newDate(), 1)} onYearChange={onYearChangeSpy} onMonthChange={onMonthChangeSpy} />, ); return { calendar: container, instance, }; }; it("calls onYearChange prop when selection is changed from month-year dropdown", () => { const { calendar } = renderCalendar(); const monthYearDropdownContainer = safeQuerySelector( calendar, ".react-datepicker__month-year-dropdown-container", ); const select = safeQuerySelector(monthYearDropdownContainer, "select"); const minMonthYearOptionsLen = 4; const options = safeQuerySelectorAll( select, "option", minMonthYearOptionsLen, ); const option = options[3]!; fireEvent.change(select, { target: { value: option.value, }, }); expect(onYearChangeSpy).toHaveBeenCalled(); }); it("calls onMonthChange prop when selection is changed from month-year dropdown", () => { const { calendar } = renderCalendar(); const monthYearDropdownContainer = safeQuerySelector( calendar, ".react-datepicker__month-year-dropdown-container", ); const select = safeQuerySelector(monthYearDropdownContainer, "select"); const options = safeQuerySelectorAll(select, "option"); const option = options[3]; if (!option) { throw new Error("option is undefined"); } fireEvent.change(select, { target: { value: option.value, }, }); expect(onMonthChangeSpy).toHaveBeenCalled(); }); }); describe("onDropdownFocus", () => { let onDropdownFocusSpy = jest.fn(); let calendar: HTMLElement; beforeEach(() => { onDropdownFocusSpy = jest.fn(); calendar = render( {}} onClickOutside={() => {}} dropdownMode="select" showYearDropdown showMonthDropdown showMonthYearDropdown minDate={subYears(newDate(), 1)} maxDate={addYears(newDate(), 1)} onDropdownFocus={onDropdownFocusSpy} />, ).container; }); it("calls onDropdownFocus prop when year select is focused", () => { const select = safeQuerySelector( calendar, ".react-datepicker__year-select", ); fireEvent.focus(select); expect(onDropdownFocusSpy).toHaveBeenCalled(); }); it("calls onDropdownFocus prop when month select is focused", () => { const select = safeQuerySelector( calendar, ".react-datepicker__month-select", ); fireEvent.focus(select); expect(onDropdownFocusSpy).toHaveBeenCalled(); }); it("calls onDropdownFocus prop when year-month select is focused", () => { const select = safeQuerySelector( calendar, ".react-datepicker__month-year-select", ); fireEvent.focus(select); expect(onDropdownFocusSpy).toHaveBeenCalled(); }); it("does not call onDropdownFocus prop when the dropdown container div is focused", () => { const select = safeQuerySelector( calendar, ".react-datepicker__header__dropdown", ); fireEvent.focus(select); expect(onDropdownFocusSpy).toHaveBeenCalledTimes(0); }); }); describe("localization", () => { function testLocale( calendar: HTMLElement, selected: Date, locale?: Locale, calendarStartDay?: Day, ) { const calendarText = calendar.querySelector( ".react-datepicker__current-month", ); expect(calendarText?.textContent).toBe( formatDate(selected, dateFormat, locale), ); const firstDateOfWeek = getStartOfWeek( selected, locale, calendarStartDay, ); const firstWeekDayMin = getWeekdayMinInLocale(firstDateOfWeek, locale); const firstHeader = calendar.querySelector( ".react-datepicker__day-name > span[aria-hidden='true']", ); expect(firstHeader?.textContent).toBe(firstWeekDayMin); } it("should use the 'en' locale by default", () => { const selected = newDate(); const { calendar } = getCalendar({ selected }); testLocale(calendar, selected); }); it("should use the default locale when set", () => { const selected = newDate(); setDefaultLocale("fi"); const { calendar } = getCalendar({ selected }); testLocale(calendar, selected, "fi"); setDefaultLocale(""); }); it("should use the locale specified as a prop", () => { registerLocale("fi", fi); const locale = "fi"; const selected = newDate(); const { calendar } = getCalendar({ selected, locale }); testLocale(calendar, selected, locale); }); it("should override the default locale with the locale prop", () => { const locale = "en"; const selected = newDate(); setDefaultLocale("fi"); const { calendar } = getCalendar({ selected, locale }); testLocale(calendar, selected, locale); setDefaultLocale(""); }); it("should accept a raw date-fns locale object", () => { // Note that we explicitly do not call `registerLocale`, because that // would create a global variable, which we want to avoid. const locale = eo; const selected = newDate(); const { calendar } = getCalendar({ selected, locale }); testLocale(calendar, selected, locale); // Other tests touch this global, so it will always be present, but at the // very least we can make sure the test worked without 'eo' being added. expect( window as unknown as { __localeData__: object }["__localeData__"], ).not.toHaveProperty("eo"); }); it("should render empty custom header", () => { const { calendar } = getCalendar({ renderCustomHeader: (_props: ReactDatePickerCustomHeaderProps) => <>, }); const header = calendar.querySelectorAll( ".react-datepicker__header--custom", ); expect(header).toHaveLength(1); }); }); describe("renderInputTimeSection", () => { const renderCalendar = ( props?: Partial< Pick< CalendarProps, "dateFormat" | "onSelect" | "onClickOutside" | "dropdownMode" > > & Omit< CalendarProps, | "dateFormat" | "onSelect" | "onClickOutside" | "dropdownMode" | "showMonthYearDropdown" | "showYearDropdown" | "showTimeInput" >, ) => render( {}} onClickOutside={() => {}} dropdownMode="select" showYearDropdown showTimeInput {...props} />, ); const timeInputSelector = ".react-datepicker__input-time-container"; it("should render InputTime component", () => { const { container } = renderCalendar(); const timeInputClassname = container.querySelectorAll(timeInputSelector); expect(timeInputClassname).toHaveLength(1); }); it("should pass empty string to InputTime when no selected date", () => { const { container } = renderCalendar(); const timeInputEl: HTMLInputElement | null = container.querySelector( `${timeInputSelector} input`, ); expect(timeInputEl?.value).toBe(""); }); }); describe("renderYearPicker", () => { it("should render YearPicker component", () => { const { container } = render( {}} onClickOutside={() => {}} dropdownMode="select" showYearPicker />, ); const timeInputClassname = container.querySelectorAll( ".react-datepicker__year", ); expect(timeInputClassname).toHaveLength(1); }); it("calls increaseYear when next year button clicked", () => { let instance: Calendar | null = null; const { rerender } = render( { instance = node; }} dateFormat={DATE_FORMAT} onSelect={() => {}} onClickOutside={() => {}} showYearPicker dropdownMode="scroll" />, ); expect(instance).not.toBeFalsy(); act(() => { (instance!.state as { date: Required["date"] }).date = parseDate("09/28/1993", DATE_FORMAT, undefined, false)!; }); rerender( { instance = node; }} dateFormat={DATE_FORMAT} onSelect={() => {}} onClickOutside={() => {}} showYearPicker dropdownMode="scroll" />, ); const increaseYear = instance!.increaseYear; act(() => { increaseYear(); }); expect(getYear(instance!.state.date)).toBe(2005); }); it("calls decreaseYear when previous year button clicked", () => { let instance: Calendar | null = null; const { rerender } = render( { instance = node; }} dateFormat={DATE_FORMAT} onSelect={() => {}} onClickOutside={() => {}} showYearPicker dropdownMode="scroll" />, ); expect(instance).not.toBeFalsy(); act(() => { (instance!.state as { date: Required["date"] }).date = parseDate("09/28/1993", DATE_FORMAT, undefined, false)!; }); rerender( { instance = node; }} dateFormat={DATE_FORMAT} onSelect={() => {}} onClickOutside={() => {}} showYearPicker dropdownMode="scroll" />, ); const decreaseYear = instance!.decreaseYear; act(() => { decreaseYear(); }); expect(getYear(instance!.state.date)).toBe(1981); }); it("calls increaseYear for custom year item number when next year button clicked", () => { let instance: Calendar | null = null; const { rerender } = render( { instance = node; }} dateFormat={DATE_FORMAT} onClickOutside={() => {}} onSelect={() => {}} showYearPicker yearItemNumber={10} dropdownMode="scroll" />, ); expect(instance).not.toBeFalsy(); act(() => { (instance!.state as { date: Required["date"] }).date = parseDate("09/28/1993", DATE_FORMAT, undefined, true)!; }); rerender( { instance = node; }} dateFormat={DATE_FORMAT} onClickOutside={() => {}} onSelect={() => {}} showYearPicker yearItemNumber={10} dropdownMode="scroll" />, ); act(() => { instance!.increaseYear(); }); expect(getYear(instance!.state.date)).toBe(2003); }); it("calls decreaseYear for custom year item number when previous year button clicked", () => { let instance: Calendar | null = null; const { rerender } = render( { instance = node; }} onClickOutside={() => {}} onSelect={() => {}} dateFormat={DATE_FORMAT} showYearPicker yearItemNumber={10} dropdownMode="scroll" />, ); expect(instance).not.toBeFalsy(); act(() => { (instance!.state as { date: Required["date"] }).date = parseDate("09/28/1993", DATE_FORMAT, undefined, true)!; }); rerender( { instance = node; }} onClickOutside={() => {}} onSelect={() => {}} dateFormat={DATE_FORMAT} showYearPicker yearItemNumber={10} dropdownMode="scroll" />, ); act(() => { instance!.decreaseYear(); }); expect(getYear(instance!.state.date)).toBe(1983); }); }); describe("monthHeaderPosition with default header", () => { it("should render navigation buttons at top when monthHeaderPosition is 'top'", () => { const { container } = render( {}} onClickOutside={() => {}} dropdownMode="scroll" monthHeaderPosition="top" />, ); // Navigation buttons should be direct children of calendar container const prevButton = container.querySelector( ".react-datepicker > .react-datepicker__navigation--previous", ); const nextButton = container.querySelector( ".react-datepicker > .react-datepicker__navigation--next", ); expect(prevButton).not.toBeNull(); expect(nextButton).not.toBeNull(); // Should not have header-wrapper const headerWrapper = container.querySelector( ".react-datepicker__header-wrapper", ); expect(headerWrapper).toBeNull(); }); it("should wrap header with navigation buttons when monthHeaderPosition is 'middle'", () => { const { container } = render( {}} onClickOutside={() => {}} dropdownMode="scroll" monthHeaderPosition="middle" />, ); // Should have header-wrapper const headerWrapper = container.querySelector( ".react-datepicker__header-wrapper", ); expect(headerWrapper).not.toBeNull(); // Navigation buttons should be inside header-wrapper const prevButton = headerWrapper?.querySelector( ".react-datepicker__navigation--previous", ); const nextButton = headerWrapper?.querySelector( ".react-datepicker__navigation--next", ); expect(prevButton).not.toBeNull(); expect(nextButton).not.toBeNull(); // Header should have middle class const header = container.querySelector( ".react-datepicker__header--middle", ); expect(header).not.toBeNull(); }); it("should wrap header with navigation buttons when monthHeaderPosition is 'bottom'", () => { const { container } = render( {}} onClickOutside={() => {}} dropdownMode="scroll" monthHeaderPosition="bottom" />, ); // Should have header-wrapper const headerWrapper = container.querySelector( ".react-datepicker__header-wrapper", ); expect(headerWrapper).not.toBeNull(); // Navigation buttons should be inside header-wrapper const prevButton = headerWrapper?.querySelector( ".react-datepicker__navigation--previous", ); const nextButton = headerWrapper?.querySelector( ".react-datepicker__navigation--next", ); expect(prevButton).not.toBeNull(); expect(nextButton).not.toBeNull(); // Header should have bottom class const header = container.querySelector( ".react-datepicker__header--bottom", ); expect(header).not.toBeNull(); }); }); describe("when showMonthYearPicker is enabled", () => { it("should change the next and previous labels", () => { const { container } = render( {}} onClickOutside={() => {}} showMonthYearPicker dropdownMode="scroll" />, ); const previous = container.querySelector( ".react-datepicker__navigation--previous", ); const next = container.querySelector( ".react-datepicker__navigation--next", ); expect(previous?.textContent).toBe("Previous Year"); expect(next?.textContent).toBe("Next Year"); }); it("should render custom next and previous labels", () => { const { container } = render( {}} onClickOutside={() => {}} showMonthYearPicker previousYearButtonLabel="Custom Previous Year Label" nextYearButtonLabel="Custom Next Year Label" dropdownMode="scroll" />, ); const previous = container.querySelector( ".react-datepicker__navigation--previous", ); const next = container.querySelector( ".react-datepicker__navigation--next", ); expect(previous?.textContent).toBe("Custom Previous Year Label"); expect(next?.textContent).toBe("Custom Next Year Label"); }); it("calls decreaseYear when previous month button clicked", () => { let instance: Calendar | null = null; const { rerender } = render( { instance = node; }} dateFormat={DATE_FORMAT} onSelect={() => {}} onClickOutside={() => {}} showMonthYearPicker dropdownMode="scroll" />, ); expect(instance).not.toBeFalsy(); act(() => { (instance!.state as { date: Required["date"] }).date = parseDate("09/28/1993", DATE_FORMAT, undefined, false)!; }); rerender( { instance = node; }} dateFormat={DATE_FORMAT} onSelect={() => {}} onClickOutside={() => {}} showMonthYearPicker dropdownMode="scroll" />, ); const decreaseYear = instance!.decreaseYear; act(() => { decreaseYear(); }); expect(getYear(instance!.state.date)).toBe(1992); }); it("calls increaseYear when next month button clicked", () => { let instance: Calendar | null = null; const { rerender } = render( { instance = node; }} dateFormat={DATE_FORMAT} onSelect={() => {}} onClickOutside={() => {}} showMonthYearPicker dropdownMode="scroll" />, ); expect(instance).not.toBeFalsy(); act(() => { (instance!.state as { date: Required["date"] }).date = parseDate("09/28/1993", DATE_FORMAT, undefined, false)!; }); rerender( { instance = node; }} dateFormat={DATE_FORMAT} onSelect={() => {}} onClickOutside={() => {}} showMonthYearPicker dropdownMode="scroll" />, ); const increaseYear = instance!.increaseYear; act(() => { increaseYear(); }); expect(getYear(instance!.state.date)).toBe(1994); }); }); describe("when showQuarterYearPicker is enabled", () => { it("should change the next and previous labels", () => { const { container } = render( {}} onClickOutside={() => {}} showQuarterYearPicker dropdownMode="scroll" />, ); const previous = container.querySelector( ".react-datepicker__navigation--previous", ); const next = container.querySelector( ".react-datepicker__navigation--next", ); expect(previous?.textContent).toBe("Previous Year"); expect(next?.textContent).toBe("Next Year"); }); it("should render custom next and previous labels", () => { const { container } = render( {}} onClickOutside={() => {}} showQuarterYearPicker previousYearButtonLabel="Custom Previous Year Label" nextYearButtonLabel="Custom Next Year Label" dropdownMode="scroll" />, ); const previous = container.querySelector( ".react-datepicker__navigation--previous", ); const next = container.querySelector( ".react-datepicker__navigation--next", ); expect(previous?.textContent).toBe("Custom Previous Year Label"); expect(next?.textContent).toBe("Custom Next Year Label"); }); it("calls decreaseYear when previous month button clicked", () => { let instance: Calendar | null = null; const { rerender } = render( { instance = node; }} dateFormat={DATE_FORMAT} onSelect={() => {}} onClickOutside={() => {}} showQuarterYearPicker dropdownMode="scroll" />, ); expect(instance).not.toBeFalsy(); act(() => { (instance!.state as { date: Required["date"] }).date = parseDate("09/28/1993", DATE_FORMAT, undefined, false)!; }); rerender( { instance = node; }} dateFormat={DATE_FORMAT} onSelect={() => {}} onClickOutside={() => {}} showQuarterYearPicker dropdownMode="scroll" />, ); const decreaseYear = instance!.decreaseYear; act(() => { decreaseYear(); }); expect(getYear(instance!.state.date)).toBe(1992); }); it("calls increaseYear when next month button clicked", () => { let instance: Calendar | null = null; const { rerender } = render( { instance = node; }} dateFormat={DATE_FORMAT} onSelect={() => {}} onClickOutside={() => {}} showQuarterYearPicker dropdownMode="scroll" />, ); expect(instance).not.toBeFalsy(); act(() => { (instance!.state as { date: Required["date"] }).date = parseDate("09/28/1993", DATE_FORMAT, undefined, false)!; }); rerender( { instance = node; }} dateFormat={DATE_FORMAT} onSelect={() => {}} onClickOutside={() => {}} showQuarterYearPicker dropdownMode="scroll" />, ); const increaseYear = instance!.increaseYear; act(() => { increaseYear(); }); expect(getYear(instance!.state.date)).toBe(1994); }); it("should hide the previous year navigation arrow button when the minDate falls under the currently visible year", () => { const { container } = render( {}} onSelect={() => {}} dropdownMode="scroll" />, ); const previous = container.querySelector( ".react-datepicker__navigation--previous", ); expect(previous).toBeNull(); }); it("should hide the next year navigation arrow button when the maxDate falls under the currently visible year", () => { const { container } = render( {}} onSelect={() => {}} dropdownMode="scroll" />, ); const next = container.querySelector( ".react-datepicker__navigation--next", ); expect(next).toBeNull(); }); }); describe("using click outside", () => { const clickOutsideSpy = jest.fn(); const renderCalendar = () => { let instance: Calendar | null = null; render( { instance = node; }} dateFormat={DATE_FORMAT} onSelect={() => {}} onClickOutside={clickOutsideSpy} dropdownMode="scroll" />, ); expect(instance).not.toBeFalsy(); return { instance: instance!, }; }; it("calls onClickOutside prop when handles click outside", () => { const { instance } = renderCalendar(); act(() => { instance.handleClickOutside("__event__" as unknown as MouseEvent); }); expect(clickOutsideSpy).toHaveBeenCalledWith("__event__"); }); it("setClickOutsideRef function returns container ref", () => { const { instance } = renderCalendar(); const ref = instance.setClickOutsideRef(); expect(ref).not.toBeNull(); expect(ref).toEqual(instance.containerRef.current); }); }); it("should add the aria-label correctly to day names", () => { const expectedAriaLabels = [ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", ]; const { container } = render( {}} onClickOutside={() => {}} dropdownMode="scroll" />, ); const header = container.querySelector(".react-datepicker__header"); const dayNameElements = header?.querySelectorAll( ".react-datepicker__day-name > span.react-datepicker__sr-only", ); dayNameElements?.forEach((element, index) => { expect(element.textContent).toBe(expectedAriaLabels[index]); }); }); it("should have a next-button with the provided aria-label for year", () => { const ariaLabel = "A label in my native language for next year"; const { container } = render( {}} onClickOutside={() => {}} showQuarterYearPicker dropdownMode="scroll" />, ); expect(container.innerHTML.indexOf(`aria-label="${ariaLabel}"`)).not.toBe( -1, ); }); it("should have a previous-button with the provided aria-label for year", () => { const ariaLabel = "A label in my native language for previous year"; const { container } = render( {}} onClickOutside={() => {}} showQuarterYearPicker dropdownMode="scroll" />, ); expect(container.innerHTML.indexOf(`aria-label="${ariaLabel}"`)).not.toBe( -1, ); }); it("should have a next-button with the provided aria-label for month", () => { const ariaLabel = "A label in my native language for next month"; const { container } = render( {}} onClickOutside={() => {}} dropdownMode="scroll" />, ); expect(container.innerHTML.indexOf(`aria-label="${ariaLabel}"`)).not.toBe( -1, ); }); it("should have a previous-button with the provided aria-label for month", () => { const ariaLabel = "A label in my native language for previous month"; const { container } = render( {}} onClickOutside={() => {}} dropdownMode="scroll" />, ); expect(container.innerHTML.indexOf(`aria-label="${ariaLabel}"`)).not.toBe( -1, ); }); describe("changing the month also changes the preselection to preserve keyboard navigation abilities", () => { it("updates the preselection when you choose Next Month", () => { const selected = new Date(); selected.setDate(1); const currentMonth = selected.getMonth(); let instance: DatePicker | null = null; render( { instance = node; }} selected={selected} />, ); expect(instance).not.toBeFalsy(); expect(instance!.input).not.toBeFalsy(); const dateInput = instance!.input!; fireEvent.focus(dateInput); const calendar = instance!.calendar?.containerRef.current; const navigation = calendar?.querySelector( ".react-datepicker__navigation--next", ); expect(navigation).not.toBeFalsy(); fireEvent.click(navigation!); expect(instance!.state.preSelection?.getMonth()).toBe( currentMonth === 11 ? 0 : currentMonth + 1, ); }); it("updates the preselection when you choose Previous Month", () => { const selected = new Date(); selected.setDate(1); const currentMonth = selected.getMonth(); let instance: DatePicker | null = null; render( { instance = node; }} selected={selected} />, ); expect(instance).not.toBeFalsy(); expect(instance!.input).not.toBeFalsy(); const dateInput = instance!.input!; fireEvent.focus(dateInput); const calendar = instance!.calendar?.containerRef.current; expect(calendar).not.toBeFalsy(); const navigation = calendar!.querySelector( ".react-datepicker__navigation--previous", ); expect(navigation).not.toBeFalsy(); fireEvent.click(navigation!); expect(instance!.state.preSelection?.getMonth()).toBe( currentMonth === 0 ? 11 : currentMonth - 1, ); }); describe("pre-selection & disabled dates", () => { it("should update the pre-selected dates to the first enabled day in a month when the next month is selected", () => { const selected = new Date("2024-06-01"); const excludeDate = addMonths(selected, 1); const { container } = render( , ); const input = safeQuerySelector(container, "input"); fireEvent.focus(input); const nextButton = safeQuerySelector( container, ".react-datepicker__navigation--next", ); fireEvent.click(nextButton); const preSelectedNewDate = safeQuerySelector( container, ".react-datepicker__day--keyboard-selected", ).textContent; const expectedPreSelectedNewDate = addDays(excludeDate, 1); expect(Number(preSelectedNewDate)).toBe( getDate(expectedPreSelectedNewDate), ); }); it("should update the pre-selected dates to the first enabled day in a month when the previous month is selected", () => { const selected = new Date("2024-06-08"); const excludeDate = addMonths(selected, 1); const { container } = render( , ); const input = safeQuerySelector(container, "input"); fireEvent.focus(input); const nextButton = safeQuerySelector( container, ".react-datepicker__navigation--next", ); fireEvent.click(nextButton); const preSelectedNewDate = safeQuerySelector( container, ".react-datepicker__day--keyboard-selected", ).textContent; const expectedPreSelectedNewDate = setDate(excludeDate, 1); expect(Number(preSelectedNewDate)).toBe( getDate(expectedPreSelectedNewDate), ); }); it("shouldn't set pre-select any date if all dates of a next month is disabled", () => { const selected = new Date("2024-06-08"); const nextMonth = addMonths(selected, 1); const excludeDates = eachDayOfInterval({ start: startOfMonth(nextMonth), end: endOfMonth(nextMonth), }); const { container } = render( , ); const input = safeQuerySelector(container, "input"); fireEvent.focus(input); const nextButton = safeQuerySelector( container, ".react-datepicker__navigation--next", ); fireEvent.click(nextButton); expect( container.querySelector(".react-datepicker__day--keyboard-selected"), ).toBeNull(); }); it("shouldn't set pre-select any date if all dates of a last month is disabled", () => { const selected = new Date("2024-06-08"); const lastMonth = subMonths(selected, 1); const excludeDates = eachDayOfInterval({ start: startOfMonth(lastMonth), end: endOfMonth(lastMonth), }); const { container } = render( , ); const input = safeQuerySelector(container, "input"); fireEvent.focus(input); const nextButton = safeQuerySelector( container, ".react-datepicker__navigation--previous", ); fireEvent.click(nextButton); expect( container.querySelector(".react-datepicker__day--keyboard-selected"), ).toBeNull(); }); }); }); describe("showTimeSelect", () => { it("should not contain the time select classname in header by default", () => { const { calendar } = getCalendar(); const header = calendar.querySelectorAll( ".react-datepicker__header--has-time-select", ); expect(header).toHaveLength(0); }); it("should contain the time select classname in header if enabled", () => { const { calendar } = getCalendar({ showTimeSelect: true }); const header = calendar.querySelectorAll( ".react-datepicker__header--has-time-select", ); expect(header).toHaveLength(1); }); }); describe("calendarStartDay", () => { it("should have default sunday as start day if No prop passed", () => { const { calendar } = getCalendar(); const calendarDays = calendar.querySelectorAll( ".react-datepicker__day-name > span[aria-hidden='true']", ); expect(calendarDays[0]?.textContent).toBe("Su"); expect(calendarDays[6]?.textContent).toBe("Sa"); }); it("should have default wednesday as start day if No prop passed", () => { const { calendar } = getCalendar({ calendarStartDay: 3 }); const calendarDays = calendar.querySelectorAll( ".react-datepicker__day-name > span[aria-hidden='true']", ); expect(calendarDays[0]?.textContent).toBe("We"); expect(calendarDays[6]?.textContent).toBe("Tu"); }); }); describe("prev/next month button onKeyDown handler", () => { it("should call the prevMonthButton onKeyDown handler on Tab press", () => { const onKeyDownSpy = jest.fn(); const { container } = render( , ); const input = safeQuerySelector(container, "input"); fireEvent.focus(input); const prevMonthButton = safeQuerySelector( container, ".react-datepicker__navigation--previous", ); fireEvent.keyDown(prevMonthButton, getKey(KeyType.Tab)); expect(onKeyDownSpy).toHaveBeenCalledTimes(1); }); it("should call the nextMonthButton onKeyDown handler on Tab press", () => { const onKeyDownSpy = jest.fn(); const { container } = render( , ); const input = safeQuerySelector(container, "input"); fireEvent.focus(input); const nextMonthButton = safeQuerySelector( container, ".react-datepicker__navigation--next", ); fireEvent.keyDown(nextMonthButton, getKey(KeyType.Tab)); expect(onKeyDownSpy).toHaveBeenCalledTimes(1); }); }); describe("renderChildren", () => { const renderCalendar = ( props?: Partial< Pick< CalendarProps, "dateFormat" | "onSelect" | "onClickOutside" | "dropdownMode" > > & Omit< CalendarProps, | "dateFormat" | "onSelect" | "onClickOutside" | "dropdownMode" | "showMonthYearDropdown" >, ) => render( {}} onClickOutside={() => {}} dropdownMode="scroll" {...props} />, ); const childrenContainerSelector = ".react-datepicker__children-container"; it("should render children components", () => { const { container } = renderCalendar({ children:
This is a child component for test.
, }); const childrenContainer = container.querySelectorAll( childrenContainerSelector, ); expect(childrenContainer).toHaveLength(1); }); it("should not render children components", () => { const { container } = renderCalendar(); const childrenContainer = container.querySelectorAll( childrenContainerSelector, ); expect(childrenContainer).toHaveLength(0); }); }); describe("should render aria live region after month/year change", () => { it("should render aria live region after month change", () => { const { container } = render(); const input = safeQuerySelector(container, "input"); fireEvent.focus(input); const nextNavigationButton = safeQuerySelector( container, ".react-datepicker__navigation--next", ); fireEvent.click(nextNavigationButton); const currentMonthText = container.querySelector( ".react-datepicker__current-month", )?.textContent; const ariaLiveMessage = container.querySelector( ".react-datepicker__aria-live", )?.textContent; expect(currentMonthText).toBe(ariaLiveMessage); }); it("should render aria live region after year change", async () => { let instance: DatePicker | null = null; render( { instance = node; }} showYearDropdown selected={newDate()} />, ); expect((instance as DatePicker | null)?.input).not.toBeFalsy(); const dateInput = instance!.input!; fireEvent.focus(dateInput); expect((instance as DatePicker | null)?.calendar).not.toBeFalsy(); const calendar = instance!.calendar!.containerRef.current; const yearDropdown = calendar?.querySelector( ".react-datepicker__year-read-view", ); expect(yearDropdown).not.toBeFalsy(); fireEvent.click(yearDropdown!); const option = calendar?.querySelectorAll( ".react-datepicker__year-option", )[7]; expect(option).not.toBeFalsy(); fireEvent.click(option!); const ariaLiveMessage = calendar?.querySelector( ".react-datepicker__aria-live", )?.textContent; await waitFor(() => { expect((instance as DatePicker | null)?.calendar).not.toBeFalsy(); expect(ariaLiveMessage).toBe( `${getMonthInLocale( getMonth(instance!.calendar!.state.date), instance!.props.locale, )} ${getYear(instance!.calendar!.state.date)}`, ); }); }); }); describe("calendar container", () => { it("should render Calendar in popover mode with dialog accessibility props", () => { const { container } = render( {}} onSelect={() => {}} dropdownMode="scroll" />, ); const dialog = container.querySelector(".react-datepicker"); expect(dialog).not.toBeNull(); expect(dialog?.getAttribute("role")).toBe("dialog"); expect(dialog?.getAttribute("aria-modal")).toBe("true"); expect(dialog?.getAttribute("aria-label")).toBe("Choose Date"); }); it("should render Calendar in inline mode without dialog accessibility props", () => { const { container } = render( {}} onSelect={() => {}} dropdownMode="scroll" inline />, ); const dialog = container.querySelector(".react-datepicker"); expect(dialog).not.toBeNull(); expect(dialog?.getAttribute("role")).toBeNull(); expect(dialog?.getAttribute("aria-modal")).toBeNull(); expect(dialog?.getAttribute("aria-label")).toBe("Choose Date"); }); it("should display corresponding aria-label for Calendar with showTimeSelect", () => { const { container } = render( {}} onSelect={() => {}} dropdownMode="scroll" />, ); const dialog = container.querySelector(".react-datepicker"); expect(dialog).not.toBeNull(); expect(dialog?.getAttribute("aria-label")?.toLowerCase().trim()).toBe( "choose date and time", ); }); it("should display corresponding aria-label for Calendar with showTimeInput", () => { const { container } = render( {}} onSelect={() => {}} dropdownMode="scroll" />, ); const dialog = container.querySelector(".react-datepicker"); expect(dialog).not.toBeNull(); expect(dialog?.getAttribute("aria-label")?.toLowerCase().trim()).toBe( "choose date and time", ); }); it("should display corresponding aria-label for Calendar with showTimeSelectOnly", () => { const { container } = render( {}} onSelect={() => {}} dropdownMode="scroll" />, ); const dialog = container.querySelector(".react-datepicker"); expect(dialog).not.toBeNull(); expect(dialog?.getAttribute("aria-label")?.toLowerCase().trim()).toBe( "choose time", ); }); }); describe("handleMonthChange with adjustDateOnChange but without setOpen", () => { it("should call onSelect when adjustDateOnChange is true but setOpen is not provided", () => { const onSelect = jest.fn(); const setPreSelection = jest.fn(); const { instance } = getCalendar({ adjustDateOnChange: true, onSelect, setPreSelection, // setOpen is intentionally NOT provided to cover line 442 selected: new Date("2024-01-15T00:00:00"), }); const targetMonth = new Date("2024-02-01T00:00:00"); act(() => { instance?.handleMonthChange(targetMonth); }); expect(onSelect).toHaveBeenCalled(); expect(setPreSelection).toHaveBeenCalled(); }); }); describe("header method with invalid date", () => { it("should return empty array when date is invalid", () => { const { instance } = getCalendar({ selected: new Date("2024-01-15T00:00:00"), }); // Call header method with invalid date const result = instance?.header(new Date("invalid")); expect(result).toEqual([]); }); it("should use default date parameter when not provided", () => { const { instance } = getCalendar({ selected: new Date("2024-01-15T00:00:00"), }); // Call header method without arguments to cover default parameter const result = instance?.header(); expect(result).toBeDefined(); expect(Array.isArray(result)).toBe(true); expect(result?.length).toBeGreaterThan(0); }); }); });