Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(Store): bootstrap store with partial initial state #1163

Merged
merged 9 commits into from
Jul 13, 2018
67 changes: 51 additions & 16 deletions modules/store/spec/integration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,25 +176,25 @@ describe('ngRx Integration spec', () => {
});

describe('feature state', () => {
const initialState = {
todos: [
{
id: 1,
text: 'do things',
completed: false,
},
],
visibilityFilter: VisibilityFilters.SHOW_ALL,
};
it('should initialize properly', () => {
const initialState = {
todos: [
{
id: 1,
text: 'do things',
completed: false,
},
],
visibilityFilter: VisibilityFilters.SHOW_ALL,
};

const reducers: ActionReducerMap<TodoAppSchema, any> = {
todos: todos,
visibilityFilter: visibilityFilter,
};
const reducers: ActionReducerMap<TodoAppSchema, any> = {
todos: todos,
visibilityFilter: visibilityFilter,
};

const featureInitialState = [{ id: 1, completed: false, text: 'Item' }];
const featureInitialState = [{ id: 1, completed: false, text: 'Item' }];

it('should initialize properly', () => {
TestBed.configureTestingModule({
imports: [
StoreModule.forRoot(reducers, { initialState }),
Expand All @@ -218,5 +218,40 @@ describe('ngRx Integration spec', () => {
expect(state).toEqual(expected.shift());
});
});

it('should initialize properly with a partial state', () => {
const initialState = {
items: [{ id: 1, completed: false, text: 'Item' }],
};

const reducers: ActionReducerMap<TodoAppSchema, any> = {
todos: todos,
visibilityFilter: visibilityFilter,
};

TestBed.configureTestingModule({
imports: [
StoreModule.forRoot({} as any, {
initialState,
}),
StoreModule.forFeature('todos', reducers),
StoreModule.forFeature('items', todos),
],
});

const store: Store<any> = TestBed.get(Store);

const expected = {
todos: {
todos: [],
visibilityFilter: VisibilityFilters.SHOW_ALL,
},
items: [{ id: 1, completed: false, text: 'Item' }],
};

store.pipe(select(state => state)).subscribe(state => {
expect(state).toEqual(expected);
});
});
});
});
230 changes: 188 additions & 42 deletions modules/store/spec/store.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import {
Store,
StoreModule,
select,
ReducerManagerDispatcher,
UPDATE,
REDUCER_FACTORY,
} from '../';
import {
counterReducer,
Expand Down Expand Up @@ -47,56 +50,42 @@ describe('ngRx Store', () => {
describe('initial state', () => {
it('should handle an initial state object', (done: any) => {
setup();

store.pipe(take(1)).subscribe({
next(val) {
expect(val).toEqual({ counter1: 0, counter2: 1, counter3: 0 });
},
error: done,
complete: done,
});
testStoreValue({ counter1: 0, counter2: 1, counter3: 0 }, done);
});

it('should handle an initial state function', (done: any) => {
setup(() => ({ counter1: 0, counter2: 5 }));

store.pipe(take(1)).subscribe({
next(val) {
expect(val).toEqual({ counter1: 0, counter2: 5, counter3: 0 });
},
error: done,
complete: done,
});
testStoreValue({ counter1: 0, counter2: 5, counter3: 0 }, done);
});

function testInitialState(feature?: string) {
store = TestBed.get(Store);
dispatcher = TestBed.get(ActionsSubject);

const actionSequence = '--a--b--c--d--e--f--g';
const stateSequence = 'i-w-----x-----y--z---';
const actionValues = {
a: { type: INCREMENT },
b: { type: 'OTHER' },
c: { type: RESET },
d: { type: 'OTHER' }, // reproduces https://github.com/ngrx/platform/issues/880 because state is falsey
e: { type: INCREMENT },
f: { type: INCREMENT },
g: { type: 'OTHER' },
};
const counterSteps = hot(actionSequence, actionValues);
counterSteps.subscribe(action => store.dispatch(action));

const counterStateWithString = feature
? (store as any).select(feature, 'counter1')
: store.select('counter1');

const counter1Values = { i: 1, w: 2, x: 0, y: 1, z: 2 };
it('should keep initial state values when state is partially initialized', (done: DoneFn) => {
TestBed.configureTestingModule({
imports: [
StoreModule.forRoot({} as any, {
initialState: {
feature1: {
counter1: 1,
},
feature3: {
counter3: 3,
},
},
}),
StoreModule.forFeature('feature1', { counter1: counterReducer }),
StoreModule.forFeature('feature2', { counter2: counterReducer }),
StoreModule.forFeature('feature3', { counter3: counterReducer }),
],
});

expect(counterStateWithString).toBeObservable(
hot(stateSequence, counter1Values)
testStoreValue(
{
feature1: { counter1: 1 },
feature2: { counter2: 0 },
feature3: { counter3: 3 },
},
done
);
}
});

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

testInitialState('feature1');
});

function testInitialState(feature?: string) {
store = TestBed.get(Store);
dispatcher = TestBed.get(ActionsSubject);

const actionSequence = '--a--b--c--d--e--f--g';
const stateSequence = 'i-w-----x-----y--z---';
const actionValues = {
a: { type: INCREMENT },
b: { type: 'OTHER' },
c: { type: RESET },
d: { type: 'OTHER' }, // reproduces https://github.com/ngrx/platform/issues/880 because state is falsey
e: { type: INCREMENT },
f: { type: INCREMENT },
g: { type: 'OTHER' },
};
const counterSteps = hot(actionSequence, actionValues);
counterSteps.subscribe(action => store.dispatch(action));

const counterStateWithString = feature
? (store as any).select(feature, 'counter1')
: store.select('counter1');

const counter1Values = { i: 1, w: 2, x: 0, y: 1, z: 2 };

expect(counterStateWithString).toBeObservable(
hot(stateSequence, counter1Values)
);
}

function testStoreValue(expected: any, done: DoneFn) {
store = TestBed.get(Store);

store.pipe(take(1)).subscribe({
next(val) {
expect(val).toEqual(expected);
},
error: done,
complete: done,
});
}
});

describe('basic store actions', () => {
Expand Down Expand Up @@ -267,16 +297,19 @@ describe('ngRx Store', () => {
describe(`add/remove reducers`, () => {
let addReducerSpy: Spy;
let removeReducerSpy: Spy;
let reducerManagerDispatcherSpy: Spy;
const key = 'counter4';

beforeEach(() => {
setup();
const reducerManager = TestBed.get(ReducerManager);
const dispatcher = TestBed.get(ReducerManagerDispatcher);
addReducerSpy = spyOn(reducerManager, 'addReducer').and.callThrough();
removeReducerSpy = spyOn(
reducerManager,
'removeReducer'
).and.callThrough();
reducerManagerDispatcherSpy = spyOn(dispatcher, 'next').and.callThrough();
});

it(`should delegate add/remove to ReducerManager`, () => {
Expand All @@ -299,5 +332,118 @@ describe('ngRx Store', () => {
expect(val.counter4).toBeUndefined();
});
});

it('should dispatch an update reducers action when a reducer is added', () => {
store.addReducer(key, counterReducer);
expect(reducerManagerDispatcherSpy).toHaveBeenCalledWith({
type: UPDATE,
feature: key,
});
});

it('should dispatch an update reducers action when a reducer is removed', () => {
store.removeReducer(key);
expect(reducerManagerDispatcherSpy).toHaveBeenCalledWith({
type: UPDATE,
feature: key,
});
});
});

describe('add/remove features', () => {
let reducerManager: ReducerManager;
let reducerManagerDispatcherSpy: Spy;

beforeEach(() => {
TestBed.configureTestingModule({
imports: [StoreModule.forRoot({})],
});

reducerManager = TestBed.get(ReducerManager);
const dispatcher = TestBed.get(ReducerManagerDispatcher);
reducerManagerDispatcherSpy = spyOn(dispatcher, 'next').and.callThrough();
});

it('should dispatch an update reducers action when a feature is added', () => {
reducerManager.addFeature(
createFeature({
key: 'feature1',
})
);

expect(reducerManagerDispatcherSpy).toHaveBeenCalledWith({
type: UPDATE,
feature: 'feature1',
});
});

it('should dispatch an update reducers action for each feature that is added', () => {
reducerManager.addFeatures([
createFeature({
key: 'feature1',
}),
createFeature({
key: 'feature2',
}),
]);

expect(reducerManagerDispatcherSpy).toHaveBeenCalledTimes(2);

// get the first argument for the first call
expect(reducerManagerDispatcherSpy.calls.argsFor(0)[0]).toEqual({
type: UPDATE,
feature: 'feature1',
});

// get the first argument for the second call
expect(reducerManagerDispatcherSpy.calls.argsFor(1)[0]).toEqual({
type: UPDATE,
feature: 'feature2',
});
});

it('should dispatch an update reducers action when a feature is removed', () => {
reducerManager.removeFeature(
createFeature({
key: 'feature1',
})
);

expect(reducerManagerDispatcherSpy).toHaveBeenCalledWith({
type: UPDATE,
feature: 'feature1',
});
});

it('should dispatch an update reducers action for each feature that is removed', () => {
reducerManager.removeFeatures([
createFeature({
key: 'feature1',
}),
createFeature({
key: 'feature2',
}),
]);

// get the first argument for the first call
expect(reducerManagerDispatcherSpy.calls.argsFor(0)[0]).toEqual({
type: UPDATE,
feature: 'feature1',
});

// get the first argument for the second call
expect(reducerManagerDispatcherSpy.calls.argsFor(1)[0]).toEqual({
type: UPDATE,
feature: 'feature2',
});
});

function createFeature({ key }: { key: string }) {
return {
key,
reducers: {},
reducerFactory: jasmine.createSpy(`reducerFactory_${key}`),
};
}
});
});
Loading