import * as React from 'react'; import { mount, ReactWrapper } from 'enzyme'; import sinon, { SinonFakeTimers } from 'sinon'; import Pikaday from 'pikaday'; import { MessageDescriptor } from 'react-intl'; import noop from 'lodash/noop'; import { TooltipPosition } from '../../tooltip'; import DatePicker, { DateFormat, DatePickerBase } from '../DatePicker'; const DATE_PICKER_DEFAULT_INPUT_CLASS_NAME = 'date-picker-input'; const DATE_PICKER_CUSTOM_INPUT_CLASS_NAME = 'date-picker-custom-input'; const customInput = ; let clock: SinonFakeTimers; jest.mock('pikaday'); describe('components/date-picker/DatePicker', () => { const getWrapper = (props = {}) => mount(); const getInputField = (wrapper: ReactWrapper) => wrapper .find('.date-picker-unix-time-input') .at(0) .getDOMNode() as HTMLInputElement; beforeEach(() => { clock = sinon.useFakeTimers(); }); afterEach(() => { jest.restoreAllMocks(); clock.restore(); }); test('should pass hideLabel to Label', () => { const wrapper = mount(); expect(wrapper.find('Label').prop('hideLabel')).toBe(true); }); test('should add resin target to datepicker input when specified', () => { const resinTarget = 'target'; const wrapper = mount( , ); expect(wrapper.find('.date-picker-input').prop('data-resin-target')).toEqual(resinTarget); }); test('should pass inputProps to datepicker input when provided', () => { const wrapper = mount( , ); expect(wrapper.find('.date-picker-input').prop('data-prop')).toEqual('hello'); }); test('should set hidden input to readOnly', () => { const wrapper = mount(); expect(wrapper.find('.date-picker-unix-time-input').prop('readOnly')).toBe(true); }); test('should set value in UTC time when UTC date format is specified', () => { const expectedOffset = new Date().getTimezoneOffset() * 60 * 1000 * -1; const wrapper = mount( , ); expect(getInputField(wrapper).value).toEqual(expectedOffset.toString()); }); test('should set value in utc iso format when utc iso date format is specified', () => { // utc time if clock is 0 const expectedOffset = new Date().getTimezoneOffset() * 60 * 1000 * -1; const date = new Date(expectedOffset); const wrapper = mount( , ); expect(getInputField(wrapper).value).toEqual(date.toISOString()); }); test('should hide optional label text when specified', () => { const wrapper = mount(); expect(wrapper.find('Label').prop('showOptionalText')).toBe(false); }); test('should set value if one is defined', () => { const wrapper = mount(); expect(getInputField(wrapper).value).toEqual('0'); }); test('should set value in iso format when iso date format is specified', () => { const date = new Date(1461953802469); const wrapper = mount( , ); expect(getInputField(wrapper).value).toEqual(date.toISOString()); }); test('should show clear button when formatted date exists', () => { const wrapper = mount(); expect(wrapper.find('PlainButton.date-picker-clear-btn').length).toEqual(1); expect(wrapper.find('IconClear').length).toEqual(1); }); test('should clear datepicker and call onChange() prop when clear button is clicked', () => { const onChangeSpy = jest.fn(); const wrapper = mount( , ); wrapper.find('PlainButton.date-picker-clear-btn').simulate('click', { preventDefault: noop }); expect(onChangeSpy).toHaveBeenCalledWith(null, ''); }); test('should hide clear button when disabled is passed in', () => { const wrapper = mount( , ); expect(wrapper.find('.date-picker-clear-btn').length).toEqual(0); }); test('should not have clear button when isClearable prop is false', () => { const wrapper = mount( , ); expect(wrapper.find('IconClear').length).toEqual(0); }); test('should show tooltip when error exists', () => { const wrapper = mount( , ); const tooltip = wrapper.find('Tooltip'); expect(tooltip.prop('text')).toEqual('error!'); expect(tooltip.prop('position')).toEqual('middle-right'); expect(tooltip.prop('isShown')).toBe(true); expect(tooltip.prop('className')).toEqual('date-picker-error-tooltip'); }); test('should fire the onChange prop on blur if isTextInputAllowed is true', () => { const mockOnChangeHandler = jest.fn(); const wrapper = getWrapper({ isTextInputAllowed: true, onChange: mockOnChangeHandler, value: new Date(), }); wrapper.find('.date-picker-input').simulate('blur'); expect(mockOnChangeHandler).toHaveBeenCalled(); }); // eslint-disable-next-line @typescript-eslint/no-explicit-any let intlFake: any; const renderDatePicker = (props = {}) => mount( , ); beforeEach(() => { intlFake = { formatMessage: (message: MessageDescriptor) => message.defaultMessage, formatDate: (date: string | number | Date | undefined) => (date ? date.toString() : ''), locale: 'en-US', }; }); describe('componentDidMount()', () => { test('should set first day of week to Monday if locale is not US, CA, or JP', () => { renderDatePicker(); expect(Pikaday).toBeCalledWith( expect.objectContaining({ firstDay: 0, }), ); intlFake.locale = 'en-UK'; renderDatePicker(); expect(Pikaday).toBeCalledWith( expect.objectContaining({ firstDay: 1, }), ); }); test.each` customInputProp | bound | description ${{ customInput }} | ${false} | ${'false if customInput is provided'} ${{}} | ${true} | ${'true if customInput is not provided'} `('should set bound to $description', ({ customInputProp, bound }) => { renderDatePicker(customInputProp); expect(Pikaday).toHaveBeenCalledWith(expect.objectContaining({ bound })); }); }); describe('onSelectHandler()', () => { test('should call onChange prop with formatted date param', () => { const formattedDate = 1234567; const onChangeStub = jest.fn(); const testDate = new Date(); const wrapper = renderDatePicker({ onChange: onChangeStub, value: testDate, }); const instance = wrapper.instance(); instance.formatValue = jest.fn().mockReturnValueOnce(formattedDate); instance.onSelectHandler(testDate); expect(onChangeStub).toHaveBeenCalledWith(testDate, formattedDate); }); }); describe('focusDatePicker()', () => { test('should call focus on DatePicker input when called', () => { const wrapper = renderDatePicker(); const instance = wrapper.instance(); const inputEl: HTMLInputElement = wrapper .find('input') .at(0) .getDOMNode(); inputEl.focus = jest.fn(); instance.focusDatePicker(); expect(inputEl.focus).toHaveBeenCalled(); }); }); describe('handleInputKeyDown()', () => { test('should stop propagation when datepicker is visible', () => { const wrapper = renderDatePicker(); const instance = wrapper.instance(); const inputEl = wrapper.find('input').at(0); const stopPropagationSpy = jest.fn(); if (instance.datePicker) { instance.datePicker.isVisible = jest.fn().mockReturnValue(true); } inputEl.simulate('keyDown', { preventDefault: noop, stopPropagation: stopPropagationSpy, key: 'anything', }); expect(stopPropagationSpy).toHaveBeenCalled(); }); test('should not stop propagation when datepicker is not visible', () => { const wrapper = renderDatePicker(); const instance = wrapper.instance(); const inputEl = wrapper.find('input').at(0); const stopPropagationSpy = jest.fn(); if (instance.datePicker) { instance.datePicker.isVisible = jest.fn().mockReturnValue(false); } inputEl.simulate('keyDown', { preventDefault: noop, stopPropagation: stopPropagationSpy, key: 'anything', }); expect(stopPropagationSpy).not.toHaveBeenCalled(); }); test('should prevent default on input when key pressed was not a Tab', () => { const wrapper = renderDatePicker(); const inputEl = wrapper.find('input').at(0); const preventDefaultSpy = jest.fn(); inputEl.simulate('keyDown', { preventDefault: preventDefaultSpy, stopPropagation: noop, key: 'ArrowDown', }); expect(preventDefaultSpy).toHaveBeenCalled(); }); test('should not prevent default on input when key pressed was a Tab', () => { const wrapper = renderDatePicker(); const inputEl = wrapper.find('input').at(0); const preventDefaultSpy = jest.fn(); inputEl.simulate('keyDown', { preventDefault: preventDefaultSpy, stopPropagation: noop, key: 'Tab', }); expect(preventDefaultSpy).not.toHaveBeenCalled(); }); test.each` key | keyName ${'Enter'} | ${'enter key'} ${'Escape'} | ${'escape key'} ${' '} | ${'spacebar'} `( 'should hide DatePicker when $keyName is pressed in the input field and the DatePicker is visible', ({ key }) => { const wrapper = renderDatePicker(); const instance = wrapper.instance(); const inputEl = wrapper.find('input').at(0); if (instance.datePicker) { instance.datePicker.isVisible = jest.fn().mockReturnValue(true); instance.datePicker.hide = jest.fn(); } inputEl.simulate('keyDown', { preventDefault: noop, stopPropagation: noop, key, }); expect(instance.datePicker && instance.datePicker.hide).toHaveBeenCalled(); }, ); test.each` key | keyName ${'Enter'} | ${'enter key'} ${'Escape'} | ${'escape key'} ${' '} | ${'spacebar'} `( 'should not hide DatePicker when %s is pressed in the input field and the DatePicker is not visible', ({ key }) => { const wrapper = renderDatePicker(); const instance = wrapper.instance(); const inputEl = wrapper.find('input').at(0); if (instance.datePicker) { instance.datePicker.isVisible = jest.fn().mockReturnValue(false); instance.datePicker.hide = jest.fn(); } inputEl.simulate('keyDown', { preventDefault: noop, stopPropagation: noop, key, }); expect(instance.datePicker && instance.datePicker.hide).not.toHaveBeenCalled(); }, ); }); describe('handleInputBlur()', () => { test('should call onBlur prop when called', () => { const onBlurSpy = jest.fn(); const wrapper = renderDatePicker({ onBlur: onBlurSpy }); const instance = wrapper.instance(); const inputEl = wrapper.find('input').at(0); if (instance.datePicker) { instance.datePicker.isVisible = jest.fn().mockReturnValue(false); } inputEl.simulate('blur'); expect(onBlurSpy).toHaveBeenCalled(); }); test.each` isVisible | target | shouldStayClosed | visibility ${true} | ${'the DatePicker button'} | ${true} | ${'visible'} ${false} | ${'the DatePicker button'} | ${false} | ${'not visible'} ${true} | ${null} | ${false} | ${'visible'} ${false} | ${null} | ${false} | ${'not visible'} `( 'should set shouldStayClosed when related/active target is $target and DatePicker is $visibility', ({ isVisible, target, shouldStayClosed }) => { const wrapper = renderDatePicker(); const instance = wrapper.instance(); const inputEl = wrapper.find('input').at(0); const targetEl = target ? wrapper.find('PlainButton.date-picker-open-btn').getDOMNode() : null; if (instance.datePicker) { instance.datePicker.isVisible = jest.fn().mockReturnValue(isVisible); } inputEl.simulate('blur', { relatedTarget: targetEl, }); expect(instance.shouldStayClosed).toEqual(shouldStayClosed); }, ); }); test('should reset shouldStayClosed after a brief delay', () => { const wrapper = renderDatePicker(); const instance = wrapper.instance(); const inputEl = wrapper.find('input').at(0); const datePickerButtonEl = wrapper.find('PlainButton.date-picker-open-btn').getDOMNode(); if (instance.datePicker) { instance.datePicker.isVisible = jest.fn().mockReturnValue(true); } inputEl.simulate('blur', { relatedTarget: datePickerButtonEl, }); expect(instance.shouldStayClosed).toBe(true); clock.tick(400); expect(instance.shouldStayClosed).toBe(false); }); describe('handleButtonClick()', () => { test('should call event.preventDefault when called', () => { const preventDefaultSpy = jest.fn(); const wrapper = renderDatePicker(); const datePickerButtonEl = wrapper.find('PlainButton.date-picker-open-btn'); datePickerButtonEl.simulate('click', { preventDefault: preventDefaultSpy, }); }); test('should not focus input when shouldStayClosed is true', () => { const wrapper = renderDatePicker(); const instance = wrapper.instance(); const datePickerButtonEl = wrapper.find('PlainButton.date-picker-open-btn'); instance.shouldStayClosed = true; instance.focusDatePicker = jest.fn(); datePickerButtonEl.simulate('click', { preventDefault: noop, }); expect(instance.focusDatePicker).not.toHaveBeenCalled(); }); test('should focus input when shouldStayClosed is false', () => { const wrapper = renderDatePicker(); const instance = wrapper.instance(); const datePickerButtonEl = wrapper.find('PlainButton.date-picker-open-btn'); instance.shouldStayClosed = false; instance.focusDatePicker = jest.fn(); datePickerButtonEl.simulate('click', { preventDefault: noop, }); expect(instance.focusDatePicker).toHaveBeenCalled(); }); }); describe('render()', () => { test.each` props | buttonExists | description ${{}} | ${true} | ${'should render the date-picker-open btn by default'} ${{ isAlwaysVisible: true }} | ${false} | ${'should not render the date-picker-open btn if isAlwaysVisible is true'} `('$description', ({ props, buttonExists }) => { const wrapper = renderDatePicker(props); const buttonEl = wrapper.find('PlainButton.date-picker-open-btn'); expect(buttonEl.exists()).toBe(buttonExists); }); test('should render a disabled date-picker-open btn when DatePicker is disabled', () => { const wrapper = renderDatePicker({ isDisabled: true, }); const buttonEl = wrapper.find('PlainButton.date-picker-open-btn'); expect(buttonEl.prop('isDisabled')).toBe(true); }); test.each` customInputProp | renderedClassName | absentClassName | isDisabled | isRequired | resinTarget | description ${undefined} | ${DATE_PICKER_DEFAULT_INPUT_CLASS_NAME} | ${DATE_PICKER_CUSTOM_INPUT_CLASS_NAME} | ${true} | ${true} | ${'target'} | ${'should render the default input field with provided props'} ${customInput} | ${DATE_PICKER_CUSTOM_INPUT_CLASS_NAME} | ${DATE_PICKER_DEFAULT_INPUT_CLASS_NAME} | ${true} | ${true} | ${'target'} | ${'should render the custom input field with provided props if provided'} `( '$description', ({ customInputProp, renderedClassName, absentClassName, isDisabled, isRequired, resinTarget }) => { const input = renderDatePicker({ customInput: customInputProp, isDisabled, isRequired, resinTarget }) .find('input') .at(0); expect(input.prop('className')).toBe(renderedClassName); expect(input.prop('className')).not.toBe(absentClassName); expect(input.prop('disabled')).toBe(isDisabled); expect(input.prop('required')).toBe(isRequired); expect(input.prop('aria-required')).toBe(isRequired); expect(input.prop('data-resin-target')).toEqual(resinTarget); }, ); }); describe('componentWillUnmount()', () => { test('should destroy the DatePicker widget', () => { const wrapper = renderDatePicker(); const instance = wrapper.instance(); if (instance.datePicker) { instance.datePicker.destroy = jest.fn(); } wrapper.unmount(); expect(instance.datePicker && instance.datePicker.destroy).toHaveBeenCalled(); }); }); describe('UNSAFE_componentWillReceiveProps()', () => { test('should call setDate() when value changes', () => { const wrapper = renderDatePicker(); const nextValue = new Date(123); const instance = wrapper.instance(); if (instance.datePicker) { instance.datePicker.setDate = jest.fn(); } wrapper.setProps({ value: nextValue, }); expect(instance.datePicker && instance.datePicker.setDate).toHaveBeenCalledWith(nextValue); }); test('should call setMinDate() when minDate prop is set after being null', () => { const currentDate = new Date(500); const wrapper = renderDatePicker({ value: currentDate, }); const instance = wrapper.instance(); if (instance.datePicker) { instance.datePicker.getDate = jest.fn().mockReturnValue(currentDate); instance.datePicker.gotoDate = jest.fn(); instance.datePicker.setMinDate = jest.fn(); } const minDate = new Date(100); wrapper.setProps({ minDate, }); expect(instance.datePicker && instance.datePicker.setMinDate).toHaveBeenCalledWith(minDate); expect(instance.datePicker && instance.datePicker.gotoDate).not.toHaveBeenCalled(); // Current date is way ahead of timestamp 100 }); test('should call setMinDate() when minDate prop changes value', () => { const currentDate = new Date(500); const wrapper = renderDatePicker({ value: currentDate, minDate: new Date(200), }); const instance = wrapper.instance(); if (instance.datePicker) { instance.datePicker.setMinDate = jest.fn(); } const minDate = new Date(100); wrapper.setProps({ minDate, }); expect(instance.datePicker && instance.datePicker.setMinDate).toHaveBeenCalledWith(minDate); }); test('should call gotoDate() when minDate prop passed is further in the future than current date', () => { const currentDate = new Date(100); const wrapper = renderDatePicker({ value: currentDate, }); const instance = wrapper.instance(); if (instance.datePicker) { instance.datePicker.getDate = jest.fn().mockReturnValue(currentDate); instance.datePicker.gotoDate = jest.fn(); } const minDate = new Date(500); wrapper.setProps({ minDate, }); expect(instance.datePicker && instance.datePicker.gotoDate).toHaveBeenCalledWith(minDate); }); test('should call setMaxDate() when maxDate prop after being null', () => { const currentDate = new Date(500); const wrapper = renderDatePicker({ value: currentDate, }); const instance = wrapper.instance(); if (instance.datePicker) { instance.datePicker.getDate = jest.fn().mockReturnValue(currentDate); instance.datePicker.gotoDate = jest.fn(); instance.datePicker.setMaxDate = jest.fn(); } const maxDate = new Date(1000000); wrapper.setProps({ maxDate, }); expect(instance.datePicker && instance.datePicker.setMaxDate).toHaveBeenCalledWith(maxDate); expect(instance.datePicker && instance.datePicker.gotoDate).not.toHaveBeenCalled(); // Current date is way behind of timestamp 1000000 }); test('should call setMaxDate() when maxDate prop changes value', () => { const currentDate = new Date(500); const wrapper = renderDatePicker({ value: currentDate, maxDate: new Date(200000), }); const instance = wrapper.instance(); if (instance.datePicker) { instance.datePicker.getDate = jest.fn().mockReturnValue(currentDate); instance.datePicker.setMaxDate = jest.fn(); } const maxDate = new Date(1000000); wrapper.setProps({ maxDate, }); expect(instance.datePicker && instance.datePicker.setMaxDate).toHaveBeenCalledWith(maxDate); }); test('should call gotoDate() when maxDate prop passed is behind the current date', () => { const currentDate = new Date(1000000); const wrapper = renderDatePicker({ value: currentDate, }); const instance = wrapper.instance(); if (instance.datePicker) { instance.datePicker.getDate = jest.fn().mockReturnValue(currentDate); instance.datePicker.gotoDate = jest.fn(); } const maxDate = new Date(500); wrapper.setProps({ maxDate, }); expect(instance.datePicker && instance.datePicker.gotoDate).toHaveBeenCalledWith(maxDate); }); }); });