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, sdkBrowserWithConfig } from './testUtils/sdkConfigs'; import { CONTROL, EXCEPTION_NO_SFP } from '../constants'; /** Test target */ import { SplitFactoryProvider } from '../SplitFactoryProvider'; import { useTreatments } from '../useTreatments'; import { SplitContext } from '../SplitContext'; import { IUseTreatmentsResult } from '../types'; describe('useTreatments', () => { const featureFlagNames = ['split1']; const flagSets = ['set1']; const attributes = { att1: 'att1' }; const properties = { prop1: 'prop1' }; test('returns the treatments 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 treatments: SplitIO.Treatments; let treatmentsByFlagSets: SplitIO.Treatments; render( {React.createElement(() => { treatments = useTreatments({ names: featureFlagNames, attributes, properties }).treatments; treatmentsByFlagSets = useTreatments({ flagSets, attributes, properties }).treatments; // @ts-expect-error Options object must provide either names or flagSets expect(useTreatments({}).treatments).toEqual({}); return null; })} ); // returns control treatment if not operational (SDK not ready or destroyed), without calling `getTreatments` method expect(client.getTreatments).not.toBeCalled(); expect(treatments!).toEqual({ split1: CONTROL }); // returns empty treatments object if not operational, without calling `getTreatmentsByFlagSets` method expect(client.getTreatmentsByFlagSets).not.toBeCalled(); expect(treatmentsByFlagSets!).toEqual({}); // once operational (SDK_READY), it evaluates feature flags act(() => client.__emitter__.emit(Event.SDK_READY)); expect(client.getTreatments).toBeCalledWith(featureFlagNames, attributes, { properties }); expect(client.getTreatments).toHaveReturnedWith(treatments!); expect(client.getTreatmentsByFlagSets).toBeCalledWith(flagSets, attributes, { properties }); expect(client.getTreatmentsByFlagSets).toHaveReturnedWith(treatmentsByFlagSets!); }); 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( {React.createElement(() => { const treatments = useTreatments({ names: featureFlagNames, attributes, properties, splitKey: 'user2', updateOnSdkUpdate: false }).treatments; renderTimes++; switch (renderTimes) { case 1: // returns control if not operational (SDK not ready), without calling `getTreatments` method expect(client.getTreatments).not.toBeCalled(); expect(treatments).toEqual({ split1: CONTROL }); break; case 2: case 3: // once operational (SDK_READY or SDK_READY_FROM_CACHE), it evaluates feature flags expect(client.getTreatments).toHaveBeenLastCalledWith(featureFlagNames, attributes, { properties }); expect(client.getTreatments).toHaveLastReturnedWith(treatments); break; default: throw new Error('Unexpected render'); } return null; })} ); 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.getTreatments).toBeCalledTimes(2); }); test('throws error if invoked outside of SplitFactoryProvider.', () => { expect(() => { render( React.createElement(() => { useTreatments({ names: featureFlagNames, attributes }).treatments; useTreatments({ flagSets: featureFlagNames }).treatments; return null; }) ); }).toThrow(EXCEPTION_NO_SFP); }); /** * Input validation: sanitize invalid feature flag names and return control while the SDK is not ready. */ test('Input validation: invalid names are sanitized.', () => { render( { React.createElement(() => { // @ts-expect-error Test error handling let treatments = useTreatments('split1').treatments; expect(treatments).toEqual({}); // @ts-expect-error Test error handling treatments = useTreatments({ names: [true, ' flag_1 ', ' '] }).treatments; expect(treatments).toEqual({ flag_1: CONTROL }); return null; }) } ); }); test('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, countUseTreatments = 0, countUseTreatmentsUser2 = 0, countUseTreatmentsUser2WithoutUpdate = 0; const lastUpdateSetUser2 = new Set(); const lastUpdateSetUser2WithUpdate = new Set(); function validateTreatments({ treatments, isReady, isReadyFromCache }: IUseTreatmentsResult) { if (isReady || isReadyFromCache) { expect(treatments).toEqual({ split_test: 'on' }) } else { expect(treatments).toEqual({ split_test: 'control' }) } } render( <> {() => countSplitContext++} {React.createElement(() => { const context = useTreatments({ names: ['split_test'], attributes: { att1: 'att1' } }); expect(context.client).toBe(mainClient); // Assert that the main client was retrieved. validateTreatments(context); countUseTreatments++; return null; })} {React.createElement(() => { const context = useTreatments({ names: ['split_test'], splitKey: 'user_2' }); expect(context.client).toBe(user2Client); validateTreatments(context); lastUpdateSetUser2.add(context.lastUpdate); countUseTreatmentsUser2++; return null; })} {React.createElement(() => { const context = useTreatments({ names: ['split_test'], splitKey: 'user_2', updateOnSdkUpdate: false }); expect(context.client).toBe(user2Client); validateTreatments(context); lastUpdateSetUser2WithUpdate.add(context.lastUpdate); countUseTreatmentsUser2WithoutUpdate++; return null; })} ); 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 useTreatments evaluates with the main client and have default update options, it re-renders for each main client event. expect(countUseTreatments).toEqual(4); expect(mainClient.getTreatments).toHaveBeenCalledTimes(3); // when ready from cache, ready and update expect(mainClient.getTreatments).toHaveBeenLastCalledWith(['split_test'], { att1: 'att1' }, undefined); // If useTreatments evaluates with a different client and have default update options, it re-renders for each event of the new client. expect(countUseTreatmentsUser2).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(countUseTreatmentsUser2WithoutUpdate).toEqual(3); expect(lastUpdateSetUser2WithUpdate.size).toEqual(3); expect(user2Client.getTreatments).toHaveBeenCalledTimes(5); // when ready from cache x2, ready x2 and update x1 expect(user2Client.getTreatments).toHaveBeenLastCalledWith(['split_test'], undefined, undefined); }); test('ignores flagSets if both names and flagSets params are provided.', () => { render( { React.createElement(() => { // @ts-expect-error names and flagSets are mutually exclusive const treatments = useTreatments({ names: featureFlagNames, flagSets, attributes }).treatments; expect(treatments).toEqual({ split1: CONTROL }); return null; }) } ); }); test('returns fallback treatments if the client is not operational', () => { render( {React.createElement(() => { const { treatments } = useTreatments({ names: ['ff1', 'ff2'], attributes, properties }); expect(treatments).toEqual({ ff1: 'control_ff1', ff2: 'control_global' }); return null; })} ); }); });