Skip to content

fix(hooks): Return latest decision values on first call, and re-render in response to decision input changes #64

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Aug 26, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ module.exports = {
node: true,
},
extends: [
'plugin:react-hooks/recommended',
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'prettier/@typescript-eslint',
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"eslint-config-prettier": "^6.10.0",
"eslint-plugin-prettier": "^3.1.2",
"eslint-plugin-react": "^7.19.0",
"eslint-plugin-react-hooks": "^4.1.0",
"jest": "^23.6.0",
"prettier": "1.19.1",
"react": "^16.8.0",
Expand Down
27 changes: 26 additions & 1 deletion src/Experiment.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ describe('<OptimizelyExperiment>', () => {
id: 'testuser',
attributes: {},
},
isReady: jest.fn().mockReturnValue(false),
} as unknown) as ReactSDKClient;
});

Expand All @@ -62,7 +63,7 @@ describe('<OptimizelyExperiment>', () => {
});

describe('when isServerSide prop is false', () => {
it('should wait until onReady() is resolved then render result of activate', async () => {
it('should wait client is ready then render result of activate', async () => {
const component = mount(
<OptimizelyProvider optimizely={optimizelyMock} timeout={100}>
<OptimizelyExperiment experiment="experiment1">{variation => variation}</OptimizelyExperiment>
Expand All @@ -72,7 +73,10 @@ describe('<OptimizelyExperiment>', () => {
expect(optimizelyMock.onReady).toHaveBeenCalledWith({ timeout: 100 });
// while it's waiting for onReady()
expect(component.text()).toBe('');

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

await optimizelyMock.onReady();

Expand All @@ -94,7 +98,10 @@ describe('<OptimizelyExperiment>', () => {
expect(optimizelyMock.onReady).toHaveBeenCalledWith({ timeout: 200 });
// while it's waiting for onReady()
expect(component.text()).toBe('');

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

await optimizelyMock.onReady();

Expand All @@ -113,7 +120,10 @@ describe('<OptimizelyExperiment>', () => {
expect(optimizelyMock.onReady).toHaveBeenCalledWith({ timeout: 200 });
// while it's waiting for onReady()
expect(component.text()).toBe('');

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

await optimizelyMock.onReady();

Expand All @@ -133,7 +143,10 @@ describe('<OptimizelyExperiment>', () => {

// while it's waiting for onReady()
expect(component.text()).toBe('');

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

await optimizelyMock.onReady();

Expand Down Expand Up @@ -198,7 +211,10 @@ describe('<OptimizelyExperiment>', () => {
expect(optimizelyMock.onReady).toHaveBeenCalledWith({ timeout: 100 });
// while it's waiting for onReady()
expect(component.text()).toBe('');

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

await optimizelyMock.onReady();

Expand All @@ -220,7 +236,10 @@ describe('<OptimizelyExperiment>', () => {

// while it's waiting for onReady()
expect(component.text()).toBe('');

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

await optimizelyMock.onReady();

Expand Down Expand Up @@ -265,7 +284,10 @@ describe('<OptimizelyExperiment>', () => {
expect(optimizelyMock.onReady).toHaveBeenCalledWith({ timeout: 100 });
// while it's waiting for onReady()
expect(component.text()).toBe('');

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

await optimizelyMock.onReady();

Expand Down Expand Up @@ -304,6 +326,9 @@ describe('<OptimizelyExperiment>', () => {
expect(component.text()).toBe('');
resolver.resolve({ success: true });

// Simulate config update update notification firing after datafile becomes available.
(optimizelyMock.notificationCenter.addNotificationListener as jest.Mock).mock.calls[0][1]();

await optimizelyMock.onReady();

component.update();
Expand Down
26 changes: 25 additions & 1 deletion src/Feature.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ describe('<OptimizelyFeature>', () => {
id: 'testuser',
attributes: {},
},
isReady: jest.fn().mockReturnValue(false),
} as unknown) as ReactSDKClient;
});
it('throws an error when not rendered in the context of an OptimizelyProvider', () => {
Expand All @@ -75,7 +76,10 @@ describe('<OptimizelyFeature>', () => {

// while it's waiting for onReady()
expect(component.text()).toBe('');

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

await optimizelyMock.onReady();

Expand All @@ -99,7 +103,10 @@ describe('<OptimizelyFeature>', () => {

// while it's waiting for onReady()
expect(component.text()).toBe('');

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

await optimizelyMock.onReady();

Expand All @@ -123,7 +130,10 @@ describe('<OptimizelyFeature>', () => {

// while it's waiting for onReady()
expect(component.text()).toBe('');

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

await optimizelyMock.onReady();

Expand All @@ -147,7 +157,10 @@ describe('<OptimizelyFeature>', () => {

// while it's waiting for onReady()
expect(component.text()).toBe('');

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

await optimizelyMock.onReady();

Expand All @@ -169,7 +182,10 @@ describe('<OptimizelyFeature>', () => {

// while it's waiting for onReady()
expect(component.text()).toBe('');

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

await optimizelyMock.onReady();

Expand All @@ -194,7 +210,11 @@ describe('<OptimizelyFeature>', () => {

// while it's waiting for onReady()
expect(component.text()).toBe('');

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

await optimizelyMock.onReady();

Expand All @@ -204,7 +224,6 @@ describe('<OptimizelyFeature>', () => {
expect(optimizelyMock.getFeatureVariables).toHaveBeenCalledWith('feature1', undefined, undefined);
expect(component.text()).toBe('true|bar');

const updateFn = (optimizelyMock.notificationCenter.addNotificationListener as jest.Mock).mock.calls[0][1];
// change the return value of activate
const mockIFE = optimizelyMock.isFeatureEnabled as jest.Mock;
mockIFE.mockImplementationOnce(() => false);
Expand Down Expand Up @@ -236,6 +255,9 @@ describe('<OptimizelyFeature>', () => {

// while it's waiting for onReady()
expect(component.text()).toBe('');

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

await optimizelyMock.onReady();
Expand Down Expand Up @@ -281,6 +303,8 @@ describe('<OptimizelyFeature>', () => {
expect(component.text()).toBe('');
resolver.resolve({ success: false, reason: 'fail', dataReadyPromise: Promise.resolve() });

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

component.update();
Expand Down
8 changes: 8 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export interface ReactSDKClient extends optimizely.Client {
onReady(opts?: { timeout?: number }): Promise<any>;
setUser(userInfo: { id: string; attributes?: { [key: string]: any } }): void;
onUserUpdate(handler: OnUserUpdateHandler): DisposeFn;
isReady(): boolean;

activate(
experimentKey: string,
Expand Down Expand Up @@ -155,6 +156,8 @@ class OptimizelyReactSDKClient implements ReactSDKClient {
// promise keeping track of async requests for initializing client instance
private dataReadyPromise: Promise<OnReadyResult>;

private dataReadyPromiseFulfilled = false;

/**
* Creates an instance of OptimizelyReactSDKClient.
* @param {optimizely.Config} [config={}]
Expand All @@ -176,6 +179,7 @@ class OptimizelyReactSDKClient implements ReactSDKClient {
}).then(() => ({ success: true }));

this.dataReadyPromise = Promise.all([this.userPromise, this._client.onReady()]).then(() => {
this.dataReadyPromiseFulfilled = true;
return {
success: true,
reason: 'datafile and user resolved',
Expand Down Expand Up @@ -233,6 +237,10 @@ class OptimizelyReactSDKClient implements ReactSDKClient {
};
}

isReady(): boolean {
return this.dataReadyPromiseFulfilled;
}

/**
* Buckets visitor and sends impression event to Optimizely
* @param {string} experimentKey
Expand Down
Loading