Skip to content
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
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Below is a simple example that describes the instantiation and most basic usage
import React from 'react';

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

// Define your config object
const CONFIG = {
Expand All @@ -29,18 +29,18 @@ const CONFIG = {
};

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

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

if (FEATURE_FLAG_NAME.treatment === 'on') {
// return JSX for on treatment
} else if (FEATURE_FLAG_NAME.treatment === 'off') {
// return JSX for off treatment
if (treatment === 'on') {
// return JSX for 'on' treatment
} else if (treatment === 'off') {
// return JSX for 'off' treatment
} else {
// return JSX for control treatment
// return JSX for 'control' treatment
};
}

Expand Down
2 changes: 0 additions & 2 deletions src/SplitClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ import { useSplitClient } from './useSplitClient';
*
* The underlying SDK client can be changed during the component lifecycle
* if the component is updated with a different splitKey prop.
*
* @deprecated `SplitClient` will be removed in a future major release. We recommend replacing it with the `useSplitClient` hook.
*/
export function SplitClient(props: ISplitClientProps) {
const { children } = props;
Expand Down
2 changes: 1 addition & 1 deletion src/SplitTreatments.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { useSplitTreatments } from './useSplitTreatments';
* call the 'client.getTreatmentsWithConfig()' method if the `names` prop is provided, or the 'client.getTreatmentsWithConfigByFlagSets()' method
* if the `flagSets` prop is provided. It then passes the resulting treatments to a child component as a function.
*
* @deprecated `SplitTreatments` will be removed in a future major release. We recommend replacing it with the `useSplitTreatments` hook.
* @deprecated `SplitTreatments` will be removed in a future major release. We recommend replacing it with the `useTreatment*` hooks.
*/
export function SplitTreatments(props: ISplitTreatmentsProps) {
const { children } = props;
Expand Down
12 changes: 12 additions & 0 deletions src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import {
useSplitClient as exportedUseSplitClient,
useSplitTreatments as exportedUseSplitTreatments,
useSplitManager as exportedUseSplitManager,
useTreatment as exportedUseTreatment,
useTreatmentWithConfig as exportedUseTreatmentWithConfig,
useTreatments as exportedUseTreatments,
useTreatmentsWithConfig as exportedUseTreatmentsWithConfig,
// Checks that types are exported. Otherwise, the test would fail with a TS error.
GetTreatmentsOptions,
ISplitClientChildProps,
Expand Down Expand Up @@ -39,6 +43,10 @@ import { useTrack } from '../useTrack';
import { useSplitClient } from '../useSplitClient';
import { useSplitTreatments } from '../useSplitTreatments';
import { useSplitManager } from '../useSplitManager';
import { useTreatment } from '../useTreatment';
import { useTreatmentWithConfig } from '../useTreatmentWithConfig';
import { useTreatments } from '../useTreatments';
import { useTreatmentsWithConfig } from '../useTreatmentsWithConfig';

describe('index', () => {

Expand All @@ -59,6 +67,10 @@ describe('index', () => {
expect(exportedUseSplitClient).toBe(useSplitClient);
expect(exportedUseSplitTreatments).toBe(useSplitTreatments);
expect(exportedUseSplitManager).toBe(useSplitManager);
expect(exportedUseTreatment).toBe(useTreatment);
expect(exportedUseTreatmentWithConfig).toBe(useTreatmentWithConfig);
expect(exportedUseTreatments).toBe(useTreatments);
expect(exportedUseTreatmentsWithConfig).toBe(useTreatmentsWithConfig);
});

it('should export SplitContext', () => {
Expand Down
23 changes: 23 additions & 0 deletions src/__tests__/testUtils/mockSplitFactory.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { EventEmitter } from 'events';
import jsSdkPackageJson from '@splitsoftware/splitio/package.json';
import reactSdkPackageJson from '../../../package.json';
import { CONTROL, CONTROL_WITH_CONFIG } from '../../constants';

export const jsSdkVersion = `javascript-${jsSdkPackageJson.version}`;
export const reactSdkVersion = `react-${reactSdkPackageJson.version}`;
Expand Down Expand Up @@ -65,6 +66,24 @@ export function mockSdk() {
const track: jest.Mock = jest.fn(() => {
return true;
});
const getTreatment: jest.Mock = jest.fn((featureFlagName: string) => {
return typeof featureFlagName === 'string' ? 'on' : CONTROL;
});
const getTreatments: jest.Mock = jest.fn((featureFlagNames: string[]) => {
return featureFlagNames.reduce((result: SplitIO.Treatments, featureName: string) => {
result[featureName] = 'on';
return result;
}, {});
});
const getTreatmentsByFlagSets: jest.Mock = jest.fn((flagSets: string[]) => {
return flagSets.reduce((result: SplitIO.Treatments, flagSet: string) => {
result[flagSet + '_feature_flag'] = 'on';
return result;
}, {});
});
const getTreatmentWithConfig: jest.Mock = jest.fn((featureFlagName: string) => {
return typeof featureFlagName === 'string' ? { treatment: 'on', config: null } : CONTROL_WITH_CONFIG;
});
const getTreatmentsWithConfig: jest.Mock = jest.fn((featureFlagNames: string[]) => {
return featureFlagNames.reduce((result: SplitIO.TreatmentsWithConfig, featureName: string) => {
result[featureName] = { treatment: 'on', config: null };
Expand Down Expand Up @@ -113,6 +132,10 @@ export function mockSdk() {
});

return Object.assign(Object.create(__emitter__), {
getTreatment,
getTreatments,
getTreatmentsByFlagSets,
getTreatmentWithConfig,
getTreatmentsWithConfig,
getTreatmentsWithConfigByFlagSets,
track,
Expand Down
174 changes: 174 additions & 0 deletions src/__tests__/useTreatment.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import * as React from 'react';
import { act, render } from '@testing-library/react';

/** Mocks */
import { mockSdk, Event } from './testUtils/mockSplitFactory';
jest.mock('@splitsoftware/splitio/client', () => {
return { SplitFactory: mockSdk() };
});
import { SplitFactory } from '@splitsoftware/splitio/client';
import { sdkBrowser } from './testUtils/sdkConfigs';
import { CONTROL, EXCEPTION_NO_SFP } from '../constants';

/** Test target */
import { SplitFactoryProvider } from '../SplitFactoryProvider';
import { useTreatment } from '../useTreatment';
import { SplitContext } from '../SplitContext';
import { IUseTreatmentResult } from '../types';

describe('useTreatment', () => {

const featureFlagName = 'split1';
const attributes = { att1: 'att1' };
const properties = { prop1: 'prop1' };

test('returns the treatment evaluated by the main client of the factory at Split context, or control if the client is not operational.', () => {
const outerFactory = SplitFactory(sdkBrowser);
const client: any = outerFactory.client();
let treatment: SplitIO.Treatment;

render(
<SplitFactoryProvider factory={outerFactory} >
{React.createElement(() => {
treatment = useTreatment({ name: featureFlagName, attributes, properties }).treatment;
return null;
})}
</SplitFactoryProvider>
);

// returns control treatment if not operational (SDK not ready or destroyed), without calling `getTreatment` method
expect(client.getTreatment).not.toBeCalled();
expect(treatment!).toEqual(CONTROL);

// once operational (SDK_READY), it evaluates feature flags
act(() => client.__emitter__.emit(Event.SDK_READY));

expect(client.getTreatment).toBeCalledWith(featureFlagName, attributes, { properties });
expect(client.getTreatment).toHaveReturnedWith(treatment!);
});

test('returns the treatments from a new client given a splitKey, and re-evaluates on SDK events.', () => {
const outerFactory = SplitFactory(sdkBrowser);
const client: any = outerFactory.client('user2');
let renderTimes = 0;

render(
<SplitFactoryProvider factory={outerFactory} >
{React.createElement(() => {
const treatment = useTreatment({ name: featureFlagName, attributes, properties, splitKey: 'user2', updateOnSdkUpdate: false }).treatment;

renderTimes++;
switch (renderTimes) {
case 1:
// returns control if not operational (SDK not ready), without calling `getTreatment` method
expect(client.getTreatment).not.toBeCalled();
expect(treatment).toEqual(CONTROL);
break;
case 2:
case 3:
// once operational (SDK_READY or SDK_READY_FROM_CACHE), it evaluates feature flags
expect(client.getTreatment).toHaveBeenLastCalledWith(featureFlagName, attributes, { properties });
expect(client.getTreatment).toHaveLastReturnedWith(treatment);
break;
default:
throw new Error('Unexpected render');
}

return null;
})}
</SplitFactoryProvider>
);

act(() => client.__emitter__.emit(Event.SDK_READY_FROM_CACHE));
act(() => client.__emitter__.emit(Event.SDK_READY));
act(() => client.__emitter__.emit(Event.SDK_UPDATE));
expect(client.getTreatment).toBeCalledTimes(2);
});

test('throws error if invoked outside of SplitFactoryProvider.', () => {
expect(() => {
render(
React.createElement(() => {
useTreatment({ name: featureFlagName, attributes }).treatment;
return null;
})
);
}).toThrow(EXCEPTION_NO_SFP);
});

test('useTreatment must update on SDK events', async () => {
const outerFactory = SplitFactory(sdkBrowser);
const mainClient = outerFactory.client() as any;
const user2Client = outerFactory.client('user_2') as any;

let countSplitContext = 0, countUseTreatment = 0, countUseTreatmentUser2 = 0, countUseTreatmentUser2WithoutUpdate = 0;
const lastUpdateSetUser2 = new Set<number>();
const lastUpdateSetUser2WithUpdate = new Set<number>();

function validateTreatment({ treatment, isReady, isReadyFromCache }: IUseTreatmentResult) {
if (isReady || isReadyFromCache) {
expect(treatment).toEqual('on')
} else {
expect(treatment).toEqual('control')
}
}

render(
<SplitFactoryProvider factory={outerFactory} >
<>
<SplitContext.Consumer>
{() => countSplitContext++}
</SplitContext.Consumer>
{React.createElement(() => {
const context = useTreatment({ name: 'split_test', attributes: { att1: 'att1' } });
expect(context.client).toBe(mainClient); // Assert that the main client was retrieved.
validateTreatment(context);
countUseTreatment++;
return null;
})}
{React.createElement(() => {
const context = useTreatment({ name: 'split_test', splitKey: 'user_2' });
expect(context.client).toBe(user2Client);
validateTreatment(context);
lastUpdateSetUser2.add(context.lastUpdate);
countUseTreatmentUser2++;
return null;
})}
{React.createElement(() => {
const context = useTreatment({ name: 'split_test', splitKey: 'user_2', updateOnSdkUpdate: false });
expect(context.client).toBe(user2Client);
validateTreatment(context);
lastUpdateSetUser2WithUpdate.add(context.lastUpdate);
countUseTreatmentUser2WithoutUpdate++;
return null;
})}
</>
</SplitFactoryProvider>
);

act(() => mainClient.__emitter__.emit(Event.SDK_READY_FROM_CACHE));
act(() => mainClient.__emitter__.emit(Event.SDK_READY));
act(() => mainClient.__emitter__.emit(Event.SDK_UPDATE));
act(() => user2Client.__emitter__.emit(Event.SDK_READY_FROM_CACHE));
act(() => user2Client.__emitter__.emit(Event.SDK_READY));
act(() => user2Client.__emitter__.emit(Event.SDK_UPDATE));

// SplitFactoryProvider renders once
expect(countSplitContext).toEqual(1);

// If useTreatment evaluates with the main client and have default update options, it re-renders for each main client event.
expect(countUseTreatment).toEqual(4);
expect(mainClient.getTreatment).toHaveBeenCalledTimes(3); // when ready from cache, ready and update
expect(mainClient.getTreatment).toHaveBeenLastCalledWith('split_test', { att1: 'att1' }, undefined);

// If useTreatment evaluates with a different client and have default update options, it re-renders for each event of the new client.
expect(countUseTreatmentUser2).toEqual(4);
expect(lastUpdateSetUser2.size).toEqual(4);
// If it is used with `updateOnSdkUpdate: false`, it doesn't render when the client emits an SDK_UPDATE event.
expect(countUseTreatmentUser2WithoutUpdate).toEqual(3);
expect(lastUpdateSetUser2WithUpdate.size).toEqual(3);
expect(user2Client.getTreatment).toHaveBeenCalledTimes(5); // when ready from cache x2, ready x2 and update x1
expect(user2Client.getTreatment).toHaveBeenLastCalledWith('split_test', undefined, undefined);
});

});
Loading