@@ -4,6 +4,7 @@ import { fireEvent, mockTimeout, render, vi } from '@test/utils';
44
55import Button from '../../button' ;
66import Input from '../../input' ;
7+ import Radio from '../../radio' ;
78import FormList from '../FormList' ;
89import Form , { type FormProps } from '../index' ;
910
@@ -605,4 +606,199 @@ describe('Form List 组件测试', () => {
605606 await mockTimeout ( ) ;
606607 expect ( queryByText ( '用户名必填' ) ) . not . toBeTruthy ( ) ;
607608 } ) ;
609+
610+ test ( 'FormList with shouldUpdate' , async ( ) => {
611+ const TestView = ( ) => {
612+ const [ form ] = Form . useForm ( ) ;
613+
614+ const INIT_DATA = {
615+ services : [
616+ {
617+ modelName : 'modelA' ,
618+ routes : [
619+ { type : 'weight' , weight : 50 , abtest : 'cid' } ,
620+ { type : 'abtest' , weight : 30 , abtest : 'uid' } ,
621+ ] ,
622+ } ,
623+ ] ,
624+ } ;
625+
626+ return (
627+ < Form form = { form } initialData = { INIT_DATA } >
628+ < FormList name = "services" >
629+ { ( fields ) => (
630+ < >
631+ { fields . map ( ( { key, name : serviceName } ) => (
632+ < div key = { key } >
633+ < FormList name = { [ serviceName , 'routes' ] } >
634+ { ( routeFields , { add : addRoute } ) => (
635+ < div >
636+ { routeFields . map ( ( f ) => (
637+ < div key = { f . key } data-route-index = { f . name } >
638+ < FormItem name = { [ f . name , 'type' ] } label = "类型" >
639+ < Radio . Group
640+ options = { [
641+ { label : '权重' , value : 'weight' } ,
642+ { label : 'ABTest' , value : 'abtest' } ,
643+ ] }
644+ />
645+ </ FormItem >
646+
647+ < FormItem
648+ shouldUpdate = { ( p , n ) =>
649+ p . services ?. [ serviceName ] ?. routes ?. [ f . name ] ?. type !==
650+ n . services ?. [ serviceName ] ?. routes ?. [ f . name ] ?. type
651+ }
652+ >
653+ { ( { getFieldValue } ) => {
654+ const type = getFieldValue ( [ 'services' , serviceName , 'routes' , f . name , 'type' ] ) ;
655+ if ( type === 'weight' ) {
656+ return (
657+ < FormItem key = { type } name = { [ f . name , 'weight' ] } label = "权重" >
658+ < Input placeholder = { `route-weight-${ serviceName } -${ f . name } ` } />
659+ </ FormItem >
660+ ) ;
661+ }
662+ if ( type === 'abtest' ) {
663+ return (
664+ < FormItem key = { type } name = { [ f . name , 'abtest' ] } label = "分流Key" >
665+ < Input placeholder = { `route-abtest-${ serviceName } -${ f . name } ` } />
666+ </ FormItem >
667+ ) ;
668+ }
669+ return null ;
670+ } }
671+ </ FormItem >
672+ </ div >
673+ ) ) }
674+ < Button id = { `test-add-route-${ serviceName } -default` } onClick = { ( ) => addRoute ( ) } >
675+ 新增默认路由
676+ </ Button >
677+ < Button
678+ id = { `test-add-route-${ serviceName } -specified` }
679+ onClick = { ( ) => addRoute ( INIT_DATA . services [ 0 ] . routes [ 0 ] ) }
680+ >
681+ 新增指定路由
682+ </ Button >
683+ </ div >
684+ ) }
685+ </ FormList >
686+ </ div >
687+ ) ) }
688+ </ >
689+ ) }
690+ </ FormList >
691+ </ Form >
692+ ) ;
693+ } ;
694+
695+ const { container, getByPlaceholderText } = render ( < TestView /> ) ;
696+
697+ // Test initial data - first route (type: weight)
698+ const weightRadio0 = container . querySelector ( '[data-route-index="0"] input[value="weight"]' ) as HTMLInputElement ;
699+ expect ( weightRadio0 . checked ) . toBe ( true ) ;
700+ expect ( ( getByPlaceholderText ( 'route-weight-0-0' ) as HTMLInputElement ) . value ) . toBe ( '50' ) ;
701+ expect ( container . querySelector ( '[placeholder="route-abtest-0-0"]' ) ) . toBeFalsy ( ) ;
702+
703+ // Test initial data - second route (type: abtest)
704+ const abtestRadio1 = container . querySelector ( '[data-route-index="1"] input[value="abtest"]' ) as HTMLInputElement ;
705+ expect ( abtestRadio1 . checked ) . toBe ( true ) ;
706+ expect ( ( getByPlaceholderText ( 'route-abtest-0-1' ) as HTMLInputElement ) . value ) . toBe ( 'uid' ) ;
707+ expect ( container . querySelector ( '[placeholder="route-weight-0-1"]' ) ) . toBeFalsy ( ) ;
708+
709+ // Test switching first route from weight to abtest
710+ const abtestRadio0 = container . querySelector ( '[data-route-index="0"] input[value="abtest"]' ) as HTMLInputElement ;
711+ fireEvent . click ( abtestRadio0 ) ;
712+ await mockTimeout ( ) ;
713+ expect ( ( getByPlaceholderText ( 'route-abtest-0-0' ) as HTMLInputElement ) . value ) . toBe ( 'cid' ) ;
714+ expect ( container . querySelector ( '[placeholder="route-weight-0-0"]' ) ) . toBeFalsy ( ) ;
715+
716+ // Test switching first route back to weight
717+ fireEvent . click ( weightRadio0 ) ;
718+ await mockTimeout ( ) ;
719+ expect ( ( getByPlaceholderText ( 'route-weight-0-0' ) as HTMLInputElement ) . value ) . toBe ( '50' ) ;
720+ expect ( container . querySelector ( '[placeholder="route-abtest-0-0"]' ) ) . toBeFalsy ( ) ;
721+
722+ // Test switching second route from abtest to weight
723+ const weightRadio1 = container . querySelector ( '[data-route-index="1"] input[value="weight"]' ) as HTMLInputElement ;
724+ fireEvent . click ( weightRadio1 ) ;
725+ await mockTimeout ( ) ;
726+ expect ( ( getByPlaceholderText ( 'route-weight-0-1' ) as HTMLInputElement ) . value ) . toBe ( '30' ) ;
727+ expect ( container . querySelector ( '[placeholder="route-abtest-0-1"]' ) ) . toBeFalsy ( ) ;
728+
729+ // Test switching second route back to abtest
730+ fireEvent . click ( abtestRadio1 ) ;
731+ await mockTimeout ( ) ;
732+ expect ( ( getByPlaceholderText ( 'route-abtest-0-1' ) as HTMLInputElement ) . value ) . toBe ( 'uid' ) ;
733+ expect ( container . querySelector ( '[placeholder="route-weight-0-1"]' ) ) . toBeFalsy ( ) ;
734+
735+ // Test adding default route (empty data)
736+ const addDefaultBtn = container . querySelector ( '#test-add-route-0-default' ) ;
737+ fireEvent . click ( addDefaultBtn ) ;
738+ await mockTimeout ( ) ;
739+ const newRouteRadios = container . querySelectorAll ( '[data-route-index="2"] input[type="radio"]' ) ;
740+ expect ( newRouteRadios . length ) . toBe ( 2 ) ;
741+ // No radio should be checked initially
742+ const checkedRadio = container . querySelector ( '[data-route-index="2"] input[type="radio"]:checked' ) ;
743+ expect ( checkedRadio ) . toBeFalsy ( ) ;
744+ // No conditional field should be rendered when type is empty
745+ expect ( container . querySelector ( '[placeholder="route-weight-0-2"]' ) ) . toBeFalsy ( ) ;
746+ expect ( container . querySelector ( '[placeholder="route-abtest-0-2"]' ) ) . toBeFalsy ( ) ;
747+
748+ // Test setting type to weight for new route
749+ const newWeightRadio = container . querySelector ( '[data-route-index="2"] input[value="weight"]' ) as HTMLInputElement ;
750+ fireEvent . click ( newWeightRadio ) ;
751+ await mockTimeout ( ) ;
752+ const newWeightInput = getByPlaceholderText ( 'route-weight-0-2' ) as HTMLInputElement ;
753+ expect ( newWeightInput ) . toBeTruthy ( ) ;
754+ expect ( newWeightInput . value ) . toBe ( '' ) ;
755+
756+ // Test setting weight value
757+ fireEvent . change ( newWeightInput , { target : { value : '100' } } ) ;
758+ await mockTimeout ( ) ;
759+ expect ( newWeightInput . value ) . toBe ( '100' ) ;
760+
761+ // Test switching new route to abtest
762+ const newAbtestRadio = container . querySelector ( '[data-route-index="2"] input[value="abtest"]' ) as HTMLInputElement ;
763+ fireEvent . click ( newAbtestRadio ) ;
764+ await mockTimeout ( ) ;
765+ expect ( container . querySelector ( '[placeholder="route-weight-0-2"]' ) ) . toBeFalsy ( ) ;
766+ const newAbtestInput = getByPlaceholderText ( 'route-abtest-0-2' ) as HTMLInputElement ;
767+ expect ( newAbtestInput ) . toBeTruthy ( ) ;
768+ expect ( newAbtestInput . value ) . toBe ( '' ) ;
769+
770+ // Test setting abtest value
771+ fireEvent . change ( newAbtestInput , { target : { value : 'new-key' } } ) ;
772+ await mockTimeout ( ) ;
773+ expect ( newAbtestInput . value ) . toBe ( 'new-key' ) ;
774+
775+ // Test switching back to weight - previous weight value should be preserved
776+ fireEvent . click ( newWeightRadio ) ;
777+ await mockTimeout ( ) ;
778+ expect ( container . querySelector ( '[placeholder="route-abtest-0-2"]' ) ) . toBeFalsy ( ) ;
779+ const weightInputAgain = getByPlaceholderText ( 'route-weight-0-2' ) as HTMLInputElement ;
780+ expect ( weightInputAgain . value ) . toBe ( '100' ) ;
781+
782+ // Test adding specified route (with initial data)
783+ const addSpecifiedBtn = container . querySelector ( '#test-add-route-0-specified' ) ;
784+ fireEvent . click ( addSpecifiedBtn ) ;
785+ await mockTimeout ( ) ;
786+ const specifiedWeightRadio = container . querySelector (
787+ '[data-route-index="3"] input[value="weight"]' ,
788+ ) as HTMLInputElement ;
789+ expect ( specifiedWeightRadio . checked ) . toBe ( true ) ;
790+ const specifiedWeightInput = getByPlaceholderText ( 'route-weight-0-3' ) as HTMLInputElement ;
791+ expect ( specifiedWeightInput . value ) . toBe ( '50' ) ;
792+ expect ( container . querySelector ( '[placeholder="route-abtest-0-3"]' ) ) . toBeFalsy ( ) ;
793+
794+ // Test switching specified route to abtest
795+ const specifiedAbtestRadio = container . querySelector (
796+ '[data-route-index="3"] input[value="abtest"]' ,
797+ ) as HTMLInputElement ;
798+ fireEvent . click ( specifiedAbtestRadio ) ;
799+ await mockTimeout ( ) ;
800+ const specifiedAbtestInput = getByPlaceholderText ( 'route-abtest-0-3' ) as HTMLInputElement ;
801+ expect ( specifiedAbtestInput . value ) . toBe ( 'cid' ) ;
802+ expect ( container . querySelector ( '[placeholder="route-weight-0-3"]' ) ) . toBeFalsy ( ) ;
803+ } ) ;
608804} ) ;
0 commit comments