Skip to content

Commit 571bec2

Browse files
Merge pull request #250 from splitio/FME-10595-add-evaluation-hooks-tests
[FME-10595] Add `useTreatment*` hooks for evaluation: tests and polishing
2 parents c90509f + 2251ad5 commit 571bec2

File tree

10 files changed

+648
-38
lines changed

10 files changed

+648
-38
lines changed

README.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Below is a simple example that describes the instantiation and most basic usage
1818
import React from 'react';
1919

2020
// Import SDK functions
21-
import { SplitFactoryProvider, useSplitTreatments } from '@splitsoftware/splitio-react';
21+
import { SplitFactoryProvider, useTreatment } from '@splitsoftware/splitio-react';
2222

2323
// Define your config object
2424
const CONFIG = {
@@ -29,18 +29,18 @@ const CONFIG = {
2929
};
3030

3131
function MyComponent() {
32-
// Evaluate feature flags with useSplitTreatments hook
33-
const { treatments: { FEATURE_FLAG_NAME }, isReady } = useSplitTreatments({ names: ['FEATURE_FLAG_NAME'] });
32+
// Evaluate a feature flag with useTreatment hook
33+
const { treatment, isReady } = useTreatment({ name: 'FEATURE_FLAG_NAME' });
3434

3535
// Check SDK readiness using isReady prop
3636
if (!isReady) return <div>Loading SDK ...</div>;
3737

38-
if (FEATURE_FLAG_NAME.treatment === 'on') {
39-
// return JSX for on treatment
40-
} else if (FEATURE_FLAG_NAME.treatment === 'off') {
41-
// return JSX for off treatment
38+
if (treatment === 'on') {
39+
// return JSX for 'on' treatment
40+
} else if (treatment === 'off') {
41+
// return JSX for 'off' treatment
4242
} else {
43-
// return JSX for control treatment
43+
// return JSX for 'control' treatment
4444
};
4545
}
4646

src/SplitClient.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ import { useSplitClient } from './useSplitClient';
99
*
1010
* The underlying SDK client can be changed during the component lifecycle
1111
* if the component is updated with a different splitKey prop.
12-
*
13-
* @deprecated `SplitClient` will be removed in a future major release. We recommend replacing it with the `useSplitClient` hook.
1412
*/
1513
export function SplitClient(props: ISplitClientProps) {
1614
const { children } = props;

src/SplitTreatments.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { useSplitTreatments } from './useSplitTreatments';
99
* call the 'client.getTreatmentsWithConfig()' method if the `names` prop is provided, or the 'client.getTreatmentsWithConfigByFlagSets()' method
1010
* if the `flagSets` prop is provided. It then passes the resulting treatments to a child component as a function.
1111
*
12-
* @deprecated `SplitTreatments` will be removed in a future major release. We recommend replacing it with the `useSplitTreatments` hook.
12+
* @deprecated `SplitTreatments` will be removed in a future major release. We recommend replacing it with the `useTreatment*` hooks.
1313
*/
1414
export function SplitTreatments(props: ISplitTreatmentsProps) {
1515
const { children } = props;

src/__tests__/index.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ import {
1212
useSplitClient as exportedUseSplitClient,
1313
useSplitTreatments as exportedUseSplitTreatments,
1414
useSplitManager as exportedUseSplitManager,
15+
useTreatment as exportedUseTreatment,
16+
useTreatmentWithConfig as exportedUseTreatmentWithConfig,
17+
useTreatments as exportedUseTreatments,
18+
useTreatmentsWithConfig as exportedUseTreatmentsWithConfig,
1519
// Checks that types are exported. Otherwise, the test would fail with a TS error.
1620
GetTreatmentsOptions,
1721
ISplitClientChildProps,
@@ -39,6 +43,10 @@ import { useTrack } from '../useTrack';
3943
import { useSplitClient } from '../useSplitClient';
4044
import { useSplitTreatments } from '../useSplitTreatments';
4145
import { useSplitManager } from '../useSplitManager';
46+
import { useTreatment } from '../useTreatment';
47+
import { useTreatmentWithConfig } from '../useTreatmentWithConfig';
48+
import { useTreatments } from '../useTreatments';
49+
import { useTreatmentsWithConfig } from '../useTreatmentsWithConfig';
4250

4351
describe('index', () => {
4452

@@ -59,6 +67,10 @@ describe('index', () => {
5967
expect(exportedUseSplitClient).toBe(useSplitClient);
6068
expect(exportedUseSplitTreatments).toBe(useSplitTreatments);
6169
expect(exportedUseSplitManager).toBe(useSplitManager);
70+
expect(exportedUseTreatment).toBe(useTreatment);
71+
expect(exportedUseTreatmentWithConfig).toBe(useTreatmentWithConfig);
72+
expect(exportedUseTreatments).toBe(useTreatments);
73+
expect(exportedUseTreatmentsWithConfig).toBe(useTreatmentsWithConfig);
6274
});
6375

6476
it('should export SplitContext', () => {

src/__tests__/testUtils/mockSplitFactory.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { EventEmitter } from 'events';
22
import jsSdkPackageJson from '@splitsoftware/splitio/package.json';
33
import reactSdkPackageJson from '../../../package.json';
4+
import { CONTROL, CONTROL_WITH_CONFIG } from '../../constants';
45

56
export const jsSdkVersion = `javascript-${jsSdkPackageJson.version}`;
67
export const reactSdkVersion = `react-${reactSdkPackageJson.version}`;
@@ -65,6 +66,24 @@ export function mockSdk() {
6566
const track: jest.Mock = jest.fn(() => {
6667
return true;
6768
});
69+
const getTreatment: jest.Mock = jest.fn((featureFlagName: string) => {
70+
return typeof featureFlagName === 'string' ? 'on' : CONTROL;
71+
});
72+
const getTreatments: jest.Mock = jest.fn((featureFlagNames: string[]) => {
73+
return featureFlagNames.reduce((result: SplitIO.Treatments, featureName: string) => {
74+
result[featureName] = 'on';
75+
return result;
76+
}, {});
77+
});
78+
const getTreatmentsByFlagSets: jest.Mock = jest.fn((flagSets: string[]) => {
79+
return flagSets.reduce((result: SplitIO.Treatments, flagSet: string) => {
80+
result[flagSet + '_feature_flag'] = 'on';
81+
return result;
82+
}, {});
83+
});
84+
const getTreatmentWithConfig: jest.Mock = jest.fn((featureFlagName: string) => {
85+
return typeof featureFlagName === 'string' ? { treatment: 'on', config: null } : CONTROL_WITH_CONFIG;
86+
});
6887
const getTreatmentsWithConfig: jest.Mock = jest.fn((featureFlagNames: string[]) => {
6988
return featureFlagNames.reduce((result: SplitIO.TreatmentsWithConfig, featureName: string) => {
7089
result[featureName] = { treatment: 'on', config: null };
@@ -113,6 +132,10 @@ export function mockSdk() {
113132
});
114133

115134
return Object.assign(Object.create(__emitter__), {
135+
getTreatment,
136+
getTreatments,
137+
getTreatmentsByFlagSets,
138+
getTreatmentWithConfig,
116139
getTreatmentsWithConfig,
117140
getTreatmentsWithConfigByFlagSets,
118141
track,
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import * as React from 'react';
2+
import { act, render } from '@testing-library/react';
3+
4+
/** Mocks */
5+
import { mockSdk, Event } from './testUtils/mockSplitFactory';
6+
jest.mock('@splitsoftware/splitio/client', () => {
7+
return { SplitFactory: mockSdk() };
8+
});
9+
import { SplitFactory } from '@splitsoftware/splitio/client';
10+
import { sdkBrowser } from './testUtils/sdkConfigs';
11+
import { CONTROL, EXCEPTION_NO_SFP } from '../constants';
12+
13+
/** Test target */
14+
import { SplitFactoryProvider } from '../SplitFactoryProvider';
15+
import { useTreatment } from '../useTreatment';
16+
import { SplitContext } from '../SplitContext';
17+
import { IUseTreatmentResult } from '../types';
18+
19+
describe('useTreatment', () => {
20+
21+
const featureFlagName = 'split1';
22+
const attributes = { att1: 'att1' };
23+
const properties = { prop1: 'prop1' };
24+
25+
test('returns the treatment evaluated by the main client of the factory at Split context, or control if the client is not operational.', () => {
26+
const outerFactory = SplitFactory(sdkBrowser);
27+
const client: any = outerFactory.client();
28+
let treatment: SplitIO.Treatment;
29+
30+
render(
31+
<SplitFactoryProvider factory={outerFactory} >
32+
{React.createElement(() => {
33+
treatment = useTreatment({ name: featureFlagName, attributes, properties }).treatment;
34+
return null;
35+
})}
36+
</SplitFactoryProvider>
37+
);
38+
39+
// returns control treatment if not operational (SDK not ready or destroyed), without calling `getTreatment` method
40+
expect(client.getTreatment).not.toBeCalled();
41+
expect(treatment!).toEqual(CONTROL);
42+
43+
// once operational (SDK_READY), it evaluates feature flags
44+
act(() => client.__emitter__.emit(Event.SDK_READY));
45+
46+
expect(client.getTreatment).toBeCalledWith(featureFlagName, attributes, { properties });
47+
expect(client.getTreatment).toHaveReturnedWith(treatment!);
48+
});
49+
50+
test('returns the treatments from a new client given a splitKey, and re-evaluates on SDK events.', () => {
51+
const outerFactory = SplitFactory(sdkBrowser);
52+
const client: any = outerFactory.client('user2');
53+
let renderTimes = 0;
54+
55+
render(
56+
<SplitFactoryProvider factory={outerFactory} >
57+
{React.createElement(() => {
58+
const treatment = useTreatment({ name: featureFlagName, attributes, properties, splitKey: 'user2', updateOnSdkUpdate: false }).treatment;
59+
60+
renderTimes++;
61+
switch (renderTimes) {
62+
case 1:
63+
// returns control if not operational (SDK not ready), without calling `getTreatment` method
64+
expect(client.getTreatment).not.toBeCalled();
65+
expect(treatment).toEqual(CONTROL);
66+
break;
67+
case 2:
68+
case 3:
69+
// once operational (SDK_READY or SDK_READY_FROM_CACHE), it evaluates feature flags
70+
expect(client.getTreatment).toHaveBeenLastCalledWith(featureFlagName, attributes, { properties });
71+
expect(client.getTreatment).toHaveLastReturnedWith(treatment);
72+
break;
73+
default:
74+
throw new Error('Unexpected render');
75+
}
76+
77+
return null;
78+
})}
79+
</SplitFactoryProvider>
80+
);
81+
82+
act(() => client.__emitter__.emit(Event.SDK_READY_FROM_CACHE));
83+
act(() => client.__emitter__.emit(Event.SDK_READY));
84+
act(() => client.__emitter__.emit(Event.SDK_UPDATE));
85+
expect(client.getTreatment).toBeCalledTimes(2);
86+
});
87+
88+
test('throws error if invoked outside of SplitFactoryProvider.', () => {
89+
expect(() => {
90+
render(
91+
React.createElement(() => {
92+
useTreatment({ name: featureFlagName, attributes }).treatment;
93+
return null;
94+
})
95+
);
96+
}).toThrow(EXCEPTION_NO_SFP);
97+
});
98+
99+
test('useTreatment must update on SDK events', async () => {
100+
const outerFactory = SplitFactory(sdkBrowser);
101+
const mainClient = outerFactory.client() as any;
102+
const user2Client = outerFactory.client('user_2') as any;
103+
104+
let countSplitContext = 0, countUseTreatment = 0, countUseTreatmentUser2 = 0, countUseTreatmentUser2WithoutUpdate = 0;
105+
const lastUpdateSetUser2 = new Set<number>();
106+
const lastUpdateSetUser2WithUpdate = new Set<number>();
107+
108+
function validateTreatment({ treatment, isReady, isReadyFromCache }: IUseTreatmentResult) {
109+
if (isReady || isReadyFromCache) {
110+
expect(treatment).toEqual('on')
111+
} else {
112+
expect(treatment).toEqual('control')
113+
}
114+
}
115+
116+
render(
117+
<SplitFactoryProvider factory={outerFactory} >
118+
<>
119+
<SplitContext.Consumer>
120+
{() => countSplitContext++}
121+
</SplitContext.Consumer>
122+
{React.createElement(() => {
123+
const context = useTreatment({ name: 'split_test', attributes: { att1: 'att1' } });
124+
expect(context.client).toBe(mainClient); // Assert that the main client was retrieved.
125+
validateTreatment(context);
126+
countUseTreatment++;
127+
return null;
128+
})}
129+
{React.createElement(() => {
130+
const context = useTreatment({ name: 'split_test', splitKey: 'user_2' });
131+
expect(context.client).toBe(user2Client);
132+
validateTreatment(context);
133+
lastUpdateSetUser2.add(context.lastUpdate);
134+
countUseTreatmentUser2++;
135+
return null;
136+
})}
137+
{React.createElement(() => {
138+
const context = useTreatment({ name: 'split_test', splitKey: 'user_2', updateOnSdkUpdate: false });
139+
expect(context.client).toBe(user2Client);
140+
validateTreatment(context);
141+
lastUpdateSetUser2WithUpdate.add(context.lastUpdate);
142+
countUseTreatmentUser2WithoutUpdate++;
143+
return null;
144+
})}
145+
</>
146+
</SplitFactoryProvider>
147+
);
148+
149+
act(() => mainClient.__emitter__.emit(Event.SDK_READY_FROM_CACHE));
150+
act(() => mainClient.__emitter__.emit(Event.SDK_READY));
151+
act(() => mainClient.__emitter__.emit(Event.SDK_UPDATE));
152+
act(() => user2Client.__emitter__.emit(Event.SDK_READY_FROM_CACHE));
153+
act(() => user2Client.__emitter__.emit(Event.SDK_READY));
154+
act(() => user2Client.__emitter__.emit(Event.SDK_UPDATE));
155+
156+
// SplitFactoryProvider renders once
157+
expect(countSplitContext).toEqual(1);
158+
159+
// If useTreatment evaluates with the main client and have default update options, it re-renders for each main client event.
160+
expect(countUseTreatment).toEqual(4);
161+
expect(mainClient.getTreatment).toHaveBeenCalledTimes(3); // when ready from cache, ready and update
162+
expect(mainClient.getTreatment).toHaveBeenLastCalledWith('split_test', { att1: 'att1' }, undefined);
163+
164+
// If useTreatment evaluates with a different client and have default update options, it re-renders for each event of the new client.
165+
expect(countUseTreatmentUser2).toEqual(4);
166+
expect(lastUpdateSetUser2.size).toEqual(4);
167+
// If it is used with `updateOnSdkUpdate: false`, it doesn't render when the client emits an SDK_UPDATE event.
168+
expect(countUseTreatmentUser2WithoutUpdate).toEqual(3);
169+
expect(lastUpdateSetUser2WithUpdate.size).toEqual(3);
170+
expect(user2Client.getTreatment).toHaveBeenCalledTimes(5); // when ready from cache x2, ready x2 and update x1
171+
expect(user2Client.getTreatment).toHaveBeenLastCalledWith('split_test', undefined, undefined);
172+
});
173+
174+
});

0 commit comments

Comments
 (0)