1- jest . mock ( './initLDClient' , ( ) => jest . fn ( ) ) ;
1+ jest . mock ( 'launchdarkly-js-client-sdk' , ( ) => {
2+ const actual = jest . requireActual ( 'launchdarkly-js-client-sdk' ) ;
3+
4+ return {
5+ ...actual ,
6+ initialize : jest . fn ( ) ,
7+ } ;
8+ } ) ;
29jest . mock ( './utils' , ( ) => {
310 const originalModule = jest . requireActual ( './utils' ) ;
411
@@ -10,32 +17,40 @@ jest.mock('./utils', () => {
1017
1118import React from 'react' ;
1219import { render } from '@testing-library/react' ;
13- import { LDContext , LDFlagChangeset , LDOptions } from 'launchdarkly-js-client-sdk' ;
14- import initLDClient from './initLDClient' ;
20+ import { initialize , LDContext , LDFlagChangeset , LDOptions } from 'launchdarkly-js-client-sdk' ;
1521import { AsyncProviderConfig , LDReactOptions } from './types' ;
1622import { Consumer } from './context' ;
1723import asyncWithLDProvider from './asyncWithLDProvider' ;
24+ import wrapperOptions from './wrapperOptions' ;
25+ import { fetchFlags } from './utils' ;
1826
19- const clientSideID = 'deadbeef ' ;
27+ const clientSideID = 'test-client-side-id ' ;
2028const context : LDContext = { key : 'yus' , kind : 'user' , name : 'yus ng' } ;
21- const App = ( ) => < > My App</ > ;
22- const mockInitLDClient = initLDClient as jest . Mock ;
2329const rawFlags = { 'test-flag' : true , 'another-test-flag' : true } ;
24- let mockLDClient : { on : jest . Mock ; off : jest . Mock ; variation : jest . Mock } ;
30+
31+ const App = ( ) => < > My App</ > ;
32+ const mockInitialize = initialize as jest . Mock ;
33+ const mockFetchFlags = fetchFlags as jest . Mock ;
34+ let mockLDClient : { on : jest . Mock ; off : jest . Mock ; variation : jest . Mock ; waitForInitialization : jest . Mock } ;
2535
2636const renderWithConfig = async ( config : AsyncProviderConfig ) => {
2737 const LDProvider = await asyncWithLDProvider ( config ) ;
2838
2939 const { getByText } = render (
3040 < LDProvider >
31- < Consumer > { ( value ) => < span > Received: { JSON . stringify ( value . flags ) } </ span > } </ Consumer >
41+ < Consumer >
42+ { ( value ) => < span > Received: { `Flags: ${ JSON . stringify ( value . flags ) } .\nError: ${ value . error ?. message } .` } </ span > }
43+ </ Consumer >
3244 </ LDProvider > ,
3345 ) ;
3446
3547 return getByText ( / ^ R e c e i v e d : / ) ;
3648} ;
3749
3850describe ( 'asyncWithLDProvider' , ( ) => {
51+ let options : LDOptions ;
52+ let rejectWaitForInitialization : ( ) => void ;
53+
3954 beforeEach ( ( ) => {
4055 mockLDClient = {
4156 on : jest . fn ( ( e : string , cb : ( ) => void ) => {
@@ -44,12 +59,16 @@ describe('asyncWithLDProvider', () => {
4459 off : jest . fn ( ) ,
4560 // tslint:disable-next-line: no-unsafe-any
4661 variation : jest . fn ( ( _ : string , v ) => v ) ,
62+ waitForInitialization : jest . fn ( ) ,
4763 } ;
48-
49- mockInitLDClient . mockImplementation ( ( ) => ( {
50- ldClient : mockLDClient ,
51- flags : rawFlags ,
52- } ) ) ;
64+ mockInitialize . mockImplementation ( ( ) => mockLDClient ) ;
65+ mockFetchFlags . mockImplementation ( ( ) => rawFlags ) ;
66+ rejectWaitForInitialization = ( ) => {
67+ const timeoutError = new Error ( 'waitForInitialization timed out' ) ;
68+ timeoutError . name = 'TimeoutError' ;
69+ mockLDClient . waitForInitialization . mockRejectedValue ( timeoutError ) ;
70+ } ;
71+ options = { bootstrap : { } , ...wrapperOptions } ;
5372 } ) ;
5473
5574 afterEach ( ( ) => {
@@ -67,32 +86,110 @@ describe('asyncWithLDProvider', () => {
6786 expect ( container ) . toMatchSnapshot ( ) ;
6887 } ) ;
6988
89+ test ( 'provider unmounts and unsubscribes correctly' , async ( ) => {
90+ const LDProvider = await asyncWithLDProvider ( { clientSideID } ) ;
91+ const { unmount } = render (
92+ < LDProvider >
93+ < App />
94+ </ LDProvider > ,
95+ ) ;
96+ unmount ( ) ;
97+
98+ expect ( mockLDClient . off ) . toHaveBeenCalledWith ( 'change' , expect . any ( Function ) ) ;
99+ expect ( mockLDClient . off ) . toHaveBeenCalledWith ( 'failed' , expect . any ( Function ) ) ;
100+ expect ( mockLDClient . off ) . toHaveBeenCalledWith ( 'ready' , expect . any ( Function ) ) ;
101+ } ) ;
102+
103+ test ( 'timeout error; provider unmounts and unsubscribes correctly' , async ( ) => {
104+ rejectWaitForInitialization ( ) ;
105+ const LDProvider = await asyncWithLDProvider ( { clientSideID } ) ;
106+ const { unmount } = render (
107+ < LDProvider >
108+ < App />
109+ </ LDProvider > ,
110+ ) ;
111+ unmount ( ) ;
112+
113+ expect ( mockLDClient . off ) . toHaveBeenCalledWith ( 'change' , expect . any ( Function ) ) ;
114+ expect ( mockLDClient . off ) . toHaveBeenCalledWith ( 'failed' , expect . any ( Function ) ) ;
115+ expect ( mockLDClient . off ) . toHaveBeenCalledWith ( 'ready' , expect . any ( Function ) ) ;
116+ } ) ;
117+
118+ test ( 'waitForInitialization error (not timeout)' , async ( ) => {
119+ mockLDClient . waitForInitialization . mockRejectedValue ( new Error ( 'TestError' ) ) ;
120+ const receivedNode = await renderWithConfig ( { clientSideID } ) ;
121+
122+ expect ( receivedNode ) . toHaveTextContent ( 'TestError' ) ;
123+ expect ( mockLDClient . on ) . not . toHaveBeenCalledWith ( 'ready' , expect . any ( Function ) ) ;
124+ expect ( mockLDClient . on ) . not . toHaveBeenCalledWith ( 'failed' , expect . any ( Function ) ) ;
125+ } ) ;
126+
127+ test ( 'subscribe to ready and failed events if waitForInitialization timed out' , async ( ) => {
128+ rejectWaitForInitialization ( ) ;
129+ const LDProvider = await asyncWithLDProvider ( { clientSideID } ) ;
130+ render (
131+ < LDProvider >
132+ < App />
133+ </ LDProvider > ,
134+ ) ;
135+
136+ expect ( mockLDClient . on ) . toHaveBeenCalledWith ( 'ready' , expect . any ( Function ) ) ;
137+ expect ( mockLDClient . on ) . toHaveBeenCalledWith ( 'failed' , expect . any ( Function ) ) ;
138+ } ) ;
139+
140+ test ( 'ready handler should update flags' , async ( ) => {
141+ mockLDClient . on . mockImplementation ( ( e : string , cb : ( ) => void ) => {
142+ // focus only on the ready handler and ignore other change and failed.
143+ if ( e === 'ready' ) {
144+ cb ( ) ;
145+ }
146+ } ) ;
147+ rejectWaitForInitialization ( ) ;
148+ const receivedNode = await renderWithConfig ( { clientSideID } ) ;
149+
150+ expect ( mockLDClient . on ) . toHaveBeenCalledWith ( 'ready' , expect . any ( Function ) ) ;
151+ expect ( receivedNode ) . toHaveTextContent ( '{"testFlag":true,"anotherTestFlag":true}' ) ;
152+ } ) ;
153+
154+ test ( 'failed handler should update error' , async ( ) => {
155+ mockLDClient . on . mockImplementation ( ( e : string , cb : ( e : Error ) => void ) => {
156+ // focus only on the ready handler and ignore other change and failed.
157+ if ( e === 'failed' ) {
158+ cb ( new Error ( 'Test sdk failure' ) ) ;
159+ }
160+ } ) ;
161+ rejectWaitForInitialization ( ) ;
162+ const receivedNode = await renderWithConfig ( { clientSideID } ) ;
163+
164+ expect ( mockLDClient . on ) . toHaveBeenCalledWith ( 'ready' , expect . any ( Function ) ) ;
165+ expect ( receivedNode ) . toHaveTextContent ( '{}' ) ;
166+ expect ( receivedNode ) . toHaveTextContent ( 'Error: Test sdk failure' ) ;
167+ } ) ;
168+
70169 test ( 'ldClient is initialised correctly' , async ( ) => {
71- const options : LDOptions = { bootstrap : { } } ;
72170 const reactOptions : LDReactOptions = { useCamelCaseFlagKeys : false } ;
73171 await asyncWithLDProvider ( { clientSideID, context, options, reactOptions } ) ;
74172
75- expect ( mockInitLDClient ) . toHaveBeenCalledWith ( clientSideID , context , options , undefined ) ;
173+ expect ( mockInitialize ) . toHaveBeenCalledWith ( clientSideID , context , options ) ;
76174 } ) ;
77175
78176 test ( 'ld client is initialised correctly with deprecated user object' , async ( ) => {
79177 const user : LDContext = { key : 'deprecatedUser' } ;
80- const options : LDOptions = { bootstrap : { } } ;
81178 const reactOptions : LDReactOptions = { useCamelCaseFlagKeys : false } ;
82179 await asyncWithLDProvider ( { clientSideID, user, options, reactOptions } ) ;
83- expect ( mockInitLDClient ) . toHaveBeenCalledWith ( clientSideID , user , options , undefined ) ;
180+
181+ expect ( mockInitialize ) . toHaveBeenCalledWith ( clientSideID , user , options ) ;
84182 } ) ;
85183
86184 test ( 'use context ignore user at init if both are present' , async ( ) => {
87185 const user : LDContext = { key : 'deprecatedUser' } ;
88- const options : LDOptions = { bootstrap : { } } ;
89186 const reactOptions : LDReactOptions = { useCamelCaseFlagKeys : false } ;
90187
91188 // this should not happen in real usage. Only one of context or user should be specified.
92189 // if both are specified, context will be used and user ignored.
93190 await asyncWithLDProvider ( { clientSideID, context, user, options, reactOptions } ) ;
94191
95- expect ( mockInitLDClient ) . toHaveBeenCalledWith ( clientSideID , context , options , undefined ) ;
192+ expect ( mockInitialize ) . toHaveBeenCalledWith ( clientSideID , context , options ) ;
96193 } ) ;
97194
98195 test ( 'subscribe to changes on mount' , async ( ) => {
@@ -114,13 +211,10 @@ describe('asyncWithLDProvider', () => {
114211
115212 expect ( mockLDClient . on ) . toHaveBeenNthCalledWith ( 1 , 'change' , expect . any ( Function ) ) ;
116213 expect ( receivedNode ) . toHaveTextContent ( '{"testFlag":false,"anotherTestFlag":true}' ) ;
214+ expect ( receivedNode ) . toHaveTextContent ( 'Error: undefined' ) ;
117215 } ) ;
118216
119217 test ( 'subscribe to changes with kebab-case' , async ( ) => {
120- mockInitLDClient . mockImplementation ( ( ) => ( {
121- ldClient : mockLDClient ,
122- flags : rawFlags ,
123- } ) ) ;
124218 mockLDClient . on . mockImplementation ( ( e : string , cb : ( c : LDFlagChangeset ) => void ) => {
125219 cb ( { 'another-test-flag' : { current : false , previous : true } , 'test-flag' : { current : false , previous : true } } ) ;
126220 } ) ;
@@ -149,7 +243,7 @@ describe('asyncWithLDProvider', () => {
149243 mockLDClient . on . mockImplementation ( ( e : string , cb : ( c : LDFlagChangeset ) => void ) => {
150244 return ;
151245 } ) ;
152- const options : LDOptions = {
246+ options = {
153247 bootstrap : {
154248 'another-test-flag' : false ,
155249 'test-flag' : true ,
@@ -159,12 +253,37 @@ describe('asyncWithLDProvider', () => {
159253 expect ( receivedNode ) . toHaveTextContent ( '{"anotherTestFlag":false,"testFlag":true}' ) ;
160254 } ) ;
161255
256+ test ( 'undefined bootstrap' , async ( ) => {
257+ mockLDClient . on . mockImplementation ( ( e : string , cb : ( c : LDFlagChangeset ) => void ) => {
258+ return ;
259+ } ) ;
260+ options = { ...options , bootstrap : undefined } ;
261+ mockFetchFlags . mockReturnValueOnce ( { aNewFlag : true } ) ;
262+ const receivedNode = await renderWithConfig ( { clientSideID, context, options } ) ;
263+
264+ expect ( mockFetchFlags ) . toHaveBeenCalledTimes ( 1 ) ;
265+ expect ( receivedNode ) . toHaveTextContent ( '{"aNewFlag":true}' ) ;
266+ } ) ;
267+
268+ test ( 'bootstrap used if there is a timeout' , async ( ) => {
269+ mockLDClient . on . mockImplementation ( ( e : string , cb : ( c : LDFlagChangeset ) => void ) => {
270+ return ;
271+ } ) ;
272+ rejectWaitForInitialization ( ) ;
273+ options = { ...options , bootstrap : { myBootstrap : true } } ;
274+ const receivedNode = await renderWithConfig ( { clientSideID, context, options } ) ;
275+
276+ expect ( mockFetchFlags ) . not . toHaveBeenCalled ( ) ;
277+ expect ( receivedNode ) . toHaveTextContent ( '{"myBootstrap":true}' ) ;
278+ expect ( receivedNode ) . toHaveTextContent ( 'timed out' ) ;
279+ } ) ;
280+
162281 test ( 'ldClient bootstraps with empty flags' , async ( ) => {
163282 // don't subscribe to changes to test bootstrap
164283 mockLDClient . on . mockImplementation ( ( e : string , cb : ( c : LDFlagChangeset ) => void ) => {
165284 return ;
166285 } ) ;
167- const options : LDOptions = {
286+ options = {
168287 bootstrap : { } ,
169288 } ;
170289 const receivedNode = await renderWithConfig ( { clientSideID, context, options } ) ;
@@ -176,7 +295,7 @@ describe('asyncWithLDProvider', () => {
176295 mockLDClient . on . mockImplementation ( ( e : string , cb : ( c : LDFlagChangeset ) => void ) => {
177296 return ;
178297 } ) ;
179- const options : LDOptions = {
298+ options = {
180299 bootstrap : {
181300 'another-test-flag' : false ,
182301 'test-flag' : true ,
@@ -192,36 +311,27 @@ describe('asyncWithLDProvider', () => {
192311 } ) ;
193312
194313 test ( 'internal flags state should be initialised to all flags' , async ( ) => {
195- const options : LDOptions = {
314+ options = {
196315 bootstrap : 'localStorage' ,
197316 } ;
198317 const receivedNode = await renderWithConfig ( { clientSideID, context, options } ) ;
199318 expect ( receivedNode ) . toHaveTextContent ( '{"testFlag":true,"anotherTestFlag":true}' ) ;
200319 } ) ;
201320
202321 test ( 'ldClient is initialised correctly with target flags' , async ( ) => {
203- mockInitLDClient . mockImplementation ( ( ) => ( {
204- ldClient : mockLDClient ,
205- flags : rawFlags ,
206- } ) ) ;
207-
208- const options : LDOptions = { } ;
322+ options = { ...wrapperOptions } ;
209323 const flags = { 'test-flag' : false } ;
210324 const receivedNode = await renderWithConfig ( { clientSideID, context, options, flags } ) ;
211325
212- expect ( mockInitLDClient ) . toHaveBeenCalledWith ( clientSideID , context , options , flags ) ;
326+ expect ( mockInitialize ) . toHaveBeenCalledWith ( clientSideID , context , options ) ;
213327 expect ( receivedNode ) . toHaveTextContent ( '{"testFlag":true}' ) ;
214328 } ) ;
215329
216330 test ( 'only updates to subscribed flags are pushed to the Provider' , async ( ) => {
217- mockInitLDClient . mockImplementation ( ( ) => ( {
218- ldClient : mockLDClient ,
219- flags : rawFlags ,
220- } ) ) ;
221331 mockLDClient . on . mockImplementation ( ( e : string , cb : ( c : LDFlagChangeset ) => void ) => {
222332 cb ( { 'test-flag' : { current : false , previous : true } , 'another-test-flag' : { current : false , previous : true } } ) ;
223333 } ) ;
224- const options : LDOptions = { } ;
334+ options = { } ;
225335 const subscribedFlags = { 'test-flag' : true } ;
226336 const receivedNode = await renderWithConfig ( { clientSideID, context, options, flags : subscribedFlags } ) ;
227337
0 commit comments