1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127 | 1x
1x
1x
1x
22x
22x
45x
23x
23x
80x
23x
80x
80x
80x
23x
23x
23x
23x
23x
19x
19x
19x
19x
19x
19x
19x
23x
4x
4x
4x
4x
4x
4x
23x
10x
10x
10x
23x
1x
1x
19x
19x
19x
19x
19x
19x
19x
19x
19x
19x
19x
19x
4x
4x
4x
4x
4x
4x
4x
4x
10x
30x
20x
1x
1x
1x
130x
| import { deepClone } from '../util/deep-clone';
import { toHavePropertiesMatcher } from 'testing-helpers/jasmine-matchers/to-have-properties.matcher';
import { ObjectShapeComparer } from '../../common/object-shape-comparer/object-shape-comparer';
export interface ReducerTestConfig {
action: object;
stateForReducer: object;
payloadExpectedShape?: object;
}
export function runAllReducerTests(reducer: Function, tests: ReducerTestConfig[]) {
tests.forEach(({ action, stateForReducer, payloadExpectedShape }: ReducerTestConfig) => {
runReducerTests(stateForReducer, reducer, action, payloadExpectedShape);
});
}
export function runReducerTests(state, reducer, action, expectedShape= null) {
describe(action.type, () => {
beforeEach( () => {
jasmine.addMatchers(toHavePropertiesMatcher);
});
let testState, cleanAction, priorStateSnapshot;
beforeEach(() => {
testState = deepClone(state); // Ensure clean initialState for each test
cleanAction = {...action}; // Reset action before each, since action.type is changed in reducer
priorStateSnapshot = deepClone(testState); // Snapshot the test state before each for comparison
});
it(`should return state unchanged for nop action`, () => {
const nopAction = { type: 'NOP' };
const result = reducer(testState, nopAction);
expect(result).toBe(testState);
});
if (action.payload) {
it(`should add payload to state`, () => {
const afterState = reducer(testState, cleanAction);
check_ActionWithPayload_ChangesState(priorStateSnapshot, afterState, cleanAction);
check_ActionWithPayload_InputStateIsUnchanged(priorStateSnapshot, testState, action);
});
it(`should clone the state`, () => {
const afterState = reducer(testState, cleanAction);
check_ActionWithPayload_CreatesNewStateObject(testState, afterState, cleanAction);
});
}
if (!action.payload) {
it(`should not change state`, () => {
const afterState = reducer(testState, cleanAction);
check_ActionWithoutPayload_DoesNotChangeState(priorStateSnapshot, afterState, cleanAction);
});
it(`should not clone the state`, () => {
const afterState = reducer(testState, cleanAction);
check_ActionWithoutPayload_DoesNotCreateNewStateObject(testState, afterState, cleanAction);
});
}
if (action.subState) {
it(`should not affect other sub state`, () => {
const afterState = reducer(testState, cleanAction);
check_SubState_OtherStateIsUnchanged(testState, afterState, cleanAction);
});
}
if (expectedShape) {
it('should have payload in expected shape', () => {
check_Payload_HasExpectedShape(action, expectedShape);
});
}
});
}
function check_ActionWithPayload_ChangesState(priorStateSnapshot, afterState, action) {
const priorSubStateSnapshot = resolveSubstate(priorStateSnapshot, action);
const afterSubState = resolveSubstate(afterState, action);
expect(afterSubState).toHaveProperties(action.payload);
expect(priorSubStateSnapshot).not.toHaveProperties(afterSubState); // Guard against false positive
}
function check_ActionWithPayload_InputStateIsUnchanged(priorStateSnapshot, inputState, action) {
const inputSubState = resolveSubstate(inputState, action);
const priorSubStateSnapshot = resolveSubstate(priorStateSnapshot, action);
expect(inputState).toEqual(priorStateSnapshot);
expect(inputSubState).toEqual(priorSubStateSnapshot);
}
function check_ActionWithPayload_CreatesNewStateObject(inputState, afterState, action) {
const inputSubState = resolveSubstate(inputState, action);
const afterSubState = resolveSubstate(afterState, action);
expect(afterState).not.toBe(inputState);
expect(afterSubState).not.toBe(inputSubState);
}
function check_ActionWithoutPayload_DoesNotChangeState(priorStateSnapshot, afterState, action) {
const priorSubStateSnapshot = resolveSubstate(priorStateSnapshot, action);
const afterSubState = resolveSubstate(afterState, action);
expect(afterState).toEqual(priorStateSnapshot);
expect(afterSubState).toEqual(priorSubStateSnapshot);
}
function check_ActionWithoutPayload_DoesNotCreateNewStateObject(inputState, afterState, action) {
const inputSubState = resolveSubstate(inputState, action);
const afterSubState = resolveSubstate(afterState, action);
expect(afterState).toBe(inputState);
expect(afterSubState).toBe(inputSubState);
}
function check_SubState_OtherStateIsUnchanged(inputState, afterState, action) {
Object.keys(inputState)
// tslint:disable-next-line:triple-equals
.filter(key => key != action.subState)
.forEach(key => {
expect(afterState[key]).toBe(inputState[key]);
});
}
function check_Payload_HasExpectedShape(action, expectedShape) {
const objectShapeComparer = new ObjectShapeComparer();
const shapeErrors = objectShapeComparer.compare(expectedShape, action.payload);
expect(shapeErrors.length).toBe(0, shapeErrors);
}
function resolveSubstate(state, action) {
return action.subState
? state[action.subState]
: state;
}
|