Skip to content

Commit 4fcea59

Browse files
authored
fix(hooks): Always recompute decision after resolution of ready promise (#91)
Summary: Fix the following bug: when client is initialized with datafile, and no SDK key, OptimizelyExperiment and OptimizelyFeature components don't properly render their decisions. The issue is in the implementation in hooks and the interaction with the client's ready state (via isReady/onReady). When initialized with datafile, the client begins in a non-ready state, but immediately becomes ready on the next tick. But the hook does not recompute the decision when this occurs - it's only recomputing the decision when a CONFIG_UPDATE is fired. CONFIG_UPDATE is never fired when the instance was initialized with datafile only. To fix, add an effect in the useFeature/useExperiment hooks that recomputes the decision after the client becomes ready. Test plan: - Manual testing - Updated unit tests
1 parent 097e675 commit 4fcea59

File tree

4 files changed

+149
-125
lines changed

4 files changed

+149
-125
lines changed

src/Experiment.spec.tsx

+8-16
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,6 @@ describe('<OptimizelyExperiment>', () => {
7676

7777
// Simulate client becoming ready: onReady resolving, firing config update notification
7878
resolver.resolve({ success: true });
79-
(optimizelyMock.notificationCenter.addNotificationListener as jest.Mock).mock.calls[0][1]();
8079

8180
await optimizelyMock.onReady();
8281

@@ -101,7 +100,6 @@ describe('<OptimizelyExperiment>', () => {
101100

102101
// Simulate client becoming ready: onReady resolving, firing config update notification
103102
resolver.resolve({ success: true });
104-
(optimizelyMock.notificationCenter.addNotificationListener as jest.Mock).mock.calls[0][1]();
105103

106104
await optimizelyMock.onReady();
107105

@@ -121,9 +119,8 @@ describe('<OptimizelyExperiment>', () => {
121119
// while it's waiting for onReady()
122120
expect(component.text()).toBe('');
123121

124-
// Simulate client becoming ready: onReady resolving, firing config update notification
122+
// Simulate client becoming ready
125123
resolver.resolve({ success: true });
126-
(optimizelyMock.notificationCenter.addNotificationListener as jest.Mock).mock.calls[0][1]();
127124

128125
await optimizelyMock.onReady();
129126

@@ -144,9 +141,8 @@ describe('<OptimizelyExperiment>', () => {
144141
// while it's waiting for onReady()
145142
expect(component.text()).toBe('');
146143

147-
// Simulate client becoming ready: onReady resolving, firing config update notification
144+
// Simulate client becoming ready
148145
resolver.resolve({ success: true });
149-
(optimizelyMock.notificationCenter.addNotificationListener as jest.Mock).mock.calls[0][1]();
150146

151147
await optimizelyMock.onReady();
152148

@@ -212,9 +208,8 @@ describe('<OptimizelyExperiment>', () => {
212208
// while it's waiting for onReady()
213209
expect(component.text()).toBe('');
214210

215-
// Simulate client becoming ready: onReady resolving, firing config update notification
211+
// Simulate client becoming ready
216212
resolver.resolve({ success: true });
217-
(optimizelyMock.notificationCenter.addNotificationListener as jest.Mock).mock.calls[0][1]();
218213

219214
await optimizelyMock.onReady();
220215

@@ -237,9 +232,8 @@ describe('<OptimizelyExperiment>', () => {
237232
// while it's waiting for onReady()
238233
expect(component.text()).toBe('');
239234

240-
// Simulate client becoming ready: onReady resolving, firing config update notification
235+
// Simulate client becoming ready
241236
resolver.resolve({ success: true });
242-
(optimizelyMock.notificationCenter.addNotificationListener as jest.Mock).mock.calls[0][1]();
243237

244238
await optimizelyMock.onReady();
245239

@@ -285,9 +279,8 @@ describe('<OptimizelyExperiment>', () => {
285279
// while it's waiting for onReady()
286280
expect(component.text()).toBe('');
287281

288-
// Simulate client becoming ready: onReady resolving, firing config update notification
282+
// Simulate client becoming ready
289283
resolver.resolve({ success: true });
290-
(optimizelyMock.notificationCenter.addNotificationListener as jest.Mock).mock.calls[0][1]();
291284

292285
await optimizelyMock.onReady();
293286

@@ -298,11 +291,11 @@ describe('<OptimizelyExperiment>', () => {
298291
expect(component.text()).toBe('variationResult');
299292

300293
// capture the OPTIMIZELY_CONFIG_UPDATE function
301-
const updateFn = (optimizelyMock.notificationCenter.addNotificationListener as jest.Mock).mock.calls[0][1];
302294
// change the return value of activate
303295
const mockActivate = optimizelyMock.activate as jest.Mock;
304296
mockActivate.mockImplementationOnce(() => 'newVariation');
305297

298+
const updateFn = (optimizelyMock.notificationCenter.addNotificationListener as jest.Mock).mock.calls[0][1];
306299
updateFn();
307300
expect(optimizelyMock.activate).toBeCalledTimes(2);
308301

@@ -324,10 +317,9 @@ describe('<OptimizelyExperiment>', () => {
324317
expect(optimizelyMock.onReady).toHaveBeenCalledWith({ timeout: 100 });
325318
// while it's waiting for onReady()
326319
expect(component.text()).toBe('');
327-
resolver.resolve({ success: true });
328320

329-
// Simulate config update update notification firing after datafile becomes available.
330-
(optimizelyMock.notificationCenter.addNotificationListener as jest.Mock).mock.calls[0][1]();
321+
// Simulate client becoming ready
322+
resolver.resolve({ success: true });
331323

332324
await optimizelyMock.onReady();
333325

src/Feature.spec.tsx

+1-9
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,6 @@ describe('<OptimizelyFeature>', () => {
7979

8080
// Simulate client becoming ready
8181
resolver.resolve({ success: true });
82-
(optimizelyMock.notificationCenter.addNotificationListener as jest.Mock).mock.calls[0][1]();
8382

8483
await optimizelyMock.onReady();
8584

@@ -106,7 +105,6 @@ describe('<OptimizelyFeature>', () => {
106105

107106
// Simulate client becoming ready
108107
resolver.resolve({ success: true });
109-
(optimizelyMock.notificationCenter.addNotificationListener as jest.Mock).mock.calls[0][1]();
110108

111109
await optimizelyMock.onReady();
112110

@@ -133,7 +131,6 @@ describe('<OptimizelyFeature>', () => {
133131

134132
// Simulate client becoming ready
135133
resolver.resolve({ success: true });
136-
(optimizelyMock.notificationCenter.addNotificationListener as jest.Mock).mock.calls[0][1]();
137134

138135
await optimizelyMock.onReady();
139136

@@ -160,7 +157,6 @@ describe('<OptimizelyFeature>', () => {
160157

161158
// Simulate client becoming ready
162159
resolver.resolve({ success: true });
163-
(optimizelyMock.notificationCenter.addNotificationListener as jest.Mock).mock.calls[0][1]();
164160

165161
await optimizelyMock.onReady();
166162

@@ -185,7 +181,6 @@ describe('<OptimizelyFeature>', () => {
185181

186182
// Simulate client becoming ready
187183
resolver.resolve({ success: true });
188-
(optimizelyMock.notificationCenter.addNotificationListener as jest.Mock).mock.calls[0][1]();
189184

190185
await optimizelyMock.onReady();
191186

@@ -212,9 +207,7 @@ describe('<OptimizelyFeature>', () => {
212207
expect(component.text()).toBe('');
213208

214209
// Simulate client becoming ready
215-
const updateFn = (optimizelyMock.notificationCenter.addNotificationListener as jest.Mock).mock.calls[0][1];
216210
resolver.resolve({ success: true });
217-
updateFn();
218211

219212
await optimizelyMock.onReady();
220213

@@ -232,6 +225,7 @@ describe('<OptimizelyFeature>', () => {
232225
foo: 'baz',
233226
}));
234227

228+
const updateFn = (optimizelyMock.notificationCenter.addNotificationListener as jest.Mock).mock.calls[0][1];
235229
updateFn();
236230

237231
component.update();
@@ -257,7 +251,6 @@ describe('<OptimizelyFeature>', () => {
257251
expect(component.text()).toBe('');
258252

259253
// Simulate client becoming ready
260-
(optimizelyMock.notificationCenter.addNotificationListener as jest.Mock).mock.calls[0][1]();
261254
resolver.resolve({ success: true });
262255

263256
await optimizelyMock.onReady();
@@ -304,7 +297,6 @@ describe('<OptimizelyFeature>', () => {
304297
resolver.resolve({ success: false, reason: 'fail', dataReadyPromise: Promise.resolve() });
305298

306299
// Simulate config update notification firing after datafile fetched
307-
(optimizelyMock.notificationCenter.addNotificationListener as jest.Mock).mock.calls[0][1]();
308300
await optimizelyMock.onReady().then(res => res.dataReadyPromise);
309301

310302
component.update();

src/hooks.spec.tsx

+42-16
Original file line numberDiff line numberDiff line change
@@ -147,20 +147,24 @@ describe('hooks', () => {
147147
});
148148

149149
it('should respect the timeout option passed', async () => {
150-
activateMock.mockReturnValue('12345');
150+
activateMock.mockReturnValue(null);
151151
readySuccess = false;
152+
152153
const component = Enzyme.mount(
153154
<OptimizelyProvider optimizely={optimizelyMock}>
154155
<MyExperimentComponent options={{ timeout: mockDelay }} />
155156
</OptimizelyProvider>
156157
);
157158
expect(component.text()).toBe('null|false|false'); // initial render
159+
158160
await optimizelyMock.onReady();
159161
component.update();
160162
expect(component.text()).toBe('null|false|true'); // when didTimeout
161-
readySuccess = true;
162-
// Simulate CONFIG_UPDATE notification, causing decision state to recompute
163-
notificationListenerCallbacks[0]();
163+
164+
// Simulate datafile fetch completing after timeout has already passed
165+
// Activate now returns a variation
166+
activateMock.mockReturnValue('12345');
167+
// Wait for completion of dataReadyPromise
164168
await optimizelyMock.onReady().then(res => res.dataReadyPromise);
165169
component.update();
166170
expect(component.text()).toBe('12345|true|true'); // when clientReady
@@ -243,7 +247,7 @@ describe('hooks', () => {
243247
expect(mockLog).toHaveBeenCalledWith('12345');
244248
});
245249

246-
it('should re-render after the client becomes ready and triggers a config update notification', async () => {
250+
it('should re-render after the client becomes ready', async () => {
247251
readySuccess = false;
248252
let resolveReadyPromise: (result: { success: boolean; dataReadyPromise: Promise<any> }) => void;
249253
const readyPromise: Promise<any> = new Promise(res => {
@@ -266,10 +270,14 @@ describe('hooks', () => {
266270
expect(mockLog).toHaveBeenCalledWith(null);
267271

268272
mockLog.mockReset();
273+
274+
// Simulate datafile fetch completing after timeout has already passed
275+
// Activate now returns a variation
269276
activateMock.mockReturnValue('12345');
270-
// Simulate CONFIG_UPDATE notification, causing decision state to recompute
271-
notificationListenerCallbacks[0]();
272-
resolveReadyPromise!({ success: true, dataReadyPromise: Promise.resolve() });
277+
// Wait for completion of dataReadyPromise
278+
const dataReadyPromise = Promise.resolve();
279+
resolveReadyPromise!({ success: true, dataReadyPromise });
280+
await dataReadyPromise;
273281
component.update();
274282

275283
expect(mockLog).toHaveBeenCalledTimes(1);
@@ -380,22 +388,36 @@ describe('hooks', () => {
380388
});
381389

382390
it('should respect the timeout option passed', async () => {
383-
isFeatureEnabledMock.mockReturnValue(true);
391+
isFeatureEnabledMock.mockReturnValue(false);
392+
featureVariables = {};
384393
readySuccess = false;
394+
385395
const component = Enzyme.mount(
386396
<OptimizelyProvider optimizely={optimizelyMock}>
387397
<MyFeatureComponent options={{ timeout: mockDelay }} />
388398
</OptimizelyProvider>
389399
);
390400
expect(component.text()).toBe('false|{}|false|false'); // initial render
401+
391402
await optimizelyMock.onReady();
392403
component.update();
393404
expect(component.text()).toBe('false|{}|false|true'); // when didTimeout
394-
readySuccess = true;
395-
// Simulate CONFIG_UPDATE notification, causing decision state to recompute
396-
notificationListenerCallbacks[0]();
405+
406+
// Simulate datafile fetch completing after timeout has already passed
407+
// isFeatureEnabled now returns true, getFeatureVariables returns variable values
408+
isFeatureEnabledMock.mockReturnValue(true);
409+
featureVariables = mockFeatureVariables;
410+
// Wait for completion of dataReadyPromise
397411
await optimizelyMock.onReady().then(res => res.dataReadyPromise);
398412
component.update();
413+
414+
// Simulate datafile fetch completing after timeout has already passed
415+
// Activate now returns a variation
416+
activateMock.mockReturnValue('12345');
417+
// Wait for completion of dataReadyPromise
418+
await optimizelyMock.onReady().then(res => res.dataReadyPromise);
419+
component.update();
420+
399421
expect(component.text()).toBe('true|{"foo":"bar"}|true|true'); // when clientReady
400422
});
401423

@@ -480,7 +502,7 @@ describe('hooks', () => {
480502
expect(mockLog).toHaveBeenCalledWith(false);
481503
});
482504

483-
it('should re-render after the client becomes ready and triggers a config update notification', async () => {
505+
it('should re-render after the client becomes ready', async () => {
484506
readySuccess = false;
485507
let resolveReadyPromise: (result: { success: boolean; dataReadyPromise: Promise<any> }) => void;
486508
const readyPromise: Promise<any> = new Promise(res => {
@@ -503,10 +525,14 @@ describe('hooks', () => {
503525
expect(mockLog).toHaveBeenCalledWith(false);
504526

505527
mockLog.mockReset();
528+
529+
// Simulate datafile fetch completing after timeout has already passed
530+
// isFeatureEnabled now returns true
506531
isFeatureEnabledMock.mockReturnValue(true);
507-
// Simulate CONFIG_UPDATE notification, causing decision state to recompute
508-
notificationListenerCallbacks[0]();
509-
resolveReadyPromise!({ success: true, dataReadyPromise: Promise.resolve() });
532+
// Wait for completion of dataReadyPromise
533+
const dataReadyPromise = Promise.resolve();
534+
resolveReadyPromise!({ success: true, dataReadyPromise });
535+
await dataReadyPromise;
510536
component.update();
511537

512538
expect(mockLog).toHaveBeenCalledTimes(1);

0 commit comments

Comments
 (0)