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

314 lines
12 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
*/
import {BehaviorSubject} from 'rxjs';
2016-06-21 14:49:42 -04:00
import {createUrlTree} from '../src/create_url_tree';
2016-06-02 17:44:57 -04:00
import {ActivatedRoute, ActivatedRouteSnapshot, advanceActivatedRoute} from '../src/router_state';
import {Params, PRIMARY_OUTLET} from '../src/shared';
import {DefaultUrlSerializer, UrlSegment, UrlSegmentGroup, UrlTree} from '../src/url_tree';
2016-05-26 19:51:19 -04:00
describe('createUrlTree', () => {
2016-05-26 19:51:19 -04:00
const serializer = new DefaultUrlSerializer();
describe('query parameters', () => {
it('should support parameter with multiple values', () => {
const p1 = serializer.parse('/');
const t1 = createRoot(p1, ['/'], {m: ['v1', 'v2']});
expect(serializer.serialize(t1)).toEqual('/?m=v1&m=v2');
const p2 = serializer.parse('/a/c');
const t2 = create(p2.root.children[PRIMARY_OUTLET], 1, p2, ['c2'], {m: ['v1', 'v2']});
expect(serializer.serialize(t2)).toEqual('/a/c/c2?m=v1&m=v2');
});
it('should set query params', () => {
const p = serializer.parse('/');
const t = createRoot(p, [], {a: 'hey'});
expect(t.queryParams).toEqual({a: 'hey'});
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(t.queryParamMap.get('a')).toEqual('hey');
});
it('should stringify query params', () => {
const p = serializer.parse('/');
const t = createRoot(p, [], <any>{a: 1});
expect(t.queryParams).toEqual({a: '1'});
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(t.queryParamMap.get('a')).toEqual('1');
});
});
2016-06-21 14:49:42 -04:00
it('should navigate to the root', () => {
const p = serializer.parse('/');
const t = createRoot(p, ['/']);
expect(serializer.serialize(t)).toEqual('/');
2016-05-26 19:51:19 -04:00
});
it('should error when navigating to the root segment with params', () => {
const p = serializer.parse('/');
expect(() => createRoot(p, ['/', {p: 11}]))
.toThrowError(/Root segment cannot have matrix parameters/);
});
2016-06-21 14:49:42 -04:00
it('should support nested segments', () => {
const p = serializer.parse('/a/b');
const t = createRoot(p, ['/one', 11, 'two', 22]);
expect(serializer.serialize(t)).toEqual('/one/11/two/22');
2016-05-26 19:51:19 -04:00
});
2016-06-21 14:49:42 -04:00
it('should stringify positional parameters', () => {
const p = serializer.parse('/a/b');
const t = createRoot(p, ['/one', 11]);
const params = t.root.children[PRIMARY_OUTLET].segments;
2016-06-21 14:49:42 -04:00
expect(params[0].path).toEqual('one');
expect(params[1].path).toEqual('11');
});
it('should work if command = null', () => {
const p = serializer.parse('/a/b');
const t = createRoot(p, [null]);
const params = t.root.children[PRIMARY_OUTLET].segments;
expect(params[0].path).toEqual('null');
});
it('should work if command is undefined', () => {
const p = serializer.parse('/a/b');
const t = createRoot(p, [undefined]);
const params = t.root.children[PRIMARY_OUTLET].segments;
expect(params[0].path).toEqual('undefined');
});
it('should support first segments containing slashes', () => {
const p = serializer.parse('/');
const t = createRoot(p, [{segmentPath: '/one'}, 'two/three']);
expect(serializer.serialize(t)).toEqual('/%2Fone/two%2Fthree');
});
2016-06-21 14:49:42 -04:00
it('should preserve secondary segments', () => {
const p = serializer.parse('/a/11/b(right:c)');
const t = createRoot(p, ['/a', 11, 'd']);
expect(serializer.serialize(t)).toEqual('/a/11/d(right:c)');
2016-05-26 19:51:19 -04:00
});
it('should support updating secondary segments (absolute)', () => {
const p = serializer.parse('/a(right:b)');
const t = createRoot(p, ['/', {outlets: {right: ['c']}}]);
expect(serializer.serialize(t)).toEqual('/a(right:c)');
});
2016-06-21 14:49:42 -04:00
it('should support updating secondary segments', () => {
const p = serializer.parse('/a(right:b)');
const t = createRoot(p, [{outlets: {right: ['c', 11, 'd']}}]);
2016-06-21 14:49:42 -04:00
expect(serializer.serialize(t)).toEqual('/a(right:c/11/d)');
});
2016-06-21 14:49:42 -04:00
it('should support updating secondary segments (nested case)', () => {
const p = serializer.parse('/a/(b//right:c)');
const t = createRoot(p, ['a', {outlets: {right: ['d', 11, 'e']}}]);
2016-06-21 14:49:42 -04:00
expect(serializer.serialize(t)).toEqual('/a/(b//right:d/11/e)');
});
2016-06-21 14:49:42 -04:00
it('should throw when outlets is not the last command', () => {
const p = serializer.parse('/a');
expect(() => createRoot(p, ['a', {outlets: {right: ['c']}}, 'c']))
.toThrowError('{outlets:{}} has to be the last command');
});
it('should support updating using a string', () => {
const p = serializer.parse('/a(right:b)');
const t = createRoot(p, [{outlets: {right: 'c/11/d'}}]);
expect(serializer.serialize(t)).toEqual('/a(right:c/11/d)');
});
it('should support updating primary and secondary segments at once', () => {
const p = serializer.parse('/a(right:b)');
const t = createRoot(p, [{outlets: {primary: 'y/z', right: 'c/11/d'}}]);
expect(serializer.serialize(t)).toEqual('/y/z(right:c/11/d)');
});
it('should support removing primary segment', () => {
const p = serializer.parse('/a/(b//right:c)');
const t = createRoot(p, ['a', {outlets: {primary: null, right: 'd'}}]);
expect(serializer.serialize(t)).toEqual('/a/(right:d)');
});
it('should support removing secondary segments', () => {
const p = serializer.parse('/a(right:b)');
const t = createRoot(p, [{outlets: {right: null}}]);
expect(serializer.serialize(t)).toEqual('/a');
});
it('should support removing parenthesis for primary segment on second path element', () => {
const p = serializer.parse('/a/(b//right:c)');
const t = createRoot(p, ['a', {outlets: {right: null}}]);
expect(serializer.serialize(t)).toEqual('/a/b');
});
2016-05-26 19:51:19 -04:00
it('should update matrix parameters', () => {
2016-06-21 14:49:42 -04:00
const p = serializer.parse('/a;pp=11');
const t = createRoot(p, ['/a', {pp: 22, dd: 33}]);
expect(serializer.serialize(t)).toEqual('/a;pp=22;dd=33');
2016-05-26 19:51:19 -04:00
});
it('should create matrix parameters', () => {
2016-06-21 14:49:42 -04:00
const p = serializer.parse('/a');
const t = createRoot(p, ['/a', {pp: 22, dd: 33}]);
expect(serializer.serialize(t)).toEqual('/a;pp=22;dd=33');
2016-05-26 19:51:19 -04:00
});
it('should create matrix parameters together with other segments', () => {
2016-06-21 14:49:42 -04:00
const p = serializer.parse('/a');
const t = createRoot(p, ['/a', 'b', {aa: 22, bb: 33}]);
2016-06-21 14:49:42 -04:00
expect(serializer.serialize(t)).toEqual('/a/b;aa=22;bb=33');
2016-05-26 19:51:19 -04:00
});
2016-06-21 14:49:42 -04:00
describe('relative navigation', () => {
it('should work', () => {
const p = serializer.parse('/a/(c//left:cp)(left:ap)');
const t = create(p.root.children[PRIMARY_OUTLET], 0, p, ['c2']);
expect(serializer.serialize(t)).toEqual('/a/(c2//left:cp)(left:ap)');
2016-05-26 19:51:19 -04:00
});
2016-06-21 14:49:42 -04:00
it('should work when the first command starts with a ./', () => {
const p = serializer.parse('/a/(c//left:cp)(left:ap)');
const t = create(p.root.children[PRIMARY_OUTLET], 0, p, ['./c2']);
expect(serializer.serialize(t)).toEqual('/a/(c2//left:cp)(left:ap)');
2016-05-26 19:51:19 -04:00
});
2016-06-21 14:49:42 -04:00
it('should work when the first command is ./)', () => {
const p = serializer.parse('/a/(c//left:cp)(left:ap)');
const t = create(p.root.children[PRIMARY_OUTLET], 0, p, ['./', 'c2']);
expect(serializer.serialize(t)).toEqual('/a/(c2//left:cp)(left:ap)');
2016-05-26 19:51:19 -04:00
});
it('should support parameters-only navigation', () => {
const p = serializer.parse('/a');
const t = create(p.root.children[PRIMARY_OUTLET], 0, p, [{k: 99}]);
expect(serializer.serialize(t)).toEqual('/a;k=99');
});
it('should support parameters-only navigation (nested case)', () => {
2016-06-21 14:49:42 -04:00
const p = serializer.parse('/a/(c//left:cp)(left:ap)');
2016-06-14 17:55:59 -04:00
const t = create(p.root.children[PRIMARY_OUTLET], 0, p, [{'x': 99}]);
expect(serializer.serialize(t)).toEqual('/a;x=99(left:ap)');
});
it('should support parameters-only navigation (with a double dot)', () => {
const p = serializer.parse('/a/(c//left:cp)(left:ap)');
const t =
create(p.root.children[PRIMARY_OUTLET].children[PRIMARY_OUTLET], 0, p, ['../', {x: 5}]);
expect(serializer.serialize(t)).toEqual('/a;x=5(left:ap)');
2016-05-26 19:51:19 -04:00
});
2016-06-21 14:49:42 -04:00
it('should work when index > 0', () => {
const p = serializer.parse('/a/c');
const t = create(p.root.children[PRIMARY_OUTLET], 1, p, ['c2']);
expect(serializer.serialize(t)).toEqual('/a/c/c2');
2016-05-26 19:51:19 -04:00
});
2016-06-21 14:49:42 -04:00
it('should support going to a parent (within a segment)', () => {
const p = serializer.parse('/a/c');
const t = create(p.root.children[PRIMARY_OUTLET], 1, p, ['../c2']);
expect(serializer.serialize(t)).toEqual('/a/c2');
2016-05-26 19:51:19 -04:00
});
it('should support going to a parent (across segments)', () => {
2016-06-21 14:49:42 -04:00
const p = serializer.parse('/q/(a/(c//left:cp)//left:qp)(left:ap)');
2016-06-14 17:55:59 -04:00
2016-06-21 14:49:42 -04:00
const t =
create(p.root.children[PRIMARY_OUTLET].children[PRIMARY_OUTLET], 0, p, ['../../q2']);
expect(serializer.serialize(t)).toEqual('/q2(left:ap)');
2016-06-14 17:55:59 -04:00
});
it('should navigate to the root', () => {
2016-06-21 14:49:42 -04:00
const p = serializer.parse('/a/c');
2016-06-14 17:55:59 -04:00
const t = create(p.root.children[PRIMARY_OUTLET], 0, p, ['../']);
expect(serializer.serialize(t)).toEqual('/');
});
it('should work with ../ when absolute url', () => {
const p = serializer.parse('/a/c');
const t = create(p.root.children[PRIMARY_OUTLET], 1, p, ['../', 'c2']);
expect(serializer.serialize(t)).toEqual('/a/c2');
});
it('should work with position = -1', () => {
const p = serializer.parse('/');
const t = create(p.root, -1, p, ['11']);
expect(serializer.serialize(t)).toEqual('/11');
2016-06-14 17:55:59 -04:00
});
2016-06-21 14:49:42 -04:00
it('should throw when too many ..', () => {
const p = serializer.parse('/a/(c//left:cp)(left:ap)');
expect(() => create(p.root.children[PRIMARY_OUTLET], 0, p, ['../../']))
.toThrowError('Invalid number of \'../\'');
2016-05-26 19:51:19 -04:00
});
it('should support updating secondary segments', () => {
const p = serializer.parse('/a/b');
const t = create(p.root.children[PRIMARY_OUTLET], 1, p, [{outlets: {right: ['c']}}]);
expect(serializer.serialize(t)).toEqual('/a/b/(right:c)');
});
2016-05-26 19:51:19 -04:00
});
2016-06-21 14:49:42 -04:00
it('should set fragment', () => {
const p = serializer.parse('/');
const t = createRoot(p, [], {}, 'fragment');
expect(t.fragment).toEqual('fragment');
2016-05-26 19:51:19 -04:00
});
it('should support pathless route', () => {
const p = serializer.parse('/a');
const t = create(p.root.children[PRIMARY_OUTLET], -1, p, ['b']);
expect(serializer.serialize(t)).toEqual('/b');
});
it('should support pathless route with ../ at root', () => {
const p = serializer.parse('/a');
const t = create(p.root.children[PRIMARY_OUTLET], -1, p, ['../b']);
expect(serializer.serialize(t)).toEqual('/b');
});
it('should support pathless child of pathless root', () => {
// i.e. routes = {path: '', loadChildren: () => import('child')...}
// forChild: {path: '', component: Comp}
const p = serializer.parse('');
const empty = new UrlSegmentGroup([], {});
p.root.children[PRIMARY_OUTLET] = empty;
empty.parent = p.root;
const t = create(empty, -1, p, ['lazy']);
expect(serializer.serialize(t)).toEqual('/lazy');
});
2016-05-26 19:51:19 -04:00
});
2016-06-14 17:55:59 -04:00
function createRoot(tree: UrlTree, commands: any[], queryParams?: Params, fragment?: string) {
const s = new (ActivatedRouteSnapshot as any)(
[], <any>{}, <any>{}, '', <any>{}, PRIMARY_OUTLET, 'someComponent', null, tree.root, -1,
<any>null);
const a = new (ActivatedRoute as any)(
new BehaviorSubject(null!), new BehaviorSubject(null!), new BehaviorSubject(null!),
new BehaviorSubject(null!), new BehaviorSubject(null!), PRIMARY_OUTLET, 'someComponent', s);
2016-06-14 17:55:59 -04:00
advanceActivatedRoute(a);
return createUrlTree(a, tree, commands, queryParams!, fragment!);
2016-06-14 17:55:59 -04:00
}
2016-06-21 14:49:42 -04:00
function create(
segment: UrlSegmentGroup, startIndex: number, tree: UrlTree, commands: any[],
queryParams?: Params, fragment?: string) {
2016-06-14 17:55:59 -04:00
if (!segment) {
expect(segment).toBeDefined();
2016-05-26 19:51:19 -04:00
}
const s = new (ActivatedRouteSnapshot as any)(
segment.segments, <any>{}, <any>{}, '', <any>{}, PRIMARY_OUTLET, 'someComponent', null,
<any>segment, startIndex, <any>null);
const a = new (ActivatedRoute as any)(
new BehaviorSubject(null!), new BehaviorSubject(null!), new BehaviorSubject(null!),
new BehaviorSubject(null!), new BehaviorSubject(null!), PRIMARY_OUTLET, 'someComponent', s);
2016-06-02 17:44:57 -04:00
advanceActivatedRoute(a);
return createUrlTree(a, tree, commands, queryParams!, fragment!);
}