@@ -336,6 +336,120 @@ describe('<Field.Root />', () => {
336
336
} ) ;
337
337
} ) ;
338
338
339
+ describe ( 'revalidation' , ( ) => {
340
+ it ( 'revalidates on change for `valueMissing`' , async ( ) => {
341
+ await render (
342
+ < Field . Root >
343
+ < Field . Control required />
344
+ < Field . Error />
345
+ </ Field . Root > ,
346
+ ) ;
347
+
348
+ const control = screen . getByRole ( 'textbox' ) ;
349
+ const message = screen . queryByText ( 'error' ) ;
350
+
351
+ expect ( message ) . to . equal ( null ) ;
352
+
353
+ fireEvent . focus ( control ) ;
354
+ fireEvent . change ( control , { target : { value : 't' } } ) ;
355
+ fireEvent . blur ( control ) ;
356
+
357
+ expect ( control ) . not . to . have . attribute ( 'aria-invalid' , 'true' ) ;
358
+
359
+ fireEvent . focus ( control ) ;
360
+ fireEvent . change ( control , { target : { value : '' } } ) ;
361
+ fireEvent . blur ( control ) ;
362
+
363
+ expect ( control ) . to . have . attribute ( 'aria-invalid' ) ;
364
+ } ) ;
365
+
366
+ it ( 'handles both `required` and `typeMismatch`' , async ( ) => {
367
+ await render (
368
+ < Field . Root >
369
+ < Field . Control type = "email" required />
370
+ < Field . Error data-testid = "error" />
371
+ </ Field . Root > ,
372
+ ) ;
373
+
374
+ const control = screen . getByRole ( 'textbox' ) ;
375
+ const message = screen . queryByTestId ( 'error' ) ;
376
+
377
+ expect ( message ) . to . equal ( null ) ;
378
+
379
+ fireEvent . focus ( control ) ;
380
+ fireEvent . blur ( control ) ;
381
+
382
+ expect ( control ) . not . to . have . attribute ( 'aria-invalid' ) ;
383
+
384
+ fireEvent . focus ( control ) ;
385
+ fireEvent . change ( control , { target : { value : 'tt' } } ) ;
386
+ fireEvent . blur ( control ) ;
387
+
388
+ expect ( control ) . to . have . attribute ( 'aria-invalid' , 'true' ) ;
389
+
390
+ fireEvent . focus ( control ) ;
391
+ fireEvent . change ( control , { target : { value : '' } } ) ;
392
+ fireEvent . blur ( control ) ;
393
+
394
+ expect ( control ) . to . have . attribute ( 'aria-invalid' , 'true' ) ;
395
+
396
+ fireEvent . focus ( control ) ;
397
+ fireEvent . change ( control , { target :
{ value :
'[email protected] ' } } ) ;
398
+ fireEvent . blur ( control ) ;
399
+
400
+ expect ( control ) . not . to . have . attribute ( 'aria-invalid' ) ;
401
+ } ) ;
402
+
403
+ it ( 'clears valueMissing on change but defers other native errors like typeMismatch until blur when both are active' , async ( ) => {
404
+ await render (
405
+ < Field . Root >
406
+ < Field . Control type = "email" required data-testid = "control" />
407
+ < Field . Error data-testid = "error" />
408
+ </ Field . Root > ,
409
+ ) ;
410
+
411
+ const control = screen . getByTestId ( 'control' ) ;
412
+
413
+ fireEvent . focus ( control ) ;
414
+ fireEvent . blur ( control ) ;
415
+ expect ( control ) . not . to . have . attribute ( 'aria-invalid' , 'true' ) ;
416
+ expect ( screen . queryByTestId ( 'error' ) ) . to . equal ( null ) ;
417
+
418
+ fireEvent . focus ( control ) ;
419
+ fireEvent . change ( control , { target : { value : 'a' } } ) ;
420
+ fireEvent . change ( control , { target : { value : '' } } ) ;
421
+ fireEvent . blur ( control ) ;
422
+
423
+ expect ( control ) . to . have . attribute ( 'aria-invalid' , 'true' ) ;
424
+ expect ( screen . getByTestId ( 'error' ) ) . not . to . equal ( null ) ;
425
+
426
+ fireEvent . focus ( control ) ;
427
+ fireEvent . change ( control , { target : { value : 't' } } ) ;
428
+
429
+ // The field becomes temporarily valid because only 'valueMissing' is checked for immediate clearing.
430
+ // Other errors like 'typeMismatch' are deferred to the next blur/submit.
431
+ expect ( control ) . not . to . have . attribute ( 'aria-invalid' , 'true' ) ;
432
+ expect ( screen . queryByTestId ( 'error' ) ) . to . equal ( null ) ;
433
+
434
+ fireEvent . blur ( control ) ;
435
+
436
+ expect ( control ) . to . have . attribute ( 'aria-invalid' , 'true' ) ;
437
+ expect ( screen . getByTestId ( 'error' ) ) . not . to . equal ( null ) ;
438
+ expect ( screen . getByTestId ( 'error' ) . textContent ) . not . to . equal ( '' ) ;
439
+
440
+ fireEvent . focus ( control ) ;
441
+ fireEvent . change ( control , { target :
{ value :
'[email protected] ' } } ) ;
442
+
443
+ expect ( control ) . not . to . have . attribute ( 'aria-invalid' , 'true' ) ;
444
+ expect ( screen . queryByTestId ( 'error' ) ) . to . equal ( null ) ;
445
+
446
+ fireEvent . blur ( control ) ;
447
+
448
+ expect ( control ) . not . to . have . attribute ( 'aria-invalid' , 'true' ) ;
449
+ expect ( screen . queryByTestId ( 'error' ) ) . to . equal ( null ) ;
450
+ } ) ;
451
+ } ) ;
452
+
339
453
describe ( 'style hooks' , ( ) => {
340
454
describe ( 'touched' , ( ) => {
341
455
it ( 'should apply [data-touched] style hook to all components when touched' , async ( ) => {
0 commit comments