@@ -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,257 @@ 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 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 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 manual modification persistence - modify weight value manually
723+ const weightInput0 = getByPlaceholderText ( 'route-weight-0-0' ) as HTMLInputElement ;
724+ fireEvent . change ( weightInput0 , { target : { value : '200' } } ) ;
725+ await mockTimeout ( ) ;
726+ expect ( weightInput0 . value ) . toBe ( '200' ) ;
727+
728+ // Switch to abtest
729+ fireEvent . click ( abtestRadio0 ) ;
730+ await mockTimeout ( ) ;
731+ expect ( container . querySelector ( '[placeholder="route-weight-0-0"]' ) ) . toBeFalsy ( ) ;
732+ expect ( ( getByPlaceholderText ( 'route-abtest-0-0' ) as HTMLInputElement ) . value ) . toBe ( 'cid' ) ;
733+
734+ // Switch back to weight - should show initial value (50), not manually modified value (200)
735+ fireEvent . click ( weightRadio0 ) ;
736+ await mockTimeout ( ) ;
737+ expect ( ( getByPlaceholderText ( 'route-weight-0-0' ) as HTMLInputElement ) . value ) . toBe ( '50' ) ;
738+ expect ( container . querySelector ( '[placeholder="route-abtest-0-0"]' ) ) . toBeFalsy ( ) ;
739+
740+ // Test switching second route from abtest to weight
741+ const weightRadio1 = container . querySelector ( '[data-route-index="1"] input[value="weight"]' ) as HTMLInputElement ;
742+ fireEvent . click ( weightRadio1 ) ;
743+ await mockTimeout ( ) ;
744+ expect ( ( getByPlaceholderText ( 'route-weight-0-1' ) as HTMLInputElement ) . value ) . toBe ( '30' ) ;
745+ expect ( container . querySelector ( '[placeholder="route-abtest-0-1"]' ) ) . toBeFalsy ( ) ;
746+
747+ // Test manual modification persistence - modify weight value manually after switching
748+ const weightInput1 = getByPlaceholderText ( 'route-weight-0-1' ) as HTMLInputElement ;
749+ fireEvent . change ( weightInput1 , { target : { value : '100' } } ) ;
750+ await mockTimeout ( ) ;
751+ expect ( weightInput1 . value ) . toBe ( '100' ) ;
752+
753+ // Switch back to abtest - should show initial abtest value (uid)
754+ fireEvent . click ( abtestRadio1 ) ;
755+ await mockTimeout ( ) ;
756+ expect ( ( getByPlaceholderText ( 'route-abtest-0-1' ) as HTMLInputElement ) . value ) . toBe ( 'uid' ) ;
757+ expect ( container . querySelector ( '[placeholder="route-weight-0-1"]' ) ) . toBeFalsy ( ) ;
758+
759+ // Switch to weight again - should show initial weight value (30), not manually modified value (100)
760+ fireEvent . click ( weightRadio1 ) ;
761+ await mockTimeout ( ) ;
762+ expect ( ( getByPlaceholderText ( 'route-weight-0-1' ) as HTMLInputElement ) . value ) . toBe ( '30' ) ;
763+ expect ( container . querySelector ( '[placeholder="route-abtest-0-1"]' ) ) . toBeFalsy ( ) ;
764+
765+ // Modify abtest value manually
766+ fireEvent . click ( abtestRadio1 ) ;
767+ await mockTimeout ( ) ;
768+ const abtestInput1 = getByPlaceholderText ( 'route-abtest-0-1' ) as HTMLInputElement ;
769+ expect ( abtestInput1 . value ) . toBe ( 'uid' ) ;
770+ fireEvent . change ( abtestInput1 , { target : { value : 'custom-key' } } ) ;
771+ await mockTimeout ( ) ;
772+ expect ( abtestInput1 . value ) . toBe ( 'custom-key' ) ;
773+
774+ // Switch to weight
775+ fireEvent . click ( weightRadio1 ) ;
776+ await mockTimeout ( ) ;
777+ expect ( container . querySelector ( '[placeholder="route-abtest-0-1"]' ) ) . toBeFalsy ( ) ;
778+ expect ( ( getByPlaceholderText ( 'route-weight-0-1' ) as HTMLInputElement ) . value ) . toBe ( '30' ) ;
779+
780+ // Switch back to abtest - should show initial value (uid), not manually modified value (custom-key)
781+ fireEvent . click ( abtestRadio1 ) ;
782+ await mockTimeout ( ) ;
783+ expect ( ( getByPlaceholderText ( 'route-abtest-0-1' ) as HTMLInputElement ) . value ) . toBe ( 'uid' ) ;
784+ expect ( container . querySelector ( '[placeholder="route-weight-0-1"]' ) ) . toBeFalsy ( ) ;
785+
786+ // Test adding default route (empty data)
787+ const addDefaultBtn = container . querySelector ( '#test-add-route-0-default' ) ;
788+ fireEvent . click ( addDefaultBtn ) ;
789+ await mockTimeout ( ) ;
790+ const newRouteRadios = container . querySelectorAll ( '[data-route-index="2"] input[type="radio"]' ) ;
791+ expect ( newRouteRadios . length ) . toBe ( 2 ) ;
792+ // No radio should be checked initially
793+ const checkedRadio = container . querySelector ( '[data-route-index="2"] input[type="radio"]:checked' ) ;
794+ expect ( checkedRadio ) . toBeFalsy ( ) ;
795+ // No conditional field should be rendered when type is empty
796+ expect ( container . querySelector ( '[placeholder="route-weight-0-2"]' ) ) . toBeFalsy ( ) ;
797+ expect ( container . querySelector ( '[placeholder="route-abtest-0-2"]' ) ) . toBeFalsy ( ) ;
798+
799+ // Test setting type to weight for new route
800+ const newWeightRadio = container . querySelector ( '[data-route-index="2"] input[value="weight"]' ) as HTMLInputElement ;
801+ fireEvent . click ( newWeightRadio ) ;
802+ await mockTimeout ( ) ;
803+ const newWeightInput = getByPlaceholderText ( 'route-weight-0-2' ) as HTMLInputElement ;
804+ expect ( newWeightInput ) . toBeTruthy ( ) ;
805+ expect ( newWeightInput . value ) . toBe ( '' ) ;
806+
807+ // Test setting weight value
808+ fireEvent . change ( newWeightInput , { target : { value : '100' } } ) ;
809+ await mockTimeout ( ) ;
810+ expect ( newWeightInput . value ) . toBe ( '100' ) ;
811+
812+ // Test switching new route to abtest
813+ const newAbtestRadio = container . querySelector ( '[data-route-index="2"] input[value="abtest"]' ) as HTMLInputElement ;
814+ fireEvent . click ( newAbtestRadio ) ;
815+ await mockTimeout ( ) ;
816+ expect ( container . querySelector ( '[placeholder="route-weight-0-2"]' ) ) . toBeFalsy ( ) ;
817+ const newAbtestInput = getByPlaceholderText ( 'route-abtest-0-2' ) as HTMLInputElement ;
818+ expect ( newAbtestInput ) . toBeTruthy ( ) ;
819+ expect ( newAbtestInput . value ) . toBe ( '' ) ;
820+
821+ // Test setting abtest value
822+ fireEvent . change ( newAbtestInput , { target : { value : 'new-key' } } ) ;
823+ await mockTimeout ( ) ;
824+ expect ( newAbtestInput . value ) . toBe ( 'new-key' ) ;
825+
826+ // Test switching back to weight - should show empty value, not previous manually modified value
827+ fireEvent . click ( newWeightRadio ) ;
828+ await mockTimeout ( ) ;
829+ expect ( container . querySelector ( '[placeholder="route-abtest-0-2"]' ) ) . toBeFalsy ( ) ;
830+ const weightInputAgain = getByPlaceholderText ( 'route-weight-0-2' ) as HTMLInputElement ;
831+ expect ( weightInputAgain . value ) . toBe ( '' ) ;
832+
833+ // Test adding specified route (with initial data)
834+ const addSpecifiedBtn = container . querySelector ( '#test-add-route-0-specified' ) ;
835+ fireEvent . click ( addSpecifiedBtn ) ;
836+ await mockTimeout ( ) ;
837+ const specifiedWeightRadio = container . querySelector (
838+ '[data-route-index="3"] input[value="weight"]' ,
839+ ) as HTMLInputElement ;
840+ expect ( specifiedWeightRadio . checked ) . toBe ( true ) ;
841+ const specifiedWeightInput = getByPlaceholderText ( 'route-weight-0-3' ) as HTMLInputElement ;
842+ expect ( specifiedWeightInput . value ) . toBe ( '50' ) ;
843+ expect ( container . querySelector ( '[placeholder="route-abtest-0-3"]' ) ) . toBeFalsy ( ) ;
844+
845+ // Test switching specified route to abtest
846+ const specifiedAbtestRadio = container . querySelector (
847+ '[data-route-index="3"] input[value="abtest"]' ,
848+ ) as HTMLInputElement ;
849+ fireEvent . click ( specifiedAbtestRadio ) ;
850+ await mockTimeout ( ) ;
851+ const specifiedAbtestInput = getByPlaceholderText ( 'route-abtest-0-3' ) as HTMLInputElement ;
852+ expect ( specifiedAbtestInput . value ) . toBe ( 'cid' ) ;
853+ expect ( container . querySelector ( '[placeholder="route-weight-0-3"]' ) ) . toBeFalsy ( ) ;
854+
855+ // Test switching specified route back to weight - should show initial weight value
856+ fireEvent . click ( specifiedWeightRadio ) ;
857+ await mockTimeout ( ) ;
858+ const specifiedWeightInputAgain = getByPlaceholderText ( 'route-weight-0-3' ) as HTMLInputElement ;
859+ expect ( specifiedWeightInputAgain . value ) . toBe ( '50' ) ;
860+ expect ( container . querySelector ( '[placeholder="route-abtest-0-3"]' ) ) . toBeFalsy ( ) ;
861+ } ) ;
608862} ) ;
0 commit comments