@@ -21,6 +21,9 @@ const createField = (...params: Parameters<typeof useFormControl>) => {
2121 { ...props }
2222 />
2323 < button onClick = { ( ) => field . setError ( 'some error' ) } > set error</ button >
24+ < button onClick = { ( ) => field . setSuccess ( 'some success' ) } > set success</ button >
25+ < button onClick = { ( ) => field . setWarning ( 'some warning' ) } > set warning</ button >
26+ < button onClick = { ( ) => field . setInfo ( 'some info' ) } > set info</ button >
2427 </ >
2528 ) ;
2629 } ) ;
@@ -132,15 +135,20 @@ describe('PlainInput', () => {
132135 placeholder : 'some placeholder' ,
133136 } ) ;
134137
135- const { getByRole, getByLabelText, findByText } = render ( < Field /> , { wrapper } ) ;
138+ const { getByRole, getByLabelText, findByText, container } = render ( < Field /> , { wrapper } ) ;
136139
137140 await userEvent . click ( getByRole ( 'button' , { name : / s e t e r r o r / i } ) ) ;
138141
139142 expect ( await findByText ( / S o m e E r r o r / i) ) . toBeInTheDocument ( ) ;
140143
141- const label = getByLabelText ( / s o m e l a b e l / i) ;
142- expect ( label ) . toHaveAttribute ( 'aria-invalid' , 'true' ) ;
143- expect ( label ) . toHaveAttribute ( 'aria-describedby' , 'error-firstname' ) ;
144+ const input = getByLabelText ( / s o m e l a b e l / i) ;
145+ expect ( input ) . toHaveAttribute ( 'aria-invalid' , 'true' ) ;
146+ expect ( input ) . toHaveAttribute ( 'aria-describedby' , 'error-firstname' ) ;
147+
148+ // Verify the error message element has the correct ID
149+ const errorElement = container . querySelector ( '#error-firstname' ) ;
150+ expect ( errorElement ) . toBeInTheDocument ( ) ;
151+ expect ( errorElement ) . toHaveTextContent ( / S o m e E r r o r / i) ;
144152 } ) ;
145153
146154 it ( 'with info' , async ( ) => {
@@ -157,4 +165,92 @@ describe('PlainInput', () => {
157165 fireEvent . focus ( await findByLabelText ( / s o m e l a b e l / i) ) ;
158166 expect ( await findByText ( / s o m e i n f o / i) ) . toBeInTheDocument ( ) ;
159167 } ) ;
168+
169+ it ( 'with success feedback and aria-describedby' , async ( ) => {
170+ const { wrapper } = await createFixtures ( ) ;
171+ const { Field } = createField ( 'firstname' , 'init value' , {
172+ type : 'text' ,
173+ label : 'some label' ,
174+ placeholder : 'some placeholder' ,
175+ } ) ;
176+
177+ const { getByRole, getByLabelText, findByText, container } = render ( < Field /> , { wrapper } ) ;
178+
179+ await userEvent . click ( getByRole ( 'button' , { name : / s e t s u c c e s s / i } ) ) ;
180+
181+ expect ( await findByText ( / S o m e S u c c e s s / i) ) . toBeInTheDocument ( ) ;
182+
183+ const input = getByLabelText ( / s o m e l a b e l / i) ;
184+ expect ( input ) . toHaveAttribute ( 'aria-invalid' , 'false' ) ;
185+ expect ( input ) . toHaveAttribute ( 'aria-describedby' , 'firstname-success-feedback' ) ;
186+
187+ // Verify the success message element has the correct ID
188+ const successElement = container . querySelector ( '#firstname-success-feedback' ) ;
189+ expect ( successElement ) . toBeInTheDocument ( ) ;
190+ expect ( successElement ) . toHaveTextContent ( / S o m e S u c c e s s / i) ;
191+ } ) ;
192+
193+ it ( 'transitions between error and success feedback types' , async ( ) => {
194+ const { wrapper } = await createFixtures ( ) ;
195+ const { Field } = createField ( 'firstname' , 'init value' , {
196+ type : 'text' ,
197+ label : 'some label' ,
198+ placeholder : 'some placeholder' ,
199+ } ) ;
200+
201+ const { getByRole, getByLabelText, findByText, container } = render ( < Field /> , { wrapper } ) ;
202+
203+ // Start with error
204+ await userEvent . click ( getByRole ( 'button' , { name : / s e t e r r o r / i } ) ) ;
205+ expect ( await findByText ( / S o m e E r r o r / i) ) . toBeInTheDocument ( ) ;
206+
207+ let input = getByLabelText ( / s o m e l a b e l / i) ;
208+ expect ( input ) . toHaveAttribute ( 'aria-invalid' , 'true' ) ;
209+ expect ( input ) . toHaveAttribute ( 'aria-describedby' , 'error-firstname' ) ;
210+
211+ // Transition to success
212+ await userEvent . click ( getByRole ( 'button' , { name : / s e t s u c c e s s / i } ) ) ;
213+ expect ( await findByText ( / S o m e S u c c e s s / i) ) . toBeInTheDocument ( ) ;
214+
215+ input = getByLabelText ( / s o m e l a b e l / i) ;
216+ expect ( input ) . toHaveAttribute ( 'aria-invalid' , 'false' ) ;
217+ expect ( input ) . toHaveAttribute ( 'aria-describedby' , 'firstname-success-feedback' ) ;
218+
219+ // Verify success element exists with proper ID
220+ const successElement = container . querySelector ( '#firstname-success-feedback' ) ;
221+ expect ( successElement ) . toBeInTheDocument ( ) ;
222+ expect ( successElement ) . toHaveTextContent ( / S o m e S u c c e s s / i) ;
223+ } ) ;
224+
225+ it ( 'aria-live attribute is correctly applied' , async ( ) => {
226+ const { wrapper } = await createFixtures ( ) ;
227+ const { Field } = createField ( 'firstname' , 'init value' , {
228+ type : 'text' ,
229+ label : 'some label' ,
230+ placeholder : 'some placeholder' ,
231+ } ) ;
232+
233+ const { getByRole, findByText, container } = render ( < Field /> , { wrapper } ) ;
234+
235+ // Set error feedback
236+ await userEvent . click ( getByRole ( 'button' , { name : / s e t e r r o r / i } ) ) ;
237+ expect ( await findByText ( / S o m e E r r o r / i) ) . toBeInTheDocument ( ) ;
238+
239+ // Verify the visible error message has aria-live="polite"
240+ const errorElement = container . querySelector ( '#error-firstname' ) ;
241+ expect ( errorElement ) . toHaveAttribute ( 'aria-live' , 'polite' ) ;
242+
243+ // Transition to success
244+ await userEvent . click ( getByRole ( 'button' , { name : / s e t s u c c e s s / i } ) ) ;
245+ expect ( await findByText ( / S o m e S u c c e s s / i) ) . toBeInTheDocument ( ) ;
246+
247+ // Verify the visible success message has aria-live="polite"
248+ const successElement = container . querySelector ( '#firstname-success-feedback' ) ;
249+ expect ( successElement ) . toHaveAttribute ( 'aria-live' , 'polite' ) ;
250+
251+ // The previous error message should now have aria-live="off" (though it might still exist in DOM but hidden)
252+ // Verify exactly one element has aria-live="polite" at a time
253+ const allAriaLivePolite = container . querySelectorAll ( '[aria-live="polite"]' ) ;
254+ expect ( allAriaLivePolite . length ) . toBe ( 1 ) ;
255+ } ) ;
160256} ) ;
0 commit comments