From b35dc782cbc7c8356dd140d72f2beba02f50dfab Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 27 Oct 2025 14:48:34 -0300 Subject: [PATCH 1/2] Add tests --- src/__tests__/index.test.ts | 12 + src/__tests__/testUtils/mockSplitFactory.ts | 23 ++ src/__tests__/useTreatment.test.tsx | 174 +++++++++++++ src/__tests__/useTreatmentWithConfig.test.tsx | 174 +++++++++++++ src/__tests__/useTreatments.test.tsx | 229 ++++++++++++++++++ ...t.tsx => useTreatmentsWithConfig.test.tsx} | 52 ++-- 6 files changed, 638 insertions(+), 26 deletions(-) create mode 100644 src/__tests__/useTreatment.test.tsx create mode 100644 src/__tests__/useTreatmentWithConfig.test.tsx create mode 100644 src/__tests__/useTreatments.test.tsx rename src/__tests__/{useSplitTreatments.test.tsx => useTreatmentsWithConfig.test.tsx} (77%) diff --git a/src/__tests__/index.test.ts b/src/__tests__/index.test.ts index a455af6..d63b749 100644 --- a/src/__tests__/index.test.ts +++ b/src/__tests__/index.test.ts @@ -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, @@ -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', () => { @@ -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', () => { diff --git a/src/__tests__/testUtils/mockSplitFactory.ts b/src/__tests__/testUtils/mockSplitFactory.ts index 2e2808e..a4068dc 100644 --- a/src/__tests__/testUtils/mockSplitFactory.ts +++ b/src/__tests__/testUtils/mockSplitFactory.ts @@ -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}`; @@ -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 }; @@ -113,6 +132,10 @@ export function mockSdk() { }); return Object.assign(Object.create(__emitter__), { + getTreatment, + getTreatments, + getTreatmentsByFlagSets, + getTreatmentWithConfig, getTreatmentsWithConfig, getTreatmentsWithConfigByFlagSets, track, diff --git a/src/__tests__/useTreatment.test.tsx b/src/__tests__/useTreatment.test.tsx new file mode 100644 index 0000000..106131e --- /dev/null +++ b/src/__tests__/useTreatment.test.tsx @@ -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( + + {React.createElement(() => { + treatment = useTreatment({ name: featureFlagName, attributes, properties }).treatment; + return null; + })} + + ); + + // 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( + + {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; + })} + + ); + + 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(); + const lastUpdateSetUser2WithUpdate = new Set(); + + function validateTreatment({ treatment, isReady, isReadyFromCache }: IUseTreatmentResult) { + if (isReady || isReadyFromCache) { + expect(treatment).toEqual('on') + } else { + expect(treatment).toEqual('control') + } + } + + render( + + <> + + {() => countSplitContext++} + + {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; + })} + + + ); + + 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); + }); + +}); diff --git a/src/__tests__/useTreatmentWithConfig.test.tsx b/src/__tests__/useTreatmentWithConfig.test.tsx new file mode 100644 index 0000000..a296126 --- /dev/null +++ b/src/__tests__/useTreatmentWithConfig.test.tsx @@ -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_WITH_CONFIG, EXCEPTION_NO_SFP } from '../constants'; + +/** Test target */ +import { SplitFactoryProvider } from '../SplitFactoryProvider'; +import { useTreatmentWithConfig } from '../useTreatmentWithConfig'; +import { SplitContext } from '../SplitContext'; +import { IUseTreatmentWithConfigResult } from '../types'; + +describe('useTreatmentWithConfig', () => { + + 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.TreatmentWithConfig; + + render( + + {React.createElement(() => { + treatment = useTreatmentWithConfig({ name: featureFlagName, attributes, properties }).treatment; + return null; + })} + + ); + + // returns control treatment if not operational (SDK not ready or destroyed), without calling `getTreatmentWithConfig` method + expect(client.getTreatmentWithConfig).not.toBeCalled(); + expect(treatment!).toEqual(CONTROL_WITH_CONFIG); + + // once operational (SDK_READY), it evaluates feature flags + act(() => client.__emitter__.emit(Event.SDK_READY)); + + expect(client.getTreatmentWithConfig).toBeCalledWith(featureFlagName, attributes, { properties }); + expect(client.getTreatmentWithConfig).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( + + {React.createElement(() => { + const treatment = useTreatmentWithConfig({ name: featureFlagName, attributes, properties, splitKey: 'user2', updateOnSdkUpdate: false }).treatment; + + renderTimes++; + switch (renderTimes) { + case 1: + // returns control if not operational (SDK not ready), without calling `getTreatmentWithConfig` method + expect(client.getTreatmentWithConfig).not.toBeCalled(); + expect(treatment).toEqual(CONTROL_WITH_CONFIG); + break; + case 2: + case 3: + // once operational (SDK_READY or SDK_READY_FROM_CACHE), it evaluates feature flags + expect(client.getTreatmentWithConfig).toHaveBeenLastCalledWith(featureFlagName, attributes, { properties }); + expect(client.getTreatmentWithConfig).toHaveLastReturnedWith(treatment); + 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.getTreatmentWithConfig).toBeCalledTimes(2); + }); + + test('throws error if invoked outside of SplitFactoryProvider.', () => { + expect(() => { + render( + React.createElement(() => { + useTreatmentWithConfig({ name: featureFlagName, attributes }).treatment; + return null; + }) + ); + }).toThrow(EXCEPTION_NO_SFP); + }); + + test('useTreatmentWithConfig 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, countUseTreatmentWithConfig = 0, countUseTreatmentWithConfigUser2 = 0, countUseTreatmentWithConfigUser2WithoutUpdate = 0; + const lastUpdateSetUser2 = new Set(); + const lastUpdateSetUser2WithUpdate = new Set(); + + function validateTreatment({ treatment, isReady, isReadyFromCache }: IUseTreatmentWithConfigResult) { + if (isReady || isReadyFromCache) { + expect(treatment).toEqual({ treatment: 'on', config: null }) + } else { + expect(treatment).toEqual({ treatment: 'control', config: null }) + } + } + + render( + + <> + + {() => countSplitContext++} + + {React.createElement(() => { + const context = useTreatmentWithConfig({ name: 'split_test', attributes: { att1: 'att1' } }); + expect(context.client).toBe(mainClient); // Assert that the main client was retrieved. + validateTreatment(context); + countUseTreatmentWithConfig++; + return null; + })} + {React.createElement(() => { + const context = useTreatmentWithConfig({ name: 'split_test', splitKey: 'user_2' }); + expect(context.client).toBe(user2Client); + validateTreatment(context); + lastUpdateSetUser2.add(context.lastUpdate); + countUseTreatmentWithConfigUser2++; + return null; + })} + {React.createElement(() => { + const context = useTreatmentWithConfig({ name: 'split_test', splitKey: 'user_2', updateOnSdkUpdate: false }); + expect(context.client).toBe(user2Client); + validateTreatment(context); + lastUpdateSetUser2WithUpdate.add(context.lastUpdate); + countUseTreatmentWithConfigUser2WithoutUpdate++; + 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 useTreatmentWithConfig evaluates with the main client and have default update options, it re-renders for each main client event. + expect(countUseTreatmentWithConfig).toEqual(4); + expect(mainClient.getTreatmentWithConfig).toHaveBeenCalledTimes(3); // when ready from cache, ready and update + expect(mainClient.getTreatmentWithConfig).toHaveBeenLastCalledWith('split_test', { att1: 'att1' }, undefined); + + // If useTreatmentWithConfig evaluates with a different client and have default update options, it re-renders for each event of the new client. + expect(countUseTreatmentWithConfigUser2).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(countUseTreatmentWithConfigUser2WithoutUpdate).toEqual(3); + expect(lastUpdateSetUser2WithUpdate.size).toEqual(3); + expect(user2Client.getTreatmentWithConfig).toHaveBeenCalledTimes(5); // when ready from cache x2, ready x2 and update x1 + expect(user2Client.getTreatmentWithConfig).toHaveBeenLastCalledWith('split_test', undefined, undefined); + }); + +}); diff --git a/src/__tests__/useTreatments.test.tsx b/src/__tests__/useTreatments.test.tsx new file mode 100644 index 0000000..9b5bb57 --- /dev/null +++ b/src/__tests__/useTreatments.test.tsx @@ -0,0 +1,229 @@ +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 { 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('useTreatments 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; + }) + } + + ); + }); + +}); diff --git a/src/__tests__/useSplitTreatments.test.tsx b/src/__tests__/useTreatmentsWithConfig.test.tsx similarity index 77% rename from src/__tests__/useSplitTreatments.test.tsx rename to src/__tests__/useTreatmentsWithConfig.test.tsx index bf32348..84b9981 100644 --- a/src/__tests__/useSplitTreatments.test.tsx +++ b/src/__tests__/useTreatmentsWithConfig.test.tsx @@ -12,11 +12,11 @@ import { CONTROL_WITH_CONFIG, EXCEPTION_NO_SFP } from '../constants'; /** Test target */ import { SplitFactoryProvider } from '../SplitFactoryProvider'; -import { useSplitTreatments } from '../useSplitTreatments'; +import { useTreatmentsWithConfig } from '../useTreatmentsWithConfig'; import { SplitContext } from '../SplitContext'; -import { ISplitTreatmentsChildProps } from '../types'; +import { IUseTreatmentsWithConfigResult } from '../types'; -describe('useSplitTreatments', () => { +describe('useTreatmentsWithConfig', () => { const featureFlagNames = ['split1']; const flagSets = ['set1']; @@ -32,11 +32,11 @@ describe('useSplitTreatments', () => { render( {React.createElement(() => { - treatments = useSplitTreatments({ names: featureFlagNames, attributes, properties }).treatments; - treatmentsByFlagSets = useSplitTreatments({ flagSets, attributes, properties }).treatments; + treatments = useTreatmentsWithConfig({ names: featureFlagNames, attributes, properties }).treatments; + treatmentsByFlagSets = useTreatmentsWithConfig({ flagSets, attributes, properties }).treatments; // @ts-expect-error Options object must provide either names or flagSets - expect(useSplitTreatments({}).treatments).toEqual({}); + expect(useTreatmentsWithConfig({}).treatments).toEqual({}); return null; })} @@ -68,7 +68,7 @@ describe('useSplitTreatments', () => { render( {React.createElement(() => { - const treatments = useSplitTreatments({ names: featureFlagNames, attributes, properties, splitKey: 'user2', updateOnSdkUpdate: false }).treatments; + const treatments = useTreatmentsWithConfig({ names: featureFlagNames, attributes, properties, splitKey: 'user2', updateOnSdkUpdate: false }).treatments; renderTimes++; switch (renderTimes) { @@ -102,8 +102,8 @@ describe('useSplitTreatments', () => { expect(() => { render( React.createElement(() => { - useSplitTreatments({ names: featureFlagNames, attributes }).treatments; - useSplitTreatments({ flagSets: featureFlagNames }).treatments; + useTreatmentsWithConfig({ names: featureFlagNames, attributes }).treatments; + useTreatmentsWithConfig({ flagSets: featureFlagNames }).treatments; return null; }) ); @@ -119,10 +119,10 @@ describe('useSplitTreatments', () => { { React.createElement(() => { // @ts-expect-error Test error handling - let treatments = useSplitTreatments('split1').treatments; + let treatments = useTreatmentsWithConfig('split1').treatments; expect(treatments).toEqual({}); // @ts-expect-error Test error handling - treatments = useSplitTreatments({ names: [true, ' flag_1 ', ' '] }).treatments; + treatments = useTreatmentsWithConfig({ names: [true, ' flag_1 ', ' '] }).treatments; expect(treatments).toEqual({ flag_1: CONTROL_WITH_CONFIG }); return null; @@ -132,16 +132,16 @@ describe('useSplitTreatments', () => { ); }); - test('useSplitTreatments must update on SDK events', async () => { + test('useTreatmentsWithConfig 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, countUseSplitTreatments = 0, countUseSplitTreatmentsUser2 = 0, countUseSplitTreatmentsUser2WithoutUpdate = 0; + let countSplitContext = 0, countUseTreatmentsWithConfig = 0, countUseTreatmentsWithConfigUser2 = 0, countUseTreatmentsWithConfigUser2WithoutUpdate = 0; const lastUpdateSetUser2 = new Set(); const lastUpdateSetUser2WithUpdate = new Set(); - function validateTreatments({ treatments, isReady, isReadyFromCache }: ISplitTreatmentsChildProps) { + function validateTreatments({ treatments, isReady, isReadyFromCache }: IUseTreatmentsWithConfigResult) { if (isReady || isReadyFromCache) { expect(treatments).toEqual({ split_test: { @@ -166,26 +166,26 @@ describe('useSplitTreatments', () => { {() => countSplitContext++} {React.createElement(() => { - const context = useSplitTreatments({ names: ['split_test'], attributes: { att1: 'att1' } }); + const context = useTreatmentsWithConfig({ names: ['split_test'], attributes: { att1: 'att1' } }); expect(context.client).toBe(mainClient); // Assert that the main client was retrieved. validateTreatments(context); - countUseSplitTreatments++; + countUseTreatmentsWithConfig++; return null; })} {React.createElement(() => { - const context = useSplitTreatments({ names: ['split_test'], splitKey: 'user_2' }); + const context = useTreatmentsWithConfig({ names: ['split_test'], splitKey: 'user_2' }); expect(context.client).toBe(user2Client); validateTreatments(context); lastUpdateSetUser2.add(context.lastUpdate); - countUseSplitTreatmentsUser2++; + countUseTreatmentsWithConfigUser2++; return null; })} {React.createElement(() => { - const context = useSplitTreatments({ names: ['split_test'], splitKey: 'user_2', updateOnSdkUpdate: false }); + const context = useTreatmentsWithConfig({ names: ['split_test'], splitKey: 'user_2', updateOnSdkUpdate: false }); expect(context.client).toBe(user2Client); validateTreatments(context); lastUpdateSetUser2WithUpdate.add(context.lastUpdate); - countUseSplitTreatmentsUser2WithoutUpdate++; + countUseTreatmentsWithConfigUser2WithoutUpdate++; return null; })} @@ -202,16 +202,16 @@ describe('useSplitTreatments', () => { // SplitFactoryProvider renders once expect(countSplitContext).toEqual(1); - // If useSplitTreatments evaluates with the main client and have default update options, it re-renders for each main client event. - expect(countUseSplitTreatments).toEqual(4); + // If useTreatmentsWithConfig evaluates with the main client and have default update options, it re-renders for each main client event. + expect(countUseTreatmentsWithConfig).toEqual(4); expect(mainClient.getTreatmentsWithConfig).toHaveBeenCalledTimes(3); // when ready from cache, ready and update expect(mainClient.getTreatmentsWithConfig).toHaveBeenLastCalledWith(['split_test'], { att1: 'att1' }, undefined); - // If useSplitTreatments evaluates with a different client and have default update options, it re-renders for each event of the new client. - expect(countUseSplitTreatmentsUser2).toEqual(4); + // If useTreatmentsWithConfig evaluates with a different client and have default update options, it re-renders for each event of the new client. + expect(countUseTreatmentsWithConfigUser2).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(countUseSplitTreatmentsUser2WithoutUpdate).toEqual(3); + expect(countUseTreatmentsWithConfigUser2WithoutUpdate).toEqual(3); expect(lastUpdateSetUser2WithUpdate.size).toEqual(3); expect(user2Client.getTreatmentsWithConfig).toHaveBeenCalledTimes(5); // when ready from cache x2, ready x2 and update x1 expect(user2Client.getTreatmentsWithConfig).toHaveBeenLastCalledWith(['split_test'], undefined, undefined); @@ -223,7 +223,7 @@ describe('useSplitTreatments', () => { { React.createElement(() => { // @ts-expect-error names and flagSets are mutually exclusive - const treatments = useSplitTreatments({ names: featureFlagNames, flagSets, attributes }).treatments; + const treatments = useTreatmentsWithConfig({ names: featureFlagNames, flagSets, attributes }).treatments; expect(treatments).toEqual({ split1: CONTROL_WITH_CONFIG }); return null; }) From 709a35b72e3cda49dd25edc475bfbe3ff759d787 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 27 Oct 2025 14:50:16 -0300 Subject: [PATCH 2/2] Update comments and metadata --- README.md | 16 ++++++++-------- src/SplitClient.tsx | 2 -- src/SplitTreatments.tsx | 2 +- src/withSplitTreatments.tsx | 2 +- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 30aec4c..987df64 100644 --- a/README.md +++ b/README.md @@ -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 = { @@ -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
Loading SDK ...
; - 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 }; } diff --git a/src/SplitClient.tsx b/src/SplitClient.tsx index fd146ae..e94984d 100644 --- a/src/SplitClient.tsx +++ b/src/SplitClient.tsx @@ -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; diff --git a/src/SplitTreatments.tsx b/src/SplitTreatments.tsx index 7f52776..e895f72 100644 --- a/src/SplitTreatments.tsx +++ b/src/SplitTreatments.tsx @@ -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; diff --git a/src/withSplitTreatments.tsx b/src/withSplitTreatments.tsx index 8119292..54bf4fa 100644 --- a/src/withSplitTreatments.tsx +++ b/src/withSplitTreatments.tsx @@ -10,7 +10,7 @@ import { SplitTreatments } from './SplitTreatments'; * @param names - list of feature flag names * @param attributes - An object of type Attributes used to evaluate the feature flags. * - * @deprecated `withSplitTreatments` will be removed in a future major release. We recommend replacing it with the `useSplitTreatments` hook. + * @deprecated `withSplitTreatments` will be removed in a future major release. We recommend replacing it with the `useTreatment*` hooks. */ export function withSplitTreatments(names: string[], attributes?: SplitIO.Attributes) {