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;
})}
);
});
});