Skip to content

Commit a5dcdb1

Browse files
dummdidummbrandonroberts
authored andcommitted
fix(StoreDevtools): Fix bug when exporting/importing state history (#855)
* Also refactors state / action sanitization * Improved consistency for config object provided to extension * Moved action/state sanitization functions into utils
1 parent 69a62f2 commit a5dcdb1

File tree

6 files changed

+1072
-865
lines changed

6 files changed

+1072
-865
lines changed
Lines changed: 295 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,343 @@
1+
import { LiftedActions, ComputedState, LiftedAction } from './../src/reducer';
2+
import { PerformAction, PERFORM_ACTION } from './../src/actions';
3+
import { ActionSanitizer, StateSanitizer } from './../src/config';
4+
import {
5+
ReduxDevtoolsExtensionConnection,
6+
ReduxDevtoolsExtensionConfig,
7+
} from './../src/extension';
18
import { Action } from '@ngrx/store';
2-
import { of } from 'rxjs/observable/of';
39

410
import { LiftedState } from '../';
511
import { DevtoolsExtension, ReduxDevtoolsExtension } from '../src/extension';
612
import { createConfig, noMonitor } from '../src/instrument';
13+
import { unliftState } from '../src/utils';
14+
15+
function createOptions(
16+
name: string = 'NgRx Store DevTools',
17+
features: any = false,
18+
serialize: boolean = false,
19+
maxAge: false | number = false
20+
) {
21+
const options: ReduxDevtoolsExtensionConfig = {
22+
instanceId: 'ngrx-store-1509655064369',
23+
name,
24+
features,
25+
serialize,
26+
};
27+
if (maxAge !== false /* support === 0 */) {
28+
options.maxAge = maxAge;
29+
}
30+
return options;
31+
}
32+
33+
function createState(
34+
actionsById?: LiftedActions,
35+
computedStates?: ComputedState[]
36+
) {
37+
return {
38+
monitorState: null,
39+
nextActionId: 1,
40+
actionsById: actionsById || {
41+
0: { type: PERFORM_ACTION, action: { type: 'SOME_ACTION' } },
42+
},
43+
stagedActionIds: [0],
44+
skippedActionIds: [],
45+
committedState: { count: 0 },
46+
currentStateIndex: 0,
47+
computedStates: computedStates || [
48+
{
49+
state: 1,
50+
error: null,
51+
},
52+
],
53+
};
54+
}
755

856
describe('DevtoolsExtension', () => {
957
let reduxDevtoolsExtension: ReduxDevtoolsExtension;
58+
let extensionConnection: ReduxDevtoolsExtensionConnection;
1059
let devtoolsExtension: DevtoolsExtension;
1160

1261
beforeEach(() => {
62+
extensionConnection = jasmine.createSpyObj(
63+
'reduxDevtoolsExtensionConnection',
64+
['init', 'subscribe', 'unsubscribe', 'send']
65+
);
1366
reduxDevtoolsExtension = jasmine.createSpyObj('reduxDevtoolsExtension', [
1467
'send',
1568
'connect',
1669
]);
17-
(reduxDevtoolsExtension.connect as jasmine.Spy).and.returnValue(of({}));
70+
(reduxDevtoolsExtension.connect as jasmine.Spy).and.returnValue(
71+
extensionConnection
72+
);
1873
spyOn(Date, 'now').and.returnValue('1509655064369');
1974
});
2075

76+
function myActionSanitizer(action: Action, idx: number) {
77+
return action;
78+
}
79+
80+
function myStateSanitizer(state: any, idx: number) {
81+
return state;
82+
}
83+
84+
it('should connect with default options', () => {
85+
devtoolsExtension = new DevtoolsExtension(
86+
reduxDevtoolsExtension,
87+
createConfig({})
88+
);
89+
// Subscription needed or else extension connection will not be established.
90+
devtoolsExtension.actions$.subscribe(() => null);
91+
const defaultOptions = createOptions();
92+
expect(reduxDevtoolsExtension.connect).toHaveBeenCalledWith(defaultOptions);
93+
});
94+
95+
it('should connect with given options', () => {
96+
devtoolsExtension = new DevtoolsExtension(
97+
reduxDevtoolsExtension,
98+
createConfig({
99+
name: 'ngrx-store-devtool-todolist',
100+
features: 'some features',
101+
maxAge: 10,
102+
serialize: true,
103+
// these two should not be added
104+
actionSanitizer: myActionSanitizer,
105+
stateSanitizer: myStateSanitizer,
106+
})
107+
);
108+
// Subscription needed or else extension connection will not be established.
109+
devtoolsExtension.actions$.subscribe(() => null);
110+
const options = createOptions(
111+
'ngrx-store-devtool-todolist',
112+
'some features',
113+
true,
114+
10
115+
);
116+
expect(reduxDevtoolsExtension.connect).toHaveBeenCalledWith(options);
117+
});
118+
21119
describe('notify', () => {
22120
it('should send notification with default options', () => {
23121
devtoolsExtension = new DevtoolsExtension(
24122
reduxDevtoolsExtension,
25123
createConfig({})
26124
);
27-
const defaultOptions = {
28-
maxAge: false,
29-
monitor: noMonitor,
30-
actionSanitizer: undefined,
31-
stateSanitizer: undefined,
32-
name: 'NgRx Store DevTools',
33-
serialize: false,
34-
logOnly: false,
35-
features: false,
36-
};
37-
const action = {} as Action;
38-
const state = {} as LiftedState;
125+
const defaultOptions = createOptions();
126+
const action = {} as LiftedAction;
127+
const state = createState();
39128
devtoolsExtension.notify(action, state);
40129
expect(reduxDevtoolsExtension.send).toHaveBeenCalledWith(
41130
null,
42-
{},
131+
state,
43132
defaultOptions,
44133
'ngrx-store-1509655064369'
45134
);
46135
});
47136

48-
function myActionSanitizer() {
49-
return { type: 'sanitizer' };
50-
}
51-
function myStateSanitizer() {
52-
return { state: 'new state' };
53-
}
54-
55137
it('should send notification with given options', () => {
56138
devtoolsExtension = new DevtoolsExtension(
57139
reduxDevtoolsExtension,
58140
createConfig({
141+
name: 'ngrx-store-devtool-todolist',
142+
features: 'some features',
143+
maxAge: 10,
144+
serialize: true,
145+
// these two should not be added
59146
actionSanitizer: myActionSanitizer,
60147
stateSanitizer: myStateSanitizer,
61-
name: 'ngrx-store-devtool-todolist',
62148
})
63149
);
64-
const defaultOptions = {
65-
maxAge: false,
66-
monitor: noMonitor,
67-
actionSanitizer: myActionSanitizer,
68-
stateSanitizer: myStateSanitizer,
69-
name: 'ngrx-store-devtool-todolist',
70-
serialize: false,
71-
logOnly: false,
72-
features: false,
73-
};
74-
const action = {} as Action;
75-
const state = {} as LiftedState;
150+
const options = createOptions(
151+
'ngrx-store-devtool-todolist',
152+
'some features',
153+
true,
154+
10
155+
);
156+
const action = {} as LiftedAction;
157+
const state = createState();
76158
devtoolsExtension.notify(action, state);
77159
expect(reduxDevtoolsExtension.send).toHaveBeenCalledWith(
78160
null,
79-
{},
80-
defaultOptions,
161+
state,
162+
options,
81163
'ngrx-store-1509655064369'
82164
);
83165
});
166+
167+
describe('[with Action and State Sanitizer]', () => {
168+
const UNSANITIZED_TOKEN = 'UNSANITIZED_ACTION';
169+
const SANITIZED_TOKEN = 'SANITIZED_ACTION';
170+
const SANITIZED_COUNTER = 42;
171+
172+
function createPerformAction() {
173+
return new PerformAction({ type: UNSANITIZED_TOKEN });
174+
}
175+
176+
function testActionSanitizer(action: Action, id: number) {
177+
return { type: SANITIZED_TOKEN };
178+
}
179+
180+
function testStateSanitizer(state: any, index: number) {
181+
return SANITIZED_COUNTER;
182+
}
183+
184+
describe('should function normally with no sanitizers', () => {
185+
beforeEach(() => {
186+
devtoolsExtension = new DevtoolsExtension(
187+
reduxDevtoolsExtension,
188+
createConfig({})
189+
);
190+
// Subscription needed or else extension connection will not be established.
191+
devtoolsExtension.actions$.subscribe(() => null);
192+
});
193+
194+
it('for normal action', () => {
195+
const action = createPerformAction();
196+
const state = createState();
197+
198+
devtoolsExtension.notify(action, state);
199+
expect(extensionConnection.send).toHaveBeenCalledWith(
200+
action,
201+
unliftState(state)
202+
);
203+
});
204+
205+
it('for action that requires full state update', () => {
206+
const options = createOptions();
207+
const action = {} as LiftedAction;
208+
const state = createState();
209+
210+
devtoolsExtension.notify(action, state);
211+
expect(reduxDevtoolsExtension.send).toHaveBeenCalledWith(
212+
null,
213+
state,
214+
options,
215+
'ngrx-store-1509655064369'
216+
);
217+
});
218+
});
219+
220+
describe('should run the action sanitizer on actions', () => {
221+
beforeEach(() => {
222+
devtoolsExtension = new DevtoolsExtension(
223+
reduxDevtoolsExtension,
224+
createConfig({
225+
actionSanitizer: testActionSanitizer,
226+
})
227+
);
228+
// Subscription needed or else extension connection will not be established.
229+
devtoolsExtension.actions$.subscribe(() => null);
230+
});
231+
232+
it('for normal action', () => {
233+
const options = createOptions();
234+
const action = createPerformAction();
235+
const state = createState();
236+
const sanitizedAction = {
237+
...action,
238+
action: testActionSanitizer(createPerformAction().action, 0),
239+
};
240+
241+
devtoolsExtension.notify(action, state);
242+
expect(extensionConnection.send).toHaveBeenCalledWith(
243+
sanitizedAction,
244+
unliftState(state)
245+
);
246+
});
247+
248+
it('for action that requires full state update', () => {
249+
const options = createOptions();
250+
const action = {} as LiftedAction;
251+
const state = createState();
252+
const sanitizedState = createState({
253+
0: { type: PERFORM_ACTION, action: { type: SANITIZED_TOKEN } },
254+
});
255+
256+
devtoolsExtension.notify(action, state);
257+
expect(reduxDevtoolsExtension.send).toHaveBeenCalledWith(
258+
null,
259+
sanitizedState,
260+
options,
261+
'ngrx-store-1509655064369'
262+
);
263+
});
264+
});
265+
266+
describe('should run the state sanitizer on store state', () => {
267+
beforeEach(() => {
268+
devtoolsExtension = new DevtoolsExtension(
269+
reduxDevtoolsExtension,
270+
createConfig({
271+
stateSanitizer: testStateSanitizer,
272+
})
273+
);
274+
// Subscription needed or else extension connection will not be established.
275+
devtoolsExtension.actions$.subscribe(() => null);
276+
});
277+
278+
it('for normal action', () => {
279+
const action = createPerformAction();
280+
const state = createState();
281+
const sanitizedState = createState(undefined, [
282+
{ state: SANITIZED_COUNTER, error: null },
283+
]);
284+
285+
devtoolsExtension.notify(action, state);
286+
expect(extensionConnection.send).toHaveBeenCalledWith(
287+
action,
288+
unliftState(sanitizedState)
289+
);
290+
});
291+
292+
it('for action that requires full state update', () => {
293+
const options = createOptions();
294+
const action = {} as LiftedAction;
295+
const state = createState();
296+
const sanitizedState = createState(undefined, [
297+
{ state: SANITIZED_COUNTER, error: null },
298+
]);
299+
300+
devtoolsExtension.notify(action, state);
301+
expect(reduxDevtoolsExtension.send).toHaveBeenCalledWith(
302+
null,
303+
sanitizedState,
304+
options,
305+
'ngrx-store-1509655064369'
306+
);
307+
});
308+
});
309+
310+
it('sanitizers should not modify original state or actions', () => {
311+
beforeEach(() => {
312+
devtoolsExtension = new DevtoolsExtension(
313+
reduxDevtoolsExtension,
314+
createConfig({
315+
actionSanitizer: testActionSanitizer,
316+
stateSanitizer: testStateSanitizer,
317+
})
318+
);
319+
// Subscription needed or else extension connection will not be established.
320+
devtoolsExtension.actions$.subscribe(() => null);
321+
});
322+
323+
it('for normal action', () => {
324+
const action = createPerformAction();
325+
const state = createState();
326+
327+
devtoolsExtension.notify(action, state);
328+
expect(state).toEqual(createState());
329+
expect(action).toEqual(createPerformAction());
330+
});
331+
332+
it('for action that requires full state update', () => {
333+
const action = {} as LiftedAction;
334+
const state = createState();
335+
336+
devtoolsExtension.notify(action, state);
337+
expect(state).toEqual(createState());
338+
expect(action).toEqual({} as LiftedAction);
339+
});
340+
});
341+
});
84342
});
85343
});

0 commit comments

Comments
 (0)