import * as React from 'react';
import { render, act } from '@testing-library/react';
/** Mocks and test utils */
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';
/** Test target */
import { ISplitClientChildProps, ISplitFactoryChildProps } from '../types';
import { SplitFactoryProvider } from '../SplitFactoryProvider';
import { SplitClient } from '../SplitClient';
import { SplitContext, useSplitContext } from '../SplitContext';
import { INITIAL_STATUS, testAttributesBinding, TestComponentProps } from './testUtils/utils';
import { getStatus } from '../utils';
import { EXCEPTION_NO_SFP } from '../constants';
describe('SplitClient', () => {
test('passes no-ready props to the child if client is not ready.', () => {
render(
{(childProps: ISplitClientChildProps) => {
expect(childProps).toEqual({
...INITIAL_STATUS,
factory: getLastInstance(SplitFactory),
client: getLastInstance(SplitFactory).client('user1'),
});
return null;
}}
);
});
test('passes ready props to the child if client is ready.', 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(
{/* Equivalent to */}
{(childProps: ISplitClientChildProps) => {
expect(childProps).toEqual({
...INITIAL_STATUS,
factory: outerFactory,
client: outerFactory.client(),
isReady: true,
isReadyFromCache: true,
isOperational: true,
lastUpdate: getStatus(outerFactory.client()).lastUpdate
});
return null;
}}
);
});
test('rerenders child on SDK_READY_TIMEDOUT, SDK_READY_FROM_CACHE, SDK_READY and SDK_UPDATE events as default behavior', async () => {
const outerFactory = SplitFactory(sdkBrowser);
(outerFactory as any).client().__emitter__.emit(Event.SDK_READY);
(outerFactory.manager().names as jest.Mock).mockReturnValue(['split1']);
await outerFactory.client().ready();
let renderTimes = 0;
let previousLastUpdate = -1;
render(
{({ client, isReady, isReadyFromCache, hasTimedout, isTimedout, lastUpdate }: ISplitClientChildProps) => {
const statusProps = [isReady, isReadyFromCache, hasTimedout, isTimedout];
switch (renderTimes) {
case 0: // No ready
expect(statusProps).toStrictEqual([false, false, false, false]);
break;
case 1: // Timedout
expect(statusProps).toStrictEqual([false, false, true, true]);
break;
case 2: // Ready from cache
expect(statusProps).toStrictEqual([false, true, true, true]);
break;
case 3: // Ready
expect(statusProps).toStrictEqual([true, true, true, false]);
break;
case 4: // Updated
expect(statusProps).toStrictEqual([true, true, true, false]);
break;
default:
fail('Child must not be rerendered');
}
expect(client).toBe(outerFactory.client('user2'));
expect(lastUpdate).toBeGreaterThan(previousLastUpdate);
renderTimes++;
previousLastUpdate = lastUpdate;
return null;
}}
);
act(() => (outerFactory as any).client('user2').__emitter__.emit(Event.SDK_READY_TIMED_OUT));
act(() => (outerFactory as any).client('user2').__emitter__.emit(Event.SDK_READY_FROM_CACHE));
act(() => (outerFactory as any).client('user2').__emitter__.emit(Event.SDK_READY));
act(() => (outerFactory as any).client('user2').__emitter__.emit(Event.SDK_UPDATE));
expect(renderTimes).toBe(5);
});
test('rerenders child on SDK_READY_TIMED_OUT and SDK_UPDATE events, but not on SDK_READY.', async () => {
const outerFactory = SplitFactory(sdkBrowser);
(outerFactory as any).client().__emitter__.emit(Event.SDK_READY);
(outerFactory.manager().names as jest.Mock).mockReturnValue(['split1']);
await outerFactory.client().ready();
let renderTimes = 0;
let previousLastUpdate = -1;
render(
{({ client, isReady, isReadyFromCache, hasTimedout, isTimedout, lastUpdate }: ISplitClientChildProps) => {
const statusProps = [isReady, isReadyFromCache, hasTimedout, isTimedout];
switch (renderTimes) {
case 0: // No ready
expect(statusProps).toStrictEqual([false, false, false, false]);
break;
case 1: // Timedout
expect(statusProps).toStrictEqual([false, false, true, true]);
break;
case 2: // Updated. Although `updateOnSdkReady` is false, status props must reflect the current status of the client.
expect(statusProps).toStrictEqual([true, true, true, false]);
break;
default:
fail('Child must not be rerendered');
}
expect(client).toBe(outerFactory.client('user2'));
expect(lastUpdate).toBeGreaterThan(previousLastUpdate);
renderTimes++;
previousLastUpdate = lastUpdate;
return null;
}}
);
act(() => (outerFactory as any).client('user2').__emitter__.emit(Event.SDK_READY_TIMED_OUT));
act(() => (outerFactory as any).client('user2').__emitter__.emit(Event.SDK_READY));
act(() => (outerFactory as any).client('user2').__emitter__.emit(Event.SDK_UPDATE));
expect(renderTimes).toBe(3);
});
test('rerenders child only on SDK_READY event, when setting updateOnSdkReadyFromCache, updateOnSdkTimedout and updateOnSdkUpdate to false.', async () => {
const outerFactory = SplitFactory(sdkBrowser);
(outerFactory as any).client().__emitter__.emit(Event.SDK_READY);
(outerFactory.manager().names as jest.Mock).mockReturnValue(['split1']);
await outerFactory.client().ready();
let renderTimes = 0;
let previousLastUpdate = -1;
render(
{({ client, isReady, isReadyFromCache, hasTimedout, isTimedout, lastUpdate }: ISplitClientChildProps) => {
const statusProps = [isReady, isReadyFromCache, hasTimedout, isTimedout];
switch (renderTimes) {
case 0: // No ready
expect(statusProps).toStrictEqual([false, false, false, false]);
break;
case 1: // Ready
expect(statusProps).toStrictEqual([true, true, true, false]); // not rerendering on SDK_TIMEOUT, but hasTimedout reflects the current state
break;
default:
fail('Child must not be rerendered');
}
expect(client).toBe(outerFactory.client('user2'));
expect(lastUpdate).toBeGreaterThan(previousLastUpdate);
renderTimes++;
previousLastUpdate = lastUpdate;
return null;
}}
);
act(() => (outerFactory as any).client('user2').__emitter__.emit(Event.SDK_READY_TIMED_OUT));
act(() => (outerFactory as any).client('user2').__emitter__.emit(Event.SDK_READY));
act(() => (outerFactory as any).client('user2').__emitter__.emit(Event.SDK_UPDATE));
expect(renderTimes).toBe(2);
});
test('must update on SDK events between the render and commit phases', () => {
const outerFactory = SplitFactory(sdkBrowser);
let count = 0;
render(
{({ client }) => {
count++;
// side effect in the render phase
if (!client!.getStatus().isReady) {
(client as any).__emitter__.emit(Event.SDK_READY);
}
return null;
}}
);
expect(count).toEqual(2);
});
test('renders a passed JSX.Element with a new SplitContext value.', () => {
const outerFactory = SplitFactory(sdkBrowser);
const Component = () => {
return (
{(value) => {
expect(value).toEqual({
...INITIAL_STATUS,
factory: outerFactory,
client: outerFactory.client('user2'),
});
return null;
}}
);
};
render(
);
});
test('throws error if invoked outside of SplitFactoryProvider.', () => {
expect(() => {
render(
{() => null}
);
}).toThrow(EXCEPTION_NO_SFP);
});
test(`passes a new client if re-rendered with a different splitKey.
Only updates the state if the new client triggers an event, but not the previous one.`, (done) => {
const outerFactory = SplitFactory(sdkBrowser);
let renderTimes = 0;
class InnerComponent extends React.Component {
constructor(props: any) {
super(props);
this.state = { splitKey: 'user1' };
}
async componentDidMount() {
await act(() => this.setState({ splitKey: 'user2' }));
await act(() => (outerFactory as any).client('user2').__emitter__.emit(Event.SDK_READY_TIMED_OUT));
await act(() => (outerFactory as any).client('user1').__emitter__.emit(Event.SDK_READY_TIMED_OUT));
await act(() => this.setState({ splitKey: 'user3' }));
await act(() => (outerFactory as any).client('user2').__emitter__.emit(Event.SDK_READY));
await act(() => (outerFactory as any).client('user3').__emitter__.emit(Event.SDK_READY));
await act(() => (outerFactory as any).client('user2').__emitter__.emit(Event.SDK_UPDATE));
await act(() => (outerFactory as any).client('user3').__emitter__.emit(Event.SDK_UPDATE));
expect(renderTimes).toBe(6);
done();
}
render() {
return (
{({ client, isReady, isReadyFromCache, hasTimedout, isTimedout }: ISplitClientChildProps) => {
const statusProps = [isReady, isReadyFromCache, hasTimedout, isTimedout];
switch (renderTimes) {
case 0:
expect(client).toBe(outerFactory.client('user1'));
expect(statusProps).toStrictEqual([false, false, false, false]);
break;
case 1:
expect(client).toBe(outerFactory.client('user2'));
expect(statusProps).toStrictEqual([false, false, false, false]);
break;
case 2:
expect(client).toBe(outerFactory.client('user2'));
expect(statusProps).toStrictEqual([false, false, true, true]);
break;
case 3:
expect(client).toBe(outerFactory.client('user3'));
expect(statusProps).toStrictEqual([false, false, false, false]);
break;
case 4:
expect(client).toBe(outerFactory.client('user3'));
expect(statusProps).toStrictEqual([true, true, false, false]);
break;
case 5:
expect(client).toBe(outerFactory.client('user3'));
expect(statusProps).toStrictEqual([true, true, false, false]);
break;
default:
fail('Child must not be rerendered');
}
renderTimes++;
return null;
}}
);
}
}
render(
);
});
test('attributes binding test with utility', (done) => {
function Component({ attributesFactory, attributesClient, splitKey, testSwitch, factory }: TestComponentProps) {
return (
{() => {
testSwitch(done, splitKey);
return null;
}}
);
}
testAttributesBinding(Component);
});
test('must overwrite `updateOn` options in context', () => {
render(
{React.createElement(() => {
expect(useSplitContext()).toEqual({
...INITIAL_STATUS,
updateOnSdkReadyFromCache: false
});
return null;
})}
{React.createElement(() => {
expect(useSplitContext()).toEqual({
...INITIAL_STATUS,
updateOnSdkReady: false,
updateOnSdkReadyFromCache: false,
updateOnSdkTimedout: false
});
return null;
})}
);
});
});
// Tests to validate the migration from `SplitFactoryProvider` with child as a function in v1, to `SplitFactoryProvider` + `SplitClient` with child as a function in v2.
describe('SplitFactoryProvider + SplitClient', () => {
test('rerenders child on SDK_READY_TIMEDOUT, SDK_READY_FROM_CACHE, SDK_READY and SDK_UPDATE events as default behavior (config prop)', async () => {
let renderTimes = 0;
let previousLastUpdate = -1;
render(
{({ factory, isReady, isReadyFromCache, hasTimedout, isTimedout, lastUpdate }: ISplitFactoryChildProps) => {
const statusProps = [isReady, isReadyFromCache, hasTimedout, isTimedout];
switch (renderTimes) {
case 0: // No ready
expect(statusProps).toStrictEqual([false, false, false, false]);
break;
case 1: // Timedout
expect(statusProps).toStrictEqual([false, false, true, true]);
break;
case 2: // Ready from cache
expect(statusProps).toStrictEqual([false, true, true, true]);
break;
case 3: // Ready
expect(statusProps).toStrictEqual([true, true, true, false]);
break;
case 4: // Updated
expect(statusProps).toStrictEqual([true, true, true, false]);
break;
default:
fail('Child must not be rerendered');
} // eslint-disable-next-line no-use-before-define
expect(factory).toBe(getLastInstance(SplitFactory));
expect(lastUpdate).toBeGreaterThan(previousLastUpdate);
renderTimes++;
previousLastUpdate = lastUpdate;
return null;
}}
);
const innerFactory = (SplitFactory as jest.Mock).mock.results.slice(-1)[0].value;
act(() => (innerFactory as any).client().__emitter__.emit(Event.SDK_READY_TIMED_OUT));
act(() => (innerFactory as any).client().__emitter__.emit(Event.SDK_READY_FROM_CACHE));
act(() => (innerFactory as any).client().__emitter__.emit(Event.SDK_READY));
act(() => (innerFactory as any).client().__emitter__.emit(Event.SDK_UPDATE));
expect(renderTimes).toBe(5);
});
test('rerenders child on SDK_READY_TIMEDOUT, SDK_READY_FROM_CACHE, SDK_READY and SDK_UPDATE events as default behavior (factory prop)', async () => {
const outerFactory = SplitFactory(sdkBrowser);
let renderTimes = 0;
let previousLastUpdate = -1;
render(
{({ factory, isReady, isReadyFromCache, hasTimedout, isTimedout, lastUpdate }: ISplitFactoryChildProps) => {
const statusProps = [isReady, isReadyFromCache, hasTimedout, isTimedout];
switch (renderTimes) {
case 0: // No ready
expect(statusProps).toStrictEqual([false, false, false, false]);
break;
case 1: // Timedout
expect(statusProps).toStrictEqual([false, false, true, true]);
break;
case 2: // Ready from cache
expect(statusProps).toStrictEqual([false, true, true, true]);
break;
case 3: // Ready
expect(statusProps).toStrictEqual([true, true, true, false]);
break;
case 4: // Updated
expect(statusProps).toStrictEqual([true, true, true, false]);
break;
default:
fail('Child must not be rerendered');
}
expect(factory).toBe(outerFactory);
expect(lastUpdate).toBeGreaterThan(previousLastUpdate);
renderTimes++;
previousLastUpdate = lastUpdate;
return null;
}}
);
act(() => (outerFactory as any).client().__emitter__.emit(Event.SDK_READY_TIMED_OUT));
act(() => (outerFactory as any).client().__emitter__.emit(Event.SDK_READY_FROM_CACHE));
act(() => (outerFactory as any).client().__emitter__.emit(Event.SDK_READY));
act(() => (outerFactory as any).client().__emitter__.emit(Event.SDK_UPDATE));
expect(renderTimes).toBe(5);
});
test('rerenders child on SDK_READY_TIMED_OUT and SDK_UPDATE events, but not on SDK_READY (config prop)', async () => {
let renderTimes = 0;
let previousLastUpdate = -1;
render(
{({ factory, isReady, isReadyFromCache, hasTimedout, isTimedout, lastUpdate }: ISplitFactoryChildProps) => {
const statusProps = [isReady, isReadyFromCache, hasTimedout, isTimedout];
switch (renderTimes) {
case 0: // No ready
expect(statusProps).toStrictEqual([false, false, false, false]);
break;
case 1: // Timedout
expect(statusProps).toStrictEqual([false, false, true, true]);
break;
case 2: // Updated. Although `updateOnSdkReady` is false, status props must reflect the current status of the client.
expect(statusProps).toStrictEqual([true, true, true, false]);
break;
default:
fail('Child must not be rerendered');
} // eslint-disable-next-line no-use-before-define
expect(factory).toBe(getLastInstance(SplitFactory));
expect(lastUpdate).toBeGreaterThan(previousLastUpdate);
renderTimes++;
previousLastUpdate = lastUpdate;
return null;
}}
);
const innerFactory = (SplitFactory as jest.Mock).mock.results.slice(-1)[0].value;
act(() => (innerFactory as any).client().__emitter__.emit(Event.SDK_READY_TIMED_OUT));
act(() => (innerFactory as any).client().__emitter__.emit(Event.SDK_READY));
act(() => (innerFactory as any).client().__emitter__.emit(Event.SDK_UPDATE));
expect(renderTimes).toBe(3);
});
test('rerenders child on SDK_READY_TIMED_OUT and SDK_UPDATE events, but not on SDK_READY (factory prop)', async () => {
const outerFactory = SplitFactory(sdkBrowser);
let renderTimes = 0;
let previousLastUpdate = -1;
render(
{({ factory, isReady, isReadyFromCache, hasTimedout, isTimedout, lastUpdate }: ISplitFactoryChildProps) => {
const statusProps = [isReady, isReadyFromCache, hasTimedout, isTimedout];
switch (renderTimes) {
case 0: // No ready
expect(statusProps).toStrictEqual([false, false, false, false]);
break;
case 1: // Timedout
expect(statusProps).toStrictEqual([false, false, true, true]);
break;
case 2: // Updated. Although `updateOnSdkReady` is false, status props must reflect the current status of the client.
expect(statusProps).toStrictEqual([true, true, true, false]);
break;
default:
fail('Child must not be rerendered');
}
expect(factory).toBe(outerFactory);
expect(lastUpdate).toBeGreaterThan(previousLastUpdate);
renderTimes++;
previousLastUpdate = lastUpdate;
return null;
}}
);
act(() => (outerFactory as any).client().__emitter__.emit(Event.SDK_READY_TIMED_OUT));
act(() => (outerFactory as any).client().__emitter__.emit(Event.SDK_READY));
act(() => (outerFactory as any).client().__emitter__.emit(Event.SDK_UPDATE));
expect(renderTimes).toBe(3);
});
test('rerenders child only on SDK_READY and SDK_READY_FROM_CACHE event (config prop)', async () => {
let renderTimes = 0;
let previousLastUpdate = -1;
render(
{({ factory, isReady, isReadyFromCache, hasTimedout, isTimedout, lastUpdate }: ISplitFactoryChildProps) => {
const statusProps = [isReady, isReadyFromCache, hasTimedout, isTimedout];
switch (renderTimes) {
case 0: // No ready
expect(statusProps).toStrictEqual([false, false, false, false]);
break;
case 1: // Ready
expect(statusProps).toStrictEqual([true, true, true, false]); // not rerendering on SDK_TIMEOUT, but hasTimedout reflects the current state
break;
default:
fail('Child must not be rerendered');
} // eslint-disable-next-line no-use-before-define
expect(factory).toBe(getLastInstance(SplitFactory));
expect(lastUpdate).toBeGreaterThan(previousLastUpdate);
renderTimes++;
previousLastUpdate = lastUpdate;
return null;
}}
);
const innerFactory = (SplitFactory as jest.Mock).mock.results.slice(-1)[0].value;
act(() => (innerFactory as any).client().__emitter__.emit(Event.SDK_READY_TIMED_OUT));
act(() => (innerFactory as any).client().__emitter__.emit(Event.SDK_READY));
act(() => (innerFactory as any).client().__emitter__.emit(Event.SDK_UPDATE));
expect(renderTimes).toBe(2);
});
test('rerenders child only on SDK_READY and SDK_READY_FROM_CACHE event (factory prop)', async () => {
const outerFactory = SplitFactory(sdkBrowser);
let renderTimes = 0;
let previousLastUpdate = -1;
render(
{({ factory, isReady, isReadyFromCache, hasTimedout, isTimedout, lastUpdate }: ISplitFactoryChildProps) => {
const statusProps = [isReady, isReadyFromCache, hasTimedout, isTimedout];
switch (renderTimes) {
case 0: // No ready
expect(statusProps).toStrictEqual([false, false, false, false]);
break;
case 1: // Ready
expect(statusProps).toStrictEqual([true, true, true, false]); // not rerendering on SDK_TIMEOUT, but hasTimedout reflects the current state
break;
default:
fail('Child must not be rerendered');
}
expect(factory).toBe(outerFactory);
expect(lastUpdate).toBeGreaterThan(previousLastUpdate);
renderTimes++;
previousLastUpdate = lastUpdate;
return null;
}}
);
act(() => (outerFactory as any).client().__emitter__.emit(Event.SDK_READY_TIMED_OUT));
act(() => (outerFactory as any).client().__emitter__.emit(Event.SDK_READY));
act(() => (outerFactory as any).client().__emitter__.emit(Event.SDK_UPDATE));
expect(renderTimes).toBe(2);
});
});