From 0a3398d4e334d93e35fd61c15c0a9b9c919f5c44 Mon Sep 17 00:00:00 2001 From: Brandon Date: Mon, 18 Dec 2017 10:22:40 -0800 Subject: [PATCH] fix(Effects): Ensure Store modules are loaded eagerly (#658) Closes #642 --- .../spec/effects_feature_module.spec.ts | 145 ++++++++++++++++++ .../effects/spec/effects_feature_module.ts | 40 ----- modules/effects/src/effects_feature_module.ts | 5 +- modules/effects/src/effects_root_module.ts | 10 +- 4 files changed, 156 insertions(+), 44 deletions(-) create mode 100644 modules/effects/spec/effects_feature_module.spec.ts delete mode 100644 modules/effects/spec/effects_feature_module.ts diff --git a/modules/effects/spec/effects_feature_module.spec.ts b/modules/effects/spec/effects_feature_module.spec.ts new file mode 100644 index 0000000000..4a558f4e03 --- /dev/null +++ b/modules/effects/spec/effects_feature_module.spec.ts @@ -0,0 +1,145 @@ +import { TestBed } from '@angular/core/testing'; +import { NgModule, Injectable } from '@angular/core'; +import { + StoreModule, + Store, + Action, + createFeatureSelector, + createSelector, + select, +} from '@ngrx/store'; +import { tap, withLatestFrom, map, mergeMap, filter } from 'rxjs/operators'; +import { Observable } from 'rxjs/Observable'; +import { cold } from 'jasmine-marbles'; +import { EffectSources } from '../src/effect_sources'; +import { FEATURE_EFFECTS } from '../src/tokens'; +import { EffectsFeatureModule } from '../src/effects_feature_module'; +import { EffectsRootModule } from '../src/effects_root_module'; +import { EffectsModule, Effect, Actions, ofType } from '../'; + +describe('Effects Feature Module', () => { + describe('when registered', () => { + const sourceA = 'sourceA'; + const sourceB = 'sourceB'; + const sourceC = 'sourceC'; + const effectSourceGroups = [[sourceA], [sourceB], [sourceC]]; + let mockEffectSources: { addEffects: jasmine.Spy }; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + { + provide: EffectsRootModule, + useValue: { + addEffects: jasmine.createSpy('addEffects'), + }, + }, + { + provide: FEATURE_EFFECTS, + useValue: effectSourceGroups, + }, + EffectsFeatureModule, + ], + }); + + mockEffectSources = TestBed.get(EffectsRootModule); + }); + + it('should add all effects when instantiated', () => { + TestBed.get(EffectsFeatureModule); + + expect(mockEffectSources.addEffects).toHaveBeenCalledWith(sourceA); + expect(mockEffectSources.addEffects).toHaveBeenCalledWith(sourceB); + expect(mockEffectSources.addEffects).toHaveBeenCalledWith(sourceC); + }); + }); + + describe('when registered in a different NgModule from the feature state', () => { + let effects: FeatureEffects; + let actions$: Observable; + let store: Store; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [AppModule], + }); + + effects = TestBed.get(FeatureEffects); + store = TestBed.get(Store); + }); + + it('should have the feature state defined to select from the effect', ( + done: any + ) => { + const action = { type: 'INCREMENT' }; + const result = { type: 'INCREASE' }; + + effects.effectWithStore.subscribe(res => { + expect(res).toEqual(result); + }); + + store.dispatch(action); + + store.pipe(select(getDataState)).subscribe(res => { + expect(res).toBe(110); + done(); + }); + }); + }); +}); + +const FEATURE_KEY = 'feature'; + +interface State { + FEATURE_KEY: DataState; +} + +interface DataState { + data: number; +} + +const initialState: DataState = { + data: 100, +}; + +function reducer(state: DataState = initialState, action: Action) { + switch (action.type) { + case 'INCREASE': + return { + data: state.data + 10, + }; + } + return state; +} + +const getFeatureState = createFeatureSelector(FEATURE_KEY); + +const getDataState = createSelector(getFeatureState, state => state.data); + +@Injectable() +class FeatureEffects { + constructor(private actions: Actions, private store: Store) {} + + @Effect() + effectWithStore = this.actions + .ofType('INCREMENT') + .pipe( + withLatestFrom(this.store.select(getDataState)), + map(([action, state]) => ({ type: 'INCREASE' })) + ); +} + +@NgModule({ + imports: [EffectsModule.forFeature([FeatureEffects])], +}) +class FeatureEffectsModule {} + +@NgModule({ + imports: [FeatureEffectsModule, StoreModule.forFeature(FEATURE_KEY, reducer)], +}) +class FeatureModule {} + +@NgModule({ + imports: [StoreModule.forRoot({}), EffectsModule.forRoot([]), FeatureModule], +}) +class AppModule {} diff --git a/modules/effects/spec/effects_feature_module.ts b/modules/effects/spec/effects_feature_module.ts deleted file mode 100644 index 31a4ec0ab4..0000000000 --- a/modules/effects/spec/effects_feature_module.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { TestBed } from '@angular/core/testing'; -import { EffectSources } from '../src/effect_sources'; -import { FEATURE_EFFECTS } from '../src/tokens'; -import { EffectsFeatureModule } from '../src/effects_feature_module'; - -describe('Effects Feature Module', () => { - const sourceA = 'sourceA'; - const sourceB = 'sourceB'; - const sourceC = 'sourceC'; - const effectSourceGroups = [[sourceA], [sourceB], [sourceC]]; - let mockEffectSources: { addEffects: jasmine.Spy }; - - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [ - { - provide: EffectSources, - useValue: { - addEffects: jasmine.createSpy('addEffects'), - }, - }, - { - provide: FEATURE_EFFECTS, - useValue: effectSourceGroups, - }, - EffectsFeatureModule, - ], - }); - - mockEffectSources = TestBed.get(mockEffectSources); - }); - - it('should add all effects when instantiated', () => { - TestBed.get(EffectsFeatureModule); - - expect(mockEffectSources.addEffects).toHaveBeenCalledWith(sourceA); - expect(mockEffectSources.addEffects).toHaveBeenCalledWith(sourceB); - expect(mockEffectSources.addEffects).toHaveBeenCalledWith(sourceC); - }); -}); diff --git a/modules/effects/src/effects_feature_module.ts b/modules/effects/src/effects_feature_module.ts index 77cc6ae2ea..3df5aa59d8 100644 --- a/modules/effects/src/effects_feature_module.ts +++ b/modules/effects/src/effects_feature_module.ts @@ -1,5 +1,5 @@ import { NgModule, Inject, Optional } from '@angular/core'; -import { StoreModule } from '@ngrx/store'; +import { StoreRootModule, StoreFeatureModule } from '@ngrx/store'; import { EffectsRootModule } from './effects_root_module'; import { FEATURE_EFFECTS } from './tokens'; @@ -8,7 +8,8 @@ export class EffectsFeatureModule { constructor( private root: EffectsRootModule, @Inject(FEATURE_EFFECTS) effectSourceGroups: any[][], - @Optional() storeModule: StoreModule + @Optional() storeRootModule: StoreRootModule, + @Optional() storeFeatureModule: StoreFeatureModule ) { effectSourceGroups.forEach(group => group.forEach(effectSourceInstance => diff --git a/modules/effects/src/effects_root_module.ts b/modules/effects/src/effects_root_module.ts index 009a194d42..dfd5dd3681 100644 --- a/modules/effects/src/effects_root_module.ts +++ b/modules/effects/src/effects_root_module.ts @@ -1,5 +1,10 @@ import { NgModule, Inject, Optional } from '@angular/core'; -import { StoreModule, Store } from '@ngrx/store'; +import { + StoreModule, + Store, + StoreRootModule, + StoreFeatureModule, +} from '@ngrx/store'; import { EffectsRunner } from './effects_runner'; import { EffectSources } from './effect_sources'; import { ROOT_EFFECTS } from './tokens'; @@ -13,7 +18,8 @@ export class EffectsRootModule { runner: EffectsRunner, store: Store, @Inject(ROOT_EFFECTS) rootEffects: any[], - @Optional() storeModule: StoreModule + @Optional() storeRootModule: StoreRootModule, + @Optional() storeFeatureModule: StoreFeatureModule ) { runner.start();