Skip to content

Commit c59c211

Browse files
timdeschryverbrandonroberts
authored andcommitted
feat(store): split immutibility checks in state and action checks (#1894)
1 parent 2fb8d67 commit c59c211

17 files changed

Lines changed: 359 additions & 288 deletions

modules/store/spec/meta-reducers/action_serialization_reducer.spec.ts

Lines changed: 0 additions & 23 deletions
This file was deleted.

modules/store/spec/meta-reducers/immutability_reducer.spec.ts

Lines changed: 64 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,55 +3,106 @@ import { immutabilityCheckMetaReducer } from '../../src/meta-reducers';
33
describe('immutabilityCheckMetaReducer:', () => {
44
describe('actions:', () => {
55
it('should not throw if left untouched', () => {
6-
expect(() => invokeReducer((action: any) => action)).not.toThrow();
6+
expect(() => invokeActionReducer((state: any) => state)).not.toThrow();
77
});
88

99
it('should throw when mutating an action', () => {
1010
expect(() =>
11-
invokeReducer((action: any) => {
11+
invokeActionReducer((state: any, action: any) => {
1212
action.foo = '123';
13+
return state;
1314
})
1415
).toThrow();
1516
expect(() =>
16-
invokeReducer((action: any) => {
17+
invokeActionReducer((state: any, action: any) => {
1718
action.numbers.push(4);
19+
return state;
1820
})
1921
).toThrow();
2022
});
2123

22-
function invokeReducer(reduce: Function) {
23-
immutabilityCheckMetaReducer((state, action) => {
24-
reduce(action);
24+
it('should throw when mutating action outside of reducer', () => {
25+
let dispatchedAction: any;
26+
invokeActionReducer((state: any, action: any) => {
27+
dispatchedAction = action;
2528
return state;
29+
});
30+
31+
expect(() => {
32+
dispatchedAction.foo = '123';
33+
}).toThrow();
34+
});
35+
36+
it('should not throw when check is off', () => {
37+
expect(() =>
38+
invokeActionReducer((state: any, action: any) => {
39+
action.foo = '123';
40+
return state;
41+
}, false)
42+
).not.toThrow();
43+
});
44+
45+
function invokeActionReducer(reduce: Function, checkIsOn = true) {
46+
immutabilityCheckMetaReducer((state, action) => reduce(state, action), {
47+
action: checkIsOn,
48+
state: false,
2649
})({}, { type: 'invoke', numbers: [1, 2, 3], fun: function() {} });
2750
}
2851
});
2952

3053
describe('state:', () => {
3154
it('should not throw if left untouched', () => {
3255
expect(() =>
33-
invokeReducer((state: any) => ({ ...state, foo: 'bar' }))
56+
invokeStateReducer((state: any) => ({ ...state, foo: 'bar' }))
3457
).not.toThrow();
3558
});
3659

3760
it('should throw when mutating state', () => {
3861
expect(() =>
39-
invokeReducer((state: any) => {
62+
invokeStateReducer((state: any) => {
4063
state.foo = '123';
64+
return state;
4165
})
4266
).toThrow();
4367
expect(() =>
44-
invokeReducer((state: any) => {
68+
invokeStateReducer((state: any) => {
4569
state.numbers.push(4);
70+
return state;
4671
})
4772
).toThrow();
4873
});
4974

50-
function invokeReducer(reduce: Function) {
51-
immutabilityCheckMetaReducer(state => reduce(state))(
52-
{ numbers: [1, 2, 3] },
53-
{ type: 'invoke' }
75+
it('should throw when mutating state outside of reducer', () => {
76+
const nextState = invokeStateReducer((state: any) => state);
77+
expect(() => {
78+
nextState.foo = '123';
79+
}).toThrow();
80+
});
81+
82+
it('should not throw when check is off', () => {
83+
expect(() =>
84+
invokeStateReducer((state: any) => {
85+
state.foo = '123';
86+
return state;
87+
}, false)
88+
).not.toThrow();
89+
});
90+
91+
function invokeStateReducer(reduce: Function, checkIsOn = true) {
92+
const reducer = immutabilityCheckMetaReducer(
93+
(state, action) => {
94+
if (action.type === 'init') return state;
95+
return reduce(state, action);
96+
},
97+
{
98+
state: checkIsOn,
99+
action: false,
100+
}
54101
);
102+
103+
// dispatch init noop action because it's the next state that is frozen
104+
const state = reducer({ numbers: [1, 2, 3] }, { type: 'init' });
105+
return reducer(state, { type: 'invoke' });
55106
}
56107
});
57108
});
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { serializationCheckMetaReducer } from '../../src/meta-reducers';
2+
3+
describe('serializationCheckMetaReducer:', () => {
4+
const serializables: Record<string, any> = {
5+
number: { value: 4 },
6+
boolean: { value: true },
7+
string: { value: 'foobar' },
8+
array: { value: [1, 2, 3] },
9+
object: { value: {} },
10+
nested: { value: { number: 7, array: ['n', 'g', 'r', 'x'] } },
11+
null: { value: null },
12+
undefined: { value: undefined },
13+
};
14+
15+
const unSerializables: Record<string, any> = {
16+
date: { value: new Date() },
17+
map: { value: new Map() },
18+
set: { value: new Set() },
19+
class: { value: new class {}() },
20+
function: { value: () => {} },
21+
};
22+
23+
describe('serializable:', () => {
24+
Object.keys(serializables).forEach(key => {
25+
it(`action with ${key} should not throw`, () => {
26+
expect(() =>
27+
invokeActionReducer({ type: 'valid', payload: serializables[key] })
28+
).not.toThrow();
29+
});
30+
31+
it(`state with ${key} should not throw`, () => {
32+
expect(() => invokeStateReducer(serializables[key])).not.toThrow();
33+
});
34+
});
35+
});
36+
37+
describe('unserializable:', () => {
38+
Object.keys(unSerializables).forEach(key => {
39+
it(`action with ${key} should throw`, () => {
40+
expect(() =>
41+
invokeActionReducer({ type: 'valid', payload: unSerializables[key] })
42+
).toThrow();
43+
});
44+
45+
it(`state with ${key} should throw`, () => {
46+
expect(() => invokeStateReducer(unSerializables[key])).toThrow();
47+
});
48+
});
49+
});
50+
51+
describe('actions: ', () => {
52+
it('should not throw if check is off', () => {
53+
expect(() =>
54+
invokeActionReducer({ type: 'valid', payload: unSerializables }, false)
55+
);
56+
});
57+
58+
it('should log the path that is not serializable', () => {
59+
expect(() =>
60+
invokeActionReducer({
61+
type: 'valid',
62+
payload: { foo: { bar: unSerializables['date'] } },
63+
})
64+
).toThrowError(
65+
/Detected unserializable action at "payload.foo.bar.value"/
66+
);
67+
});
68+
});
69+
70+
describe('state: ', () => {
71+
it('should not throw if check is off', () => {
72+
expect(() => invokeStateReducer(unSerializables, false)).not.toThrow();
73+
});
74+
75+
it('should log the path that is not serializable', () => {
76+
expect(() =>
77+
invokeStateReducer({
78+
foo: { bar: unSerializables['date'] },
79+
})
80+
).toThrowError(/Detected unserializable state at "foo.bar.value"/);
81+
});
82+
83+
it('should not throw if state is null', () => {
84+
expect(() => invokeStateReducer(null)).toThrowError(
85+
/Detected unserializable state at "root"/
86+
);
87+
});
88+
89+
it('should not throw if state is undefined', () => {
90+
expect(() => invokeStateReducer(undefined)).toThrowError(
91+
/Detected unserializable state at "root"/
92+
);
93+
});
94+
});
95+
96+
function invokeActionReducer(action: any, checkIsOn = true) {
97+
serializationCheckMetaReducer(state => state, {
98+
action: checkIsOn,
99+
state: false,
100+
})(undefined, action);
101+
}
102+
103+
function invokeStateReducer(nextState?: any, checkIsOn = true) {
104+
serializationCheckMetaReducer(() => nextState, {
105+
state: checkIsOn,
106+
action: false,
107+
})(undefined, {
108+
type: 'invokeReducer',
109+
});
110+
}
111+
});

modules/store/spec/meta-reducers/state_serialization_reducer.spec.ts

Lines changed: 0 additions & 25 deletions
This file was deleted.

modules/store/spec/meta-reducers/utils.spec.ts

Lines changed: 0 additions & 82 deletions
This file was deleted.

0 commit comments

Comments
 (0)