Skip to content

Commit

Permalink
feat(router-store): update stateKey definition to take a string or se…
Browse files Browse the repository at this point in the history
…lector

Closes #1300
  • Loading branch information
koumatsumoto authored and timdeschryver committed Sep 26, 2018
1 parent df8fc60 commit 4ad9a94
Show file tree
Hide file tree
Showing 5 changed files with 282 additions and 69 deletions.
147 changes: 82 additions & 65 deletions modules/router-store/spec/integration.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, Provider, Injectable, ErrorHandler } from '@angular/core';
import { Injectable, ErrorHandler } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import {
NavigationEnd,
Expand All @@ -7,8 +7,7 @@ import {
NavigationCancel,
NavigationError,
} from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { Store, StoreModule, ScannedActionsSubject } from '@ngrx/store';
import { Store, ScannedActionsSubject } from '@ngrx/store';
import { filter, first, mapTo, take } from 'rxjs/operators';

import {
Expand All @@ -22,9 +21,9 @@ import {
routerReducer,
RouterReducerState,
RouterStateSerializer,
StoreRouterConfig,
StoreRouterConnectingModule,
StateKeyOrSelector,
} from '../src';
import { createTestModule } from './utils';

describe('integration spec', () => {
it('should work', (done: any) => {
Expand Down Expand Up @@ -677,12 +676,76 @@ describe('integration spec', () => {
};

createTestModule({
reducers: { reducer },
reducers: { 'router-reducer': reducer },
config: { stateKey: 'router-reducer' },
});

const router: Router = TestBed.get(Router);
const log = logOfRouterAndActionsAndStore();
const log = logOfRouterAndActionsAndStore({ stateKey: 'router-reducer' });

router
.navigateByUrl('/')
.then(() => {
expect(log).toEqual([
{ type: 'store', state: '' }, // init event. has nothing to do with the router
{ type: 'store', state: '' }, // ROUTER_REQUEST event in the store
{ type: 'action', action: ROUTER_REQUEST },
{ type: 'router', event: 'NavigationStart', url: '/' },
{ type: 'store', state: '/' }, // ROUTER_NAVIGATION event in the store
{ type: 'action', action: ROUTER_NAVIGATION },
{ type: 'router', event: 'RoutesRecognized', url: '/' },
{ type: 'router', event: 'GuardsCheckStart', url: '/' },
{ type: 'router', event: 'GuardsCheckEnd', url: '/' },
{ type: 'router', event: 'ResolveStart', url: '/' },
{ type: 'router', event: 'ResolveEnd', url: '/' },
{ type: 'store', state: '/' }, // ROUTER_NAVIGATED event in the store
{ type: 'action', action: ROUTER_NAVIGATED },
{ type: 'router', event: 'NavigationEnd', url: '/' },
]);
})
.then(() => {
log.splice(0);
return router.navigateByUrl('next');
})
.then(() => {
expect(log).toEqual([
{ type: 'store', state: '/' },
{ type: 'action', action: ROUTER_REQUEST },
{ type: 'router', event: 'NavigationStart', url: '/next' },
{ type: 'store', state: '/next' },
{ type: 'action', action: ROUTER_NAVIGATION },
{ type: 'router', event: 'RoutesRecognized', url: '/next' },
{ type: 'router', event: 'GuardsCheckStart', url: '/next' },
{ type: 'router', event: 'GuardsCheckEnd', url: '/next' },
{ type: 'router', event: 'ResolveStart', url: '/next' },
{ type: 'router', event: 'ResolveEnd', url: '/next' },
{ type: 'store', state: '/next' },
{ type: 'action', action: ROUTER_NAVIGATED },
{ type: 'router', event: 'NavigationEnd', url: '/next' },
]);

done();
});
});

it('should work when defining state selector', (done: any) => {
const reducer = (state: string = '', action: RouterAction<any>) => {
if (action.type === ROUTER_NAVIGATION) {
return action.payload.routerState.url.toString();
} else {
return state;
}
};

createTestModule({
reducers: { routerReducer: reducer },
config: { stateKey: (state: any) => state.routerReducer },
});

const router: Router = TestBed.get(Router);
const log = logOfRouterAndActionsAndStore({
stateKey: (state: any) => state.routerReducer,
});

router
.navigateByUrl('/')
Expand Down Expand Up @@ -825,62 +888,6 @@ describe('integration spec', () => {
});
});

function createTestModule(
opts: {
reducers?: any;
canActivate?: Function;
canLoad?: Function;
providers?: Provider[];
config?: StoreRouterConfig;
} = {}
) {
@Component({
selector: 'test-app',
template: '<router-outlet></router-outlet>',
})
class AppCmp {}

@Component({
selector: 'pagea-cmp',
template: 'pagea-cmp',
})
class SimpleCmp {}

TestBed.configureTestingModule({
declarations: [AppCmp, SimpleCmp],
imports: [
StoreModule.forRoot(opts.reducers),
RouterTestingModule.withRoutes([
{ path: '', component: SimpleCmp },
{
path: 'next',
component: SimpleCmp,
canActivate: ['CanActivateNext'],
},
{
path: 'load',
loadChildren: 'test',
canLoad: ['CanLoadNext'],
},
]),
StoreRouterConnectingModule.forRoot(opts.config),
],
providers: [
{
provide: 'CanActivateNext',
useValue: opts.canActivate || (() => true),
},
{
provide: 'CanLoadNext',
useValue: opts.canLoad || (() => true),
},
opts.providers || [],
],
});

TestBed.createComponent(AppCmp);
}

function waitForNavigation(router: Router, event: any = NavigationEnd) {
return router.events
.pipe(
Expand All @@ -897,7 +904,11 @@ function waitForNavigation(router: Router, event: any = NavigationEnd) {
* Example: router event is fired -> store is updated -> store log appears before router log
* Also, actions$ always fires the next action AFTER the store is updated
*/
function logOfRouterAndActionsAndStore(): any[] {
function logOfRouterAndActionsAndStore(
options: { stateKey: StateKeyOrSelector } = {
stateKey: 'reducer',
}
): any[] {
const router: Router = TestBed.get(Router);
const store: Store<any> = TestBed.get(Store);
// Not using effects' Actions to avoid @ngrx/effects dependency
Expand All @@ -915,6 +926,12 @@ function logOfRouterAndActionsAndStore(): any[] {
actions$.subscribe(action =>
log.push({ type: 'action', action: action.type })
);
store.subscribe(store => log.push({ type: 'store', state: store.reducer }));
store.subscribe(store => {
if (typeof options.stateKey === 'function') {
log.push({ type: 'store', state: options.stateKey(store) });
} else {
log.push({ type: 'store', state: store[options.stateKey] });
}
});
return log;
}
131 changes: 131 additions & 0 deletions modules/router-store/spec/router_store_module.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { TestBed } from '@angular/core/testing';
import { Router } from '@angular/router';
import {
routerReducer,
RouterReducerState,
StoreRouterConnectingModule,
} from '@ngrx/router-store';
import { select, Store } from '@ngrx/store';
import { withLatestFrom } from 'rxjs/operators';

import { createTestModule } from './utils';

describe('Router Store Module', () => {
describe('with defining state key', () => {
const customStateKey = 'router-reducer';
let storeRouterConnectingModule: StoreRouterConnectingModule;
let store: Store<State>;
let router: Router;

interface State {
[customStateKey]: RouterReducerState;
}

beforeEach(() => {
createTestModule({
reducers: {
[customStateKey]: routerReducer,
},
config: {
stateKey: customStateKey,
},
});

store = TestBed.get(Store);
router = TestBed.get(Router);
storeRouterConnectingModule = TestBed.get(StoreRouterConnectingModule);
});

it('should have custom state key as own property', () => {
expect((<any>storeRouterConnectingModule).stateKey).toBe(customStateKey);
});

it('should call navigateIfNeeded with args selected by custom state key', (done: any) => {
let logs: any[] = [];
store
.pipe(
select(customStateKey),
withLatestFrom(store)
)
.subscribe(([routerStoreState, storeState]) => {
logs.push([routerStoreState, storeState]);
});

spyOn(storeRouterConnectingModule, 'navigateIfNeeded').and.callThrough();
logs = [];

// this dispatches `@ngrx/router-store/navigation` action
// and store emits its payload.
router.navigateByUrl('/').then(() => {
const actual = (<any>(
storeRouterConnectingModule
)).navigateIfNeeded.calls.allArgs();

expect(actual.length).toBe(1);
expect(actual[0]).toEqual(logs[0]);
done();
});
});
});

describe('with defining state selector', () => {
const customStateKey = 'routerReducer';
const customStateSelector = (state: State) => state.routerReducer;

let storeRouterConnectingModule: StoreRouterConnectingModule;
let store: Store<State>;
let router: Router;

interface State {
[customStateKey]: RouterReducerState;
}

beforeEach(() => {
createTestModule({
reducers: {
[customStateKey]: routerReducer,
},
config: {
stateKey: customStateSelector,
},
});

store = TestBed.get(Store);
router = TestBed.get(Router);
storeRouterConnectingModule = TestBed.get(StoreRouterConnectingModule);
});

it('should have same state selector as own property', () => {
expect((<any>storeRouterConnectingModule).stateKey).toBe(
customStateSelector
);
});

it('should call navigateIfNeeded with args selected by custom state selector', (done: any) => {
let logs: any[] = [];
store
.pipe(
select(customStateSelector),
withLatestFrom(store)
)
.subscribe(([routerStoreState, storeState]) => {
logs.push([routerStoreState, storeState]);
});

spyOn(storeRouterConnectingModule, 'navigateIfNeeded').and.callThrough();
logs = [];

// this dispatches `@ngrx/router-store/navigation` action
// and store emits its payload.
router.navigateByUrl('/').then(() => {
const actual = (<any>(
storeRouterConnectingModule
)).navigateIfNeeded.calls.allArgs();

expect(actual.length).toBe(1);
expect(actual[0]).toEqual(logs[0]);
done();
});
});
});
});
62 changes: 62 additions & 0 deletions modules/router-store/spec/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { Component, Provider } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { StoreModule } from '@ngrx/store';

import { StoreRouterConfig, StoreRouterConnectingModule } from '../src';

export function createTestModule(
opts: {
reducers?: any;
canActivate?: Function;
canLoad?: Function;
providers?: Provider[];
config?: StoreRouterConfig;
} = {}
) {
@Component({
selector: 'test-app',
template: '<router-outlet></router-outlet>',
})
class AppCmp {}

@Component({
selector: 'page-cmp',
template: 'page-cmp',
})
class SimpleCmp {}

TestBed.configureTestingModule({
declarations: [AppCmp, SimpleCmp],
imports: [
StoreModule.forRoot(opts.reducers),
RouterTestingModule.withRoutes([
{ path: '', component: SimpleCmp },
{
path: 'next',
component: SimpleCmp,
canActivate: ['CanActivateNext'],
},
{
path: 'load',
loadChildren: 'test',
canLoad: ['CanLoadNext'],
},
]),
StoreRouterConnectingModule.forRoot(opts.config),
],
providers: [
{
provide: 'CanActivateNext',
useValue: opts.canActivate || (() => true),
},
{
provide: 'CanLoadNext',
useValue: opts.canLoad || (() => true),
},
opts.providers || [],
],
});

TestBed.createComponent(AppCmp);
}
1 change: 1 addition & 0 deletions modules/router-store/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export {
} from './actions';
export { routerReducer, RouterReducerState } from './reducer';
export {
StateKeyOrSelector,
StoreRouterConnectingModule,
StoreRouterConfig,
NavigationActionTiming,
Expand Down
Loading

0 comments on commit 4ad9a94

Please sign in to comment.