import * as React from 'react'; import { render, act } from '@testing-library/react'; /** Mocks */ import { mockSdk, Event, getLastInstance } from './testUtils/mockSplitFactory'; jest.mock('@splitsoftware/splitio/client', () => { return { SplitFactory: mockSdk() }; }); import { SplitFactory } from '@splitsoftware/splitio/client'; import { sdkBrowser } from './testUtils/sdkConfigs'; const logSpy = jest.spyOn(console, 'log'); /** Test target */ import { SplitFactoryProvider } from '../SplitFactoryProvider'; import { SplitContext, useSplitContext } from '../SplitContext'; import { getStatus } from '../utils'; import { WARN_SF_CONFIG_AND_FACTORY } from '../constants'; import { INITIAL_STATUS } from './testUtils/utils'; import { useSplitClient } from '../useSplitClient'; describe('SplitFactoryProvider', () => { test('passes no-ready properties, no factory and no client to the context if initialized without a config and factory props.', () => { render( {React.createElement(() => { const context = useSplitContext(); expect(context).toEqual({ ...INITIAL_STATUS, factory: undefined, client: undefined, }); return null; })} ); }); test('passes no-ready properties to the context if initialized with a config.', () => { render( {React.createElement(() => { const context = useSplitContext(); expect(context).toEqual({ ...INITIAL_STATUS, factory: getLastInstance(SplitFactory), client: getLastInstance(SplitFactory).client(), }); return null; })} ); }); test('passes ready properties to the context if initialized with a ready factory.', async () => { const outerFactory = SplitFactory(sdkBrowser); (outerFactory as any).client().__emitter__.emit(Event.SDK_READY_FROM_CACHE); (outerFactory as any).client().__emitter__.emit(Event.SDK_READY); (outerFactory.manager().names as jest.Mock).mockReturnValue(['split1']); await outerFactory.client().ready(); render( {React.createElement(() => { const context = useSplitClient(); expect(context).toEqual({ ...INITIAL_STATUS, factory: outerFactory, client: outerFactory.client(), isReady: true, isReadyFromCache: true, isOperational: true, lastUpdate: getStatus(outerFactory.client()).lastUpdate }); return null; })} ); }); test('renders a passed JSX.Element with a new SplitContext value.', (done) => { const Component = () => { return ( {(value) => { expect(value).toEqual({ ...INITIAL_STATUS, factory: getLastInstance(SplitFactory), client: getLastInstance(SplitFactory).client(), }); done(); return null; }} ); }; render( ); }); test('logs warning if both a config and factory are passed as props.', () => { const outerFactory = SplitFactory(sdkBrowser); render( {React.createElement(() => { return null; })} ); expect(logSpy).toBeCalledWith('[WARN] splitio => ' + WARN_SF_CONFIG_AND_FACTORY); logSpy.mockRestore(); }); test('cleans up on update and unmount if config prop is provided.', () => { let renderTimes = 0; const createdFactories = new Set(); const factoryDestroySpies: jest.SpyInstance[] = []; const outerFactory = SplitFactory(sdkBrowser); const Component = () => { const { factory, isReady, hasTimedout } = useSplitClient(); renderTimes++; switch (renderTimes) { case 1: expect(factory).toBe(outerFactory); return null; case 2: case 5: expect(isReady).toBe(false); expect(hasTimedout).toBe(false); expect(factory).toBe(getLastInstance(SplitFactory)); if (!createdFactories.has(factory!)) factoryDestroySpies.push(jest.spyOn(factory!, 'destroy')); createdFactories.add(factory!); return null; case 3: case 4: case 6: expect(isReady).toBe(true); expect(hasTimedout).toBe(true); expect(factory).toBe(getLastInstance(SplitFactory)); if (!createdFactories.has(factory!)) factoryDestroySpies.push(jest.spyOn(factory!, 'destroy')); createdFactories.add(factory!); return null; case 7: throw new Error('Must not rerender'); } return null; }; const emitSdkEvents = () => { const factory = getLastInstance(SplitFactory); factory.client().__emitter__.emit(Event.SDK_READY_TIMED_OUT) factory.client().__emitter__.emit(Event.SDK_READY) }; // 1st render: factory provided const wrapper = render( ); // 2nd render: factory created, not ready wrapper.rerender( ); // 3rd render: SDK timeout and ready events emitted (only one re-render due to batched state updates in React) act(emitSdkEvents); // 4th render: same config prop -> factory is not recreated wrapper.rerender( ); act(emitSdkEvents); // Emitting events again has no effect expect(createdFactories.size).toBe(1); // 5th render: Update config prop -> factory is recreated, not ready yet wrapper.rerender( ); // 6th render: SDK events emitted act(emitSdkEvents); wrapper.unmount(); // factory `destroy` methods are called expect(createdFactories.size).toBe(2); expect(factoryDestroySpies.length).toBe(2); factoryDestroySpies.forEach(spy => expect(spy).toBeCalledTimes(1)); }); test('doesn\'t clean up on unmount if the factory is provided as a prop.', () => { let destroySpy; const outerFactory = SplitFactory(sdkBrowser); const wrapper = render( {React.createElement(() => { const { factory } = useSplitClient(); destroySpy = jest.spyOn(factory!, 'destroy'); return null; })} ); wrapper.unmount(); expect(destroySpy).not.toBeCalled(); }); test('passes attributes to the main client if provided.', () => { (SplitFactory as jest.Mock).mockClear(); let client; const Component = () => { client = useSplitContext().client; return null; } const wrapper = render( ); expect(client.getAttributes()).toEqual({ attr1: 'value1' }); wrapper.rerender( ); expect(client.getAttributes()).toEqual({ attr1: 'value2' }); expect(SplitFactory).toBeCalledTimes(1); }); });