Skip to content

Commit

Permalink
Merge pull request #115 from splitio/refactor_reducer_and_actions
Browse files Browse the repository at this point in the history
Refactor reducer and actions
  • Loading branch information
EmilianoSanchez authored Sep 10, 2024
2 parents cccc086 + 792927d commit a51613a
Show file tree
Hide file tree
Showing 11 changed files with 197 additions and 113 deletions.
5 changes: 4 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,8 @@ module.exports = {
collectCoverageFrom: [
'src/**/*.{js,jsx,ts,tsx}',
'!src/__tests__/**',
]
],

// Custom jest matcher
setupFilesAfterEnv: ['<rootDir>/src/__tests__/utils/toBeWithinRange.ts'],
};
1 change: 0 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@
"devDependencies": {
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0",
"@types/jest": "^27.0.0",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"@types/redux-mock-store": "^1.0.1",
Expand Down
54 changes: 36 additions & 18 deletions src/__tests__/actions.browser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,19 +49,25 @@ describe('initSplitSdk', () => {
actionResult.then(() => {
// return of async action
let action = store.getActions()[0];
expect(action.type).toEqual(SPLIT_READY);
expect(action.payload.timestamp).toBeLessThanOrEqual(Date.now());
expect(action.payload.timestamp).toBeGreaterThanOrEqual(timestamp);
expect(action).toEqual({
type: SPLIT_READY,
payload: {
timestamp: expect.toBeWithinRange(timestamp, Date.now() + 1),
}
});
expect((SplitFactory as jest.Mock).mock.calls.length).toBe(1);
expect(onReadyCb.mock.calls.length).toBe(1);

timestamp = Date.now();
(splitSdk.factory as any).client().__emitter__.emit(Event.SDK_UPDATE);
setTimeout(() => {
action = store.getActions()[1];
expect(action.type).toEqual(SPLIT_UPDATE);
expect(action.payload.timestamp).toBeLessThanOrEqual(Date.now());
expect(action.payload.timestamp).toBeGreaterThanOrEqual(timestamp);
expect(action).toEqual({
type: SPLIT_UPDATE,
payload: {
timestamp: expect.toBeWithinRange(timestamp, Date.now() + 1),
}
});
expect(onUpdateCb.mock.calls.length).toBe(1);
done();
}, 0);
Expand All @@ -79,19 +85,25 @@ describe('initSplitSdk', () => {
actionResult.catch(() => {
// return of async action
let action = store.getActions()[0];
expect(action.type).toEqual(SPLIT_TIMEDOUT);
expect(action.payload.timestamp).toBeLessThanOrEqual(Date.now());
expect(action.payload.timestamp).toBeGreaterThanOrEqual(timestamp);
expect(action).toEqual({
type: SPLIT_TIMEDOUT,
payload: {
timestamp: expect.toBeWithinRange(timestamp, Date.now() + 1),
}
});
expect((SplitFactory as jest.Mock).mock.calls.length).toBe(1);
expect(onTimedoutCb.mock.calls.length).toBe(1);

timestamp = Date.now();
(splitSdk.factory as any).client().__emitter__.emit(Event.SDK_READY);
setTimeout(() => {
action = store.getActions()[1];
expect(action.type).toEqual(SPLIT_READY);
expect(action.payload.timestamp).toBeLessThanOrEqual(Date.now());
expect(action.payload.timestamp).toBeGreaterThanOrEqual(timestamp);
expect(action).toEqual({
type: SPLIT_READY,
payload: {
timestamp: expect.toBeWithinRange(timestamp, Date.now() + 1),
}
});
expect(onReadyCb.mock.calls.length).toBe(1);
done();
}, 0);
Expand All @@ -105,9 +117,12 @@ describe('initSplitSdk', () => {
const onReadyFromCacheCb = jest.fn(() => {
// action should be already dispatched when the callback is called
const action = store.getActions()[0];
expect(action.type).toEqual(SPLIT_READY_FROM_CACHE);
expect(action.payload.timestamp).toBeLessThanOrEqual(Date.now());
expect(action.payload.timestamp).toBeGreaterThanOrEqual(timestamp);
expect(action).toEqual({
type: SPLIT_READY_FROM_CACHE,
payload: {
timestamp: expect.toBeWithinRange(timestamp, Date.now() + 1),
}
});
});
const onReadyCb = jest.fn(() => {
const action = store.getActions()[1];
Expand Down Expand Up @@ -568,9 +583,12 @@ describe('destroySplitSdk', () => {

actionResult.then(() => {
const action = store.getActions()[3];
expect(action.type).toEqual(SPLIT_DESTROY);
expect(action.payload.timestamp).toBeLessThanOrEqual(Date.now());
expect(action.payload.timestamp).toBeGreaterThanOrEqual(timestamp);
expect(action).toEqual({
type: SPLIT_DESTROY,
payload: {
timestamp: expect.toBeWithinRange(timestamp, Date.now() + 1),
}
});
// assert that all client's destroy methods were called
expect(splitSdk.factory.client().destroy).toBeCalledTimes(1);
expect(splitSdk.factory.client('other-user-key').destroy).toBeCalledTimes(1);
Expand Down
36 changes: 24 additions & 12 deletions src/__tests__/actions.node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,12 @@ describe('initSplitSdk', () => {

// Action is dispatched synchronously
const action = store.getActions()[0];
expect(action.type).toEqual(SPLIT_READY);
expect(action.payload.timestamp).toBeLessThanOrEqual(Date.now());
expect(action.payload.timestamp).toBeGreaterThanOrEqual(timestamp);
expect(action).toEqual({
type: SPLIT_READY,
payload: {
timestamp: expect.toBeWithinRange(timestamp, Date.now()),
}
});
}

// create multiple stores
Expand Down Expand Up @@ -86,9 +89,12 @@ describe('initSplitSdk', () => {
store.dispatch<any>(initSplitSdkAction);

const action = store.getActions()[0];
expect(action.type).toEqual(SPLIT_TIMEDOUT);
expect(action.payload.timestamp).toBeLessThanOrEqual(Date.now());
expect(action.payload.timestamp).toBeGreaterThanOrEqual(timestamp);
expect(action).toEqual({
type: SPLIT_TIMEDOUT,
payload: {
timestamp: expect.toBeWithinRange(timestamp, Date.now() + 1)
}
});
expect((SplitFactory as jest.Mock).mock.calls.length).toBe(1);

timestamp = Date.now();
Expand All @@ -105,14 +111,20 @@ describe('initSplitSdk', () => {

// Actions are dispatched synchronously
const timeoutAction = store.getActions()[0];
expect(timeoutAction.type).toEqual(SPLIT_TIMEDOUT);
expect(timeoutAction.payload.timestamp).toBeLessThanOrEqual(Date.now());
expect(timeoutAction.payload.timestamp).toBeGreaterThanOrEqual(timestamp);
expect(timeoutAction).toEqual({
type: SPLIT_TIMEDOUT,
payload: {
timestamp: expect.toBeWithinRange(timestamp, Date.now() + 1)
}
});

const readyAction = store.getActions()[1];
expect(readyAction.type).toEqual(SPLIT_READY);
expect(readyAction.payload.timestamp).toBeLessThanOrEqual(Date.now());
expect(readyAction.payload.timestamp).toBeGreaterThanOrEqual(timestamp);
expect(readyAction).toEqual({
type: SPLIT_READY,
payload: {
timestamp: expect.toBeWithinRange(timestamp, Date.now() + 1)
}
});
}

// create multiple stores
Expand Down
44 changes: 22 additions & 22 deletions src/__tests__/reducer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,75 +38,75 @@ describe('Split reducer', () => {
});

it('should handle SPLIT_READY', () => {
const readyAction = splitReady();
const readyAction = splitReady(100);
expect(
splitReducer(initialState, readyAction),
).toEqual({
...initialState,
isReady: true,
lastUpdate: readyAction.payload.timestamp,
lastUpdate: 100,
});
});

it('should handle SPLIT_READY_FROM_CACHE', () => {
const readyAction = splitReadyFromCache();
const readyAction = splitReadyFromCache(200);
expect(
splitReducer(initialState, readyAction),
).toEqual({
...initialState,
isReadyFromCache: true,
lastUpdate: readyAction.payload.timestamp,
lastUpdate: 200,
});
});

it('should handle SPLIT_TIMEDOUT', () => {
const timedoutAction = splitTimedout();
const timedoutAction = splitTimedout(300);
expect(
splitReducer(initialState, timedoutAction),
).toEqual({
...initialState,
isTimedout: true,
hasTimedout: true,
lastUpdate: timedoutAction.payload.timestamp,
lastUpdate: 300,
});
});

it('should handle SPLIT_READY after SPLIT_TIMEDOUT', () => {
const timedoutAction = splitTimedout();
const readyAction = splitReady();
const timedoutAction = splitTimedout(100);
const readyAction = splitReady(200);
expect(
splitReducer(splitReducer(initialState, timedoutAction), readyAction),
).toEqual({
...initialState,
isReady: true,
isTimedout: false,
hasTimedout: true,
lastUpdate: readyAction.payload.timestamp,
lastUpdate: 200,
});
});

it('should handle SPLIT_UPDATE', () => {
const updateAction = splitUpdate();
const updateAction = splitUpdate(300);
expect(
splitReducer(initialState, updateAction),
).toEqual({
...initialState,
lastUpdate: updateAction.payload.timestamp,
lastUpdate: 300,
});
});

it('should handle SPLIT_DESTROY', () => {
const destroyAction = splitDestroy();
const destroyAction = splitDestroy(400);
expect(
splitReducer(initialState, destroyAction),
).toEqual({
...initialState,
isDestroyed: true,
lastUpdate: destroyAction.payload.timestamp,
lastUpdate: 400,
});
});

const actionCreatorsWithEvaluations: Array<[string, (key: SplitIO.SplitKey, treatments: SplitIO.TreatmentsWithConfig) => AnyAction, boolean, boolean]> = [
const actionCreatorsWithEvaluations: Array<[string, (key: SplitIO.SplitKey, treatments: SplitIO.TreatmentsWithConfig, timestamp: number) => AnyAction, boolean, boolean]> = [
['ADD_TREATMENTS', addTreatments, false, false],
['SPLIT_READY_WITH_EVALUATIONS', splitReadyWithEvaluations, true, false],
['SPLIT_READY_FROM_CACHE_WITH_EVALUATIONS', splitReadyFromCacheWithEvaluations, false, true],
Expand All @@ -115,7 +115,7 @@ describe('Split reducer', () => {

it.each(actionCreatorsWithEvaluations)('should handle %s', (_, actionCreator, isReady, isReadyFromCache) => {
const initialTreatments = initialState.treatments;
const action = actionCreator(key, treatments);
const action = actionCreator(key, treatments, 1000);

// control assertion - reduced state has the expected shape
expect(
Expand All @@ -124,7 +124,7 @@ describe('Split reducer', () => {
...initialState,
isReady,
isReadyFromCache,
lastUpdate: action.payload.timestamp || initialState.lastUpdate,
lastUpdate: action.type === 'ADD_TREATMENTS' ? initialState.lastUpdate : 1000,
treatments: {
test_split: {
[key]: treatments.test_split,
Expand All @@ -142,7 +142,7 @@ describe('Split reducer', () => {
const newTreatments: SplitIO.TreatmentsWithConfig = {
test_split: { ...previousTreatment },
};
const action = actionCreator(key, newTreatments);
const action = actionCreator(key, newTreatments, 1000);
const reduxState = splitReducer(stateWithTreatments, action);

// control assertion - treatment object was not replaced in the state
Expand All @@ -155,7 +155,7 @@ describe('Split reducer', () => {
...initialState,
isReady,
isReadyFromCache,
lastUpdate: action.payload.timestamp || initialState.lastUpdate,
lastUpdate: action.type === 'ADD_TREATMENTS' ? initialState.lastUpdate : 1000,
treatments: {
test_split: {
[key]: newTreatments.test_split,
Expand All @@ -173,7 +173,7 @@ describe('Split reducer', () => {
config: previousTreatment.config,
},
};
const action = actionCreator(key, newTreatments);
const action = actionCreator(key, newTreatments, 1000);
const reduxState = splitReducer(stateWithTreatments, action);

// control assertion - treatment object was replaced in the state
Expand All @@ -185,7 +185,7 @@ describe('Split reducer', () => {
...initialState,
isReady,
isReadyFromCache,
lastUpdate: action.payload.timestamp || initialState.lastUpdate,
lastUpdate: action.type === 'ADD_TREATMENTS' ? initialState.lastUpdate : 1000,
treatments: {
test_split: {
[key]: newTreatments.test_split,
Expand All @@ -204,7 +204,7 @@ describe('Split reducer', () => {
},
};
// const action = addTreatments(key, newTreatments);
const action = actionCreator(key, newTreatments);
const action = actionCreator(key, newTreatments, 1000);
const reduxState = splitReducer(stateWithTreatments, action);

// control assertion - treatment object was replaced in the state
Expand All @@ -216,7 +216,7 @@ describe('Split reducer', () => {
...initialState,
isReady,
isReadyFromCache,
lastUpdate: action.payload.timestamp || initialState.lastUpdate,
lastUpdate: action.type === 'ADD_TREATMENTS' ? initialState.lastUpdate : 1000,
treatments: {
test_split: {
[key]: newTreatments.test_split,
Expand Down
36 changes: 36 additions & 0 deletions src/__tests__/utils/toBeWithinRange.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/* eslint-disable @typescript-eslint/no-namespace */
/* eslint-disable @typescript-eslint/no-empty-interface */

// Custom matcher https://jest-archive-august-2023.netlify.app/docs/27.x/expect#expectextendmatchers
import { expect } from '@jest/globals';

expect.extend({
toBeWithinRange(received: any, floor: number, ceiling: number) {
const pass = received >= floor && received <= ceiling;
if (pass) {
return {
message: () =>
`expected ${received} not to be within range ${floor} - ${ceiling}`,
pass: true,
};
} else {
return {
message: () =>
`expected ${received} to be within range ${floor} - ${ceiling}`,
pass: false,
};
}
},
});

interface CustomMatchers<R = unknown> {
toBeWithinRange(floor: number, ceiling: number): R;
}

declare global {
namespace jest {
interface Expect extends CustomMatchers { }
interface Matchers<R> extends CustomMatchers<R> { }
interface InverseAsymmetricMatchers extends CustomMatchers { }
}
}
Loading

0 comments on commit a51613a

Please sign in to comment.