diff --git a/components/calendar/src/calendar-input/__tests__/calendar-input.test.js b/components/calendar/src/calendar-input/__tests__/calendar-input.test.js index 75be8ec2fe..d0507ae741 100644 --- a/components/calendar/src/calendar-input/__tests__/calendar-input.test.js +++ b/components/calendar/src/calendar-input/__tests__/calendar-input.test.js @@ -1,8 +1,17 @@ +import { Button } from '@dhis2-ui/button' import { fireEvent, render, waitFor, within } from '@testing-library/react' -import React from 'react' +import userEvent from '@testing-library/user-event' +import React, { useState } from 'react' +import { Field, Form } from 'react-final-form' import { CalendarInput } from '../calendar-input.js' describe('Calendar Input', () => { + beforeEach(() => { + jest.useFakeTimers() + jest.setSystemTime(new Date('2024-10-22T09:05:00.000Z')) + }) + afterEach(jest.useRealTimers) + it('allow selection of a date through the calendar widget', async () => { const onDateSelectMock = jest.fn() const screen = render( @@ -16,7 +25,7 @@ describe('Calendar Input', () => { const calendar = await screen.findByTestId('calendar') expect(calendar).toBeInTheDocument() - const todayString = new Date().toISOString().slice(0, -14) + const todayString = '2024-10-22' const today = within(calendar).getByTestId(todayString) fireEvent.click(today) @@ -51,4 +60,284 @@ describe('Calendar Input', () => { }) ) }) + + describe('validation', () => { + it('should validate minimum date', async () => { + const onDateSelectMock = jest.fn() + const screen = render( + + ) + + const dateInputString = '2023-10-12' + const dateInput = within( + screen.getByTestId('dhis2-uicore-input') + ).getByRole('textbox') + + userEvent.clear(dateInput) + userEvent.type(dateInput, dateInputString) + userEvent.tab() + + expect( + await screen.findByText( + 'Date 2023-10-12 is less than the minimum allowed date 2024-01-01.' + ) + ) + expect(onDateSelectMock).toHaveBeenCalledTimes(1) + }) + it('should validate maximum date', async () => { + const { getByTestId, findByText } = render( + + ) + + const dateInputString = '2024-10-12' + const dateInput = within( + getByTestId('dhis2-uicore-input') + ).getByRole('textbox') + + userEvent.clear(dateInput) + userEvent.type(dateInput, dateInputString) + userEvent.tab() + + expect( + await findByText( + 'Date 2024-10-12 is greater than the maximum allowed date 2024-01-01.' + ) + ) + }) + it('should validate date in ethiopic calendar', async () => { + const onDateSelectMock = jest.fn() + const { getByTestId, findByText, queryByText } = render( + + ) + + let dateInputString = '2018-13-02' + const dateInput = within( + getByTestId('dhis2-uicore-input') + ).getByRole('textbox') + + userEvent.clear(dateInput) + userEvent.type(dateInput, dateInputString) + userEvent.tab() + + expect( + await findByText( + 'Date 2018-13-02 is less than the minimum allowed date 2018-13-04.' + ) + ) + + dateInputString = '2018-13-05' + userEvent.clear(dateInput) + userEvent.type(dateInput, dateInputString) + userEvent.tab() + + expect( + queryByText( + 'Date 2018-13-04 is less than the minimum allowed date 2018-13-05.' + ) + ).not.toBeInTheDocument() + + dateInputString = '2018-13-07' + userEvent.clear(dateInput) + userEvent.type(dateInput, dateInputString) + userEvent.tab() + + expect( + await findByText('Invalid date in specified calendar') + ).toBeInTheDocument() + }) + it('should validate date in nepali calendar', async () => { + const onDateSelectMock = jest.fn() + const { getByTestId, findByText, queryByText } = render( + + ) + + let dateInputString = '2080-06-01' + const dateInput = within( + getByTestId('dhis2-uicore-input') + ).getByRole('textbox') + + userEvent.clear(dateInput) + userEvent.type(dateInput, dateInputString) + userEvent.tab() + + expect( + await findByText( + 'Date 2080-06-01 is greater than the maximum allowed date 2080-05-30.' + ) + ) + + dateInputString = '2080-04-32' + userEvent.clear(dateInput) + userEvent.type(dateInput, dateInputString) + userEvent.tab() + + expect( + queryByText(/greater than the maximum allowed date/) + ).not.toBeInTheDocument() + + dateInputString = '2080-01-32' + userEvent.clear(dateInput) + userEvent.type(dateInput, dateInputString) + userEvent.tab() + + expect( + await findByText('Invalid date in specified calendar') + ).toBeInTheDocument() + }) + it('should validate from date picker', async () => { + const onDateSelectMock = jest.fn() + const { queryByText, getByText, getByTestId } = render( + + ) + + const dateInput = within( + getByTestId('dhis2-uicore-input') + ).getByRole('textbox') + + await userEvent.click(dateInput) + await userEvent.click(getByText('17')) + expect(queryByText('17')).not.toBeInTheDocument() + + // Bug where callback used to be called first with undefined + expect(onDateSelectMock).toHaveBeenCalledTimes(1) + expect(onDateSelectMock).toHaveBeenCalledWith({ + calendarDateString: '2024-10-17', + validation: { error: false, valid: true, warning: false }, + }) + }) + + it('should validate with Clear', async () => { + const onDateSelectMock = jest.fn() + const { queryByText, getByText, getByTestId } = render( + + ) + + const dateInputString = '2023-10-12' + const dateInput = within( + getByTestId('dhis2-uicore-input') + ).getByRole('textbox') + + userEvent.clear(dateInput) + userEvent.type(dateInput, dateInputString) + userEvent.tab() + + expect( + getByTestId('dhis2-uiwidgets-calendar-inputfield-validation') + ).toBeInTheDocument() + + await userEvent.click(getByText('Clear')) + expect(queryByText('17')).not.toBeInTheDocument() + + expect(onDateSelectMock).toHaveBeenLastCalledWith({ + calendarDateString: null, + validation: { valid: true }, + }) + }) + + it('should validate when Clearing manually (i.e. deleting text not using clear button)', async () => { + const onDateSelectMock = jest.fn() + const { getByTestId } = render( + + ) + + const dateInputString = '2023-10-12' + const dateInput = within( + getByTestId('dhis2-uicore-input') + ).getByRole('textbox') + + userEvent.clear(dateInput) + userEvent.type(dateInput, dateInputString) + userEvent.tab() + + expect( + getByTestId('dhis2-uiwidgets-calendar-inputfield-validation') + ).toBeInTheDocument() + + userEvent.clear(dateInput) + userEvent.tab() + + expect(onDateSelectMock).toHaveBeenCalledWith({ + calendarDateString: null, + validation: { valid: true }, + }) + }) + }) }) + +const CalendarWithValidation = (propsFromParent) => { + const [date, setDate] = useState() + + const [validation, setValidation] = useState({}) + + const errored = () => { + if (validation?.error) { + return { calendar: validation.validationText } + } + } + + return ( +
{}} validate={errored}> + {({ handleSubmit, invalid }) => { + return ( + + + {(props) => ( + { + setDate(date?.calendarDateString) + setValidation(date?.validation) + propsFromParent.onDateSelect?.(date) + }} + /> + )} + + + +
+ ) + }} + + ) +} diff --git a/components/calendar/src/calendar-input/calendar-input.js b/components/calendar/src/calendar-input/calendar-input.js index 4aebe9865e..c8f17c3cb2 100644 --- a/components/calendar/src/calendar-input/calendar-input.js +++ b/components/calendar/src/calendar-input/calendar-input.js @@ -57,27 +57,47 @@ export const CalendarInput = ({ [calendar, locale, numberingSystem, weekDayFormat] ) - const pickerResults = useDatePicker({ - onDateSelect: (result) => { - const validation = validateDateString(result.calendarDateString, { - calendar, - format, - minDateString: minDate, - maxDateString: maxDate, - strictValidation, + const onChooseDate = (date, validationOptions) => { + // Handling clearing (with clicking the Clear button, or deleting input) + if (clearable && (date === null || date === '')) { + parentOnDateSelect?.({ + calendarDateString: null, + validation: { valid: true }, }) + return + } + // ToDo: This is now a workaround for handling choosing from the date picker + // where the blur event gets triggered causing a call with undefined first + if (date === undefined) { + return + } + + const validation = validateDateString(date, validationOptions) + parentOnDateSelect?.({ + calendarDateString: date, + validation, + }) + } + + const validationOptions = useMemo( + () => ({ + calendar, + format, + minDateString: minDate, + maxDateString: maxDate, + strictValidation, + }), + [calendar, format, maxDate, minDate, strictValidation] + ) + + const pickerResults = useDatePicker({ + onDateSelect: (result) => { + onChooseDate(result.calendarDateString, validationOptions) setOpen(false) - parentOnDateSelect?.({ - calendarDateString: result.calendarDateString, - validation, - }) }, date, - minDate: minDate, - maxDate: maxDate, - strictValidation: strictValidation, - format: format, + ...validationOptions, options: useDatePickerOptions, }) @@ -87,15 +107,7 @@ export const CalendarInput = ({ } const handleBlur = (_, e) => { - const validation = validateDateString(partialDate, { - calendar, - format, - minDateString: minDate, - maxDateString: maxDate, - strictValidation, - }) - parentOnDateSelect?.({ calendarDateString: partialDate, validation }) - + onChooseDate(partialDate, validationOptions) if ( excludeRef.current && !excludeRef.current.contains(e.relatedTarget) @@ -158,7 +170,7 @@ export const CalendarInput = ({ secondary small onClick={() => { - parentOnDateSelect?.(null) + onChooseDate(null) }} type="button" >