Skip to content

Commit 11bd465

Browse files
timdeschryverbrandonroberts
authored andcommitted
fix(Store): bootstrap store with partial initial state (#1163)
Closes #906, #909
1 parent de78141 commit 11bd465

File tree

4 files changed

+364
-161
lines changed

4 files changed

+364
-161
lines changed

modules/store/spec/integration.spec.ts

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -176,25 +176,25 @@ describe('ngRx Integration spec', () => {
176176
});
177177

178178
describe('feature state', () => {
179-
const initialState = {
180-
todos: [
181-
{
182-
id: 1,
183-
text: 'do things',
184-
completed: false,
185-
},
186-
],
187-
visibilityFilter: VisibilityFilters.SHOW_ALL,
188-
};
179+
it('should initialize properly', () => {
180+
const initialState = {
181+
todos: [
182+
{
183+
id: 1,
184+
text: 'do things',
185+
completed: false,
186+
},
187+
],
188+
visibilityFilter: VisibilityFilters.SHOW_ALL,
189+
};
189190

190-
const reducers: ActionReducerMap<TodoAppSchema, any> = {
191-
todos: todos,
192-
visibilityFilter: visibilityFilter,
193-
};
191+
const reducers: ActionReducerMap<TodoAppSchema, any> = {
192+
todos: todos,
193+
visibilityFilter: visibilityFilter,
194+
};
194195

195-
const featureInitialState = [{ id: 1, completed: false, text: 'Item' }];
196+
const featureInitialState = [{ id: 1, completed: false, text: 'Item' }];
196197

197-
it('should initialize properly', () => {
198198
TestBed.configureTestingModule({
199199
imports: [
200200
StoreModule.forRoot(reducers, { initialState }),
@@ -218,5 +218,40 @@ describe('ngRx Integration spec', () => {
218218
expect(state).toEqual(expected.shift());
219219
});
220220
});
221+
222+
it('should initialize properly with a partial state', () => {
223+
const initialState = {
224+
items: [{ id: 1, completed: false, text: 'Item' }],
225+
};
226+
227+
const reducers: ActionReducerMap<TodoAppSchema, any> = {
228+
todos: todos,
229+
visibilityFilter: visibilityFilter,
230+
};
231+
232+
TestBed.configureTestingModule({
233+
imports: [
234+
StoreModule.forRoot({} as any, {
235+
initialState,
236+
}),
237+
StoreModule.forFeature('todos', reducers),
238+
StoreModule.forFeature('items', todos),
239+
],
240+
});
241+
242+
const store: Store<any> = TestBed.get(Store);
243+
244+
const expected = {
245+
todos: {
246+
todos: [],
247+
visibilityFilter: VisibilityFilters.SHOW_ALL,
248+
},
249+
items: [{ id: 1, completed: false, text: 'Item' }],
250+
};
251+
252+
store.pipe(select(state => state)).subscribe(state => {
253+
expect(state).toEqual(expected);
254+
});
255+
});
221256
});
222257
});

modules/store/spec/store.spec.ts

Lines changed: 188 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import {
77
Store,
88
StoreModule,
99
select,
10+
ReducerManagerDispatcher,
11+
UPDATE,
12+
REDUCER_FACTORY,
1013
} from '../';
1114
import {
1215
counterReducer,
@@ -47,56 +50,42 @@ describe('ngRx Store', () => {
4750
describe('initial state', () => {
4851
it('should handle an initial state object', (done: any) => {
4952
setup();
50-
51-
store.pipe(take(1)).subscribe({
52-
next(val) {
53-
expect(val).toEqual({ counter1: 0, counter2: 1, counter3: 0 });
54-
},
55-
error: done,
56-
complete: done,
57-
});
53+
testStoreValue({ counter1: 0, counter2: 1, counter3: 0 }, done);
5854
});
5955

6056
it('should handle an initial state function', (done: any) => {
6157
setup(() => ({ counter1: 0, counter2: 5 }));
62-
63-
store.pipe(take(1)).subscribe({
64-
next(val) {
65-
expect(val).toEqual({ counter1: 0, counter2: 5, counter3: 0 });
66-
},
67-
error: done,
68-
complete: done,
69-
});
58+
testStoreValue({ counter1: 0, counter2: 5, counter3: 0 }, done);
7059
});
7160

72-
function testInitialState(feature?: string) {
73-
store = TestBed.get(Store);
74-
dispatcher = TestBed.get(ActionsSubject);
75-
76-
const actionSequence = '--a--b--c--d--e--f--g';
77-
const stateSequence = 'i-w-----x-----y--z---';
78-
const actionValues = {
79-
a: { type: INCREMENT },
80-
b: { type: 'OTHER' },
81-
c: { type: RESET },
82-
d: { type: 'OTHER' }, // reproduces https://github.com/ngrx/platform/issues/880 because state is falsey
83-
e: { type: INCREMENT },
84-
f: { type: INCREMENT },
85-
g: { type: 'OTHER' },
86-
};
87-
const counterSteps = hot(actionSequence, actionValues);
88-
counterSteps.subscribe(action => store.dispatch(action));
89-
90-
const counterStateWithString = feature
91-
? (store as any).select(feature, 'counter1')
92-
: store.select('counter1');
93-
94-
const counter1Values = { i: 1, w: 2, x: 0, y: 1, z: 2 };
61+
it('should keep initial state values when state is partially initialized', (done: DoneFn) => {
62+
TestBed.configureTestingModule({
63+
imports: [
64+
StoreModule.forRoot({} as any, {
65+
initialState: {
66+
feature1: {
67+
counter1: 1,
68+
},
69+
feature3: {
70+
counter3: 3,
71+
},
72+
},
73+
}),
74+
StoreModule.forFeature('feature1', { counter1: counterReducer }),
75+
StoreModule.forFeature('feature2', { counter2: counterReducer }),
76+
StoreModule.forFeature('feature3', { counter3: counterReducer }),
77+
],
78+
});
9579

96-
expect(counterStateWithString).toBeObservable(
97-
hot(stateSequence, counter1Values)
80+
testStoreValue(
81+
{
82+
feature1: { counter1: 1 },
83+
feature2: { counter2: 0 },
84+
feature3: { counter3: 3 },
85+
},
86+
done
9887
);
99-
}
88+
});
10089

10190
it('should reset to initial state when undefined (root ActionReducerMap)', () => {
10291
TestBed.configureTestingModule({
@@ -138,6 +127,47 @@ describe('ngRx Store', () => {
138127

139128
testInitialState('feature1');
140129
});
130+
131+
function testInitialState(feature?: string) {
132+
store = TestBed.get(Store);
133+
dispatcher = TestBed.get(ActionsSubject);
134+
135+
const actionSequence = '--a--b--c--d--e--f--g';
136+
const stateSequence = 'i-w-----x-----y--z---';
137+
const actionValues = {
138+
a: { type: INCREMENT },
139+
b: { type: 'OTHER' },
140+
c: { type: RESET },
141+
d: { type: 'OTHER' }, // reproduces https://github.com/ngrx/platform/issues/880 because state is falsey
142+
e: { type: INCREMENT },
143+
f: { type: INCREMENT },
144+
g: { type: 'OTHER' },
145+
};
146+
const counterSteps = hot(actionSequence, actionValues);
147+
counterSteps.subscribe(action => store.dispatch(action));
148+
149+
const counterStateWithString = feature
150+
? (store as any).select(feature, 'counter1')
151+
: store.select('counter1');
152+
153+
const counter1Values = { i: 1, w: 2, x: 0, y: 1, z: 2 };
154+
155+
expect(counterStateWithString).toBeObservable(
156+
hot(stateSequence, counter1Values)
157+
);
158+
}
159+
160+
function testStoreValue(expected: any, done: DoneFn) {
161+
store = TestBed.get(Store);
162+
163+
store.pipe(take(1)).subscribe({
164+
next(val) {
165+
expect(val).toEqual(expected);
166+
},
167+
error: done,
168+
complete: done,
169+
});
170+
}
141171
});
142172

143173
describe('basic store actions', () => {
@@ -267,16 +297,19 @@ describe('ngRx Store', () => {
267297
describe(`add/remove reducers`, () => {
268298
let addReducerSpy: Spy;
269299
let removeReducerSpy: Spy;
300+
let reducerManagerDispatcherSpy: Spy;
270301
const key = 'counter4';
271302

272303
beforeEach(() => {
273304
setup();
274305
const reducerManager = TestBed.get(ReducerManager);
306+
const dispatcher = TestBed.get(ReducerManagerDispatcher);
275307
addReducerSpy = spyOn(reducerManager, 'addReducer').and.callThrough();
276308
removeReducerSpy = spyOn(
277309
reducerManager,
278310
'removeReducer'
279311
).and.callThrough();
312+
reducerManagerDispatcherSpy = spyOn(dispatcher, 'next').and.callThrough();
280313
});
281314

282315
it(`should delegate add/remove to ReducerManager`, () => {
@@ -299,5 +332,118 @@ describe('ngRx Store', () => {
299332
expect(val.counter4).toBeUndefined();
300333
});
301334
});
335+
336+
it('should dispatch an update reducers action when a reducer is added', () => {
337+
store.addReducer(key, counterReducer);
338+
expect(reducerManagerDispatcherSpy).toHaveBeenCalledWith({
339+
type: UPDATE,
340+
feature: key,
341+
});
342+
});
343+
344+
it('should dispatch an update reducers action when a reducer is removed', () => {
345+
store.removeReducer(key);
346+
expect(reducerManagerDispatcherSpy).toHaveBeenCalledWith({
347+
type: UPDATE,
348+
feature: key,
349+
});
350+
});
351+
});
352+
353+
describe('add/remove features', () => {
354+
let reducerManager: ReducerManager;
355+
let reducerManagerDispatcherSpy: Spy;
356+
357+
beforeEach(() => {
358+
TestBed.configureTestingModule({
359+
imports: [StoreModule.forRoot({})],
360+
});
361+
362+
reducerManager = TestBed.get(ReducerManager);
363+
const dispatcher = TestBed.get(ReducerManagerDispatcher);
364+
reducerManagerDispatcherSpy = spyOn(dispatcher, 'next').and.callThrough();
365+
});
366+
367+
it('should dispatch an update reducers action when a feature is added', () => {
368+
reducerManager.addFeature(
369+
createFeature({
370+
key: 'feature1',
371+
})
372+
);
373+
374+
expect(reducerManagerDispatcherSpy).toHaveBeenCalledWith({
375+
type: UPDATE,
376+
feature: 'feature1',
377+
});
378+
});
379+
380+
it('should dispatch an update reducers action for each feature that is added', () => {
381+
reducerManager.addFeatures([
382+
createFeature({
383+
key: 'feature1',
384+
}),
385+
createFeature({
386+
key: 'feature2',
387+
}),
388+
]);
389+
390+
expect(reducerManagerDispatcherSpy).toHaveBeenCalledTimes(2);
391+
392+
// get the first argument for the first call
393+
expect(reducerManagerDispatcherSpy.calls.argsFor(0)[0]).toEqual({
394+
type: UPDATE,
395+
feature: 'feature1',
396+
});
397+
398+
// get the first argument for the second call
399+
expect(reducerManagerDispatcherSpy.calls.argsFor(1)[0]).toEqual({
400+
type: UPDATE,
401+
feature: 'feature2',
402+
});
403+
});
404+
405+
it('should dispatch an update reducers action when a feature is removed', () => {
406+
reducerManager.removeFeature(
407+
createFeature({
408+
key: 'feature1',
409+
})
410+
);
411+
412+
expect(reducerManagerDispatcherSpy).toHaveBeenCalledWith({
413+
type: UPDATE,
414+
feature: 'feature1',
415+
});
416+
});
417+
418+
it('should dispatch an update reducers action for each feature that is removed', () => {
419+
reducerManager.removeFeatures([
420+
createFeature({
421+
key: 'feature1',
422+
}),
423+
createFeature({
424+
key: 'feature2',
425+
}),
426+
]);
427+
428+
// get the first argument for the first call
429+
expect(reducerManagerDispatcherSpy.calls.argsFor(0)[0]).toEqual({
430+
type: UPDATE,
431+
feature: 'feature1',
432+
});
433+
434+
// get the first argument for the second call
435+
expect(reducerManagerDispatcherSpy.calls.argsFor(1)[0]).toEqual({
436+
type: UPDATE,
437+
feature: 'feature2',
438+
});
439+
});
440+
441+
function createFeature({ key }: { key: string }) {
442+
return {
443+
key,
444+
reducers: {},
445+
reducerFactory: jasmine.createSpy(`reducerFactory_${key}`),
446+
};
447+
}
302448
});
303449
});

0 commit comments

Comments
 (0)