Skip to content

Commit

Permalink
feat(store): Add initialState fn to features
Browse files Browse the repository at this point in the history
- (BREAKING) OpaqueToken -> InjectionToken
- Stronger types for core actions (INIT, UPDATE)
- Add more tests
  • Loading branch information
bfricka committed Jul 16, 2017
1 parent 5e6c8af commit bb489e8
Show file tree
Hide file tree
Showing 9 changed files with 215 additions and 80 deletions.
2 changes: 1 addition & 1 deletion modules/store/spec/integration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ describe('ngRx Integration spec', () => {
return todos.filter(predicate);
};

let currentlyVisibleTodos: any;
let currentlyVisibleTodos: Todo[] = [];

Observable.combineLatest(
store.select('visibilityFilter'),
Expand Down
180 changes: 137 additions & 43 deletions modules/store/spec/modules.spec.ts
Original file line number Diff line number Diff line change
@@ -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 };
Expand All @@ -14,57 +21,144 @@ describe('Nested Store Modules', () => {
const reducersToken = new InjectionToken<ActionReducerMap<RootState>>(
'Root Reducers'
);
const rootFruitReducer: ActionReducer<string> = () => 'apple';
const featureAReducer: ActionReducer<FeatureAState> = () => 5;
const featureBListReducer: ActionReducer<number[]> = () => [1, 2, 3];
const featureBIndexReducer: ActionReducer<number> = () => 2;

// Trigger here is basically an action type used to trigger state update
const createDummyReducer = <T>(def: T, trigger: string): ActionReducer<T> => (
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<FeatureBState> = {
list: featureBListReducer,
index: featureBIndexReducer,
};

@NgModule({
imports: [StoreModule.forFeature('a', featureAReducer)],
})
class FeatureAModule {}

@NgModule({
imports: [StoreModule.forFeature('b', featureBReducerMap)],
})
class FeatureBModule {}

@NgModule({
imports: [
StoreModule.forRoot<RootState>(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<RootState>(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<RootState>(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,
},
});
});
});
});
Expand Down
4 changes: 2 additions & 2 deletions modules/store/spec/state.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -22,7 +22,7 @@ describe('ngRx State', () => {
injector.get(Store);

expect(reducer).toHaveBeenCalledWith(initialState, {
type: '@ngrx/store/init',
type: INIT,
});
});
});
73 changes: 52 additions & 21 deletions modules/store/spec/store.spec.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -68,7 +62,7 @@ describe('ngRx Store', () => {
});
});

describe('basic store actions', function() {
describe('basic store actions', () => {
beforeEach(() => setup());

it('should provide an Observable Store', () => {
Expand All @@ -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));
Expand All @@ -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));
Expand All @@ -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));
Expand All @@ -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));
Expand All @@ -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));
Expand All @@ -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');

Expand All @@ -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();

Expand All @@ -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();

Expand All @@ -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();
});
});
});
});
Loading

0 comments on commit bb489e8

Please sign in to comment.