import * as React from 'react'; import { act, fireEvent, 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'; /** Test target */ import { useSplitClient } from '../useSplitClient'; import { SplitFactoryProvider } from '../SplitFactoryProvider'; import { SplitContext } from '../SplitContext'; import { INITIAL_STATUS, testAttributesBinding, TestComponentProps } from './testUtils/utils'; import { EXCEPTION_NO_SFP } from '../constants'; describe('useSplitClient', () => { test('returns the main client from the context updated by SplitFactoryProvider.', () => { const outerFactory = SplitFactory(sdkBrowser); let client; render( {React.createElement(() => { client = useSplitClient().client; return null; })} ); expect(client).toBe(outerFactory.client()); }); test('returns a new client from the factory at Split context given a splitKey.', () => { const outerFactory = SplitFactory(sdkBrowser); let client; render( {React.createElement(() => { (outerFactory.client as jest.Mock).mockClear(); client = useSplitClient({ splitKey: 'user2' }).client; return null; })} ); expect(outerFactory.client as jest.Mock).toBeCalledWith('user2'); expect(outerFactory.client as jest.Mock).toHaveReturnedWith(client); }); test('throws error if invoked outside of SplitFactoryProvider.', () => { expect(() => { render( React.createElement(() => { useSplitClient(); useSplitClient({ splitKey: 'user2' }); return null; }) ); }).toThrow(EXCEPTION_NO_SFP); }); test('attributes binding test with utility', (done) => { // eslint-disable-next-line react/prop-types const InnerComponent = ({ splitKey, attributesClient, testSwitch }) => { useSplitClient({ splitKey, attributes: attributesClient }); testSwitch(done, splitKey); return null; }; function Component({ attributesFactory, attributesClient, splitKey, testSwitch, factory }: TestComponentProps) { return ( ); } testAttributesBinding(Component); }); test('must update on SDK events', () => { const outerFactory = SplitFactory(sdkBrowser); const mainClient = outerFactory.client() as any; const user2Client = outerFactory.client('user_2') as any; let countSplitContext = 0, countUseSplitClient = 0, countUseSplitClientUser2 = 0; let countUseSplitClientWithoutUpdate = 0, countUseSplitClientUser2WithoutTimeout = 0; let previousLastUpdate = -1; const { getByTestId } = render( <> {() => countSplitContext++} {React.createElement(() => { // Equivalent to using config key: `const { client } = useSplitClient({ splitKey: sdkBrowser.core.key, attributes: { att1: 'att1' } });` const { client } = useSplitClient({ attributes: { att1: 'att1' } }); expect(client).toBe(mainClient); // Assert that the main client was retrieved. expect(client!.getAttributes()).toEqual({ att1: 'att1' }); // Assert that the client was retrieved with the provided attributes. countUseSplitClient++; return null; })} {React.createElement(() => { const { client, isReady, isReadyFromCache, hasTimedout } = useSplitClient({ splitKey: 'user_2', updateOnSdkUpdate: undefined /* default is true */ }); expect(client).toBe(user2Client); countUseSplitClientUser2++; switch (countUseSplitClientUser2) { case 1: // initial render expect([isReady, isReadyFromCache, hasTimedout]).toEqual([false, false, false]); break; case 2: // SDK_READY_FROM_CACHE expect([isReady, isReadyFromCache, hasTimedout]).toEqual([false, true, false]); break; case 3: // SDK_READY_TIMED_OUT expect([isReady, isReadyFromCache, hasTimedout]).toEqual([false, true, true]); break; case 4: // SDK_READY expect([isReady, isReadyFromCache, hasTimedout]).toEqual([true, true, true]); break; case 5: // SDK_UPDATE expect([isReady, isReadyFromCache, hasTimedout]).toEqual([true, true, true]); break; default: throw new Error('Unexpected render'); } return null; })} {React.createElement(() => { const [state, setState] = React.useState(false); const { isReady, isReadyFromCache, hasTimedout, lastUpdate } = useSplitClient({ splitKey: sdkBrowser.core.key, updateOnSdkUpdate: false }); countUseSplitClientWithoutUpdate++; switch (countUseSplitClientWithoutUpdate) { case 1: // initial render expect([isReady, isReadyFromCache, hasTimedout]).toEqual([false, false, false]); expect(lastUpdate).toBe(0); break; case 2: // SDK_READY_FROM_CACHE expect([isReady, isReadyFromCache, hasTimedout]).toEqual([false, true, false]); expect(lastUpdate).toBeGreaterThan(previousLastUpdate); break; case 3: // SDK_READY expect([isReady, isReadyFromCache, hasTimedout]).toEqual([true, true, false]); expect(lastUpdate).toBeGreaterThan(previousLastUpdate); break; case 4: // Forced re-render, lastUpdate doesn't change after SDK_UPDATE due to updateOnSdkUpdate = false expect([isReady, isReadyFromCache, hasTimedout]).toEqual([true, true, false]); expect(lastUpdate).toBe(previousLastUpdate); break; default: throw new Error('Unexpected render'); } previousLastUpdate = lastUpdate; return ( ); })} {React.createElement(() => { useSplitClient({ splitKey: 'user_2', updateOnSdkTimedout: false }); countUseSplitClientUser2WithoutTimeout++; return null; })} ); act(() => mainClient.__emitter__.emit(Event.SDK_READY_FROM_CACHE)); act(() => user2Client.__emitter__.emit(Event.SDK_READY_FROM_CACHE)); act(() => mainClient.__emitter__.emit(Event.SDK_READY)); act(() => user2Client.__emitter__.emit(Event.SDK_READY_TIMED_OUT)); act(() => user2Client.__emitter__.emit(Event.SDK_READY)); act(() => mainClient.__emitter__.emit(Event.SDK_UPDATE)); act(() => user2Client.__emitter__.emit(Event.SDK_UPDATE)); act(() => fireEvent.click(getByTestId('update-button'))); // SplitFactoryProvider renders once expect(countSplitContext).toEqual(1); // If useSplitClient retrieves the main client and have default update options, it re-renders for each main client event. expect(countUseSplitClient).toEqual(4); // If useSplitClient retrieves a different client and have default update options, it re-renders for each event of the new client. expect(countUseSplitClientUser2).toEqual(5); // If useSplitClient retrieves the main client and have updateOnSdkUpdate = false, it doesn't render when the main client updates. expect(countUseSplitClientWithoutUpdate).toEqual(4); // If useSplitClient retrieves a different client and have updateOnSdkTimedout = false, it doesn't render when the the new client times out. expect(countUseSplitClientUser2WithoutTimeout).toEqual(4); }); // Remove this test once side effects are moved to the useSplitClient effect. test('must update on SDK events between the render phase (hook call) and commit phase (effect call)', () => { const outerFactory = SplitFactory(sdkBrowser); let count = 0; render( {React.createElement(() => { useSplitClient({ splitKey: 'some_user' }); count++; // side effect in the render phase const client = outerFactory.client('some_user') as any; if (!client.getStatus().isReady) client.__emitter__.emit(Event.SDK_READY); return null; })} ) expect(count).toEqual(2); }); test('must support changes in update props', () => { const outerFactory = SplitFactory(sdkBrowser); const mainClient = outerFactory.client() as any; let rendersCount = 0; let currentStatus, previousStatus; function InnerComponent(updateOptions) { previousStatus = currentStatus; currentStatus = useSplitClient(updateOptions); rendersCount++; return null; } function Component(updateOptions) { return ( ) } const wrapper = render(); expect(rendersCount).toBe(1); act(() => mainClient.__emitter__.emit(Event.SDK_READY_TIMED_OUT)); // do not trigger re-render because updateOnSdkTimedout is false expect(rendersCount).toBe(1); expect(currentStatus).toMatchObject({ ...INITIAL_STATUS, updateOnSdkUpdate: false, updateOnSdkTimedout: false }); wrapper.rerender(); expect(rendersCount).toBe(2); expect(currentStatus).toEqual(previousStatus); wrapper.rerender(); // trigger re-render because there was an SDK_READY_TIMED_OUT event expect(rendersCount).toBe(4); // @TODO optimize `useSplitClient` to avoid double render expect(currentStatus).toMatchObject({ isReady: false, isReadyFromCache: false, hasTimedout: true }); act(() => mainClient.__emitter__.emit(Event.SDK_READY)); // trigger re-render expect(rendersCount).toBe(5); expect(currentStatus).toMatchObject({ isReady: true, isReadyFromCache: true, hasTimedout: true }); act(() => mainClient.__emitter__.emit(Event.SDK_UPDATE)); // do not trigger re-render because updateOnSdkUpdate is false expect(rendersCount).toBe(5); wrapper.rerender(); // should not update the status (SDK_UPDATE event should be ignored) expect(rendersCount).toBe(6); expect(currentStatus).toEqual({ ...previousStatus, updateOnSdkTimedout: false }); wrapper.rerender(); // trigger re-render and update the status because updateOnSdkUpdate is true and there was an SDK_UPDATE event expect(rendersCount).toBe(8); // @TODO optimize `useSplitClient` to avoid double render expect(currentStatus.lastUpdate).toBeGreaterThan(previousStatus.lastUpdate); act(() => mainClient.__emitter__.emit(Event.SDK_UPDATE)); // trigger re-render because updateOnSdkUpdate is true expect(rendersCount).toBe(9); expect(currentStatus.lastUpdate).toBeGreaterThan(previousStatus.lastUpdate); wrapper.rerender(); expect(rendersCount).toBe(10); expect(currentStatus).toEqual({ ...previousStatus, updateOnSdkUpdate: false }); act(() => mainClient.__emitter__.emit(Event.SDK_UPDATE)); // do not trigger re-render because updateOnSdkUpdate is false now expect(rendersCount).toBe(10); }); test('must prioritize explicitly provided `updateOn` options over context defaults', () => { render( {React.createElement(() => { expect(useSplitClient()).toEqual({ ...INITIAL_STATUS, updateOnSdkReadyFromCache: false }); expect(useSplitClient({ updateOnSdkReady: false, updateOnSdkReadyFromCache: undefined, updateOnSdkTimedout: false })).toEqual({ ...INITIAL_STATUS, updateOnSdkReady: false, updateOnSdkReadyFromCache: false, updateOnSdkTimedout: false }); return null; })} ); }); });