@@ -228,7 +228,7 @@ describe('OptimizelyProvider', () => {
228228 } ) ;
229229
230230 describe ( 'cleanup' , ( ) => {
231- it ( 'should reset store on unmount' , async ( ) => {
231+ it ( 'should not reset store on unmount (store becomes unreachable to React tree) ' , async ( ) => {
232232 const mockClient = createMockClient ( ) ;
233233 let capturedContext : OptimizelyContextValue | null = null ;
234234
@@ -246,8 +246,11 @@ describe('OptimizelyProvider', () => {
246246
247247 unmount ( ) ;
248248
249- // Store should be reset
250- expect ( store . getState ( ) . userContext ) . toBeNull ( ) ;
249+ // Store state is preserved — on real unmount, the store becomes
250+ // unreachable to the React tree. Eagerly resetting breaks React
251+ // 18+ StrictMode (effect cleanup destroys state that the render
252+ // body set, and no re-render restores it).
253+ expect ( store . getState ( ) . userContext ) . not . toBeNull ( ) ;
251254 expect ( store . getState ( ) . error ) . toBeNull ( ) ;
252255 } ) ;
253256 } ) ;
@@ -391,7 +394,7 @@ describe('OptimizelyProvider', () => {
391394 expect ( mockClient2 . createUserContext ) . toHaveBeenCalledWith ( 'user-1' , undefined ) ;
392395 } ) ;
393396
394- it ( 'should dispose manager on unmount' , async ( ) => {
397+ it ( 'should preserve store state on unmount (no eager reset) ' , async ( ) => {
395398 const mockClient = createMockClient ( ) ;
396399 let capturedContext : OptimizelyContextValue | null = null ;
397400
@@ -406,8 +409,9 @@ describe('OptimizelyProvider', () => {
406409
407410 unmount ( ) ;
408411
409- // Store should be reset after unmount
410- expect ( capturedContext ! . store . getState ( ) . userContext ) . toBeNull ( ) ;
412+ // Store state is preserved after unmount — no eager reset.
413+ // The store becomes unreachable to the React tree.
414+ expect ( capturedContext ! . store . getState ( ) . userContext ) . not . toBeNull ( ) ;
411415 } ) ;
412416
413417 it ( 'should recreate user context when only attributes change (same id)' , async ( ) => {
@@ -634,7 +638,7 @@ describe('OptimizelyProvider', () => {
634638 } ) ;
635639
636640 describe ( 'config update subscription' , ( ) => {
637- it ( 'should subscribe to OPTIMIZELY_CONFIG_UPDATE on mount' , ( ) => {
641+ it ( 'should subscribe to OPTIMIZELY_CONFIG_UPDATE after onReady' , async ( ) => {
638642 const mockClient = createMockClient ( ) ;
639643
640644 render (
@@ -643,13 +647,15 @@ describe('OptimizelyProvider', () => {
643647 </ OptimizelyProvider >
644648 ) ;
645649
646- expect ( mockClient . notificationCenter . addNotificationListener ) . toHaveBeenCalledWith (
647- 'OPTIMIZELY_CONFIG_UPDATE' ,
648- expect . any ( Function )
649- ) ;
650+ await waitFor ( ( ) => {
651+ expect ( mockClient . notificationCenter . addNotificationListener ) . toHaveBeenCalledWith (
652+ 'OPTIMIZELY_CONFIG_UPDATE' ,
653+ expect . any ( Function )
654+ ) ;
655+ } ) ;
650656 } ) ;
651657
652- it ( 'should remove notification listener on unmount' , ( ) => {
658+ it ( 'should remove notification listener on unmount' , async ( ) => {
653659 const mockClient = createMockClient ( ) ;
654660
655661 const { unmount } = render (
@@ -658,6 +664,10 @@ describe('OptimizelyProvider', () => {
658664 </ OptimizelyProvider >
659665 ) ;
660666
667+ await waitFor ( ( ) => {
668+ expect ( mockClient . notificationCenter . addNotificationListener ) . toHaveBeenCalledTimes ( 1 ) ;
669+ } ) ;
670+
661671 unmount ( ) ;
662672
663673 expect ( mockClient . notificationCenter . removeNotificationListener ) . toHaveBeenCalledWith ( 1 ) ;
@@ -675,6 +685,7 @@ describe('OptimizelyProvider', () => {
675685
676686 await waitFor ( ( ) => {
677687 expect ( capturedContext ) . not . toBeNull ( ) ;
688+ expect ( mockClient . notificationCenter . addNotificationListener ) . toHaveBeenCalledTimes ( 1 ) ;
678689 } ) ;
679690
680691 const stateBefore = capturedContext ! . store . getState ( ) ;
@@ -694,7 +705,7 @@ describe('OptimizelyProvider', () => {
694705 expect ( stateBefore ) . not . toBe ( stateAfter ) ;
695706 } ) ;
696707
697- it ( 'should re-subscribe when client changes' , ( ) => {
708+ it ( 'should re-subscribe when client changes' , async ( ) => {
698709 const mockClient1 = createMockClient ( ) ;
699710 const mockClient2 = createMockClient ( ) ;
700711
@@ -704,17 +715,81 @@ describe('OptimizelyProvider', () => {
704715 </ OptimizelyProvider >
705716 ) ;
706717
707- expect ( mockClient1 . notificationCenter . addNotificationListener ) . toHaveBeenCalledTimes ( 1 ) ;
718+ await waitFor ( ( ) => {
719+ expect ( mockClient1 . notificationCenter . addNotificationListener ) . toHaveBeenCalledTimes ( 1 ) ;
720+ } ) ;
708721
709722 rerender (
710723 < OptimizelyProvider client = { mockClient2 } >
711724 < div > Child</ div >
712725 </ OptimizelyProvider >
713726 ) ;
714727
715- // Old listener cleaned up, new one registered
728+ await waitFor ( ( ) => {
729+ expect ( mockClient2 . notificationCenter . addNotificationListener ) . toHaveBeenCalledTimes ( 1 ) ;
730+ } ) ;
731+
732+ // Old listener cleaned up
716733 expect ( mockClient1 . notificationCenter . removeNotificationListener ) . toHaveBeenCalledWith ( 1 ) ;
717- expect ( mockClient2 . notificationCenter . addNotificationListener ) . toHaveBeenCalledTimes ( 1 ) ;
734+ } ) ;
735+ } ) ;
736+
737+ describe ( 'config revision race condition' , ( ) => {
738+ it ( 'should refresh when config revision changes between render and onReady' , async ( ) => {
739+ let resolveOnReady : ( ) => void ;
740+ const onReadyPromise = new Promise < void > ( ( resolve ) => {
741+ resolveOnReady = resolve ;
742+ } ) ;
743+
744+ const mockClient = createMockClient ( {
745+ getOptimizelyConfig : vi . fn ( ) . mockReturnValue ( { revision : '1' } ) ,
746+ onReady : vi . fn ( ) . mockReturnValue ( onReadyPromise ) ,
747+ } ) ;
748+ let capturedContext : OptimizelyContextValue | null = null ;
749+
750+ render (
751+ < OptimizelyProvider client = { mockClient } >
752+ < ContextConsumer onContext = { ( ctx ) => ( capturedContext = ctx ) } />
753+ </ OptimizelyProvider >
754+ ) ;
755+
756+ expect ( capturedContext ) . not . toBeNull ( ) ;
757+ const stateBefore = capturedContext ! . store . getState ( ) ;
758+
759+ // Config updates to v2 before onReady resolves
760+ ( mockClient . getOptimizelyConfig as ReturnType < typeof vi . fn > ) . mockReturnValue ( { revision : '2' } ) ;
761+
762+ await act ( async ( ) => {
763+ resolveOnReady ! ( ) ;
764+ } ) ;
765+
766+ const stateAfter = capturedContext ! . store . getState ( ) ;
767+ expect ( stateBefore ) . not . toBe ( stateAfter ) ;
768+ } ) ;
769+
770+ it ( 'should not refresh when config revision is unchanged at onReady' , async ( ) => {
771+ const mockClient = createMockClient ( {
772+ getOptimizelyConfig : vi . fn ( ) . mockReturnValue ( { revision : '1' } ) ,
773+ } ) ;
774+ let capturedContext : OptimizelyContextValue | null = null ;
775+
776+ render (
777+ < OptimizelyProvider client = { mockClient } >
778+ < ContextConsumer onContext = { ( ctx ) => ( capturedContext = ctx ) } />
779+ </ OptimizelyProvider >
780+ ) ;
781+
782+ await waitFor ( ( ) => {
783+ expect ( capturedContext ) . not . toBeNull ( ) ;
784+ } ) ;
785+
786+ const stateBefore = capturedContext ! . store . getState ( ) ;
787+
788+ // Flush onReady microtask — revision is still '1'
789+ await act ( async ( ) => { } ) ;
790+
791+ const stateAfter = capturedContext ! . store . getState ( ) ;
792+ expect ( stateBefore ) . toBe ( stateAfter ) ;
718793 } ) ;
719794 } ) ;
720795
0 commit comments