@@ -24,28 +24,28 @@ import { DateInput } from './DateInput.js';
2424describe ( 'DateInput' , ( ) => {
2525 const baseProps = {
2626 onChange : vi . fn ( ) ,
27- label : 'Date' ,
28- prevMonthButtonLabel : 'Previous month' ,
29- nextMonthButtonLabel : 'Previous month' ,
30- openCalendarButtonLabel : 'Change date' ,
31- closeCalendarButtonLabel : 'Close' ,
32- applyDateButtonLabel : 'Apply' ,
33- clearDateButtonLabel : 'Clear' ,
27+ label : 'Date of birth' ,
3428 yearInputLabel : 'Year' ,
3529 monthInputLabel : 'Month' ,
3630 dayInputLabel : 'Day' ,
31+ openCalendarButtonLabel : 'Change date' ,
32+ closeCalendarButtonLabel : 'Close calendar' ,
33+ prevMonthButtonLabel : 'Previous month' ,
34+ nextMonthButtonLabel : 'Previous month' ,
35+ applyDateButtonLabel : 'Apply date' ,
36+ clearDateButtonLabel : 'Clear date' ,
3737 } ;
3838
3939 beforeAll ( ( ) => {
4040 MockDate . set ( '2000-01-01' ) ;
4141 } ) ;
4242
43- // TODO: Move ref to outermost div?
4443 it ( 'should forward a ref' , ( ) => {
45- const ref = createRef < HTMLFieldSetElement > ( ) ;
46- render ( < DateInput { ...baseProps } ref = { ref } /> ) ;
47- const fieldset = screen . getByRole ( 'group' ) ;
48- expect ( ref . current ) . toBe ( fieldset ) ;
44+ const ref = createRef < HTMLDivElement > ( ) ;
45+ const { container } = render ( < DateInput { ...baseProps } ref = { ref } /> ) ;
46+ // eslint-disable-next-line testing-library/no-container
47+ const wrapper = container . querySelectorAll ( 'div' ) [ 0 ] ;
48+ expect ( ref . current ) . toBe ( wrapper ) ;
4949 } ) ;
5050
5151 it ( 'should merge a custom class name with the default ones' , ( ) => {
@@ -58,93 +58,239 @@ describe('DateInput', () => {
5858 expect ( wrapper ?. className ) . toContain ( className ) ;
5959 } ) ;
6060
61- it ( 'should select a calendar date' , async ( ) => {
62- const onChange = vi . fn ( ) ;
61+ describe ( 'semantics' , ( ) => {
62+ it ( 'should optionally have an accessible description' , ( ) => {
63+ const description = 'Description' ;
64+ render ( < DateInput { ...baseProps } validationHint = { description } /> ) ;
65+ const fieldset = screen . getByRole ( 'group' ) ;
66+ const inputs = screen . getAllByRole ( 'spinbutton' ) ;
67+
68+ expect ( fieldset ) . toHaveAccessibleDescription ( description ) ;
69+ expect ( inputs [ 0 ] ) . toHaveAccessibleDescription ( description ) ;
70+ expect ( inputs [ 1 ] ) . not . toHaveAccessibleDescription ( ) ;
71+ expect ( inputs [ 2 ] ) . not . toHaveAccessibleDescription ( ) ;
72+ } ) ;
6373
64- render ( < DateInput { ...baseProps } onChange = { onChange } /> ) ;
74+ it ( 'should accept a custom description via aria-describedby' , ( ) => {
75+ const customDescription = 'Custom description' ;
76+ const customDescriptionId = 'customDescriptionId' ;
77+ render (
78+ < >
79+ < DateInput { ...baseProps } aria-describedby = { customDescriptionId } /> ,
80+ < span id = { customDescriptionId } > { customDescription } </ span >
81+ </ > ,
82+ ) ;
83+ const fieldset = screen . getByRole ( 'group' ) ;
84+ const inputs = screen . getAllByRole ( 'spinbutton' ) ;
6585
66- const openCalendarButton = screen . getByRole ( 'button' , {
67- name : / c h a n g e d a t e / i,
86+ expect ( fieldset ) . toHaveAccessibleDescription ( customDescription ) ;
87+ expect ( inputs [ 0 ] ) . toHaveAccessibleDescription ( customDescription ) ;
88+ expect ( inputs [ 1 ] ) . not . toHaveAccessibleDescription ( ) ;
89+ expect ( inputs [ 2 ] ) . not . toHaveAccessibleDescription ( ) ;
6890 } ) ;
6991
70- await userEvent . click ( openCalendarButton ) ;
92+ it ( 'should accept a custom description in addition to a validationHint' , ( ) => {
93+ const customDescription = 'Custom description' ;
94+ const customDescriptionId = 'customDescriptionId' ;
95+ const description = 'Description' ;
96+ render (
97+ < >
98+ < DateInput
99+ { ...baseProps }
100+ validationHint = { description }
101+ aria-describedby = { customDescriptionId }
102+ />
103+ < span id = { customDescriptionId } > { customDescription } </ span > ,
104+ </ > ,
105+ ) ;
106+ const fieldset = screen . getByRole ( 'group' ) ;
107+ const inputs = screen . getAllByRole ( 'spinbutton' ) ;
71108
72- const calendarDialog = screen . getByRole ( 'dialog' ) ;
109+ expect ( fieldset ) . toHaveAccessibleDescription (
110+ `${ customDescription } ${ description } ` ,
111+ ) ;
112+ expect ( inputs [ 0 ] ) . toHaveAccessibleDescription (
113+ `${ customDescription } ${ description } ` ,
114+ ) ;
115+ expect ( inputs [ 1 ] ) . not . toHaveAccessibleDescription ( ) ;
116+ expect ( inputs [ 2 ] ) . not . toHaveAccessibleDescription ( ) ;
117+ } ) ;
73118
74- expect ( calendarDialog ) . toBeVisible ( ) ;
119+ it ( 'should render as disabled' , async ( ) => {
120+ render ( < DateInput { ...baseProps } disabled /> ) ;
121+ expect ( screen . getByLabelText ( / d a y / i) ) . toBeDisabled ( ) ;
122+ expect ( screen . getByLabelText ( / m o n t h / i) ) . toBeDisabled ( ) ;
123+ expect ( screen . getByLabelText ( / y e a r / i) ) . toBeDisabled ( ) ;
124+ expect (
125+ screen . getByRole ( 'button' , { name : baseProps . openCalendarButtonLabel } ) ,
126+ ) . toHaveAttribute ( 'aria-disabled' , 'true' ) ;
127+ } ) ;
75128
76- const dateButton = screen . getByRole ( 'button' , { name : / 1 2 / } ) ;
129+ it ( 'should render as read-only' , async ( ) => {
130+ render ( < DateInput { ...baseProps } readOnly /> ) ;
131+ expect ( screen . getByLabelText ( / d a y / i) ) . toHaveAttribute ( 'readonly' ) ;
132+ expect ( screen . getByLabelText ( / m o n t h / i) ) . toHaveAttribute ( 'readonly' ) ;
133+ expect ( screen . getByLabelText ( / y e a r / i) ) . toHaveAttribute ( 'readonly' ) ;
134+ expect (
135+ screen . getByRole ( 'button' , { name : baseProps . openCalendarButtonLabel } ) ,
136+ ) . toHaveAttribute ( 'aria-disabled' , 'true' ) ;
137+ } ) ;
77138
78- await userEvent . click ( dateButton ) ;
139+ it ( 'should render as invalid' , async ( ) => {
140+ render ( < DateInput { ...baseProps } invalid /> ) ;
141+ expect ( screen . getByLabelText ( / d a y / i) ) . toBeInvalid ( ) ;
142+ expect ( screen . getByLabelText ( / m o n t h / i) ) . toBeInvalid ( ) ;
143+ expect ( screen . getByLabelText ( / y e a r / i) ) . toBeInvalid ( ) ;
144+ } ) ;
79145
80- expect ( onChange ) . toHaveBeenCalledWith ( '2000-01-12' ) ;
81- } ) ;
146+ it ( 'should render as required' , async ( ) => {
147+ render ( < DateInput { ...baseProps } required /> ) ;
148+ expect ( screen . getByLabelText ( / d a y / i) ) . toBeRequired ( ) ;
149+ expect ( screen . getByLabelText ( / m o n t h / i) ) . toBeRequired ( ) ;
150+ expect ( screen . getByLabelText ( / y e a r / i) ) . toBeRequired ( ) ;
151+ } ) ;
82152
83- it ( 'should display the initial value correctly' , ( ) => {
84- render ( < DateInput { ...baseProps } value = "2000-01-12" /> ) ;
153+ it ( 'should have relevant minimum input values' , ( ) => {
154+ render ( < DateInput { ...baseProps } min = "2000-01-01" /> ) ;
155+ expect ( screen . getByLabelText ( / d a y / i) ) . toHaveAttribute ( 'min' , '1' ) ;
156+ expect ( screen . getByLabelText ( / m o n t h / i) ) . toHaveAttribute ( 'min' , '1' ) ;
157+ expect ( screen . getByLabelText ( / y e a r / i) ) . toHaveAttribute ( 'min' , '2000' ) ;
158+ } ) ;
85159
86- expect ( screen . getByLabelText ( / d a y / i) ) . toHaveValue ( 12 ) ;
87- expect ( screen . getByLabelText ( / m o n t h / i) ) . toHaveValue ( 1 ) ;
88- expect ( screen . getByLabelText ( / y e a r / i) ) . toHaveValue ( 2000 ) ;
89- } ) ;
160+ it ( 'should have relevant maximum input values' , ( ) => {
161+ render ( < DateInput { ...baseProps } max = "2001-01-01" /> ) ;
162+ expect ( screen . getByLabelText ( / d a y / i) ) . toHaveAttribute ( 'max' , '31' ) ;
163+ expect ( screen . getByLabelText ( / m o n t h / i) ) . toHaveAttribute ( 'max' , '12' ) ;
164+ expect ( screen . getByLabelText ( / y e a r / i) ) . toHaveAttribute ( 'max' , '2001' ) ;
165+ } ) ;
90166
91- it ( 'should render a disabled input' , ( ) => {
92- render ( < DateInput { ...baseProps } disabled /> ) ;
93- expect ( screen . getByLabelText ( / d a y / i) ) . toBeDisabled ( ) ;
94- expect ( screen . getByLabelText ( / m o n t h / i) ) . toBeDisabled ( ) ;
95- expect ( screen . getByLabelText ( / y e a r / i) ) . toBeDisabled ( ) ;
96- expect (
97- screen . getByRole ( 'button' , { name : baseProps . openCalendarButtonLabel } ) ,
98- ) . toHaveAttribute ( 'aria-disabled' , 'true' ) ;
99- } ) ;
167+ it ( 'should mark the year input as readonly when the minimum and maximum dates have the same year' , ( ) => {
168+ render ( < DateInput { ...baseProps } min = "2000-04-29" max = "2000-06-15" /> ) ;
169+ expect ( screen . getByLabelText ( / y e a r / i) ) . toHaveAttribute ( 'readonly' ) ;
170+ expect ( screen . getByLabelText ( / m o n t h / i) ) . toHaveAttribute ( 'min' , '4' ) ;
171+ expect ( screen . getByLabelText ( / m o n t h / i) ) . toHaveAttribute ( 'max' , '6' ) ;
172+ } ) ;
100173
101- it ( 'should handle min/max dates' , ( ) => {
102- render ( < DateInput { ...baseProps } min = "2000-01-01" max = "2001-01-01" /> ) ;
103- expect ( screen . getByLabelText ( / d a y / i) ) . toHaveAttribute ( 'min' , '1' ) ;
104- expect ( screen . getByLabelText ( / d a y / i) ) . toHaveAttribute ( 'max' , '31' ) ;
105- expect ( screen . getByLabelText ( / m o n t h / i) ) . toHaveAttribute ( 'min' , '1' ) ;
106- expect ( screen . getByLabelText ( / m o n t h / i) ) . toHaveAttribute ( 'max' , '12' ) ;
107- expect ( screen . getByLabelText ( / y e a r / i) ) . toHaveAttribute ( 'min' , '2000' ) ;
108- expect ( screen . getByLabelText ( / y e a r / i) ) . toHaveAttribute ( 'max' , '2001' ) ;
174+ it ( 'should mark the year and month inputs as readonly when the minimum and maximum dates have the same year and month' , ( ) => {
175+ render ( < DateInput { ...baseProps } min = "2000-04-09" max = "2000-04-27" /> ) ;
176+ expect ( screen . getByLabelText ( / y e a r / i) ) . toHaveAttribute ( 'readonly' ) ;
177+ expect ( screen . getByLabelText ( / m o n t h / i) ) . toHaveAttribute ( 'readonly' ) ;
178+ expect ( screen . getByLabelText ( / d a y / i) ) . toHaveAttribute ( 'min' , '9' ) ;
179+ expect ( screen . getByLabelText ( / d a y / i) ) . toHaveAttribute ( 'max' , '27' ) ;
180+ } ) ;
109181 } ) ;
110182
111- it ( 'should handle min/max dates as the user types year' , async ( ) => {
112- render ( < DateInput { ...baseProps } min = "2000-04-29" max = "2001-02-15" /> ) ;
183+ describe ( 'state' , ( ) => {
184+ it ( 'should display a default value' , ( ) => {
185+ render ( < DateInput { ...baseProps } defaultValue = "2000-01-12" /> ) ;
113186
114- await userEvent . type ( screen . getByLabelText ( / y e a r / i) , '2001' ) ;
115- /* expect(screen.getByLabelText(/day/i)).toHaveAttribute('min', '1');
116- expect(screen.getByLabelText(/day/i)).toHaveAttribute('max', '1'); */
117- expect ( screen . getByLabelText ( / m o n t h / i) ) . toHaveAttribute ( 'min' , '1' ) ;
118- expect ( screen . getByLabelText ( / m o n t h / i) ) . toHaveAttribute ( 'max' , '2' ) ;
119- } ) ;
187+ expect ( screen . getByLabelText ( / d a y / i) ) . toHaveValue ( 12 ) ;
188+ expect ( screen . getByLabelText ( / m o n t h / i) ) . toHaveValue ( 1 ) ;
189+ expect ( screen . getByLabelText ( / y e a r / i) ) . toHaveValue ( 2000 ) ;
190+ } ) ;
120191
121- it ( 'should handle min/max dates as the user types year and month' , async ( ) => {
122- render ( < DateInput { ...baseProps } min = "2000-04-29" max = "2001-02-15 " /> ) ;
192+ it ( 'should display an initial value' , ( ) => {
193+ render ( < DateInput { ...baseProps } value = "2000-01-12 " /> ) ;
123194
124- await userEvent . type ( screen . getByLabelText ( / y e a r / i) , '2001' ) ;
125- await userEvent . type ( screen . getByLabelText ( / m o n t h / i) , '02' ) ;
195+ expect ( screen . getByLabelText ( / d a y / i) ) . toHaveValue ( 12 ) ;
196+ expect ( screen . getByLabelText ( / m o n t h / i) ) . toHaveValue ( 1 ) ;
197+ expect ( screen . getByLabelText ( / y e a r / i) ) . toHaveValue ( 2000 ) ;
198+ } ) ;
126199
127- expect ( screen . getByLabelText ( / d a y / i) ) . toHaveAttribute ( 'min' , '1' ) ;
128- expect ( screen . getByLabelText ( / d a y / i) ) . toHaveAttribute ( 'max' , '15' ) ;
129- } ) ;
200+ it ( 'should update the displayed value' , ( ) => {
201+ const { rerender } = render (
202+ < DateInput { ...baseProps } value = "2000-01-12" /> ,
203+ ) ;
130204
131- it ( 'years field should be readonly if min/max dates have the same year' , ( ) => {
132- render ( < DateInput { ...baseProps } min = "2000-04-29" max = "2000-06-15" /> ) ;
133- expect ( screen . getByLabelText ( / y e a r / i) ) . toHaveAttribute ( 'readonly' ) ;
134- expect ( screen . getByLabelText ( / m o n t h / i) ) . toHaveAttribute ( 'min' , '4' ) ;
135- expect ( screen . getByLabelText ( / m o n t h / i) ) . toHaveAttribute ( 'max' , '6' ) ;
205+ rerender ( < DateInput { ...baseProps } value = "2000-01-15" /> ) ;
206+
207+ expect ( screen . getByLabelText ( / d a y / i) ) . toHaveValue ( 15 ) ;
208+ expect ( screen . getByLabelText ( / m o n t h / i) ) . toHaveValue ( 1 ) ;
209+ expect ( screen . getByLabelText ( / y e a r / i) ) . toHaveValue ( 2000 ) ;
210+ } ) ;
136211 } ) ;
137212
138- it ( 'years and months fields should render as readonly if min/max dates have the same year and same month' , ( ) => {
139- render ( < DateInput { ...baseProps } min = "2000-04-09" max = "2000-04-27" /> ) ;
140- expect ( screen . getByLabelText ( / y e a r / i) ) . toHaveAttribute ( 'readonly' ) ;
141- expect ( screen . getByLabelText ( / m o n t h / i) ) . toHaveAttribute ( 'readonly' ) ;
213+ describe ( 'user interactions' , ( ) => {
214+ it ( 'should focus the first input when clicking the label' , async ( ) => {
215+ render ( < DateInput { ...baseProps } /> ) ;
216+
217+ await userEvent . click ( screen . getByText ( 'Date of birth' ) ) ;
218+
219+ expect ( screen . getAllByRole ( 'spinbutton' ) [ 0 ] ) . toHaveFocus ( ) ;
220+ } ) ;
221+
222+ it ( 'should allow users to type a date' , async ( ) => {
223+ const onChange = vi . fn ( ) ;
224+
225+ render ( < DateInput { ...baseProps } onChange = { onChange } /> ) ;
226+
227+ await userEvent . type ( screen . getByLabelText ( 'Year' ) , '2017' ) ;
228+ await userEvent . type ( screen . getByLabelText ( 'Month' ) , '8' ) ;
229+ await userEvent . type ( screen . getByLabelText ( 'Day' ) , '28' ) ;
230+
231+ expect ( onChange ) . toHaveBeenCalledWith ( '2017-08-28' ) ;
232+ } ) ;
233+
234+ it ( 'should update the minimum and maximum input values as the user types' , async ( ) => {
235+ render ( < DateInput { ...baseProps } min = "2000-04-29" max = "2001-02-15" /> ) ;
236+
237+ await userEvent . type ( screen . getByLabelText ( / y e a r / i) , '2001' ) ;
238+
239+ expect ( screen . getByLabelText ( / m o n t h / i) ) . toHaveAttribute ( 'min' , '1' ) ;
240+ expect ( screen . getByLabelText ( / m o n t h / i) ) . toHaveAttribute ( 'max' , '2' ) ;
241+
242+ await userEvent . type ( screen . getByLabelText ( / m o n t h / i) , '2' ) ;
243+
244+ expect ( screen . getByLabelText ( / d a y / i) ) . toHaveAttribute ( 'min' , '1' ) ;
245+ expect ( screen . getByLabelText ( / d a y / i) ) . toHaveAttribute ( 'max' , '15' ) ;
246+ } ) ;
247+
248+ it ( 'should allow users to select a date on a calendar' , async ( ) => {
249+ const onChange = vi . fn ( ) ;
250+
251+ render ( < DateInput { ...baseProps } onChange = { onChange } /> ) ;
252+
253+ const openCalendarButton = screen . getByRole ( 'button' , {
254+ name : / c h a n g e d a t e / i,
255+ } ) ;
256+ await userEvent . click ( openCalendarButton ) ;
257+
258+ const calendarDialog = screen . getByRole ( 'dialog' ) ;
259+ expect ( calendarDialog ) . toBeVisible ( ) ;
260+
261+ const dateButton = screen . getByRole ( 'button' , { name : / 1 2 / } ) ;
262+ await userEvent . click ( dateButton ) ;
263+
264+ expect ( onChange ) . toHaveBeenCalledWith ( '2000-01-12' ) ;
265+ } ) ;
142266
143- expect ( screen . getByLabelText ( / d a y / i) ) . toHaveAttribute ( 'min' , '9' ) ;
144- expect ( screen . getByLabelText ( / d a y / i) ) . toHaveAttribute ( 'max' , '27' ) ;
267+ it ( 'should allow users to clear the date' , async ( ) => {
268+ const onChange = vi . fn ( ) ;
269+
270+ render (
271+ < DateInput
272+ { ...baseProps }
273+ defaultValue = "2000-01-12"
274+ onChange = { onChange }
275+ /> ,
276+ ) ;
277+
278+ const openCalendarButton = screen . getByRole ( 'button' , {
279+ name : / c h a n g e d a t e / i,
280+ } ) ;
281+ await userEvent . click ( openCalendarButton ) ;
282+
283+ const calendarDialog = screen . getByRole ( 'dialog' ) ;
284+ expect ( calendarDialog ) . toBeVisible ( ) ;
285+
286+ const clearButton = screen . getByRole ( 'button' , { name : / c l e a r d a t e / i } ) ;
287+ await userEvent . click ( clearButton ) ;
288+
289+ expect ( onChange ) . toHaveBeenCalledWith ( '' ) ;
290+ } ) ;
145291 } ) ;
146292
147- describe ( 'Status messages' , ( ) => {
293+ describe ( 'status messages' , ( ) => {
148294 it ( 'should render an empty live region on mount' , ( ) => {
149295 render ( < DateInput { ...baseProps } /> ) ;
150296 const liveRegionEl = screen . getByRole ( 'status' ) ;
0 commit comments