angular-cn/packages/router/test/create_router_state.spec.ts

170 lines
6.1 KiB
TypeScript
Raw Normal View History

/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
2016-07-06 19:19:52 -04:00
import {Routes} from '../src/config';
import {createRouterState} from '../src/create_router_state';
import {recognize} from '../src/recognize';
import {DefaultRouteReuseStrategy} from '../src/route_reuse_strategy';
import {ActivatedRoute, advanceActivatedRoute, createEmptyState, RouterState, RouterStateSnapshot} from '../src/router_state';
import {PRIMARY_OUTLET} from '../src/shared';
import {DefaultUrlSerializer, UrlSegmentGroup, UrlTree} from '../src/url_tree';
2016-06-21 14:49:42 -04:00
import {TreeNode} from '../src/utils/tree';
describe('create router state', () => {
let reuseStrategy: DefaultRouteReuseStrategy;
beforeEach(() => {
reuseStrategy = new DefaultRouteReuseStrategy();
});
const emptyState = () =>
createEmptyState(new (UrlTree as any)(new UrlSegmentGroup([], {}), {}, null!), RootComponent);
it('should create new state', () => {
2016-06-21 14:49:42 -04:00
const state = createRouterState(
reuseStrategy,
createState(
[
{path: 'a', component: ComponentA},
{path: 'b', component: ComponentB, outlet: 'left'},
{path: 'c', component: ComponentC, outlet: 'right'}
],
'a(left:b//right:c)'),
2016-06-21 14:49:42 -04:00
emptyState());
checkActivatedRoute(state.root, RootComponent);
const c = (state as any).children(state.root);
checkActivatedRoute(c[0], ComponentA);
checkActivatedRoute(c[1], ComponentB, 'left');
checkActivatedRoute(c[2], ComponentC, 'right');
});
it('should reuse existing nodes when it can', () => {
const config = [
2016-06-21 14:49:42 -04:00
{path: 'a', component: ComponentA}, {path: 'b', component: ComponentB, outlet: 'left'},
{path: 'c', component: ComponentC, outlet: 'left'}
];
const prevState =
createRouterState(reuseStrategy, createState(config, 'a(left:b)'), emptyState());
2016-06-02 17:44:57 -04:00
advanceState(prevState);
const state = createRouterState(reuseStrategy, createState(config, 'a(left:c)'), prevState);
expect(prevState.root).toBe(state.root);
const prevC = (prevState as any).children(prevState.root);
const currC = (state as any).children(state.root);
expect(prevC[0]).toBe(currC[0]);
expect(prevC[1]).not.toBe(currC[1]);
checkActivatedRoute(currC[1], ComponentC, 'left');
});
it('should handle componentless routes', () => {
2016-06-21 14:49:42 -04:00
const config = [{
path: 'a/:id',
children:
[{path: 'b', component: ComponentA}, {path: 'c', component: ComponentB, outlet: 'right'}]
2016-06-21 14:49:42 -04:00
}];
const prevState = createRouterState(
reuseStrategy, createState(config, 'a/1;p=11/(b//right:c)'), emptyState());
advanceState(prevState);
const state =
createRouterState(reuseStrategy, createState(config, 'a/2;p=22/(b//right:c)'), prevState);
expect(prevState.root).toBe(state.root);
const prevP = (prevState as any).firstChild(prevState.root)!;
const currP = (state as any).firstChild(state.root)!;
expect(prevP).toBe(currP);
const currC = (state as any).children(currP);
expect(currP._futureSnapshot.params).toEqual({id: '2', p: '22'});
feat(router): introduce `ParamMap` to access parameters The Router use the type `Params` for all of: - position parameters, - matrix parameters, - query parameters. `Params` is defined as follow `type Params = {[key: string]: any}` Because parameters can either have single or multiple values, the type should actually be `type Params = {[key: string]: string | string[]}`. The client code often assumes that parameters have single values, as in the following exemple: ``` class MyComponent { sessionId: Observable<string>; constructor(private route: ActivatedRoute) {} ngOnInit() { this.sessionId = this.route .queryParams .map(params => params['session_id'] || 'None'); } } ``` The problem here is that `params['session_id']` could be `string` or `string[]` but the error is not caught at build time because of the `any` type. Fixing the type as describe above would break the build because `sessionId` would becomes an `Observable<string | string[]>`. However the client code knows if it expects a single or multiple values. By using the new `ParamMap` interface the user code can decide when it needs a single value (calling `ParamMap.get(): string`) or multiple values (calling `ParamMap.getAll(): string[]`). The above exemple should be rewritten as: ``` class MyComponent { sessionId: Observable<string>; constructor(private route: ActivatedRoute) {} ngOnInit() { this.sessionId = this.route .queryParamMap .map(paramMap => paramMap.get('session_id') || 'None'); } } ``` Added APIs: - `interface ParamMap`, - `ActivatedRoute.paramMap: ParamMap`, - `ActivatedRoute.queryParamMap: ParamMap`, - `ActivatedRouteSnapshot.paramMap: ParamMap`, - `ActivatedRouteSnapshot.queryParamMap: ParamMap`, - `UrlSegment.parameterMap: ParamMap`
2017-03-17 13:09:42 -04:00
expect(currP._futureSnapshot.paramMap.get('id')).toEqual('2');
expect(currP._futureSnapshot.paramMap.get('p')).toEqual('22');
checkActivatedRoute(currC[0], ComponentA);
checkActivatedRoute(currC[1], ComponentB, 'right');
});
it('should not retrieve routes when `shouldAttach` is always false', () => {
const config = [
{path: 'a', component: ComponentA}, {path: 'b', component: ComponentB, outlet: 'left'},
{path: 'c', component: ComponentC, outlet: 'left'}
];
spyOn(reuseStrategy, 'retrieve');
const prevState =
createRouterState(reuseStrategy, createState(config, 'a(left:b)'), emptyState());
advanceState(prevState);
createRouterState(reuseStrategy, createState(config, 'a(left:c)'), prevState);
expect(reuseStrategy.retrieve).not.toHaveBeenCalled();
});
it('should consistently represent future and current state', () => {
const config = [
{path: '', pathMatch: 'full', component: ComponentA},
{path: 'product/:id', component: ComponentB}
];
spyOn(reuseStrategy, 'shouldReuseRoute').and.callThrough();
const previousState = createRouterState(reuseStrategy, createState(config, ''), emptyState());
advanceState(previousState);
(reuseStrategy.shouldReuseRoute as jasmine.Spy).calls.reset();
createRouterState(reuseStrategy, createState(config, 'product/30'), previousState);
// One call for the root and one call for each of the children
expect(reuseStrategy.shouldReuseRoute).toHaveBeenCalledTimes(2);
const reuseCalls = (reuseStrategy.shouldReuseRoute as jasmine.Spy).calls;
const future1 = reuseCalls.argsFor(0)[0];
const current1 = reuseCalls.argsFor(0)[1];
const future2 = reuseCalls.argsFor(1)[0];
const current2 = reuseCalls.argsFor(1)[1];
// Routing from '' to 'product/30'
expect(current1._routerState.url).toEqual('');
expect(future1._routerState.url).toEqual('product/30');
expect(current2._routerState.url).toEqual('');
expect(future2._routerState.url).toEqual('product/30');
});
});
2016-06-02 17:44:57 -04:00
function advanceState(state: RouterState): void {
advanceNode((state as any)._root);
2016-06-02 17:44:57 -04:00
}
function advanceNode(node: TreeNode<ActivatedRoute>): void {
advanceActivatedRoute(node.value);
node.children.forEach(advanceNode);
}
2016-07-06 19:19:52 -04:00
function createState(config: Routes, url: string): RouterStateSnapshot {
let res: RouterStateSnapshot = undefined!;
2016-06-14 17:55:59 -04:00
recognize(RootComponent, config, tree(url), url).forEach(s => res = s);
return res;
}
2016-06-21 14:49:42 -04:00
function checkActivatedRoute(
actual: ActivatedRoute, cmp: Function, outlet: string = PRIMARY_OUTLET): void {
if (actual === null) {
expect(actual).toBeDefined();
} else {
expect(actual.component as any).toBe(cmp);
expect(actual.outlet).toEqual(outlet);
}
}
function tree(url: string): UrlTree {
return new DefaultUrlSerializer().parse(url);
}
class RootComponent {}
class ComponentA {}
class ComponentB {}
class ComponentC {}