Skip to content

Commit d532979

Browse files
timdeschryverbrandonroberts
authored andcommitted
fix(StoreDevTools): out of bounds when actions are filtered (#1532)
Closes #1522
1 parent e17a787 commit d532979

File tree

4 files changed

+223
-29
lines changed

4 files changed

+223
-29
lines changed
Lines changed: 75 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,96 @@
11
import { NgModule } from '@angular/core';
22
import { TestBed } from '@angular/core/testing';
3-
import { StoreModule, Store, ActionsSubject } from '@ngrx/store';
4-
import { StoreDevtoolsModule, StoreDevtools } from '@ngrx/store-devtools';
3+
import { StoreModule, Store, Action } from '@ngrx/store';
4+
import {
5+
StoreDevtoolsModule,
6+
StoreDevtools,
7+
StoreDevtoolsOptions,
8+
} from '@ngrx/store-devtools';
59

610
describe('Devtools Integration', () => {
7-
let store: Store<any>;
8-
9-
@NgModule({
10-
imports: [StoreModule.forFeature('a', (state: any, action: any) => state)],
11-
})
12-
class EagerFeatureModule {}
13-
14-
@NgModule({
15-
imports: [
16-
StoreModule.forRoot({}),
17-
EagerFeatureModule,
18-
StoreDevtoolsModule.instrument(),
19-
],
20-
})
21-
class RootModule {}
22-
23-
beforeEach(() => {
11+
function setup(options: Partial<StoreDevtoolsOptions> = {}) {
12+
@NgModule({
13+
imports: [
14+
StoreModule.forFeature('a', (state: any, action: any) => state),
15+
],
16+
})
17+
class EagerFeatureModule {}
18+
19+
@NgModule({
20+
imports: [
21+
StoreModule.forRoot({}),
22+
EagerFeatureModule,
23+
StoreDevtoolsModule.instrument(options),
24+
],
25+
})
26+
class RootModule {}
27+
2428
TestBed.configureTestingModule({
2529
imports: [RootModule],
2630
});
31+
32+
const store = TestBed.get(Store) as Store<any>;
33+
const devtools = TestBed.get(StoreDevtools) as StoreDevtools;
34+
return { store, devtools };
35+
}
36+
37+
afterEach(() => {
38+
const devtools = TestBed.get(StoreDevtools) as StoreDevtools;
39+
devtools.reset();
2740
});
2841

2942
it('should load the store eagerly', () => {
3043
let error = false;
3144

3245
try {
33-
let store = TestBed.get(Store);
46+
const { store } = setup();
3447
store.subscribe();
3548
} catch (e) {
3649
error = true;
3750
}
3851

3952
expect(error).toBeFalsy();
4053
});
54+
55+
it('should not throw if actions are ignored', (done: any) => {
56+
const { store, devtools } = setup({
57+
predicate: (_, { type }: Action) => type !== 'FOO',
58+
});
59+
store.subscribe();
60+
devtools.dispatcher.subscribe((action: Action) => {
61+
if (action.type === 'REFRESH') {
62+
done();
63+
}
64+
});
65+
store.dispatch({ type: 'FOO' });
66+
devtools.refresh();
67+
});
68+
69+
it('should not throw if actions are blacklisted', (done: any) => {
70+
const { store, devtools } = setup({
71+
actionsBlacklist: ['FOO'],
72+
});
73+
store.subscribe();
74+
devtools.dispatcher.subscribe((action: Action) => {
75+
if (action.type === 'REFRESH') {
76+
done();
77+
}
78+
});
79+
store.dispatch({ type: 'FOO' });
80+
devtools.refresh();
81+
});
82+
83+
it('should not throw if actions are whitelisted', (done: any) => {
84+
const { store, devtools } = setup({
85+
actionsWhitelist: ['BAR'],
86+
});
87+
store.subscribe();
88+
devtools.dispatcher.subscribe((action: Action) => {
89+
if (action.type === 'REFRESH') {
90+
done();
91+
}
92+
});
93+
store.dispatch({ type: 'FOO' });
94+
devtools.refresh();
95+
});
4196
});

modules/store-devtools/spec/store.spec.ts

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,125 @@ describe('Store Devtools', () => {
438438
});
439439
});
440440

441+
describe('Filtered actions', () => {
442+
it('should respect the predicate option', () => {
443+
const fixture = createStore(counter, {
444+
predicate: (s, a) => a.type !== 'INCREMENT',
445+
});
446+
447+
expect(fixture.getState()).toBe(0);
448+
fixture.store.dispatch({ type: 'INCREMENT' });
449+
fixture.store.dispatch({ type: 'DECREMENT' });
450+
fixture.store.dispatch({ type: 'INCREMENT' });
451+
fixture.store.dispatch({ type: 'INCREMENT' });
452+
fixture.store.dispatch({ type: 'INCREMENT' });
453+
fixture.store.dispatch({ type: 'INCREMENT' });
454+
fixture.store.dispatch({ type: 'INCREMENT' });
455+
fixture.store.dispatch({ type: 'INCREMENT' });
456+
fixture.store.dispatch({ type: 'DECREMENT' });
457+
expect(fixture.getState()).toBe(5);
458+
459+
// init, decrement, decrement
460+
const {
461+
stagedActionIds,
462+
actionsById,
463+
computedStates,
464+
currentStateIndex,
465+
} = fixture.getLiftedState();
466+
expect(stagedActionIds.length).toBe(3);
467+
expect(Object.keys(actionsById).length).toBe(3);
468+
expect(computedStates.length).toBe(3);
469+
expect(currentStateIndex).toBe(2);
470+
471+
fixture.devtools.jumpToAction(0);
472+
expect(fixture.getState()).toBe(1);
473+
474+
fixture.devtools.jumpToAction(1);
475+
expect(fixture.getState()).toBe(6);
476+
477+
fixture.devtools.jumpToAction(2);
478+
expect(fixture.getState()).toBe(5);
479+
});
480+
481+
it('should respect the blacklist option', () => {
482+
const fixture = createStore(counter, {
483+
actionsBlacklist: ['INCREMENT'],
484+
});
485+
486+
expect(fixture.getState()).toBe(0);
487+
fixture.store.dispatch({ type: 'INCREMENT' });
488+
fixture.store.dispatch({ type: 'DECREMENT' });
489+
fixture.store.dispatch({ type: 'INCREMENT' });
490+
fixture.store.dispatch({ type: 'INCREMENT' });
491+
fixture.store.dispatch({ type: 'INCREMENT' });
492+
fixture.store.dispatch({ type: 'INCREMENT' });
493+
fixture.store.dispatch({ type: 'INCREMENT' });
494+
fixture.store.dispatch({ type: 'INCREMENT' });
495+
fixture.store.dispatch({ type: 'DECREMENT' });
496+
expect(fixture.getState()).toBe(5);
497+
498+
// init, decrement, decrement
499+
const {
500+
stagedActionIds,
501+
actionsById,
502+
computedStates,
503+
currentStateIndex,
504+
} = fixture.getLiftedState();
505+
expect(stagedActionIds.length).toBe(3);
506+
expect(Object.keys(actionsById).length).toBe(3);
507+
expect(computedStates.length).toBe(3);
508+
expect(currentStateIndex).toBe(2);
509+
510+
fixture.devtools.jumpToAction(0);
511+
expect(fixture.getState()).toBe(1);
512+
513+
fixture.devtools.jumpToAction(1);
514+
expect(fixture.getState()).toBe(6);
515+
516+
fixture.devtools.jumpToAction(2);
517+
expect(fixture.getState()).toBe(5);
518+
});
519+
520+
it('should respect the whitelist option', () => {
521+
const fixture = createStore(counter, {
522+
actionsWhitelist: ['DECREMENT'],
523+
});
524+
525+
expect(fixture.getState()).toBe(0);
526+
fixture.store.dispatch({ type: 'INCREMENT' });
527+
fixture.store.dispatch({ type: 'DECREMENT' });
528+
fixture.store.dispatch({ type: 'INCREMENT' });
529+
fixture.store.dispatch({ type: 'INCREMENT' });
530+
fixture.store.dispatch({ type: 'INCREMENT' });
531+
fixture.store.dispatch({ type: 'INCREMENT' });
532+
fixture.store.dispatch({ type: 'INCREMENT' });
533+
fixture.store.dispatch({ type: 'INCREMENT' });
534+
fixture.store.dispatch({ type: 'DECREMENT' });
535+
expect(fixture.getState()).toBe(5);
536+
537+
// init, decrement, decrement
538+
const {
539+
stagedActionIds,
540+
actionsById,
541+
computedStates,
542+
currentStateIndex,
543+
} = fixture.getLiftedState();
544+
expect(stagedActionIds.length).toBe(3);
545+
expect(Object.keys(actionsById).length).toBe(3);
546+
expect(computedStates.length).toBe(3);
547+
expect(currentStateIndex).toBe(2);
548+
549+
fixture.devtools.jumpToAction(0);
550+
expect(fixture.getState()).toBe(1);
551+
552+
fixture.devtools.jumpToAction(1);
553+
expect(fixture.getState()).toBe(6);
554+
555+
fixture.devtools.jumpToAction(2);
556+
expect(fixture.getState()).toBe(5);
557+
});
558+
});
559+
441560
describe('maxAge option', () => {
442561
it('should auto-commit earliest non-@@INIT action when maxAge is reached', () => {
443562
const fixture = createStore(counter, { maxAge: 3 });

modules/store-devtools/src/reducer.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
UPDATE,
88
INIT,
99
} from '@ngrx/store';
10-
import { difference, liftAction } from './utils';
10+
import { difference, liftAction, isActionFiltered } from './utils';
1111
import * as DevtoolsActions from './actions';
1212
import { StoreDevtoolsConfig, StateSanitizer } from './config';
1313
import { PerformAction } from './actions';
@@ -362,8 +362,18 @@ export function liftReducerWith(
362362
return liftedState || initialLiftedState;
363363
}
364364

365-
if (isPaused) {
366-
// If recording is paused, overwrite the last state
365+
if (
366+
isPaused ||
367+
(liftedState &&
368+
isActionFiltered(
369+
liftedState.computedStates[currentStateIndex],
370+
liftedAction,
371+
options.predicate,
372+
options.actionsWhitelist,
373+
options.actionsBlacklist
374+
))
375+
) {
376+
// If recording is paused or if the action should be ignored, overwrite the last state
367377
// (corresponds to the pause action) and keep everything else as is.
368378
// This way, the app gets the new current state while the devtools
369379
// do not record another action.

modules/store-devtools/src/utils.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,17 @@ export function difference(first: any[], second: any[]) {
2424
*/
2525
export function unliftState(liftedState: LiftedState) {
2626
const { computedStates, currentStateIndex } = liftedState;
27-
const { state } = computedStates[currentStateIndex];
2827

28+
// At start up NgRx dispatches init actions,
29+
// When these init actions are being filtered out by the predicate or black/white list options
30+
// we don't have a complete computed states yet.
31+
// At this point it could happen that we're out of bounds, when this happens we fall back to the last known state
32+
if (currentStateIndex >= computedStates.length) {
33+
const { state } = computedStates[computedStates.length - 1];
34+
return state;
35+
}
36+
37+
const { state } = computedStates[currentStateIndex];
2938
return state;
3039
}
3140

@@ -155,9 +164,10 @@ export function isActionFiltered(
155164
whitelist?: string[],
156165
blacklist?: string[]
157166
) {
158-
return (
159-
(predicate && !predicate(state, action.action)) ||
160-
(whitelist && !action.action.type.match(whitelist.join('|'))) ||
161-
(blacklist && action.action.type.match(blacklist.join('|')))
162-
);
167+
const predicateMatch = predicate && !predicate(state, action.action);
168+
const whitelistMatch =
169+
whitelist && !action.action.type.match(whitelist.join('|'));
170+
const blacklistMatch =
171+
blacklist && action.action.type.match(blacklist.join('|'));
172+
return predicateMatch || whitelistMatch || blacklistMatch;
163173
}

0 commit comments

Comments
 (0)