import { userEvent } from '@vitest/browser/context'; import { describe, expect, it, vi } from 'vitest'; import { act, fireEvent, render, waitFor, waitForElementToBeRemoved } from '@testing-library/react'; import DateRangePicker from './DateRangePicker.js'; async function waitForElementToBeRemovedOrHidden(callback: () => HTMLElement | null) { const element = callback(); if (element) { try { await waitFor(() => expect(element).toHaveAttribute('class', expect.stringContaining('--closed')), ); } catch { await waitForElementToBeRemoved(element); } } } describe('DateRangePicker', () => { it('passes default name to DateInput components', () => { const { container } = render(); const nativeInputs = container.querySelectorAll('input[type="date"]'); expect(nativeInputs[0]).toHaveAttribute('name', 'daterange_from'); expect(nativeInputs[1]).toHaveAttribute('name', 'daterange_to'); }); it('passes custom name to DateInput components', () => { const name = 'testName'; const { container } = render(); const nativeInputs = container.querySelectorAll('input[type="date"]'); expect(nativeInputs[0]).toHaveAttribute('name', `${name}_from`); expect(nativeInputs[1]).toHaveAttribute('name', `${name}_to`); }); it('passes autoFocus flag to first DateInput component', () => { const { container } = render(); const customInputs = container.querySelectorAll('input[data-input]'); expect(customInputs[0]).toHaveFocus(); }); it('passes disabled flag to DateInput components', () => { const { container } = render(); const nativeInputs = container.querySelectorAll('input[type="date"]'); expect(nativeInputs[0]).toBeDisabled(); expect(nativeInputs[1]).toBeDisabled(); }); it('passes format to DateInput components', () => { const { container } = render(); const customInputs = container.querySelectorAll('input[data-input]'); expect(customInputs).toHaveLength(2); expect(customInputs[0]).toHaveAttribute('name', 'year'); expect(customInputs[1]).toHaveAttribute('name', 'year'); }); it('passes aria-label props to DateInput components', () => { const ariaLabelProps = { calendarAriaLabel: 'Toggle calendar', clearAriaLabel: 'Clear value', dayAriaLabel: 'Day', monthAriaLabel: 'Month', nativeInputAriaLabel: 'Date', yearAriaLabel: 'Year', }; const { container } = render(); const calendarButton = container.querySelector( 'button.react-daterange-picker__calendar-button', ); const clearButton = container.querySelector('button.react-daterange-picker__clear-button'); const dateInputs = container.querySelectorAll( '.react-daterange-picker__inputGroup', ) as unknown as [HTMLDivElement, HTMLDivElement]; const [dateFromInput, dateToInput] = dateInputs; const nativeFromInput = dateFromInput.querySelector('input[type="date"]'); const dayFromInput = dateFromInput.querySelector('input[name="day"]'); const monthFromInput = dateFromInput.querySelector('input[name="month"]'); const yearFromInput = dateFromInput.querySelector('input[name="year"]'); const nativeToInput = dateToInput.querySelector('input[type="date"]'); const dayToInput = dateToInput.querySelector('input[name="day"]'); const monthToInput = dateToInput.querySelector('input[name="month"]'); const yearToInput = dateToInput.querySelector('input[name="year"]'); expect(calendarButton).toHaveAttribute('aria-label', ariaLabelProps.calendarAriaLabel); expect(clearButton).toHaveAttribute('aria-label', ariaLabelProps.clearAriaLabel); expect(nativeFromInput).toHaveAttribute('aria-label', ariaLabelProps.nativeInputAriaLabel); expect(dayFromInput).toHaveAttribute('aria-label', ariaLabelProps.dayAriaLabel); expect(monthFromInput).toHaveAttribute('aria-label', ariaLabelProps.monthAriaLabel); expect(yearFromInput).toHaveAttribute('aria-label', ariaLabelProps.yearAriaLabel); expect(nativeToInput).toHaveAttribute('aria-label', ariaLabelProps.nativeInputAriaLabel); expect(dayToInput).toHaveAttribute('aria-label', ariaLabelProps.dayAriaLabel); expect(monthToInput).toHaveAttribute('aria-label', ariaLabelProps.monthAriaLabel); expect(yearToInput).toHaveAttribute('aria-label', ariaLabelProps.yearAriaLabel); }); it('passes placeholder props to DateInput components', () => { const placeholderProps = { dayPlaceholder: 'dd', monthPlaceholder: 'mm', yearPlaceholder: 'yyyy', }; const { container } = render(); const dateInputs = container.querySelectorAll( '.react-daterange-picker__inputGroup', ) as unknown as [HTMLDivElement, HTMLDivElement]; const [dateFromInput, dateToInput] = dateInputs; const dayFromInput = dateFromInput.querySelector('input[name="day"]'); const monthFromInput = dateFromInput.querySelector('input[name="month"]'); const yearFromInput = dateFromInput.querySelector('input[name="year"]'); const dayToInput = dateToInput.querySelector('input[name="day"]'); const monthToInput = dateToInput.querySelector('input[name="month"]'); const yearToInput = dateToInput.querySelector('input[name="year"]'); expect(dayFromInput).toHaveAttribute('placeholder', placeholderProps.dayPlaceholder); expect(monthFromInput).toHaveAttribute('placeholder', placeholderProps.monthPlaceholder); expect(yearFromInput).toHaveAttribute('placeholder', placeholderProps.yearPlaceholder); expect(dayToInput).toHaveAttribute('placeholder', placeholderProps.dayPlaceholder); expect(monthToInput).toHaveAttribute('placeholder', placeholderProps.monthPlaceholder); expect(yearToInput).toHaveAttribute('placeholder', placeholderProps.yearPlaceholder); }); describe('passes value to DateInput components', () => { it('passes single value to DateInput components', () => { const value = new Date(2019, 0, 1); const { container } = render(); const nativeInputs = container.querySelectorAll('input[type="date"]'); expect(nativeInputs[0]).toHaveValue('2019-01-01'); expect(nativeInputs[1]).toHaveValue(''); }); it('passes the first item of an array of values to DateInput components', () => { const value1 = new Date(2019, 0, 1); const value2 = new Date(2019, 6, 1); const { container } = render(); const nativeInputs = container.querySelectorAll('input[type="date"]'); expect(nativeInputs[0]).toHaveValue('2019-01-01'); expect(nativeInputs[1]).toHaveValue('2019-07-01'); }); }); it('applies className to its wrapper when given a string', () => { const className = 'testClassName'; const { container } = render(); const wrapper = container.firstElementChild; expect(wrapper).toHaveClass(className); }); it('applies "--open" className to its wrapper when given isOpen flag', () => { const { container } = render(); const wrapper = container.firstElementChild; expect(wrapper).toHaveClass('react-daterange-picker--open'); }); it('applies calendarClassName to the calendar when given a string', () => { const calendarClassName = 'testClassName'; const { container } = render( , ); const calendar = container.querySelector('.react-calendar'); expect(calendar).toHaveClass(calendarClassName); }); it('renders DateInput components', () => { const { container } = render(); const nativeInputs = container.querySelectorAll('input[type="date"]'); expect(nativeInputs.length).toBe(2); }); it('renders range divider with default divider', () => { const { container } = render(); const rangeDivider = container.querySelector('.react-daterange-picker__range-divider'); expect(rangeDivider).toBeInTheDocument(); expect(rangeDivider).toHaveTextContent('–'); }); it('renders range divider with custom divider', () => { const { container } = render(); const rangeDivider = container.querySelector('.react-daterange-picker__range-divider'); expect(rangeDivider).toBeInTheDocument(); expect(rangeDivider).toHaveTextContent('to'); }); describe('renders clear button properly', () => { it('renders clear button', () => { const { container } = render(); const clearButton = container.querySelector('button.react-daterange-picker__clear-button'); expect(clearButton).toBeInTheDocument(); }); it('renders clear icon by default when clearIcon is not given', () => { const { container } = render(); const clearButton = container.querySelector( 'button.react-daterange-picker__clear-button', ) as HTMLButtonElement; const clearIcon = clearButton.querySelector('svg'); expect(clearIcon).toBeInTheDocument(); }); it('renders clear icon when given clearIcon as a string', () => { const { container } = render(); const clearButton = container.querySelector('button.react-daterange-picker__clear-button'); expect(clearButton).toHaveTextContent('❌'); }); it('renders clear icon when given clearIcon as a React element', () => { function ClearIcon() { return <>❌; } const { container } = render(} />); const clearButton = container.querySelector('button.react-daterange-picker__clear-button'); expect(clearButton).toHaveTextContent('❌'); }); it('renders clear icon when given clearIcon as a function', () => { function ClearIcon() { return <>❌; } const { container } = render(); const clearButton = container.querySelector('button.react-daterange-picker__clear-button'); expect(clearButton).toHaveTextContent('❌'); }); }); describe('renders calendar button properly', () => { it('renders calendar button', () => { const { container } = render(); const calendarButton = container.querySelector( 'button.react-daterange-picker__calendar-button', ); expect(calendarButton).toBeInTheDocument(); }); it('renders calendar icon by default when calendarIcon is not given', () => { const { container } = render(); const calendarButton = container.querySelector( 'button.react-daterange-picker__calendar-button', ) as HTMLButtonElement; const calendarIcon = calendarButton.querySelector('svg'); expect(calendarIcon).toBeInTheDocument(); }); it('renders calendar icon when given calendarIcon as a string', () => { const { container } = render(); const calendarButton = container.querySelector( 'button.react-daterange-picker__calendar-button', ); expect(calendarButton).toHaveTextContent('📅'); }); it('renders calendar icon when given calendarIcon as a React element', () => { function CalendarIcon() { return <>📅; } const { container } = render(} />); const calendarButton = container.querySelector( 'button.react-daterange-picker__calendar-button', ); expect(calendarButton).toHaveTextContent('📅'); }); it('renders calendar icon when given calendarIcon as a function', () => { function CalendarIcon() { return <>📅; } const { container } = render(); const calendarButton = container.querySelector( 'button.react-daterange-picker__calendar-button', ); expect(calendarButton).toHaveTextContent('📅'); }); }); it('renders Calendar component when given isOpen flag', () => { const { container } = render(); const calendar = container.querySelector('.react-calendar'); expect(calendar).toBeInTheDocument(); }); it('does not render Calendar component when given disableCalendar & isOpen flags', () => { const { container } = render(); const calendar = container.querySelector('.react-calendar'); expect(calendar).toBeFalsy(); }); it('opens Calendar component when given isOpen flag by changing props', () => { const { container, rerender } = render(); const calendar = container.querySelector('.react-calendar'); expect(calendar).toBeFalsy(); rerender(); const calendar2 = container.querySelector('.react-calendar'); expect(calendar2).toBeInTheDocument(); }); it('opens Calendar component when clicking on a button', () => { const { container } = render(); const calendar = container.querySelector('.react-calendar'); expect(calendar).toBeFalsy(); const button = container.querySelector( 'button.react-daterange-picker__calendar-button', ) as HTMLButtonElement; fireEvent.click(button); const calendar2 = container.querySelector('.react-calendar'); expect(calendar2).toBeInTheDocument(); }); describe('handles opening Calendar component when focusing on an input inside properly', () => { it('opens Calendar component when focusing on an input inside by default', () => { const { container } = render(); const calendar = container.querySelector('.react-calendar'); expect(calendar).toBeFalsy(); const input = container.querySelector('input[name="day"]') as HTMLInputElement; fireEvent.focus(input); const calendar2 = container.querySelector('.react-calendar'); expect(calendar2).toBeInTheDocument(); }); it('opens Calendar component when focusing on an input inside given openCalendarOnFocus = true', () => { const { container } = render(); const calendar = container.querySelector('.react-calendar'); const input = container.querySelector('input[name="day"]') as HTMLInputElement; expect(calendar).toBeFalsy(); fireEvent.focus(input); const calendar2 = container.querySelector('.react-calendar'); expect(calendar2).toBeInTheDocument(); }); it('does not open Calendar component when focusing on an input inside given openCalendarOnFocus = false', () => { const { container } = render(); const calendar = container.querySelector('.react-calendar'); const input = container.querySelector('input[name="day"]') as HTMLInputElement; expect(calendar).toBeFalsy(); fireEvent.focus(input); const calendar2 = container.querySelector('.react-calendar'); expect(calendar2).toBeFalsy(); }); it('does not open Calendar when focusing on an input inside given shouldOpenCalendar function returning false', () => { const shouldOpenCalendar = () => false; const { container } = render(); const calendar = container.querySelector('.react-calendar'); const input = container.querySelector('input[name="day"]') as HTMLInputElement; expect(calendar).toBeFalsy(); fireEvent.focus(input); const calendar2 = container.querySelector('.react-calendar'); expect(calendar2).toBeFalsy(); }); it('does not open Calendar component when focusing on a select element', () => { const { container } = render(); const calendar = container.querySelector('.react-calendar'); const select = container.querySelector('select[name="month"]') as HTMLSelectElement; expect(calendar).toBeFalsy(); fireEvent.focus(select); const calendar2 = container.querySelector('.react-calendar'); expect(calendar2).toBeFalsy(); }); }); it('closes Calendar component when clicked outside', async () => { const { container } = render(); await userEvent.click(document.body); await waitForElementToBeRemovedOrHidden(() => container.querySelector('.react-daterange-picker__calendar'), ); }); it('closes Calendar component when focused outside', async () => { const { container } = render(); fireEvent.focusIn(document.body); await waitForElementToBeRemovedOrHidden(() => container.querySelector('.react-daterange-picker__calendar'), ); }); it('closes Calendar component when tapped outside', async () => { const { container } = render(); fireEvent.touchStart(document.body); await waitForElementToBeRemovedOrHidden(() => container.querySelector('.react-daterange-picker__calendar'), ); }); it('does not close Calendar component when focused inside', () => { const { container } = render(); const customInputs = container.querySelectorAll('input[data-input]'); const monthInput = customInputs[0] as HTMLInputElement; const dayInput = customInputs[1] as HTMLInputElement; fireEvent.blur(monthInput); fireEvent.focus(dayInput); const calendar = container.querySelector('.react-calendar'); expect(calendar).toBeInTheDocument(); }); it('closes Calendar when changing value by default', async () => { const { container } = render(); const [firstTile, secondTile] = container.querySelectorAll( '.react-calendar__tile', ) as unknown as [HTMLButtonElement, HTMLButtonElement]; act(() => { fireEvent.click(firstTile); }); act(() => { fireEvent.click(secondTile); }); await waitForElementToBeRemovedOrHidden(() => container.querySelector('.react-daterange-picker__calendar'), ); }); it('closes Calendar when changing value with prop closeCalendar = true', async () => { const { container } = render(); const [firstTile, secondTile] = container.querySelectorAll( '.react-calendar__tile', ) as unknown as [HTMLButtonElement, HTMLButtonElement]; act(() => { fireEvent.click(firstTile); }); act(() => { fireEvent.click(secondTile); }); await waitForElementToBeRemovedOrHidden(() => container.querySelector('.react-daterange-picker__calendar'), ); }); it('does not close Calendar when changing value with prop closeCalendar = false', () => { const { container } = render(); const [firstTile, secondTile] = container.querySelectorAll( '.react-calendar__tile', ) as unknown as [HTMLButtonElement, HTMLButtonElement]; act(() => { fireEvent.click(firstTile); }); act(() => { fireEvent.click(secondTile); }); const calendar = container.querySelector('.react-calendar'); expect(calendar).toBeInTheDocument(); }); it('does not close Calendar when changing value with shouldCloseCalendar function returning false', () => { const shouldCloseCalendar = () => false; const { container } = render( , ); const firstTile = container.querySelector('.react-calendar__tile') as HTMLButtonElement; act(() => { fireEvent.click(firstTile); }); const calendar = container.querySelector('.react-calendar'); expect(calendar).toBeInTheDocument(); }); it('does not close Calendar when changing value using inputs', () => { const { container } = render(); const dayInput = container.querySelector('input[name="day"]') as HTMLInputElement; act(() => { fireEvent.change(dayInput, { target: { value: '1' } }); }); const calendar = container.querySelector('.react-calendar'); expect(calendar).toBeInTheDocument(); }); it('calls onChange callback when changing value', () => { const value = new Date(2023, 0, 31); const onChange = vi.fn(); const { container } = render(); const dayInput = container.querySelector('input[name="day"]') as HTMLInputElement; act(() => { fireEvent.change(dayInput, { target: { value: '1' } }); }); expect(onChange).toHaveBeenCalledWith([new Date(2023, 0, 1), null]); }); it('calls onInvalidChange callback when changing value to an invalid one', () => { const value = new Date(2023, 0, 31); const onInvalidChange = vi.fn(); const { container } = render( , ); const dayInput = container.querySelector('input[name="day"]') as HTMLInputElement; act(() => { fireEvent.change(dayInput, { target: { value: '32' } }); }); expect(onInvalidChange).toHaveBeenCalled(); }); it('clears the value when clicking on a button', () => { const onChange = vi.fn(); const { container } = render(); const calendar = container.querySelector('.react-calendar'); const button = container.querySelector( 'button.react-daterange-picker__clear-button', ) as HTMLButtonElement; expect(calendar).toBeFalsy(); fireEvent.click(button); expect(onChange).toHaveBeenCalledWith(null); }); describe('onChangeFrom', () => { it('calls onChange properly given no initial value', () => { const onChange = vi.fn(); const { container } = render(); const nextValueFrom = new Date(2018, 1, 15); const customInputs = container.querySelectorAll('input[data-input]'); const monthInput = customInputs[0] as HTMLInputElement; const dayInput = customInputs[1] as HTMLInputElement; const yearInput = customInputs[2] as HTMLInputElement; act(() => { fireEvent.change(monthInput, { target: { value: '2' } }); }); act(() => { fireEvent.change(dayInput, { target: { value: '15' } }); }); act(() => { fireEvent.change(yearInput, { target: { value: '2018' } }); }); expect(onChange).toHaveBeenCalled(); expect(onChange).toHaveBeenCalledWith([nextValueFrom, null]); }); it('calls onChange properly given single initial value', () => { const onChange = vi.fn(); const value = new Date(2018, 0, 1); const { container } = render(); const nextValueFrom = new Date(2018, 1, 15); const customInputs = container.querySelectorAll('input[data-input]'); const monthInput = customInputs[0] as HTMLInputElement; const dayInput = customInputs[1] as HTMLInputElement; const yearInput = customInputs[2] as HTMLInputElement; act(() => { fireEvent.change(monthInput, { target: { value: '2' } }); }); act(() => { fireEvent.change(dayInput, { target: { value: '15' } }); }); act(() => { fireEvent.change(yearInput, { target: { value: '2018' } }); }); expect(onChange).toHaveBeenCalled(); expect(onChange).toHaveBeenCalledWith([nextValueFrom, null]); }); it('calls onChange properly given initial value as an array', () => { const onChange = vi.fn(); const valueFrom = new Date(2018, 0, 1); const valueTo = new Date(2018, 6, 1); const value = [valueFrom, valueTo] as [Date, Date]; const { container } = render(); const nextValueFrom = new Date(2018, 1, 15); const customInputs = container.querySelectorAll('input[data-input]'); const monthInput = customInputs[0] as HTMLInputElement; const dayInput = customInputs[1] as HTMLInputElement; const yearInput = customInputs[2] as HTMLInputElement; act(() => { fireEvent.change(monthInput, { target: { value: '2' } }); }); act(() => { fireEvent.change(dayInput, { target: { value: '15' } }); }); act(() => { fireEvent.change(yearInput, { target: { value: '2018' } }); }); expect(onChange).toHaveBeenCalled(); expect(onChange).toHaveBeenCalledWith([nextValueFrom, valueTo]); }); }); describe('onChangeTo', () => { it('calls onChange properly given no initial value', () => { const onChange = vi.fn(); const { container } = render(); const nextValueTo = new Date(2018, 1, 15); nextValueTo.setDate(nextValueTo.getDate() + 1); nextValueTo.setTime(nextValueTo.getTime() - 1); const customInputs = container.querySelectorAll('input[data-input]'); const monthInput = customInputs[3] as HTMLInputElement; const dayInput = customInputs[4] as HTMLInputElement; const yearInput = customInputs[5] as HTMLInputElement; act(() => { fireEvent.change(dayInput, { target: { value: '15' } }); }); act(() => { fireEvent.change(monthInput, { target: { value: '2' } }); }); act(() => { fireEvent.change(yearInput, { target: { value: '2018' } }); }); expect(onChange).toHaveBeenCalled(); expect(onChange).toHaveBeenCalledWith([null, nextValueTo]); }); it('calls onChange properly given single initial value', () => { const onChange = vi.fn(); const value = new Date(2018, 0, 1); const { container } = render(); const nextValueTo = new Date(2018, 1, 15); nextValueTo.setDate(nextValueTo.getDate() + 1); nextValueTo.setTime(nextValueTo.getTime() - 1); const customInputs = container.querySelectorAll('input[data-input]'); const monthInput = customInputs[3] as HTMLInputElement; const dayInput = customInputs[4] as HTMLInputElement; const yearInput = customInputs[5] as HTMLInputElement; act(() => { fireEvent.change(dayInput, { target: { value: '15' } }); }); act(() => { fireEvent.change(monthInput, { target: { value: '2' } }); }); act(() => { fireEvent.change(yearInput, { target: { value: '2018' } }); }); expect(onChange).toHaveBeenCalled(); expect(onChange).toHaveBeenCalledWith([value, nextValueTo]); }); it('calls onChange properly given initial value as an array', () => { const onChange = vi.fn(); const valueFrom = new Date(2018, 0, 1); const valueTo = new Date(2018, 6, 1); const value = [valueFrom, valueTo] as [Date, Date]; const { container } = render(); const nextValueTo = new Date(2018, 1, 15); nextValueTo.setDate(nextValueTo.getDate() + 1); nextValueTo.setTime(nextValueTo.getTime() - 1); const customInputs = container.querySelectorAll('input[data-input]'); const monthInput = customInputs[3] as HTMLInputElement; const dayInput = customInputs[4] as HTMLInputElement; const yearInput = customInputs[5] as HTMLInputElement; act(() => { fireEvent.change(dayInput, { target: { value: '15' } }); }); act(() => { fireEvent.change(monthInput, { target: { value: '2' } }); }); act(() => { fireEvent.change(yearInput, { target: { value: '2018' } }); }); expect(onChange).toHaveBeenCalled(); expect(onChange).toHaveBeenCalledWith([valueFrom, nextValueTo]); }); }); it('calls onClick callback when clicked a page (sample of mouse events family)', () => { const onClick = vi.fn(); const { container } = render(); const wrapper = container.firstElementChild as HTMLDivElement; fireEvent.click(wrapper); expect(onClick).toHaveBeenCalled(); }); it('calls onTouchStart callback when touched a page (sample of touch events family)', () => { const onTouchStart = vi.fn(); const { container } = render(); const wrapper = container.firstElementChild as HTMLDivElement; fireEvent.touchStart(wrapper); expect(onTouchStart).toHaveBeenCalled(); }); });