@@ -20,10 +20,21 @@ import { act } from 'react-dom/test-utils';
20
20
21
21
import { OptimizelyProvider } from './Provider' ;
22
22
import { OnReadyResult , ReactSDKClient , VariableValuesObject } from './client' ;
23
- import { useExperiment , useFeature } from './hooks' ;
23
+ import { useExperiment , useFeature , useDecide } from './hooks' ;
24
+ import { OptimizelyDecision } from './utils' ;
24
25
25
26
Enzyme . configure ( { adapter : new Adapter ( ) } ) ;
26
27
28
+ const defaultDecision : OptimizelyDecision = {
29
+ enabled : false ,
30
+ variables : { } ,
31
+ flagKey : '' ,
32
+ reasons : [ ] ,
33
+ ruleKey : '' ,
34
+ userContext : { id : null } ,
35
+ variationKey : '' ,
36
+ } ;
37
+
27
38
const MyFeatureComponent = ( { options = { } , overrides = { } } : any ) => {
28
39
const [ isEnabled , variables , clientReady , didTimeout ] = useFeature ( 'feature1' , { ...options } , { ...overrides } ) ;
29
40
return < > { `${ isEnabled ? 'true' : 'false' } |${ JSON . stringify ( variables ) } |${ clientReady } |${ didTimeout } ` } </ > ;
@@ -34,6 +45,11 @@ const MyExperimentComponent = ({ options = {}, overrides = {} }: any) => {
34
45
return < > { `${ variation } |${ clientReady } |${ didTimeout } ` } </ > ;
35
46
} ;
36
47
48
+ const MyDecideComponent = ( { options = { } , overrides = { } } : any ) => {
49
+ const [ decision , clientReady , didTimeout ] = useDecide ( 'feature1' , { ...options } , { ...overrides } ) ;
50
+ return < > { `${ ( decision . enabled ) ? 'true' : 'false' } |${ JSON . stringify ( decision . variables ) } |${ clientReady } |${ didTimeout } ` } </ > ;
51
+ } ;
52
+
37
53
const mockFeatureVariables : VariableValuesObject = {
38
54
foo : 'bar' ,
39
55
} ;
@@ -50,8 +66,10 @@ describe('hooks', () => {
50
66
let userUpdateCallbacks : Array < ( ) => void > ;
51
67
let UseExperimentLoggingComponent : React . FunctionComponent < any > ;
52
68
let UseFeatureLoggingComponent : React . FunctionComponent < any > ;
69
+ let UseDecideLoggingComponent : React . FunctionComponent < any > ;
53
70
let mockLog : jest . Mock ;
54
71
let forcedVariationUpdateCallbacks : Array < ( ) => void > ;
72
+ let decideMock : jest . Mock < OptimizelyDecision > ;
55
73
56
74
beforeEach ( ( ) => {
57
75
getOnReadyPromise = ( { timeout = 0 } : any ) : Promise < OnReadyResult > =>
@@ -78,6 +96,7 @@ describe('hooks', () => {
78
96
readySuccess = true ;
79
97
notificationListenerCallbacks = [ ] ;
80
98
forcedVariationUpdateCallbacks = [ ] ;
99
+ decideMock = jest . fn ( ) ;
81
100
82
101
optimizelyMock = ( {
83
102
activate : activateMock ,
@@ -104,6 +123,7 @@ describe('hooks', () => {
104
123
return ( ) => { } ;
105
124
} ) ,
106
125
getForcedVariations : jest . fn ( ) . mockReturnValue ( { } ) ,
126
+ decide : decideMock ,
107
127
} as unknown ) as ReactSDKClient ;
108
128
109
129
mockLog = jest . fn ( ) ;
@@ -118,6 +138,12 @@ describe('hooks', () => {
118
138
mockLog ( isEnabled ) ;
119
139
return < div > { isEnabled } </ div > ;
120
140
} ;
141
+
142
+ UseDecideLoggingComponent = ( { options = { } , overrides = { } } : any ) => {
143
+ const [ decision ] = useDecide ( 'feature1' , { ...options } , { ...overrides } ) ;
144
+ mockLog ( decision . enabled ) ;
145
+ return < div > { decision . enabled } </ div > ;
146
+ } ;
121
147
} ) ;
122
148
123
149
afterEach ( async ( ) => {
@@ -641,4 +667,271 @@ describe('hooks', () => {
641
667
expect ( isFeatureEnabledMock ) . not . toHaveBeenCalled ( ) ;
642
668
} ) ;
643
669
} ) ;
670
+
671
+ describe ( 'useDecide' , ( ) => {
672
+ it ( 'should render true when the flag is enabled' , async ( ) => {
673
+ decideMock . mockReturnValue ( {
674
+ ... defaultDecision ,
675
+ enabled : true ,
676
+ variables : { 'foo' : 'bar' } ,
677
+ } ) ;
678
+ const component = Enzyme . mount (
679
+ < OptimizelyProvider optimizely = { optimizelyMock } >
680
+ < MyDecideComponent />
681
+ </ OptimizelyProvider >
682
+ ) ;
683
+ await optimizelyMock . onReady ( ) ;
684
+ component . update ( ) ;
685
+ expect ( component . text ( ) ) . toBe ( 'true|{"foo":"bar"}|true|false' ) ;
686
+ } ) ;
687
+
688
+ it ( 'should render false when the flag is disabled' , async ( ) => {
689
+ decideMock . mockReturnValue ( {
690
+ ... defaultDecision ,
691
+ enabled : false ,
692
+ variables : { 'foo' : 'bar' } ,
693
+ } ) ;
694
+ const component = Enzyme . mount (
695
+ < OptimizelyProvider optimizely = { optimizelyMock } >
696
+ < MyDecideComponent />
697
+ </ OptimizelyProvider >
698
+ ) ;
699
+ await optimizelyMock . onReady ( ) ;
700
+ component . update ( ) ;
701
+ expect ( component . text ( ) ) . toBe ( 'false|{"foo":"bar"}|true|false' ) ;
702
+ } ) ;
703
+
704
+ it ( 'should respect the timeout option passed' , async ( ) => {
705
+ decideMock . mockReturnValue ( { ... defaultDecision } ) ;
706
+ readySuccess = false ;
707
+
708
+ const component = Enzyme . mount (
709
+ < OptimizelyProvider optimizely = { optimizelyMock } >
710
+ < MyDecideComponent options = { { timeout : mockDelay } } />
711
+ </ OptimizelyProvider >
712
+ ) ;
713
+ expect ( component . text ( ) ) . toBe ( 'false|{}|false|false' ) ;
714
+
715
+ await optimizelyMock . onReady ( ) ;
716
+ component . update ( ) ;
717
+ expect ( component . text ( ) ) . toBe ( 'false|{}|false|true' ) ;
718
+
719
+ // Simulate datafile fetch completing after timeout has already passed
720
+ // flag is now true and decision contains variables
721
+ decideMock . mockReturnValue ( {
722
+ ... defaultDecision ,
723
+ enabled : true ,
724
+ variables : { 'foo' : 'bar' } ,
725
+ } ) ;
726
+
727
+ await optimizelyMock . onReady ( ) . then ( res => res . dataReadyPromise ) ;
728
+ component . update ( ) ;
729
+
730
+ // Simulate datafile fetch completing after timeout has already passed
731
+ // Wait for completion of dataReadyPromise
732
+ await optimizelyMock . onReady ( ) . then ( res => res . dataReadyPromise ) ;
733
+ component . update ( ) ;
734
+
735
+ expect ( component . text ( ) ) . toBe ( 'true|{"foo":"bar"}|true|true' ) ; // when clientReady
736
+ } ) ;
737
+
738
+ it ( 'should gracefully handle the client promise rejecting after timeout' , async ( ) => {
739
+ console . log ( 'hola' )
740
+ readySuccess = false ;
741
+ decideMock . mockReturnValue ( { ... defaultDecision } ) ;
742
+ getOnReadyPromise = ( ) =>
743
+ new Promise ( ( res , rej ) => {
744
+ setTimeout ( ( ) => rej ( 'some error with user' ) , mockDelay ) ;
745
+ } ) ;
746
+ const component = Enzyme . mount (
747
+ < OptimizelyProvider optimizely = { optimizelyMock } >
748
+ < MyDecideComponent options = { { timeout : mockDelay } } />
749
+ </ OptimizelyProvider >
750
+ ) ;
751
+ expect ( component . text ( ) ) . toBe ( 'false|{}|false|false' ) ; // initial render
752
+ await new Promise ( r => setTimeout ( r , mockDelay * 3 ) ) ;
753
+ component . update ( ) ;
754
+ expect ( component . text ( ) ) . toBe ( 'false|{}|false|false' ) ;
755
+ } ) ;
756
+
757
+ it ( 'should re-render when the user attributes change using autoUpdate' , async ( ) => {
758
+ decideMock . mockReturnValue ( { ...defaultDecision } ) ;
759
+ const component = Enzyme . mount (
760
+ < OptimizelyProvider optimizely = { optimizelyMock } >
761
+ < MyDecideComponent options = { { autoUpdate : true } } />
762
+ </ OptimizelyProvider >
763
+ ) ;
764
+
765
+ // TODO - Wrap this with async act() once we upgrade to React 16.9
766
+ // See https://github.com/facebook/react/issues/15379
767
+ await optimizelyMock . onReady ( ) ;
768
+ component . update ( ) ;
769
+ expect ( component . text ( ) ) . toBe ( 'false|{}|true|false' ) ;
770
+
771
+ decideMock . mockReturnValue ( {
772
+ ...defaultDecision ,
773
+ enabled : true ,
774
+ variables : { 'foo' : 'bar' }
775
+ } ) ;
776
+ // Simulate the user object changing
777
+ act ( ( ) => {
778
+ userUpdateCallbacks . forEach ( fn => fn ( ) ) ;
779
+ } ) ;
780
+ component . update ( ) ;
781
+ expect ( component . text ( ) ) . toBe ( 'true|{"foo":"bar"}|true|false' ) ;
782
+ } ) ;
783
+
784
+ it ( 'should not re-render when the user attributes change without autoUpdate' , async ( ) => {
785
+ decideMock . mockReturnValue ( { ...defaultDecision } ) ;
786
+ const component = Enzyme . mount (
787
+ < OptimizelyProvider optimizely = { optimizelyMock } >
788
+ < MyDecideComponent />
789
+ </ OptimizelyProvider >
790
+ ) ;
791
+
792
+ // TODO - Wrap this with async act() once we upgrade to React 16.9
793
+ // See https://github.com/facebook/react/issues/15379
794
+ await optimizelyMock . onReady ( ) ;
795
+ component . update ( ) ;
796
+ expect ( component . text ( ) ) . toBe ( 'false|{}|true|false' ) ;
797
+
798
+ decideMock . mockReturnValue ( {
799
+ ...defaultDecision ,
800
+ enabled : true ,
801
+ variables : { 'foo' : 'bar' }
802
+ } ) ;
803
+ // Simulate the user object changing
804
+ act ( ( ) => {
805
+ userUpdateCallbacks . forEach ( fn => fn ( ) ) ;
806
+ } ) ;
807
+ component . update ( ) ;
808
+ expect ( component . text ( ) ) . toBe ( 'false|{}|true|false' ) ;
809
+ } ) ;
810
+
811
+ it ( 'should return the decision immediately on the first call when the client is already ready' , async ( ) => {
812
+ readySuccess = true ;
813
+ decideMock . mockReturnValue ( { ...defaultDecision } ) ;
814
+ const component = Enzyme . mount (
815
+ < OptimizelyProvider optimizely = { optimizelyMock } >
816
+ < UseDecideLoggingComponent />
817
+ </ OptimizelyProvider >
818
+ ) ;
819
+ component . update ( ) ;
820
+ expect ( mockLog ) . toHaveBeenCalledTimes ( 1 ) ;
821
+ expect ( mockLog ) . toHaveBeenCalledWith ( false ) ;
822
+ } ) ;
823
+
824
+ it ( 'should re-render after the client becomes ready' , async ( ) => {
825
+ readySuccess = false ;
826
+ let resolveReadyPromise : ( result : { success : boolean ; dataReadyPromise : Promise < any > } ) => void ;
827
+ const readyPromise : Promise < any > = new Promise ( res => {
828
+ resolveReadyPromise = ( result ) : void => {
829
+ readySuccess = true ;
830
+ res ( result ) ;
831
+ } ;
832
+ } ) ;
833
+ getOnReadyPromise = ( ) : Promise < any > => readyPromise ;
834
+ decideMock . mockReturnValue ( { ...defaultDecision } ) ;
835
+
836
+ const component = Enzyme . mount (
837
+ < OptimizelyProvider optimizely = { optimizelyMock } >
838
+ < UseDecideLoggingComponent />
839
+ </ OptimizelyProvider >
840
+ ) ;
841
+ component . update ( ) ;
842
+
843
+ expect ( mockLog ) . toHaveBeenCalledTimes ( 1 ) ;
844
+ expect ( mockLog ) . toHaveBeenCalledWith ( false ) ;
845
+
846
+ mockLog . mockReset ( ) ;
847
+
848
+ // Simulate datafile fetch completing after timeout has already passed
849
+ // decision now returns true
850
+ decideMock . mockReturnValue ( { ...defaultDecision , enabled : true } ) ;
851
+ // Wait for completion of dataReadyPromise
852
+ const dataReadyPromise = Promise . resolve ( ) ;
853
+ resolveReadyPromise ! ( { success : true , dataReadyPromise } ) ;
854
+ await dataReadyPromise ;
855
+ component . update ( ) ;
856
+
857
+ expect ( mockLog ) . toHaveBeenCalledTimes ( 1 ) ;
858
+ expect ( mockLog ) . toHaveBeenCalledWith ( true ) ;
859
+ } ) ;
860
+
861
+ it ( 'should re-render after updating the override user ID argument' , async ( ) => {
862
+ decideMock . mockReturnValue ( { ...defaultDecision } ) ;
863
+ const component = Enzyme . mount (
864
+ < OptimizelyProvider optimizely = { optimizelyMock } >
865
+ < MyDecideComponent options = { { autoUpdate : true } } />
866
+ </ OptimizelyProvider >
867
+ ) ;
868
+
869
+ component . update ( ) ;
870
+ expect ( component . text ( ) ) . toBe ( 'false|{}|true|false' ) ;
871
+
872
+ decideMock . mockReturnValue ( { ...defaultDecision , enabled : true } ) ;
873
+ component . setProps ( {
874
+ children : < MyDecideComponent options = { { autoUpdate : true } } overrides = { { overrideUserId : 'matt' } } /> ,
875
+ } ) ;
876
+ component . update ( ) ;
877
+ expect ( component . text ( ) ) . toBe ( 'true|{}|true|false' ) ;
878
+ } ) ;
879
+
880
+ it ( 'should re-render after updating the override user attributes argument' , async ( ) => {
881
+ decideMock . mockReturnValue ( { ...defaultDecision } ) ;
882
+ const component = Enzyme . mount (
883
+ < OptimizelyProvider optimizely = { optimizelyMock } >
884
+ < MyDecideComponent options = { { autoUpdate : true } } />
885
+ </ OptimizelyProvider >
886
+ ) ;
887
+
888
+ component . update ( ) ;
889
+ expect ( component . text ( ) ) . toBe ( 'false|{}|true|false' ) ;
890
+
891
+ decideMock . mockReturnValue ( { ...defaultDecision , enabled : true } ) ;
892
+ component . setProps ( {
893
+ children : (
894
+ < MyDecideComponent options = { { autoUpdate : true } } overrides = { { overrideAttributes : { my_attr : 'x' } } } />
895
+ ) ,
896
+ } ) ;
897
+ component . update ( ) ;
898
+ expect ( component . text ( ) ) . toBe ( 'true|{}|true|false' ) ;
899
+
900
+ decideMock . mockReturnValue ( { ...defaultDecision , enabled : false , variables : { myvar : 3 } } ) ;
901
+ component . setProps ( {
902
+ children : (
903
+ < MyDecideComponent
904
+ options = { { autoUpdate : true } }
905
+ overrides = { { overrideAttributes : { my_attr : 'z' , other_attr : 25 } } }
906
+ />
907
+ ) ,
908
+ } ) ;
909
+ component . update ( ) ;
910
+ expect ( component . text ( ) ) . toBe ( 'false|{"myvar":3}|true|false' ) ;
911
+ } ) ;
912
+
913
+ it ( 'should not recompute the decision when passed the same override attributes' , async ( ) => {
914
+ decideMock . mockReturnValue ( { ...defaultDecision } ) ;
915
+ const component = Enzyme . mount (
916
+ < OptimizelyProvider optimizely = { optimizelyMock } >
917
+ < UseDecideLoggingComponent
918
+ options = { { autoUpdate : true } }
919
+ overrides = { { overrideAttributes : { other_attr : 'y' } } }
920
+ />
921
+ </ OptimizelyProvider >
922
+ ) ;
923
+ expect ( decideMock ) . toHaveBeenCalledTimes ( 1 ) ;
924
+ decideMock . mockReset ( ) ;
925
+ component . setProps ( {
926
+ children : (
927
+ < UseDecideLoggingComponent
928
+ options = { { autoUpdate : true } }
929
+ overrides = { { overrideAttributes : { other_attr : 'y' } } }
930
+ />
931
+ ) ,
932
+ } ) ;
933
+ component . update ( ) ;
934
+ expect ( decideMock ) . not . toHaveBeenCalled ( ) ;
935
+ } ) ;
936
+ } ) ;
644
937
} ) ;
0 commit comments