diff --git a/modules/store/spec/integration.spec.ts b/modules/store/spec/integration.spec.ts index c1ff475465..16694a80dd 100644 --- a/modules/store/spec/integration.spec.ts +++ b/modules/store/spec/integration.spec.ts @@ -139,7 +139,7 @@ describe('ngRx Integration spec', () => { return todos.filter(predicate); }; - let currentlyVisibleTodos: any; + let currentlyVisibleTodos: Todo[] = []; Observable.combineLatest( store.select('visibilityFilter'), diff --git a/modules/store/spec/modules.spec.ts b/modules/store/spec/modules.spec.ts index 4a01a25404..798b63ee92 100644 --- a/modules/store/spec/modules.spec.ts +++ b/modules/store/spec/modules.spec.ts @@ -1,9 +1,16 @@ import 'rxjs/add/operator/take'; import { TestBed } from '@angular/core/testing'; import { NgModule, InjectionToken } from '@angular/core'; -import { StoreModule, Store, ActionReducer, ActionReducerMap } from '../'; +import { + StoreModule, + Store, + ActionReducer, + ActionReducerMap, + combineReducers, +} from '../'; +import createSpy = jasmine.createSpy; -describe('Nested Store Modules', () => { +describe(`Store Modules`, () => { type RootState = { fruit: string }; type FeatureAState = number; type FeatureBState = { list: number[]; index: number }; @@ -14,57 +21,144 @@ describe('Nested Store Modules', () => { const reducersToken = new InjectionToken>( 'Root Reducers' ); - const rootFruitReducer: ActionReducer = () => 'apple'; - const featureAReducer: ActionReducer = () => 5; - const featureBListReducer: ActionReducer = () => [1, 2, 3]; - const featureBIndexReducer: ActionReducer = () => 2; + + // Trigger here is basically an action type used to trigger state update + const createDummyReducer = (def: T, trigger: string): ActionReducer => ( + s = def, + { type, payload }: any + ) => (type === trigger ? payload : s); + const rootFruitReducer = createDummyReducer('apple', 'fruit'); + const featureAReducer = createDummyReducer(5, 'a'); + const featureBListReducer = createDummyReducer([1, 2, 3], 'bList'); + const featureBIndexReducer = createDummyReducer(2, 'bIndex'); const featureBReducerMap: ActionReducerMap = { list: featureBListReducer, index: featureBIndexReducer, }; - @NgModule({ - imports: [StoreModule.forFeature('a', featureAReducer)], - }) - class FeatureAModule {} - - @NgModule({ - imports: [StoreModule.forFeature('b', featureBReducerMap)], - }) - class FeatureBModule {} - - @NgModule({ - imports: [ - StoreModule.forRoot(reducersToken), - FeatureAModule, - FeatureBModule, - ], - providers: [ - { - provide: reducersToken, - useValue: { fruit: rootFruitReducer }, - }, - ], - }) - class RootModule {} - - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [RootModule], + describe(`: Config`, () => { + let featureAReducerFactory: any; + let rootReducerFactory: any; + + const featureAInitial = () => ({ a: 42 }); + const rootInitial = { fruit: 'orange' }; + + beforeEach(() => { + featureAReducerFactory = createSpy( + 'featureAReducerFactory' + ).and.callFake((rm: any, initialState?: any) => { + return (state: any, action: any) => 4; + }); + rootReducerFactory = createSpy('rootReducerFactory').and.callFake( + combineReducers + ); + + @NgModule({ + imports: [ + StoreModule.forFeature( + 'a', + { a: featureAReducer }, + { + initialState: featureAInitial, + reducerFactory: featureAReducerFactory, + } + ), + ], + }) + class FeatureAModule {} + + @NgModule({ + imports: [ + StoreModule.forRoot(reducersToken, { + initialState: rootInitial, + reducerFactory: rootReducerFactory, + }), + FeatureAModule, + ], + providers: [ + { + provide: reducersToken, + useValue: { fruit: rootFruitReducer }, + }, + ], + }) + class RootModule {} + + TestBed.configureTestingModule({ + imports: [RootModule], + }); + + store = TestBed.get(Store); + }); + + it(`should accept configurations`, () => { + expect(featureAReducerFactory).toHaveBeenCalledWith( + { a: featureAReducer }, + featureAInitial() + ); + expect(rootReducerFactory).toHaveBeenCalledWith( + { fruit: rootFruitReducer }, + rootInitial + ); }); - store = TestBed.get(Store); + it(`should should use config.reducerFactory`, () => { + store.dispatch({ type: 'fruit', payload: 'banana' }); + store.dispatch({ type: 'a', payload: 42 }); + + store.take(1).subscribe((s: any) => { + expect(s).toEqual({ + fruit: 'banana', + a: 4, + }); + }); + }); }); - it('should nest the child module in the root store object', () => { - store.take(1).subscribe((state: State) => { - expect(state).toEqual({ - fruit: 'apple', - a: 5, - b: { - list: [1, 2, 3], - index: 2, + describe(`: Nested`, () => { + @NgModule({ + imports: [StoreModule.forFeature('a', featureAReducer)], + }) + class FeatureAModule {} + + @NgModule({ + imports: [StoreModule.forFeature('b', featureBReducerMap)], + }) + class FeatureBModule {} + + @NgModule({ + imports: [ + StoreModule.forRoot(reducersToken), + FeatureAModule, + FeatureBModule, + ], + providers: [ + { + provide: reducersToken, + useValue: { fruit: rootFruitReducer }, }, + ], + }) + class RootModule {} + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [RootModule], + }); + + store = TestBed.get(Store); + }); + + it('should nest the child module in the root store object', () => { + store.take(1).subscribe((state: State) => { + expect(state).toEqual({ + fruit: 'apple', + a: 5, + b: { + list: [1, 2, 3], + index: 2, + }, + }); }); }); }); diff --git a/modules/store/spec/state.spec.ts b/modules/store/spec/state.spec.ts index 3daaee21f7..34a5821fe1 100644 --- a/modules/store/spec/state.spec.ts +++ b/modules/store/spec/state.spec.ts @@ -2,7 +2,7 @@ import { Observable } from 'rxjs/Observable'; import { Subject } from 'rxjs/Subject'; import { ReflectiveInjector } from '@angular/core'; import { createInjector } from './helpers/injector'; -import { StoreModule, Store } from '../'; +import { StoreModule, Store, INIT } from '../'; describe('ngRx State', () => { const initialState = 123; @@ -22,7 +22,7 @@ describe('ngRx State', () => { injector.get(Store); expect(reducer).toHaveBeenCalledWith(initialState, { - type: '@ngrx/store/init', + type: INIT, }); }); }); diff --git a/modules/store/spec/store.spec.ts b/modules/store/spec/store.spec.ts index d737b432c6..cd62ea37ee 100644 --- a/modules/store/spec/store.spec.ts +++ b/modules/store/spec/store.spec.ts @@ -1,28 +1,22 @@ import 'rxjs/add/operator/take'; -import { Observable } from 'rxjs/Observable'; import { ReflectiveInjector } from '@angular/core'; import { hot } from 'jasmine-marbles'; import { createInjector } from './helpers/injector'; -import { Store, Action, combineReducers, StoreModule } from '../'; -import { ActionsSubject } from '../src/private_export'; +import { ActionsSubject, ReducerManager, Store, StoreModule } from '../'; import { counterReducer, INCREMENT, DECREMENT, RESET, } from './fixtures/counter'; +import Spy = jasmine.Spy; +import any = jasmine.any; interface TestAppSchema { counter1: number; counter2: number; counter3: number; -} - -interface Todo {} - -interface TodoAppSchema { - visibilityFilter: string; - todos: Todo[]; + counter4?: number; } describe('ngRx Store', () => { @@ -68,7 +62,7 @@ describe('ngRx Store', () => { }); }); - describe('basic store actions', function() { + describe('basic store actions', () => { beforeEach(() => setup()); it('should provide an Observable Store', () => { @@ -84,7 +78,7 @@ describe('ngRx Store', () => { e: { type: INCREMENT }, }; - it('should let you select state with a key name', function() { + it('should let you select state with a key name', () => { const counterSteps = hot(actionSequence, actionValues); counterSteps.subscribe(action => store.dispatch(action)); @@ -99,7 +93,7 @@ describe('ngRx Store', () => { ); }); - it('should let you select state with a selector function', function() { + it('should let you select state with a selector function', () => { const counterSteps = hot(actionSequence, actionValues); counterSteps.subscribe(action => store.dispatch(action)); @@ -114,13 +108,13 @@ describe('ngRx Store', () => { ); }); - it('should correctly lift itself', function() { + it('should correctly lift itself', () => { const result = store.select('counter1'); - expect(result instanceof Store).toBe(true); + expect(result).toEqual(any(Store)); }); - it('should increment and decrement counter1', function() { + it('should increment and decrement counter1', () => { const counterSteps = hot(actionSequence, actionValues); counterSteps.subscribe(action => store.dispatch(action)); @@ -133,7 +127,7 @@ describe('ngRx Store', () => { expect(counterState).toBeObservable(hot(stateSequence, counter1Values)); }); - it('should increment and decrement counter1 using the dispatcher', function() { + it('should increment and decrement counter1 using the dispatcher', () => { const counterSteps = hot(actionSequence, actionValues); counterSteps.subscribe(action => dispatcher.next(action)); @@ -146,7 +140,7 @@ describe('ngRx Store', () => { expect(counterState).toBeObservable(hot(stateSequence, counter1Values)); }); - it('should increment and decrement counter2 separately', function() { + it('should increment and decrement counter2 separately', () => { const counterSteps = hot(actionSequence, actionValues); counterSteps.subscribe(action => store.dispatch(action)); @@ -160,7 +154,7 @@ describe('ngRx Store', () => { expect(counter2State).toBeObservable(hot(stateSequence, counter2Values)); }); - it('should implement the observer interface forwarding actions and errors to the dispatcher', function() { + it('should implement the observer interface forwarding actions and errors to the dispatcher', () => { spyOn(dispatcher, 'next'); spyOn(dispatcher, 'error'); @@ -171,7 +165,7 @@ describe('ngRx Store', () => { expect(dispatcher.error).toHaveBeenCalledWith(2); }); - it('should not be completable', function() { + it('should not be completable', () => { const storeSubscription = store.subscribe(); const dispatcherSubscription = dispatcher.subscribe(); @@ -183,7 +177,7 @@ describe('ngRx Store', () => { }); // TODO: Investigate why this is no longer working - xit('should complete if the dispatcher is destroyed', () => { + it('should complete if the dispatcher is destroyed', () => { const storeSubscription = store.subscribe(); const dispatcherSubscription = dispatcher.subscribe(); @@ -192,4 +186,41 @@ describe('ngRx Store', () => { expect(dispatcherSubscription.closed).toBe(true); }); }); + + describe(`add/remove reducers`, () => { + let addReducerSpy: Spy; + let removeReducerSpy: Spy; + const key = 'counter4'; + + beforeEach(() => { + setup(); + const reducerManager = injector.get(ReducerManager); + addReducerSpy = spyOn(reducerManager, 'addReducer').and.callThrough(); + removeReducerSpy = spyOn( + reducerManager, + 'removeReducer' + ).and.callThrough(); + }); + + it(`should delegate add/remove to ReducerManager`, () => { + store.addReducer(key, counterReducer); + expect(addReducerSpy).toHaveBeenCalledWith(key, counterReducer); + + store.removeReducer(key); + expect(removeReducerSpy).toHaveBeenCalledWith(key); + }); + + it(`should work with added / removed reducers`, () => { + store.addReducer(key, counterReducer); + store.take(1).subscribe(val => { + expect(val.counter4).toBe(0); + }); + + store.removeReducer(key); + store.dispatch({ type: INCREMENT }); + store.take(1).subscribe(val => { + expect(val.counter4).toBeUndefined(); + }); + }); + }); }); diff --git a/modules/store/src/actions_subject.ts b/modules/store/src/actions_subject.ts index 418af4779c..7885efcad0 100644 --- a/modules/store/src/actions_subject.ts +++ b/modules/store/src/actions_subject.ts @@ -4,7 +4,7 @@ import { Observable } from 'rxjs/Observable'; import { Observer } from 'rxjs/Observer'; import { Action } from './models'; -export const INIT = '@ngrx/store/init'; +export const INIT = '@ngrx/store/init' as '@ngrx/store/init'; @Injectable() export class ActionsSubject extends BehaviorSubject @@ -15,9 +15,9 @@ export class ActionsSubject extends BehaviorSubject next(action: Action): void { if (typeof action === 'undefined') { - throw new Error(`Actions must be objects`); + throw new TypeError(`Actions must be objects`); } else if (typeof action.type === 'undefined') { - throw new Error(`Actions must have a type property`); + throw new TypeError(`Actions must have a type property`); } super.next(action); diff --git a/modules/store/src/reducer_manager.ts b/modules/store/src/reducer_manager.ts index 1b3a1f3d81..8b02930d30 100644 --- a/modules/store/src/reducer_manager.ts +++ b/modules/store/src/reducer_manager.ts @@ -16,7 +16,7 @@ export abstract class ReducerObservable extends Observable< ActionReducer > {} export abstract class ReducerManagerDispatcher extends ActionsSubject {} -export const UPDATE = '@ngrx/store/update-reducers'; +export const UPDATE = '@ngrx/store/update-reducers' as '@ngrx/store/update-reducers'; @Injectable() export class ReducerManager extends BehaviorSubject> diff --git a/modules/store/src/state.ts b/modules/store/src/state.ts index db37b7a839..87d50a36cb 100644 --- a/modules/store/src/state.ts +++ b/modules/store/src/state.ts @@ -4,7 +4,6 @@ import { Observable } from 'rxjs/Observable'; import { Subscription } from 'rxjs/Subscription'; import { queue } from 'rxjs/scheduler/queue'; import { observeOn } from 'rxjs/operator/observeOn'; -import { startWith } from 'rxjs/operator/startWith'; import { withLatestFrom } from 'rxjs/operator/withLatestFrom'; import { scan } from 'rxjs/operator/scan'; import { ActionsSubject } from './actions_subject'; @@ -12,12 +11,13 @@ import { Action, ActionReducer } from './models'; import { INITIAL_STATE } from './tokens'; import { ReducerObservable } from './reducer_manager'; import { ScannedActionsSubject } from './scanned_actions_subject'; +import { INIT } from './'; export abstract class StateObservable extends Observable {} @Injectable() export class State extends BehaviorSubject implements OnDestroy { - static readonly INIT = '@ngrx/store/init'; + static readonly INIT = INIT; private stateSubscription: Subscription; diff --git a/modules/store/src/store_module.ts b/modules/store/src/store_module.ts index a246e92f01..c8c03124fa 100644 --- a/modules/store/src/store_module.ts +++ b/modules/store/src/store_module.ts @@ -49,7 +49,13 @@ export class StoreFeatureModule implements OnDestroy { @Inject(STORE_FEATURES) private features: StoreFeature[], private reducerManager: ReducerManager ) { - features.forEach(feature => reducerManager.addFeature(feature)); + for (let feature of features) { + if (typeof feature.initialState === 'function') { + feature = { ...feature, initialState: feature.initialState() }; + } + + reducerManager.addFeature(feature); + } } ngOnDestroy() { diff --git a/modules/store/src/tokens.ts b/modules/store/src/tokens.ts index 45811564c4..6f91e266d3 100644 --- a/modules/store/src/tokens.ts +++ b/modules/store/src/tokens.ts @@ -1,7 +1,11 @@ -import { OpaqueToken } from '@angular/core'; +import { InjectionToken } from '@angular/core'; -export const _INITIAL_STATE = new OpaqueToken('_ngrx/store Initial State'); -export const INITIAL_STATE = new OpaqueToken('@ngrx/store Initial State'); -export const REDUCER_FACTORY = new OpaqueToken('@ngrx/store Reducer Factory'); -export const INITIAL_REDUCERS = new OpaqueToken('@ngrx/store Initial Reducers'); -export const STORE_FEATURES = new OpaqueToken('@ngrx/store Store Features'); +export const _INITIAL_STATE = new InjectionToken('_ngrx/store Initial State'); +export const INITIAL_STATE = new InjectionToken('@ngrx/store Initial State'); +export const REDUCER_FACTORY = new InjectionToken( + '@ngrx/store Reducer Factory' +); +export const INITIAL_REDUCERS = new InjectionToken( + '@ngrx/store Initial Reducers' +); +export const STORE_FEATURES = new InjectionToken('@ngrx/store Store Features');