@@ -20,10 +20,21 @@ import { act } from 'react-dom/test-utils';
2020
2121import { OptimizelyProvider } from './Provider' ;
2222import { OnReadyResult , ReactSDKClient , VariableValuesObject } from './client' ;
23- import { useExperiment , useFeature } from './hooks' ;
23+ import { useExperiment , useFeature , useDecide } from './hooks' ;
24+ import { OptimizelyDecision } from './utils' ;
2425
2526Enzyme . configure ( { adapter : new Adapter ( ) } ) ;
2627
28+ const defaultDecision : OptimizelyDecision = {
29+ enabled : false ,
30+ variables : { } ,
31+ flagKey : '' ,
32+ reasons : [ ] ,
33+ ruleKey : '' ,
34+ userContext : { id : null } ,
35+ variationKey : '' ,
36+ } ;
37+
2738const MyFeatureComponent = ( { options = { } , overrides = { } } : any ) => {
2839 const [ isEnabled , variables , clientReady , didTimeout ] = useFeature ( 'feature1' , { ...options } , { ...overrides } ) ;
2940 return < > { `${ isEnabled ? 'true' : 'false' } |${ JSON . stringify ( variables ) } |${ clientReady } |${ didTimeout } ` } </ > ;
@@ -34,6 +45,11 @@ const MyExperimentComponent = ({ options = {}, overrides = {} }: any) => {
3445 return < > { `${ variation } |${ clientReady } |${ didTimeout } ` } </ > ;
3546} ;
3647
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+
3753const mockFeatureVariables : VariableValuesObject = {
3854 foo : 'bar' ,
3955} ;
@@ -50,8 +66,10 @@ describe('hooks', () => {
5066 let userUpdateCallbacks : Array < ( ) => void > ;
5167 let UseExperimentLoggingComponent : React . FunctionComponent < any > ;
5268 let UseFeatureLoggingComponent : React . FunctionComponent < any > ;
69+ let UseDecideLoggingComponent : React . FunctionComponent < any > ;
5370 let mockLog : jest . Mock ;
5471 let forcedVariationUpdateCallbacks : Array < ( ) => void > ;
72+ let decideMock : jest . Mock < OptimizelyDecision > ;
5573
5674 beforeEach ( ( ) => {
5775 getOnReadyPromise = ( { timeout = 0 } : any ) : Promise < OnReadyResult > =>
@@ -78,6 +96,7 @@ describe('hooks', () => {
7896 readySuccess = true ;
7997 notificationListenerCallbacks = [ ] ;
8098 forcedVariationUpdateCallbacks = [ ] ;
99+ decideMock = jest . fn ( ) ;
81100
82101 optimizelyMock = ( {
83102 activate : activateMock ,
@@ -104,6 +123,7 @@ describe('hooks', () => {
104123 return ( ) => { } ;
105124 } ) ,
106125 getForcedVariations : jest . fn ( ) . mockReturnValue ( { } ) ,
126+ decide : decideMock ,
107127 } as unknown ) as ReactSDKClient ;
108128
109129 mockLog = jest . fn ( ) ;
@@ -118,6 +138,12 @@ describe('hooks', () => {
118138 mockLog ( isEnabled ) ;
119139 return < div > { isEnabled } </ div > ;
120140 } ;
141+
142+ UseDecideLoggingComponent = ( { options = { } , overrides = { } } : any ) => {
143+ const [ decision ] = useDecide ( 'feature1' , { ...options } , { ...overrides } ) ;
144+ mockLog ( decision . enabled ) ;
145+ return < div > { decision . enabled } </ div > ;
146+ } ;
121147 } ) ;
122148
123149 afterEach ( async ( ) => {
@@ -641,4 +667,271 @@ describe('hooks', () => {
641667 expect ( isFeatureEnabledMock ) . not . toHaveBeenCalled ( ) ;
642668 } ) ;
643669 } ) ;
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+ } ) ;
644937} ) ;
0 commit comments