Skip to content

Commit 338d85a

Browse files
authored
feat: Implement timeout (#148)
1 parent 9ea35bd commit 338d85a

File tree

9 files changed

+465
-330
lines changed

9 files changed

+465
-330
lines changed

src/asyncWithLDProvider.test.tsx

Lines changed: 149 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
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+
});
29
jest.mock('./utils', () => {
310
const originalModule = jest.requireActual('./utils');
411

@@ -10,32 +17,40 @@ jest.mock('./utils', () => {
1017

1118
import React from 'react';
1219
import { 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';
1521
import { AsyncProviderConfig, LDReactOptions } from './types';
1622
import { Consumer } from './context';
1723
import 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';
2028
const context: LDContext = { key: 'yus', kind: 'user', name: 'yus ng' };
21-
const App = () => <>My App</>;
22-
const mockInitLDClient = initLDClient as jest.Mock;
2329
const 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

2636
const 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(/^Received:/);
3648
};
3749

3850
describe('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

Comments
 (0)