@@ -794,6 +794,127 @@ describe('useForm', () => {
794794
795795 expect ( result . current . getFieldError ( 'address.city' ) ) . toBe ( 'City is required' ) ;
796796 } ) ;
797+
798+ test ( 'group path: returns undefined when no descendant is touched' , ( ) => {
799+ const { result } = renderHook ( ( ) =>
800+ useForm ( alwaysValidValidator , {
801+ initialValues : { r1 : [ 0 , 0 , 0 ] , r2 : [ 0 , 0 , 0 ] } ,
802+ } ) ,
803+ ) ;
804+
805+ act ( ( ) => {
806+ result . current . setServerErrors ( { r2 : 'Vectors must differ' } ) ;
807+ } ) ;
808+
809+ // Error exists but no descendant touched — invisible.
810+ expect ( result . current . getFieldError ( 'r2' ) ) . toBeUndefined ( ) ;
811+ } ) ;
812+
813+ test ( 'group path: bubbles error once a descendant is touched' , ( ) => {
814+ const { result } = renderHook ( ( ) =>
815+ useForm ( alwaysValidValidator , {
816+ initialValues : { r1 : [ 0 , 0 , 0 ] , r2 : [ 0 , 0 , 0 ] } ,
817+ } ) ,
818+ ) ;
819+
820+ act ( ( ) => {
821+ result . current . setServerErrors ( { r2 : 'Vectors must differ' } ) ;
822+ result . current . setFieldTouched ( 'r2.0' , true ) ;
823+ } ) ;
824+
825+ expect ( result . current . getFieldError ( 'r2' ) ) . toBe ( 'Vectors must differ' ) ;
826+ } ) ;
827+
828+ test ( 'group path: ignores sibling-prefix touches (r20.0 must not satisfy "r2." scan)' , ( ) => {
829+ const { result } = renderHook ( ( ) =>
830+ useForm ( alwaysValidValidator , {
831+ initialValues : {
832+ r2 : [ 0 , 0 , 0 ] ,
833+ r20 : [ 0 , 0 , 0 ] ,
834+ } ,
835+ } ) ,
836+ ) ;
837+
838+ act ( ( ) => {
839+ result . current . setServerErrors ( { r2 : 'Vectors must differ' } ) ;
840+ result . current . setFieldTouched ( 'r20.0' , true ) ;
841+ } ) ;
842+
843+ // r20.0 starts with "r20." not "r2." — the trailing-dot guard.
844+ expect ( result . current . getFieldError ( 'r2' ) ) . toBeUndefined ( ) ;
845+ } ) ;
846+
847+ test ( 'group path: nested descendant satisfies the scan' , ( ) => {
848+ const { result } = renderHook ( ( ) =>
849+ useForm ( alwaysValidValidator , {
850+ initialValues : {
851+ secondary : { position : [ 0 , 0 , 0 ] , velocity : [ 0 , 0 , 0 ] } ,
852+ } ,
853+ } ) ,
854+ ) ;
855+
856+ act ( ( ) => {
857+ result . current . setServerErrors ( {
858+ secondary : 'Primary and secondary must differ' ,
859+ } ) ;
860+ result . current . setFieldTouched ( 'secondary.position.0' , true ) ;
861+ } ) ;
862+
863+ expect ( result . current . getFieldError ( 'secondary' ) ) . toBe ( 'Primary and secondary must differ' ) ;
864+ } ) ;
865+
866+ test ( 'group path: descendant touched but no error at path → undefined' , ( ) => {
867+ const { result } = renderHook ( ( ) =>
868+ useForm ( alwaysValidValidator , {
869+ initialValues : { r1 : [ 0 , 0 , 0 ] , r2 : [ 0 , 0 , 0 ] } ,
870+ } ) ,
871+ ) ;
872+
873+ act ( ( ) => {
874+ result . current . setFieldTouched ( 'r2.0' , true ) ;
875+ } ) ;
876+
877+ // Short-circuit: no error at "r2", scan never runs.
878+ expect ( result . current . getFieldError ( 'r2' ) ) . toBeUndefined ( ) ;
879+ } ) ;
880+
881+ test ( 'leaf path: behavior unchanged when descendants are touched' , ( ) => {
882+ // Ensures the new branch doesn't accidentally affect leaf semantics.
883+ const { result } = renderHook ( ( ) =>
884+ useForm ( alwaysValidValidator , {
885+ initialValues : { name : '' } ,
886+ } ) ,
887+ ) ;
888+
889+ act ( ( ) => {
890+ result . current . setServerErrors ( { name : 'Name is required' } ) ;
891+ result . current . setFieldTouched ( 'name' , true ) ;
892+ } ) ;
893+
894+ expect ( result . current . getFieldError ( 'name' ) ) . toBe ( 'Name is required' ) ;
895+ } ) ;
896+
897+ test ( "group path: error at one parent does not bubble through a sibling parent's descendant touch" , ( ) => {
898+ const { result } = renderHook ( ( ) =>
899+ useForm ( alwaysValidValidator , {
900+ initialValues : {
901+ primary : { position : [ 0 , 0 , 0 ] } ,
902+ secondary : { position : [ 0 , 0 , 0 ] } ,
903+ } ,
904+ } ) ,
905+ ) ;
906+
907+ act ( ( ) => {
908+ result . current . setServerErrors ( {
909+ primary : 'primary error' ,
910+ secondary : 'secondary error' ,
911+ } ) ;
912+ result . current . setFieldTouched ( 'primary.position.0' , true ) ;
913+ } ) ;
914+
915+ expect ( result . current . getFieldError ( 'primary' ) ) . toBe ( 'primary error' ) ;
916+ expect ( result . current . getFieldError ( 'secondary' ) ) . toBeUndefined ( ) ;
917+ } ) ;
797918 } ) ;
798919
799920 describe ( 'getFieldId' , ( ) => {
0 commit comments