refactor: move angular source to /packages rather than modules/@angular

This commit is contained in:
Jason Aden
2017-03-02 10:48:42 -08:00
parent 5ad5301a3e
commit 3e51a19983
1051 changed files with 18 additions and 18 deletions

View File

@ -0,0 +1,647 @@
/**
* @license
* Copyright Google Inc. 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 {Observable} from 'rxjs/Observable';
import {of } from 'rxjs/observable/of';
import {applyRedirects} from '../src/apply_redirects';
import {Routes} from '../src/config';
import {LoadedRouterConfig} from '../src/router_config_loader';
import {DefaultUrlSerializer, UrlSegmentGroup, UrlTree, equalSegments} from '../src/url_tree';
describe('applyRedirects', () => {
const serializer = new DefaultUrlSerializer();
it('should return the same url tree when no redirects', () => {
checkRedirect(
[{path: 'a', component: ComponentA, children: [{path: 'b', component: ComponentB}]}],
'/a/b', (t: UrlTree) => { compareTrees(t, tree('/a/b')); });
});
it('should add new segments when needed', () => {
checkRedirect(
[{path: 'a/b', redirectTo: 'a/b/c'}, {path: '**', component: ComponentC}], '/a/b',
(t: UrlTree) => { compareTrees(t, tree('/a/b/c')); });
});
it('should handle positional parameters', () => {
checkRedirect(
[
{path: 'a/:aid/b/:bid', redirectTo: 'newa/:aid/newb/:bid'},
{path: '**', component: ComponentC}
],
'/a/1/b/2', (t: UrlTree) => { compareTrees(t, tree('/newa/1/newb/2')); });
});
it('should throw when cannot handle a positional parameter', () => {
applyRedirects(null, null, serializer, tree('/a/1'), [
{path: 'a/:id', redirectTo: 'a/:other'}
]).subscribe(() => {}, (e) => {
expect(e.message).toEqual('Cannot redirect to \'a/:other\'. Cannot find \':other\'.');
});
});
it('should pass matrix parameters', () => {
checkRedirect(
[{path: 'a/:id', redirectTo: 'd/a/:id/e'}, {path: '**', component: ComponentC}],
'/a;p1=1/1;p2=2', (t: UrlTree) => { compareTrees(t, tree('/d/a;p1=1/1;p2=2/e')); });
});
it('should handle preserve secondary routes', () => {
checkRedirect(
[
{path: 'a/:id', redirectTo: 'd/a/:id/e'},
{path: 'c/d', component: ComponentA, outlet: 'aux'}, {path: '**', component: ComponentC}
],
'/a/1(aux:c/d)', (t: UrlTree) => { compareTrees(t, tree('/d/a/1/e(aux:c/d)')); });
});
it('should redirect secondary routes', () => {
checkRedirect(
[
{path: 'a/:id', component: ComponentA},
{path: 'c/d', redirectTo: 'f/c/d/e', outlet: 'aux'},
{path: '**', component: ComponentC, outlet: 'aux'}
],
'/a/1(aux:c/d)', (t: UrlTree) => { compareTrees(t, tree('/a/1(aux:f/c/d/e)')); });
});
it('should use the configuration of the route redirected to', () => {
checkRedirect(
[
{
path: 'a',
component: ComponentA,
children: [
{path: 'b', component: ComponentB},
]
},
{path: 'c', redirectTo: 'a'}
],
'c/b', (t: UrlTree) => { compareTrees(t, tree('a/b')); });
});
it('should support redirects with both main and aux', () => {
checkRedirect(
[{
path: 'a',
children: [
{path: 'bb', component: ComponentB}, {path: 'b', redirectTo: 'bb'},
{path: 'cc', component: ComponentC, outlet: 'aux'},
{path: 'b', redirectTo: 'cc', outlet: 'aux'}
]
}],
'a/(b//aux:b)', (t: UrlTree) => { compareTrees(t, tree('a/(bb//aux:cc)')); });
});
it('should support redirects with both main and aux (with a nested redirect)', () => {
checkRedirect(
[{
path: 'a',
children: [
{path: 'bb', component: ComponentB}, {path: 'b', redirectTo: 'bb'},
{
path: 'cc',
component: ComponentC,
outlet: 'aux',
children: [{path: 'dd', component: ComponentC}, {path: 'd', redirectTo: 'dd'}]
},
{path: 'b', redirectTo: 'cc/d', outlet: 'aux'}
]
}],
'a/(b//aux:b)', (t: UrlTree) => { compareTrees(t, tree('a/(bb//aux:cc/dd)')); });
});
it('should redirect wild cards', () => {
checkRedirect(
[
{path: '404', component: ComponentA},
{path: '**', redirectTo: '/404'},
],
'/a/1(aux:c/d)', (t: UrlTree) => { compareTrees(t, tree('/404')); });
});
it('should support absolute redirects', () => {
checkRedirect(
[
{
path: 'a',
component: ComponentA,
children: [{path: 'b/:id', redirectTo: '/absolute/:id?a=1&b=:b#f1'}]
},
{path: '**', component: ComponentC}
],
'/a/b/1?b=2', (t: UrlTree) => { compareTrees(t, tree('/absolute/1?a=1&b=2#f1')); });
});
describe('lazy loading', () => {
it('should load config on demand', () => {
const loadedConfig = new LoadedRouterConfig(
[{path: 'b', component: ComponentB}], <any>'stubInjector', <any>'stubFactoryResolver',
<any>'injectorFactory');
const loader = {
load: (injector: any, p: any) => {
if (injector !== 'providedInjector') throw 'Invalid Injector';
return of (loadedConfig);
}
};
const config = [{path: 'a', component: ComponentA, loadChildren: 'children'}];
applyRedirects(<any>'providedInjector', <any>loader, serializer, tree('a/b'), config)
.forEach(r => {
compareTrees(r, tree('/a/b'));
expect((<any>config[0])._loadedConfig).toBe(loadedConfig);
});
});
it('should handle the case when the loader errors', () => {
const loader = {
load: (p: any) => new Observable<any>((obs: any) => obs.error(new Error('Loading Error')))
};
const config = [{path: 'a', component: ComponentA, loadChildren: 'children'}];
applyRedirects(null, <any>loader, serializer, tree('a/b'), config)
.subscribe(() => {}, (e) => { expect(e.message).toEqual('Loading Error'); });
});
it('should load when all canLoad guards return true', () => {
const loadedConfig = new LoadedRouterConfig(
[{path: 'b', component: ComponentB}], <any>'stubInjector', <any>'stubFactoryResolver',
<any>'injectorFactory');
const loader = {load: (injector: any, p: any) => of (loadedConfig)};
const guard = () => true;
const injector = {get: () => guard};
const config = [{
path: 'a',
component: ComponentA,
canLoad: ['guard1', 'guard2'],
loadChildren: 'children'
}];
applyRedirects(<any>injector, <any>loader, serializer, tree('a/b'), config).forEach(r => {
compareTrees(r, tree('/a/b'));
});
});
it('should not load when any canLoad guards return false', () => {
const loadedConfig = new LoadedRouterConfig(
[{path: 'b', component: ComponentB}], <any>'stubInjector', <any>'stubFactoryResolver',
<any>'injectorFactory');
const loader = {load: (injector: any, p: any) => of (loadedConfig)};
const trueGuard = () => true;
const falseGuard = () => false;
const injector = {get: (guardName: any) => guardName === 'guard1' ? trueGuard : falseGuard};
const config = [{
path: 'a',
component: ComponentA,
canLoad: ['guard1', 'guard2'],
loadChildren: 'children'
}];
applyRedirects(<any>injector, <any>loader, serializer, tree('a/b'), config)
.subscribe(
() => { throw 'Should not reach'; },
(e) => {
expect(e.message).toEqual(
`NavigationCancelingError: Cannot load children because the guard of the route "path: 'a'" returned false`);
});
});
it('should not load when any canLoad guards is rejected (promises)', () => {
const loadedConfig = new LoadedRouterConfig(
[{path: 'b', component: ComponentB}], <any>'stubInjector', <any>'stubFactoryResolver',
<any>'injectorFactory');
const loader = {load: (injector: any, p: any) => of (loadedConfig)};
const trueGuard = () => Promise.resolve(true);
const falseGuard = () => Promise.reject('someError');
const injector = {get: (guardName: any) => guardName === 'guard1' ? trueGuard : falseGuard};
const config = [{
path: 'a',
component: ComponentA,
canLoad: ['guard1', 'guard2'],
loadChildren: 'children'
}];
applyRedirects(<any>injector, <any>loader, serializer, tree('a/b'), config)
.subscribe(
() => { throw 'Should not reach'; }, (e) => { expect(e).toEqual('someError'); });
});
it('should work with objects implementing the CanLoad interface', () => {
const loadedConfig = new LoadedRouterConfig(
[{path: 'b', component: ComponentB}], <any>'stubInjector', <any>'stubFactoryResolver',
<any>'injectorFactory');
const loader = {load: (injector: any, p: any) => of (loadedConfig)};
const guard = {canLoad: () => Promise.resolve(true)};
const injector = {get: () => guard};
const config =
[{path: 'a', component: ComponentA, canLoad: ['guard'], loadChildren: 'children'}];
applyRedirects(<any>injector, <any>loader, serializer, tree('a/b'), config)
.subscribe(
(r) => { compareTrees(r, tree('/a/b')); }, (e) => { throw 'Should not reach'; });
});
it('should work with absolute redirects', () => {
const loadedConfig = new LoadedRouterConfig(
[{path: '', component: ComponentB}], <any>'stubInjector', <any>'stubFactoryResolver',
<any>'injectorFactory');
const loader = {load: (injector: any, p: any) => of (loadedConfig)};
const config =
[{path: '', pathMatch: 'full', redirectTo: '/a'}, {path: 'a', loadChildren: 'children'}];
applyRedirects(<any>'providedInjector', <any>loader, serializer, tree(''), config)
.forEach(r => {
compareTrees(r, tree('a'));
expect((<any>config[1])._loadedConfig).toBe(loadedConfig);
});
});
it('should load the configuration only once', () => {
const loadedConfig = new LoadedRouterConfig(
[{path: '', component: ComponentB}], <any>'stubInjector', <any>'stubFactoryResolver',
<any>'injectorFactory');
let called = false;
const loader = {
load: (injector: any, p: any) => {
if (called) throw new Error('Should not be called twice');
called = true;
return of (loadedConfig);
}
};
const config = [{path: 'a', loadChildren: 'children'}];
applyRedirects(<any>'providedInjector', <any>loader, serializer, tree('a?k1'), config)
.subscribe(r => {});
applyRedirects(<any>'providedInjector', <any>loader, serializer, tree('a?k2'), config)
.subscribe(
r => {
compareTrees(r, tree('a?k2'));
expect((<any>config[0])._loadedConfig).toBe(loadedConfig);
},
(e) => { throw 'Should not reach'; });
});
it('should load the configuration of a wildcard route', () => {
const loadedConfig = new LoadedRouterConfig(
[{path: '', component: ComponentB}], <any>'stubInjector', <any>'stubFactoryResolver',
<any>'injectorFactory');
const loader = {load: (injector: any, p: any) => of (loadedConfig)};
const config = [{path: '**', loadChildren: 'children'}];
applyRedirects(<any>'providedInjector', <any>loader, serializer, tree('xyz'), config)
.forEach(r => { expect((<any>config[0])._loadedConfig).toBe(loadedConfig); });
});
it('should load the configuration after a local redirect from a wildcard route', () => {
const loadedConfig = new LoadedRouterConfig(
[{path: '', component: ComponentB}], <any>'stubInjector', <any>'stubFactoryResolver',
<any>'injectorFactory');
const loader = {load: (injector: any, p: any) => of (loadedConfig)};
const config =
[{path: 'not-found', loadChildren: 'children'}, {path: '**', redirectTo: 'not-found'}];
applyRedirects(<any>'providedInjector', <any>loader, serializer, tree('xyz'), config)
.forEach(r => { expect((<any>config[0])._loadedConfig).toBe(loadedConfig); });
});
it('should load the configuration after an absolute redirect from a wildcard route', () => {
const loadedConfig = new LoadedRouterConfig(
[{path: '', component: ComponentB}], <any>'stubInjector', <any>'stubFactoryResolver',
<any>'injectorFactory');
const loader = {load: (injector: any, p: any) => of (loadedConfig)};
const config =
[{path: 'not-found', loadChildren: 'children'}, {path: '**', redirectTo: '/not-found'}];
applyRedirects(<any>'providedInjector', <any>loader, serializer, tree('xyz'), config)
.forEach(r => { expect((<any>config[0])._loadedConfig).toBe(loadedConfig); });
});
});
describe('empty paths', () => {
it('redirect from an empty path should work (local redirect)', () => {
checkRedirect(
[
{
path: 'a',
component: ComponentA,
children: [
{path: 'b', component: ComponentB},
]
},
{path: '', redirectTo: 'a'}
],
'b', (t: UrlTree) => { compareTrees(t, tree('a/b')); });
});
it('redirect from an empty path should work (absolute redirect)', () => {
checkRedirect(
[
{
path: 'a',
component: ComponentA,
children: [
{path: 'b', component: ComponentB},
]
},
{path: '', redirectTo: '/a/b'}
],
'', (t: UrlTree) => { compareTrees(t, tree('a/b')); });
});
it('should redirect empty path route only when terminal', () => {
const config: Routes = [
{
path: 'a',
component: ComponentA,
children: [
{path: 'b', component: ComponentB},
]
},
{path: '', redirectTo: 'a', pathMatch: 'full'}
];
applyRedirects(null, null, serializer, tree('b'), config)
.subscribe(
(_) => { throw 'Should not be reached'; },
e => { expect(e.message).toEqual('Cannot match any routes. URL Segment: \'b\''); });
});
it('redirect from an empty path should work (nested case)', () => {
checkRedirect(
[
{
path: 'a',
component: ComponentA,
children: [{path: 'b', component: ComponentB}, {path: '', redirectTo: 'b'}]
},
{path: '', redirectTo: 'a'}
],
'', (t: UrlTree) => { compareTrees(t, tree('a/b')); });
});
it('redirect to an empty path should work', () => {
checkRedirect(
[
{path: '', component: ComponentA, children: [{path: 'b', component: ComponentB}]},
{path: 'a', redirectTo: ''}
],
'a/b', (t: UrlTree) => { compareTrees(t, tree('b')); });
});
describe('aux split is in the middle', () => {
it('should create a new url segment (non-terminal)', () => {
checkRedirect(
[{
path: 'a',
children: [
{path: 'b', component: ComponentB},
{path: 'c', component: ComponentC, outlet: 'aux'},
{path: '', redirectTo: 'c', outlet: 'aux'}
]
}],
'a/b', (t: UrlTree) => { compareTrees(t, tree('a/(b//aux:c)')); });
});
it('should create a new url segment (terminal)', () => {
checkRedirect(
[{
path: 'a',
children: [
{path: 'b', component: ComponentB},
{path: 'c', component: ComponentC, outlet: 'aux'},
{path: '', pathMatch: 'full', redirectTo: 'c', outlet: 'aux'}
]
}],
'a/b', (t: UrlTree) => { compareTrees(t, tree('a/b')); });
});
});
describe('split at the end (no right child)', () => {
it('should create a new child (non-terminal)', () => {
checkRedirect(
[{
path: 'a',
children: [
{path: 'b', component: ComponentB}, {path: '', redirectTo: 'b'},
{path: 'c', component: ComponentC, outlet: 'aux'},
{path: '', redirectTo: 'c', outlet: 'aux'}
]
}],
'a', (t: UrlTree) => { compareTrees(t, tree('a/(b//aux:c)')); });
});
it('should create a new child (terminal)', () => {
checkRedirect(
[{
path: 'a',
children: [
{path: 'b', component: ComponentB}, {path: '', redirectTo: 'b'},
{path: 'c', component: ComponentC, outlet: 'aux'},
{path: '', pathMatch: 'full', redirectTo: 'c', outlet: 'aux'}
]
}],
'a', (t: UrlTree) => { compareTrees(t, tree('a/(b//aux:c)')); });
});
it('should work only only primary outlet', () => {
checkRedirect(
[{
path: 'a',
children: [
{path: 'b', component: ComponentB}, {path: '', redirectTo: 'b'},
{path: 'c', component: ComponentC, outlet: 'aux'}
]
}],
'a/(aux:c)', (t: UrlTree) => { compareTrees(t, tree('a/(b//aux:c)')); });
});
});
describe('split at the end (right child)', () => {
it('should create a new child (non-terminal)', () => {
checkRedirect(
[{
path: 'a',
children: [
{path: 'b', component: ComponentB, children: [{path: 'd', component: ComponentB}]},
{path: '', redirectTo: 'b'}, {
path: 'c',
component: ComponentC,
outlet: 'aux',
children: [{path: 'e', component: ComponentC}]
},
{path: '', redirectTo: 'c', outlet: 'aux'}
]
}],
'a/(d//aux:e)', (t: UrlTree) => { compareTrees(t, tree('a/(b/d//aux:c/e)')); });
});
it('should not create a new child (terminal)', () => {
const config: Routes = [{
path: 'a',
children: [
{path: 'b', component: ComponentB, children: [{path: 'd', component: ComponentB}]},
{path: '', redirectTo: 'b'}, {
path: 'c',
component: ComponentC,
outlet: 'aux',
children: [{path: 'e', component: ComponentC}]
},
{path: '', pathMatch: 'full', redirectTo: 'c', outlet: 'aux'}
]
}];
applyRedirects(null, null, serializer, tree('a/(d//aux:e)'), config)
.subscribe(
(_) => { throw 'Should not be reached'; },
e => { expect(e.message).toEqual('Cannot match any routes. URL Segment: \'a\''); });
});
});
});
describe('empty URL leftovers', () => {
it('should not error when no children matching and no url is left', () => {
checkRedirect(
[{path: 'a', component: ComponentA, children: [{path: 'b', component: ComponentB}]}],
'/a', (t: UrlTree) => { compareTrees(t, tree('a')); });
});
it('should not error when no children matching and no url is left (aux routes)', () => {
checkRedirect(
[{
path: 'a',
component: ComponentA,
children: [
{path: 'b', component: ComponentB},
{path: '', redirectTo: 'c', outlet: 'aux'},
{path: 'c', component: ComponentC, outlet: 'aux'},
]
}],
'/a', (t: UrlTree) => { compareTrees(t, tree('a/(aux:c)')); });
});
it('should error when no children matching and some url is left', () => {
applyRedirects(
null, null, serializer, tree('/a/c'),
[{path: 'a', component: ComponentA, children: [{path: 'b', component: ComponentB}]}])
.subscribe(
(_) => { throw 'Should not be reached'; },
e => { expect(e.message).toEqual('Cannot match any routes. URL Segment: \'a/c\''); });
});
});
describe('custom path matchers', () => {
it('should use custom path matcher', () => {
const matcher = (s: any, g: any, r: any) => {
if (s[0].path === 'a') {
return {consumed: s.slice(0, 2), posParams: {id: s[1]}};
} else {
return null;
}
};
checkRedirect(
[{
matcher: matcher,
component: ComponentA,
children: [{path: 'b', component: ComponentB}]
}],
'/a/1/b', (t: UrlTree) => { compareTrees(t, tree('a/1/b')); });
});
});
describe('redirecting to named outlets', () => {
it('should work when using absolute redirects', () => {
checkRedirect(
[
{path: 'a/:id', redirectTo: '/b/:id(aux:c/:id)'},
{path: 'b/:id', component: ComponentB},
{path: 'c/:id', component: ComponentC, outlet: 'aux'}
],
'a/1;p=99', (t: UrlTree) => { compareTrees(t, tree('/b/1;p=99(aux:c/1;p=99)')); });
});
it('should work when using absolute redirects (wildcard)', () => {
checkRedirect(
[
{path: '**', redirectTo: '/b(aux:c)'}, {path: 'b', component: ComponentB},
{path: 'c', component: ComponentC, outlet: 'aux'}
],
'a/1', (t: UrlTree) => { compareTrees(t, tree('/b(aux:c)')); });
});
it('should throw when using non-absolute redirects', () => {
applyRedirects(
null, null, serializer, tree('a'),
[
{path: 'a', redirectTo: 'b(aux:c)'},
])
.subscribe(
() => { throw new Error('should not be reached'); },
(e) => {
expect(e.message).toEqual(
'Only absolute redirects can have named outlets. redirectTo: \'b(aux:c)\'');
});
});
});
});
function checkRedirect(config: Routes, url: string, callback: any): void {
applyRedirects(null, null, new DefaultUrlSerializer(), tree(url), config)
.subscribe(callback, e => { throw e; });
}
function tree(url: string): UrlTree {
return new DefaultUrlSerializer().parse(url);
}
function compareTrees(actual: UrlTree, expected: UrlTree): void {
const serializer = new DefaultUrlSerializer();
const error =
`"${serializer.serialize(actual)}" is not equal to "${serializer.serialize(expected)}"`;
compareSegments(actual.root, expected.root, error);
expect(actual.queryParams).toEqual(expected.queryParams);
expect(actual.fragment).toEqual(expected.fragment);
}
function compareSegments(actual: UrlSegmentGroup, expected: UrlSegmentGroup, error: string): void {
expect(actual).toBeDefined(error);
expect(equalSegments(actual.segments, expected.segments)).toEqual(true, error);
expect(Object.keys(actual.children).length).toEqual(Object.keys(expected.children).length, error);
Object.keys(expected.children).forEach(key => {
compareSegments(actual.children[key], expected.children[key], error);
});
}
class ComponentA {}
class ComponentB {}
class ComponentC {}

View File

@ -0,0 +1,145 @@
/**
* @license
* Copyright Google Inc. 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 {validateConfig} from '../src/config';
import {PRIMARY_OUTLET} from '../src/shared';
describe('config', () => {
describe('validateConfig', () => {
it('should not throw when no errors', () => {
expect(
() => validateConfig([{path: 'a', redirectTo: 'b'}, {path: 'b', component: ComponentA}]))
.not.toThrow();
});
it('should not throw when a matcher is provided', () => {
expect(() => validateConfig([{matcher: <any>'someFunc', component: ComponentA}]))
.not.toThrow();
});
it('should throw for undefined route', () => {
expect(() => {
validateConfig([{path: 'a', component: ComponentA}, , {path: 'b', component: ComponentB}]);
}).toThrowError(/Invalid configuration of route ''/);
});
it('should throw for undefined route in children', () => {
expect(() => {
validateConfig([{
path: 'a',
children: [
{path: 'b', component: ComponentB},
,
]
}]);
}).toThrowError(/Invalid configuration of route 'a'/);
});
it('should throw when Array is passed', () => {
expect(() => {
validateConfig([
{path: 'a', component: ComponentA},
[{path: 'b', component: ComponentB}, {path: 'c', component: ComponentC}]
]);
}).toThrowError(`Invalid configuration of route '': Array cannot be specified`);
});
it('should throw when redirectTo and children are used together', () => {
expect(() => {
validateConfig(
[{path: 'a', redirectTo: 'b', children: [{path: 'b', component: ComponentA}]}]);
})
.toThrowError(
`Invalid configuration of route 'a': redirectTo and children cannot be used together`);
});
it('should validate children and report full path', () => {
expect(() => validateConfig([{path: 'a', children: [{path: 'b'}]}]))
.toThrowError(
`Invalid configuration of route 'a/b'. One of the following must be provided: component, redirectTo, children or loadChildren`);
});
it('should properly report deeply nested path', () => {
expect(() => validateConfig([{
path: 'a',
children: [{path: 'b', children: [{path: 'c', children: [{path: 'd'}]}]}]
}]))
.toThrowError(
`Invalid configuration of route 'a/b/c/d'. One of the following must be provided: component, redirectTo, children or loadChildren`);
});
it('should throw when redirectTo and loadChildren are used together', () => {
expect(() => { validateConfig([{path: 'a', redirectTo: 'b', loadChildren: 'value'}]); })
.toThrowError(
`Invalid configuration of route 'a': redirectTo and loadChildren cannot be used together`);
});
it('should throw when children and loadChildren are used together', () => {
expect(() => { validateConfig([{path: 'a', children: [], loadChildren: 'value'}]); })
.toThrowError(
`Invalid configuration of route 'a': children and loadChildren cannot be used together`);
});
it('should throw when component and redirectTo are used together', () => {
expect(() => { validateConfig([{path: 'a', component: ComponentA, redirectTo: 'b'}]); })
.toThrowError(
`Invalid configuration of route 'a': redirectTo and component cannot be used together`);
});
it('should throw when path and matcher are used together', () => {
expect(() => { validateConfig([{path: 'a', matcher: <any>'someFunc', children: []}]); })
.toThrowError(
`Invalid configuration of route 'a': path and matcher cannot be used together`);
});
it('should throw when path and matcher are missing', () => {
expect(() => { validateConfig([{component: null, redirectTo: 'b'}]); })
.toThrowError(
`Invalid configuration of route '': routes must have either a path or a matcher specified`);
});
it('should throw when none of component and children or direct are missing', () => {
expect(() => { validateConfig([{path: 'a'}]); })
.toThrowError(
`Invalid configuration of route 'a'. One of the following must be provided: component, redirectTo, children or loadChildren`);
});
it('should throw when path starts with a slash', () => {
expect(() => {
validateConfig([<any>{path: '/a', redirectTo: 'b'}]);
}).toThrowError(`Invalid configuration of route '/a': path cannot start with a slash`);
});
it('should throw when emptyPath is used with redirectTo without explicitly providing matching',
() => {
expect(() => {
validateConfig([<any>{path: '', redirectTo: 'b'}]);
}).toThrowError(/Invalid configuration of route '{path: "", redirectTo: "b"}'/);
});
it('should throw when pathPatch is invalid', () => {
expect(() => { validateConfig([{path: 'a', pathMatch: 'invalid', component: ComponentB}]); })
.toThrowError(
/Invalid configuration of route 'a': pathMatch can only be set to 'prefix' or 'full'/);
});
it('should throw when pathPatch is invalid', () => {
expect(() => { validateConfig([{path: 'a', outlet: 'aux', children: []}]); })
.toThrowError(
/Invalid configuration of route 'a': a componentless route cannot have a named outlet set/);
expect(() => validateConfig([{path: 'a', outlet: '', children: []}])).not.toThrow();
expect(() => validateConfig([{path: 'a', outlet: PRIMARY_OUTLET, children: []}]))
.not.toThrow();
});
});
});
class ComponentA {}
class ComponentB {}
class ComponentC {}

View File

@ -0,0 +1,124 @@
/**
* @license
* Copyright Google Inc. 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 {Routes} from '../src/config';
import {createRouterState} from '../src/create_router_state';
import {recognize} from '../src/recognize';
import {DefaultRouteReuseStrategy} from '../src/router';
import {ActivatedRoute, RouterState, RouterStateSnapshot, advanceActivatedRoute, createEmptyState} from '../src/router_state';
import {PRIMARY_OUTLET} from '../src/shared';
import {DefaultUrlSerializer, UrlSegmentGroup, UrlTree} from '../src/url_tree';
import {TreeNode} from '../src/utils/tree';
describe('create router state', () => {
const reuseStrategy = new DefaultRouteReuseStrategy();
const emptyState = () =>
createEmptyState(new UrlTree(new UrlSegmentGroup([], {}), {}, null), RootComponent);
it('should work create new state', () => {
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)'),
emptyState());
checkActivatedRoute(state.root, RootComponent);
const c = state.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 = [
{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());
advanceState(prevState);
const state = createRouterState(reuseStrategy, createState(config, 'a(left:c)'), prevState);
expect(prevState.root).toBe(state.root);
const prevC = prevState.children(prevState.root);
const currC = state.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', () => {
const config = [{
path: 'a/:id',
children: [
{path: 'b', component: ComponentA}, {path: 'c', component: ComponentB, outlet: 'right'}
]
}];
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.firstChild(prevState.root);
const currP = state.firstChild(state.root);
expect(prevP).toBe(currP);
const prevC = prevState.children(prevP);
const currC = state.children(currP);
expect(currP._futureSnapshot.params).toEqual({id: '2', p: '22'});
checkActivatedRoute(currC[0], ComponentA);
checkActivatedRoute(currC[1], ComponentB, 'right');
});
});
function advanceState(state: RouterState): void {
advanceNode(state._root);
}
function advanceNode(node: TreeNode<ActivatedRoute>): void {
advanceActivatedRoute(node.value);
node.children.forEach(advanceNode);
}
function createState(config: Routes, url: string): RouterStateSnapshot {
let res: RouterStateSnapshot;
recognize(RootComponent, config, tree(url), url).forEach(s => res = s);
return res;
}
function checkActivatedRoute(
actual: ActivatedRoute, cmp: Function, outlet: string = PRIMARY_OUTLET): void {
if (actual === null) {
expect(actual).toBeDefined();
} else {
expect(actual.component).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 {}

View File

@ -0,0 +1,256 @@
/**
* @license
* Copyright Google Inc. 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/BehaviorSubject';
import {createUrlTree} from '../src/create_url_tree';
import {ActivatedRoute, ActivatedRouteSnapshot, advanceActivatedRoute} from '../src/router_state';
import {PRIMARY_OUTLET, Params} from '../src/shared';
import {DefaultUrlSerializer, UrlSegmentGroup, UrlTree} from '../src/url_tree';
describe('createUrlTree', () => {
const serializer = new DefaultUrlSerializer();
it('should navigate to the root', () => {
const p = serializer.parse('/');
const t = createRoot(p, ['/']);
expect(serializer.serialize(t)).toEqual('/');
});
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/);
});
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');
});
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;
expect(params[0].path).toEqual('one');
expect(params[1].path).toEqual('11');
});
it('should support first segments contaings slashes', () => {
const p = serializer.parse('/');
const t = createRoot(p, [{segmentPath: '/one'}, 'two/three']);
expect(serializer.serialize(t)).toEqual('/%2Fone/two%2Fthree');
});
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)');
});
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)');
});
it('should support updating secondary segments', () => {
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 secondary segments (nested case)', () => {
const p = serializer.parse('/a/(b//right:c)');
const t = createRoot(p, ['a', {outlets: {right: ['d', 11, 'e']}}]);
expect(serializer.serialize(t)).toEqual('/a/(b//right:d/11/e)');
});
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 update matrix parameters', () => {
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');
});
it('should create matrix parameters', () => {
const p = serializer.parse('/a');
const t = createRoot(p, ['/a', {pp: 22, dd: 33}]);
expect(serializer.serialize(t)).toEqual('/a;pp=22;dd=33');
});
it('should create matrix parameters together with other segments', () => {
const p = serializer.parse('/a');
const t = createRoot(p, ['/a', 'b', {aa: 22, bb: 33}]);
expect(serializer.serialize(t)).toEqual('/a/b;aa=22;bb=33');
});
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)');
});
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)');
});
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)');
});
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)', () => {
const p = serializer.parse('/a/(c//left:cp)(left:ap)');
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)');
});
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');
});
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');
});
it('should support going to a parent (across segments)', () => {
const p = serializer.parse('/q/(a/(c//left:cp)//left:qp)(left:ap)');
const t =
create(p.root.children[PRIMARY_OUTLET].children[PRIMARY_OUTLET], 0, p, ['../../q2']);
expect(serializer.serialize(t)).toEqual('/q2(left:ap)');
});
it('should navigate to the root', () => {
const p = serializer.parse('/a/c');
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');
});
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 \'../\'');
});
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)');
});
});
it('should set query params', () => {
const p = serializer.parse('/');
const t = createRoot(p, [], {a: 'hey'});
expect(t.queryParams).toEqual({a: 'hey'});
});
it('should stringify query params', () => {
const p = serializer.parse('/');
const t = createRoot(p, [], <any>{a: 1});
expect(t.queryParams).toEqual({a: '1'});
});
it('should set fragment', () => {
const p = serializer.parse('/');
const t = createRoot(p, [], {}, 'fragment');
expect(t.fragment).toEqual('fragment');
});
});
function createRoot(tree: UrlTree, commands: any[], queryParams?: Params, fragment?: string) {
const s = new ActivatedRouteSnapshot(
[], <any>{}, <any>{}, '', <any>{}, PRIMARY_OUTLET, 'someComponent', null, tree.root, -1,
<any>null);
const a = new ActivatedRoute(
new BehaviorSubject(null), new BehaviorSubject(null), new BehaviorSubject(null),
new BehaviorSubject(null), new BehaviorSubject(null), PRIMARY_OUTLET, 'someComponent', s);
advanceActivatedRoute(a);
return createUrlTree(a, tree, commands, queryParams, fragment);
}
function create(
segment: UrlSegmentGroup, startIndex: number, tree: UrlTree, commands: any[],
queryParams?: Params, fragment?: string) {
if (!segment) {
expect(segment).toBeDefined();
}
const s = new ActivatedRouteSnapshot(
[], <any>{}, <any>{}, '', <any>{}, PRIMARY_OUTLET, 'someComponent', null, <any>segment,
startIndex, <any>null);
const a = new ActivatedRoute(
new BehaviorSubject(null), new BehaviorSubject(null), new BehaviorSubject(null),
new BehaviorSubject(null), new BehaviorSubject(null), PRIMARY_OUTLET, 'someComponent', s);
advanceActivatedRoute(a);
return createUrlTree(a, tree, commands, queryParams, fragment);
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,730 @@
/**
* @license
* Copyright Google Inc. 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 {Routes} from '../src/config';
import {recognize} from '../src/recognize';
import {ActivatedRouteSnapshot, RouterStateSnapshot} from '../src/router_state';
import {PRIMARY_OUTLET, Params} from '../src/shared';
import {DefaultUrlSerializer, UrlTree} from '../src/url_tree';
describe('recognize', () => {
it('should work', () => {
checkRecognize([{path: 'a', component: ComponentA}], 'a', (s: RouterStateSnapshot) => {
checkActivatedRoute(s.root, '', {}, RootComponent);
checkActivatedRoute(s.firstChild(s.root), 'a', {}, ComponentA);
});
});
it('should freeze params object', () => {
checkRecognize([{path: 'a/:id', component: ComponentA}], 'a/10', (s: RouterStateSnapshot) => {
checkActivatedRoute(s.root, '', {}, RootComponent);
const child = s.firstChild(s.root);
expect(Object.isFrozen(child.params)).toBeTruthy();
});
});
it('should support secondary routes', () => {
checkRecognize(
[
{path: 'a', component: ComponentA}, {path: 'b', component: ComponentB, outlet: 'left'},
{path: 'c', component: ComponentC, outlet: 'right'}
],
'a(left:b//right:c)', (s: RouterStateSnapshot) => {
const c = s.children(s.root);
checkActivatedRoute(c[0], 'a', {}, ComponentA);
checkActivatedRoute(c[1], 'b', {}, ComponentB, 'left');
checkActivatedRoute(c[2], 'c', {}, ComponentC, 'right');
});
});
it('should set url segment and index properly', () => {
const url = tree('a(left:b//right:c)');
recognize(
RootComponent,
[
{path: 'a', component: ComponentA}, {path: 'b', component: ComponentB, outlet: 'left'},
{path: 'c', component: ComponentC, outlet: 'right'}
],
url, 'a(left:b//right:c)')
.subscribe((s) => {
expect(s.root._urlSegment).toBe(url.root);
expect(s.root._lastPathIndex).toBe(-1);
const c = s.children(s.root);
expect(c[0]._urlSegment).toBe(url.root.children[PRIMARY_OUTLET]);
expect(c[0]._lastPathIndex).toBe(0);
expect(c[1]._urlSegment).toBe(url.root.children['left']);
expect(c[1]._lastPathIndex).toBe(0);
expect(c[2]._urlSegment).toBe(url.root.children['right']);
expect(c[2]._lastPathIndex).toBe(0);
});
});
it('should set url segment and index properly (nested case)', () => {
const url = tree('a/b/c');
recognize(
RootComponent,
[
{path: 'a/b', component: ComponentA, children: [{path: 'c', component: ComponentC}]},
],
url, 'a/b/c')
.subscribe((s: RouterStateSnapshot) => {
expect(s.root._urlSegment).toBe(url.root);
expect(s.root._lastPathIndex).toBe(-1);
const compA = s.firstChild(s.root);
expect(compA._urlSegment).toBe(url.root.children[PRIMARY_OUTLET]);
expect(compA._lastPathIndex).toBe(1);
const compC = s.firstChild(<any>compA);
expect(compC._urlSegment).toBe(url.root.children[PRIMARY_OUTLET]);
expect(compC._lastPathIndex).toBe(2);
});
});
it('should set url segment and index properly (wildcard)', () => {
const url = tree('a/b/c');
recognize(
RootComponent,
[
{path: 'a', component: ComponentA, children: [{path: '**', component: ComponentB}]},
],
url, 'a/b/c')
.subscribe((s: RouterStateSnapshot) => {
expect(s.root._urlSegment).toBe(url.root);
expect(s.root._lastPathIndex).toBe(-1);
const compA = s.firstChild(s.root);
expect(compA._urlSegment).toBe(url.root.children[PRIMARY_OUTLET]);
expect(compA._lastPathIndex).toBe(0);
const compC = s.firstChild(<any>compA);
expect(compC._urlSegment).toBe(url.root.children[PRIMARY_OUTLET]);
expect(compC._lastPathIndex).toBe(2);
});
});
it('should match routes in the depth first order', () => {
checkRecognize(
[
{path: 'a', component: ComponentA, children: [{path: ':id', component: ComponentB}]},
{path: 'a/:id', component: ComponentC}
],
'a/paramA', (s: RouterStateSnapshot) => {
checkActivatedRoute(s.root, '', {}, RootComponent);
checkActivatedRoute(s.firstChild(s.root), 'a', {}, ComponentA);
checkActivatedRoute(
s.firstChild(<any>s.firstChild(s.root)), 'paramA', {id: 'paramA'}, ComponentB);
});
checkRecognize(
[{path: 'a', component: ComponentA}, {path: 'a/:id', component: ComponentC}], 'a/paramA',
(s: RouterStateSnapshot) => {
checkActivatedRoute(s.root, '', {}, RootComponent);
checkActivatedRoute(s.firstChild(s.root), 'a/paramA', {id: 'paramA'}, ComponentC);
});
});
it('should use outlet name when matching secondary routes', () => {
checkRecognize(
[
{path: 'a', component: ComponentA}, {path: 'b', component: ComponentB, outlet: 'left'},
{path: 'b', component: ComponentC, outlet: 'right'}
],
'a(right:b)', (s: RouterStateSnapshot) => {
const c = s.children(s.root);
checkActivatedRoute(c[0], 'a', {}, ComponentA);
checkActivatedRoute(c[1], 'b', {}, ComponentC, 'right');
});
});
it('should handle non top-level secondary routes', () => {
checkRecognize(
[
{
path: 'a',
component: ComponentA,
children: [
{path: 'b', component: ComponentB},
{path: 'c', component: ComponentC, outlet: 'left'}
]
},
],
'a/(b//left:c)', (s: RouterStateSnapshot) => {
const c = s.children(<any>s.firstChild(s.root));
checkActivatedRoute(c[0], 'b', {}, ComponentB, PRIMARY_OUTLET);
checkActivatedRoute(c[1], 'c', {}, ComponentC, 'left');
});
});
it('should sort routes by outlet name', () => {
checkRecognize(
[
{path: 'a', component: ComponentA}, {path: 'c', component: ComponentC, outlet: 'c'},
{path: 'b', component: ComponentB, outlet: 'b'}
],
'a(c:c//b:b)', (s: RouterStateSnapshot) => {
const c = s.children(s.root);
checkActivatedRoute(c[0], 'a', {}, ComponentA);
checkActivatedRoute(c[1], 'b', {}, ComponentB, 'b');
checkActivatedRoute(c[2], 'c', {}, ComponentC, 'c');
});
});
it('should support matrix parameters', () => {
checkRecognize(
[
{path: 'a', component: ComponentA, children: [{path: 'b', component: ComponentB}]},
{path: 'c', component: ComponentC, outlet: 'left'}
],
'a;a1=11;a2=22/b;b1=111;b2=222(left:c;c1=1111;c2=2222)', (s: RouterStateSnapshot) => {
const c = s.children(s.root);
checkActivatedRoute(c[0], 'a', {a1: '11', a2: '22'}, ComponentA);
checkActivatedRoute(s.firstChild(<any>c[0]), 'b', {b1: '111', b2: '222'}, ComponentB);
checkActivatedRoute(c[1], 'c', {c1: '1111', c2: '2222'}, ComponentC, 'left');
});
});
describe('data', () => {
it('should set static data', () => {
checkRecognize(
[{path: 'a', data: {one: 1}, component: ComponentA}], 'a', (s: RouterStateSnapshot) => {
const r: ActivatedRouteSnapshot = s.firstChild(s.root);
expect(r.data).toEqual({one: 1});
});
});
it('should merge componentless route\'s data', () => {
checkRecognize(
[{
path: 'a',
data: {one: 1},
children: [{path: 'b', data: {two: 2}, component: ComponentB}]
}],
'a/b', (s: RouterStateSnapshot) => {
const r: ActivatedRouteSnapshot = s.firstChild(<any>s.firstChild(s.root));
expect(r.data).toEqual({one: 1, two: 2});
});
});
it('should set resolved data', () => {
checkRecognize(
[{path: 'a', resolve: {one: 'some-token'}, component: ComponentA}], 'a',
(s: RouterStateSnapshot) => {
const r: ActivatedRouteSnapshot = s.firstChild(s.root);
expect(r._resolve).toEqual({one: 'some-token'});
});
});
});
describe('empty path', () => {
describe('root', () => {
it('should work', () => {
checkRecognize([{path: '', component: ComponentA}], '', (s: RouterStateSnapshot) => {
checkActivatedRoute(s.firstChild(s.root), '', {}, ComponentA);
});
});
it('should match when terminal', () => {
checkRecognize(
[{path: '', pathMatch: 'full', component: ComponentA}], '',
(s: RouterStateSnapshot) => {
checkActivatedRoute(s.firstChild(s.root), '', {}, ComponentA);
});
});
it('should work (nested case)', () => {
checkRecognize(
[{path: '', component: ComponentA, children: [{path: '', component: ComponentB}]}], '',
(s: RouterStateSnapshot) => {
checkActivatedRoute(s.firstChild(s.root), '', {}, ComponentA);
checkActivatedRoute(s.firstChild(<any>s.firstChild(s.root)), '', {}, ComponentB);
});
});
it('should set url segment and index properly', () => {
const url = tree('');
recognize(
RootComponent,
[{path: '', component: ComponentA, children: [{path: '', component: ComponentB}]}], url,
'')
.forEach((s: RouterStateSnapshot) => {
expect(s.root._urlSegment).toBe(url.root);
expect(s.root._lastPathIndex).toBe(-1);
const c = s.firstChild(s.root);
expect(c._urlSegment).toBe(url.root);
expect(c._lastPathIndex).toBe(-1);
const c2 = s.firstChild(<any>s.firstChild(s.root));
expect(c2._urlSegment).toBe(url.root);
expect(c2._lastPathIndex).toBe(-1);
});
});
it('should inherit params', () => {
checkRecognize(
[{
path: 'a',
component: ComponentA,
children: [
{path: '', component: ComponentB, children: [{path: '', component: ComponentC}]}
]
}],
'/a;p=1', (s: RouterStateSnapshot) => {
checkActivatedRoute(s.firstChild(s.root), 'a', {p: '1'}, ComponentA);
checkActivatedRoute(s.firstChild(s.firstChild(s.root)), '', {p: '1'}, ComponentB);
checkActivatedRoute(
s.firstChild(s.firstChild(s.firstChild(s.root))), '', {p: '1'}, ComponentC);
});
});
});
describe('aux split is in the middle', () => {
it('should match (non-terminal)', () => {
checkRecognize(
[{
path: 'a',
component: ComponentA,
children: [
{path: 'b', component: ComponentB},
{path: '', component: ComponentC, outlet: 'aux'}
]
}],
'a/b', (s: RouterStateSnapshot) => {
checkActivatedRoute(s.firstChild(s.root), 'a', {}, ComponentA);
const c = s.children(s.firstChild(s.root));
checkActivatedRoute(c[0], 'b', {}, ComponentB);
checkActivatedRoute(c[1], '', {}, ComponentC, 'aux');
});
});
it('should match (non-termianl) when both primary and secondary and primary has a child',
() => {
const config = [{
path: 'parent',
children: [
{
path: '',
component: ComponentA,
children: [
{path: 'b', component: ComponentB},
{path: 'c', component: ComponentC},
]
},
{
path: '',
component: ComponentD,
outlet: 'secondary',
}
]
}];
checkRecognize(config, 'parent/b', (s: RouterStateSnapshot) => {
checkActivatedRoute(s.root, '', {}, RootComponent);
checkActivatedRoute(s.firstChild(s.root), 'parent', {}, undefined);
const cc = s.children(s.firstChild(s.root));
checkActivatedRoute(cc[0], '', {}, ComponentA);
checkActivatedRoute(cc[1], '', {}, ComponentD, 'secondary');
checkActivatedRoute(s.firstChild(cc[0]), 'b', {}, ComponentB);
});
});
it('should match (terminal)', () => {
checkRecognize(
[{
path: 'a',
component: ComponentA,
children: [
{path: 'b', component: ComponentB},
{path: '', pathMatch: 'full', component: ComponentC, outlet: 'aux'}
]
}],
'a/b', (s: RouterStateSnapshot) => {
checkActivatedRoute(s.firstChild(s.root), 'a', {}, ComponentA);
const c = s.children(s.firstChild(s.root));
expect(c.length).toEqual(1);
checkActivatedRoute(c[0], 'b', {}, ComponentB);
});
});
it('should set url segment and index properly', () => {
const url = tree('a/b');
recognize(
RootComponent, [{
path: 'a',
component: ComponentA,
children: [
{path: 'b', component: ComponentB},
{path: '', component: ComponentC, outlet: 'aux'}
]
}],
url, 'a/b')
.forEach((s: RouterStateSnapshot) => {
expect(s.root._urlSegment).toBe(url.root);
expect(s.root._lastPathIndex).toBe(-1);
const a = s.firstChild(s.root);
expect(a._urlSegment).toBe(url.root.children[PRIMARY_OUTLET]);
expect(a._lastPathIndex).toBe(0);
const b = s.firstChild(a);
expect(b._urlSegment).toBe(url.root.children[PRIMARY_OUTLET]);
expect(b._lastPathIndex).toBe(1);
const c = s.children(a)[1];
expect(c._urlSegment).toBe(url.root.children[PRIMARY_OUTLET]);
expect(c._lastPathIndex).toBe(0);
});
});
it('should set url segment and index properly when nested empty-path segments', () => {
const url = tree('a');
recognize(
RootComponent, [{
path: 'a',
children: [
{path: '', component: ComponentB, children: [{path: '', component: ComponentC}]}
]
}],
url, 'a')
.forEach((s: RouterStateSnapshot) => {
expect(s.root._urlSegment).toBe(url.root);
expect(s.root._lastPathIndex).toBe(-1);
const a = s.firstChild(s.root);
expect(a._urlSegment).toBe(url.root.children[PRIMARY_OUTLET]);
expect(a._lastPathIndex).toBe(0);
const b = s.firstChild(a);
expect(b._urlSegment).toBe(url.root.children[PRIMARY_OUTLET]);
expect(b._lastPathIndex).toBe(0);
const c = s.firstChild(b);
expect(c._urlSegment).toBe(url.root.children[PRIMARY_OUTLET]);
expect(c._lastPathIndex).toBe(0);
});
});
it('should set url segment and index properly when nested empty-path segments (2)', () => {
const url = tree('');
recognize(
RootComponent, [{
path: '',
children: [
{path: '', component: ComponentB, children: [{path: '', component: ComponentC}]}
]
}],
url, '')
.forEach((s: RouterStateSnapshot) => {
expect(s.root._urlSegment).toBe(url.root);
expect(s.root._lastPathIndex).toBe(-1);
const a = s.firstChild(s.root);
expect(a._urlSegment).toBe(url.root);
expect(a._lastPathIndex).toBe(-1);
const b = s.firstChild(a);
expect(b._urlSegment).toBe(url.root);
expect(b._lastPathIndex).toBe(-1);
const c = s.firstChild(b);
expect(c._urlSegment).toBe(url.root);
expect(c._lastPathIndex).toBe(-1);
});
});
});
describe('aux split at the end (no right child)', () => {
it('should match (non-terminal)', () => {
checkRecognize(
[{
path: 'a',
component: ComponentA,
children: [
{path: '', component: ComponentB},
{path: '', component: ComponentC, outlet: 'aux'},
]
}],
'a', (s: RouterStateSnapshot) => {
checkActivatedRoute(s.firstChild(s.root), 'a', {}, ComponentA);
const c = s.children(s.firstChild(s.root));
checkActivatedRoute(c[0], '', {}, ComponentB);
checkActivatedRoute(c[1], '', {}, ComponentC, 'aux');
});
});
it('should match (terminal)', () => {
checkRecognize(
[{
path: 'a',
component: ComponentA,
children: [
{path: '', pathMatch: 'full', component: ComponentB},
{path: '', pathMatch: 'full', component: ComponentC, outlet: 'aux'},
]
}],
'a', (s: RouterStateSnapshot) => {
checkActivatedRoute(s.firstChild(s.root), 'a', {}, ComponentA);
const c = s.children(s.firstChild(s.root));
checkActivatedRoute(c[0], '', {}, ComponentB);
checkActivatedRoute(c[1], '', {}, ComponentC, 'aux');
});
});
it('should work only only primary outlet', () => {
checkRecognize(
[{
path: 'a',
component: ComponentA,
children: [
{path: '', component: ComponentB},
{path: 'c', component: ComponentC, outlet: 'aux'},
]
}],
'a/(aux:c)', (s: RouterStateSnapshot) => {
checkActivatedRoute(s.firstChild(s.root), 'a', {}, ComponentA);
const c = s.children(s.firstChild(s.root));
checkActivatedRoute(c[0], '', {}, ComponentB);
checkActivatedRoute(c[1], 'c', {}, ComponentC, 'aux');
});
});
it('should work when split is at the root level', () => {
checkRecognize(
[
{path: '', component: ComponentA}, {path: 'b', component: ComponentB},
{path: 'c', component: ComponentC, outlet: 'aux'}
],
'(aux:c)', (s: RouterStateSnapshot) => {
checkActivatedRoute(s.root, '', {}, RootComponent);
const children = s.children(s.root);
expect(children.length).toEqual(2);
checkActivatedRoute(children[0], '', {}, ComponentA);
checkActivatedRoute(children[1], 'c', {}, ComponentC, 'aux');
});
});
});
describe('split at the end (right child)', () => {
it('should match (non-terminal)', () => {
checkRecognize(
[{
path: 'a',
component: ComponentA,
children: [
{path: '', component: ComponentB, children: [{path: 'd', component: ComponentD}]},
{
path: '',
component: ComponentC,
outlet: 'aux',
children: [{path: 'e', component: ComponentE}]
},
]
}],
'a/(d//aux:e)', (s: RouterStateSnapshot) => {
checkActivatedRoute(s.firstChild(s.root), 'a', {}, ComponentA);
const c = s.children(s.firstChild(s.root));
checkActivatedRoute(c[0], '', {}, ComponentB);
checkActivatedRoute(s.firstChild(c[0]), 'd', {}, ComponentD);
checkActivatedRoute(c[1], '', {}, ComponentC, 'aux');
checkActivatedRoute(s.firstChild(c[1]), 'e', {}, ComponentE);
});
});
});
});
describe('wildcards', () => {
it('should support simple wildcards', () => {
checkRecognize(
[{path: '**', component: ComponentA}], 'a/b/c/d;a1=11', (s: RouterStateSnapshot) => {
checkActivatedRoute(s.firstChild(s.root), 'a/b/c/d', {a1: '11'}, ComponentA);
});
});
});
describe('componentless routes', () => {
it('should work', () => {
checkRecognize(
[{
path: 'p/:id',
children: [
{path: 'a', component: ComponentA},
{path: 'b', component: ComponentB, outlet: 'aux'}
]
}],
'p/11;pp=22/(a;pa=33//aux:b;pb=44)', (s: RouterStateSnapshot) => {
const p = s.firstChild(s.root);
checkActivatedRoute(p, 'p/11', {id: '11', pp: '22'}, undefined);
const c = s.children(p);
checkActivatedRoute(c[0], 'a', {id: '11', pp: '22', pa: '33'}, ComponentA);
checkActivatedRoute(c[1], 'b', {id: '11', pp: '22', pb: '44'}, ComponentB, 'aux');
});
});
it('should merge params until encounters a normal route', () => {
checkRecognize(
[{
path: 'p/:id',
children: [{
path: 'a/:name',
children: [{
path: 'b',
component: ComponentB,
children: [{path: 'c', component: ComponentC}]
}]
}]
}],
'p/11/a/victor/b/c', (s: RouterStateSnapshot) => {
const p = s.firstChild(s.root);
checkActivatedRoute(p, 'p/11', {id: '11'}, undefined);
const a = s.firstChild(p);
checkActivatedRoute(a, 'a/victor', {id: '11', name: 'victor'}, undefined);
const b = s.firstChild(a);
checkActivatedRoute(b, 'b', {id: '11', name: 'victor'}, ComponentB);
const c = s.firstChild(b);
checkActivatedRoute(c, 'c', {}, ComponentC);
});
});
});
describe('empty URL leftovers', () => {
it('should not throw when no children matching', () => {
checkRecognize(
[{path: 'a', component: ComponentA, children: [{path: 'b', component: ComponentB}]}],
'/a', (s: RouterStateSnapshot) => {
const a = s.firstChild(s.root);
checkActivatedRoute(a, 'a', {}, ComponentA);
});
});
it('should not throw when no children matching (aux routes)', () => {
checkRecognize(
[{
path: 'a',
component: ComponentA,
children: [
{path: 'b', component: ComponentB},
{path: '', component: ComponentC, outlet: 'aux'},
]
}],
'/a', (s: RouterStateSnapshot) => {
const a = s.firstChild(s.root);
checkActivatedRoute(a, 'a', {}, ComponentA);
checkActivatedRoute(a.children[0], '', {}, ComponentC, 'aux');
});
});
});
describe('custom path matchers', () => {
it('should use custom path matcher', () => {
const matcher = (s: any, g: any, r: any) => {
if (s[0].path === 'a') {
return {consumed: s.slice(0, 2), posParams: {id: s[1]}};
} else {
return null;
}
};
checkRecognize(
[{
matcher: matcher,
component: ComponentA,
children: [{path: 'b', component: ComponentB}]
}],
'/a/1;p=99/b', (s: RouterStateSnapshot) => {
const a = s.root.firstChild;
checkActivatedRoute(a, 'a/1', {id: '1', p: '99'}, ComponentA);
checkActivatedRoute(a.firstChild, 'b', {}, ComponentB);
});
});
});
describe('query parameters', () => {
it('should support query params', () => {
const config = [{path: 'a', component: ComponentA}];
checkRecognize(config, 'a?q=11', (s: RouterStateSnapshot) => {
expect(s.root.queryParams).toEqual({q: '11'});
});
});
it('should freeze query params object', () => {
checkRecognize([{path: 'a', component: ComponentA}], 'a?q=11', (s: RouterStateSnapshot) => {
expect(Object.isFrozen(s.root.queryParams)).toBeTruthy();
});
});
});
describe('fragment', () => {
it('should support fragment', () => {
const config = [{path: 'a', component: ComponentA}];
checkRecognize(
config, 'a#f1', (s: RouterStateSnapshot) => { expect(s.root.fragment).toEqual('f1'); });
});
});
describe('error handling', () => {
it('should error when two routes with the same outlet name got matched', () => {
recognize(
RootComponent,
[
{path: 'a', component: ComponentA}, {path: 'b', component: ComponentB, outlet: 'aux'},
{path: 'c', component: ComponentC, outlet: 'aux'}
],
tree('a(aux:b//aux:c)'), 'a(aux:b//aux:c)')
.subscribe((_) => {}, (s: RouterStateSnapshot) => {
expect(s.toString())
.toContain(
'Two segments cannot have the same outlet name: \'aux:b\' and \'aux:c\'.');
});
});
});
});
function checkRecognize(config: Routes, url: string, callback: any): void {
recognize(RootComponent, config, tree(url), url).subscribe(callback, e => { throw e; });
}
function checkActivatedRoute(
actual: ActivatedRouteSnapshot, url: string, params: Params, cmp: Function,
outlet: string = PRIMARY_OUTLET): void {
if (actual === null) {
expect(actual).not.toBeNull();
} else {
expect(actual.url.map(s => s.path).join('/')).toEqual(url);
expect(actual.params).toEqual(params);
expect(actual.component).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 {}
class ComponentD {}
class ComponentE {}

View File

@ -0,0 +1,120 @@
/**
* @license
* Copyright Google Inc. 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 {Location} from '@angular/common';
import {TestBed, inject} from '@angular/core/testing';
import {ResolveData} from '../src/config';
import {PreActivation, Router} from '../src/router';
import {RouterOutletMap} from '../src/router_outlet_map';
import {ActivatedRouteSnapshot, RouterStateSnapshot, createEmptyStateSnapshot} from '../src/router_state';
import {DefaultUrlSerializer} from '../src/url_tree';
import {TreeNode} from '../src/utils/tree';
import {RouterTestingModule} from '../testing/router_testing_module';
describe('Router', () => {
describe('resetRootComponentType', () => {
class NewRootComponent {}
beforeEach(() => { TestBed.configureTestingModule({imports: [RouterTestingModule]}); });
it('should not change root route when updating the root component', () => {
const r: Router = TestBed.get(Router);
const root = r.routerState.root;
r.resetRootComponentType(NewRootComponent);
expect(r.routerState.root).toBe(root);
});
});
describe('setUpLocationChangeListener', () => {
beforeEach(() => { TestBed.configureTestingModule({imports: [RouterTestingModule]}); });
it('should be indempotent', inject([Router, Location], (r: Router, location: Location) => {
r.setUpLocationChangeListener();
const a = (<any>r).locationSubscription;
r.setUpLocationChangeListener();
const b = (<any>r).locationSubscription;
expect(a).toBe(b);
r.dispose();
r.setUpLocationChangeListener();
const c = (<any>r).locationSubscription;
expect(c).not.toBe(b);
}));
});
describe('PreActivation', () => {
const serializer = new DefaultUrlSerializer();
const inj = {get: (token: any) => () => `${token}_value`};
let empty: RouterStateSnapshot;
beforeEach(() => { empty = createEmptyStateSnapshot(serializer.parse('/'), null); });
it('should resolve data', () => {
const r = {data: 'resolver'};
const n = createActivatedRouteSnapshot('a', {resolve: r});
const s = new RouterStateSnapshot('url', new TreeNode(empty.root, [new TreeNode(n, [])]));
checkResolveData(s, empty, inj, () => {
expect(s.root.firstChild.data).toEqual({data: 'resolver_value'});
});
});
it('should wait for the parent resolve to complete', () => {
const parentResolve = {data: 'resolver'};
const childResolve = {};
const parent = createActivatedRouteSnapshot(null, {resolve: parentResolve});
const child = createActivatedRouteSnapshot('b', {resolve: childResolve});
const s = new RouterStateSnapshot(
'url', new TreeNode(empty.root, [new TreeNode(parent, [new TreeNode(child, [])])]));
const inj = {get: (token: any) => () => Promise.resolve(`${token}_value`)};
checkResolveData(s, empty, inj, () => {
expect(s.root.firstChild.firstChild.data).toEqual({data: 'resolver_value'});
});
});
it('should copy over data when creating a snapshot', () => {
const r1 = {data: 'resolver1'};
const r2 = {data: 'resolver2'};
const n1 = createActivatedRouteSnapshot('a', {resolve: r1});
const s1 = new RouterStateSnapshot('url', new TreeNode(empty.root, [new TreeNode(n1, [])]));
checkResolveData(s1, empty, inj, () => {});
const n21 = createActivatedRouteSnapshot('a', {resolve: r1});
const n22 = createActivatedRouteSnapshot('b', {resolve: r2});
const s2 = new RouterStateSnapshot(
'url', new TreeNode(empty.root, [new TreeNode(n21, [new TreeNode(n22, [])])]));
checkResolveData(s2, s1, inj, () => {
expect(s2.root.firstChild.data).toEqual({data: 'resolver1_value'});
expect(s2.root.firstChild.firstChild.data).toEqual({data: 'resolver2_value'});
});
});
});
});
function checkResolveData(
future: RouterStateSnapshot, curr: RouterStateSnapshot, injector: any, check: any): void {
const p = new PreActivation(future, curr, injector);
p.traverse(new RouterOutletMap());
p.resolveData().subscribe(check, (e) => { throw e; });
}
function createActivatedRouteSnapshot(cmp: string, extra: any = {}): ActivatedRouteSnapshot {
return new ActivatedRouteSnapshot(
<any>[], {}, <any>null, <any>null, <any>null, <any>null, <any>cmp, <any>{}, <any>null, -1,
extra.resolve);
}

View File

@ -0,0 +1,143 @@
/**
* @license
* Copyright Google Inc. 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 {Component, NgModule, NgModuleFactoryLoader} from '@angular/core';
import {TestBed, fakeAsync, inject, tick} from '@angular/core/testing';
import {RouteConfigLoadEnd, RouteConfigLoadStart, Router, RouterModule} from '../index';
import {PreloadAllModules, PreloadingStrategy, RouterPreloader} from '../src/router_preloader';
import {RouterTestingModule, SpyNgModuleFactoryLoader} from '../testing';
describe('RouterPreloader', () => {
@Component({template: ''})
class LazyLoadedCmp {
}
describe('should preload configurations', () => {
@NgModule({
declarations: [LazyLoadedCmp],
imports: [RouterModule.forChild([{path: 'LoadedModule2', component: LazyLoadedCmp}])]
})
class LoadedModule2 {
}
@NgModule(
{imports: [RouterModule.forChild([{path: 'LoadedModule1', loadChildren: 'expected2'}])]})
class LoadedModule1 {
}
beforeEach(() => {
TestBed.configureTestingModule({
imports: [RouterTestingModule.withRoutes([{path: 'lazy', loadChildren: 'expected'}])],
providers: [{provide: PreloadingStrategy, useExisting: PreloadAllModules}]
});
});
it('should work',
fakeAsync(inject(
[NgModuleFactoryLoader, RouterPreloader, Router],
(loader: SpyNgModuleFactoryLoader, preloader: RouterPreloader, router: Router) => {
const events: Array<RouteConfigLoadStart|RouteConfigLoadEnd> = [];
router.events.subscribe(e => {
if (e instanceof RouteConfigLoadEnd || e instanceof RouteConfigLoadStart) {
events.push(e);
}
});
loader.stubbedModules = {
expected: LoadedModule1,
expected2: LoadedModule2,
};
preloader.preload().subscribe(() => {});
tick();
const c = router.config;
expect(c[0].loadChildren).toEqual('expected');
const loaded: any = (<any>c[0])._loadedConfig.routes;
expect(loaded[0].path).toEqual('LoadedModule1');
const loaded2: any = (<any>loaded[0])._loadedConfig.routes;
expect(loaded2[0].path).toEqual('LoadedModule2');
expect(events.map(e => e.toString())).toEqual([
'RouteConfigLoadStart(path: lazy)',
'RouteConfigLoadEnd(path: lazy)',
'RouteConfigLoadStart(path: LoadedModule1)',
'RouteConfigLoadEnd(path: LoadedModule1)',
]);
})));
});
describe('should not load configurations with canLoad guard', () => {
@NgModule({
declarations: [LazyLoadedCmp],
imports: [RouterModule.forChild([{path: 'LoadedModule1', component: LazyLoadedCmp}])]
})
class LoadedModule {
}
beforeEach(() => {
TestBed.configureTestingModule({
imports: [RouterTestingModule.withRoutes(
[{path: 'lazy', loadChildren: 'expected', canLoad: ['someGuard']}])],
providers: [{provide: PreloadingStrategy, useExisting: PreloadAllModules}]
});
});
it('should work',
fakeAsync(inject(
[NgModuleFactoryLoader, RouterPreloader, Router],
(loader: SpyNgModuleFactoryLoader, preloader: RouterPreloader, router: Router) => {
loader.stubbedModules = {expected: LoadedModule};
preloader.preload().subscribe(() => {});
tick();
const c = router.config;
expect(!!((<any>c[0])._loadedConfig)).toBe(false);
})));
});
describe('should ignore errors', () => {
@NgModule({
declarations: [LazyLoadedCmp],
imports: [RouterModule.forChild([{path: 'LoadedModule1', component: LazyLoadedCmp}])]
})
class LoadedModule {
}
beforeEach(() => {
TestBed.configureTestingModule({
imports: [RouterTestingModule.withRoutes([
{path: 'lazy1', loadChildren: 'expected1'}, {path: 'lazy2', loadChildren: 'expected2'}
])],
providers: [{provide: PreloadingStrategy, useExisting: PreloadAllModules}]
});
});
it('should work',
fakeAsync(inject(
[NgModuleFactoryLoader, RouterPreloader, Router],
(loader: SpyNgModuleFactoryLoader, preloader: RouterPreloader, router: Router) => {
loader.stubbedModules = {expected2: LoadedModule};
preloader.preload().subscribe(() => {});
tick();
const c = router.config;
expect(!!((<any>c[0])._loadedConfig)).toBe(false);
expect(!!((<any>c[1])._loadedConfig)).toBe(true);
})));
});
});

View File

@ -0,0 +1,210 @@
/**
* @license
* Copyright Google Inc. 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/BehaviorSubject';
import {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot, advanceActivatedRoute, equalParamsAndUrlSegments} from '../src/router_state';
import {Params} from '../src/shared';
import {UrlSegment} from '../src/url_tree';
import {TreeNode} from '../src/utils/tree';
describe('RouterState & Snapshot', () => {
describe('RouterStateSnapshot', () => {
let state: RouterStateSnapshot;
let a: ActivatedRouteSnapshot;
let b: ActivatedRouteSnapshot;
let c: ActivatedRouteSnapshot;
beforeEach(() => {
a = createActivatedRouteSnapshot('a');
b = createActivatedRouteSnapshot('b');
c = createActivatedRouteSnapshot('c');
const root = new TreeNode(a, [new TreeNode(b, []), new TreeNode(c, [])]);
state = new RouterStateSnapshot('url', root);
});
it('should return first child', () => { expect(state.root.firstChild).toBe(b); });
it('should return children', () => {
const cc = state.root.children;
expect(cc[0]).toBe(b);
expect(cc[1]).toBe(c);
});
it('should return root', () => {
const b = state.root.firstChild;
expect(b.root).toBe(state.root);
});
it('should return parent', () => {
const b = state.root.firstChild;
expect(b.parent).toBe(state.root);
});
it('should return path from root', () => {
const b = state.root.firstChild;
const p = b.pathFromRoot;
expect(p[0]).toBe(state.root);
expect(p[1]).toBe(b);
});
});
describe('RouterState', () => {
let state: RouterState;
let a: ActivatedRoute;
let b: ActivatedRoute;
let c: ActivatedRoute;
beforeEach(() => {
a = createActivatedRoute('a');
b = createActivatedRoute('b');
c = createActivatedRoute('c');
const root = new TreeNode(a, [new TreeNode(b, []), new TreeNode(c, [])]);
state = new RouterState(root, <any>null);
});
it('should return first child', () => { expect(state.root.firstChild).toBe(b); });
it('should return children', () => {
const cc = state.root.children;
expect(cc[0]).toBe(b);
expect(cc[1]).toBe(c);
});
it('should return root', () => {
const b = state.root.firstChild;
expect(b.root).toBe(state.root);
});
it('should return parent', () => {
const b = state.root.firstChild;
expect(b.parent).toBe(state.root);
});
it('should return path from root', () => {
const b = state.root.firstChild;
const p = b.pathFromRoot;
expect(p[0]).toBe(state.root);
expect(p[1]).toBe(b);
});
});
describe('equalParamsAndUrlSegments', () => {
function createSnapshot(params: Params, url: UrlSegment[]): ActivatedRouteSnapshot {
const snapshot = new ActivatedRouteSnapshot(
url, params, <any>null, <any>null, <any>null, <any>null, <any>null, <any>null, <any>null,
-1, null);
snapshot._routerState = new RouterStateSnapshot('', new TreeNode(snapshot, []));
return snapshot;
}
function createSnapshotPairWithParent(
params: [Params, Params], parentParams: [Params, Params],
urls: [string, string]): [ActivatedRouteSnapshot, ActivatedRouteSnapshot] {
const snapshot1 = createSnapshot(params[0], []);
const snapshot2 = createSnapshot(params[1], []);
const snapshot1Parent = createSnapshot(parentParams[0], [new UrlSegment(urls[0], {})]);
const snapshot2Parent = createSnapshot(parentParams[1], [new UrlSegment(urls[1], {})]);
snapshot1._routerState =
new RouterStateSnapshot('', new TreeNode(snapshot1Parent, [new TreeNode(snapshot1, [])]));
snapshot2._routerState =
new RouterStateSnapshot('', new TreeNode(snapshot2Parent, [new TreeNode(snapshot2, [])]));
return [snapshot1, snapshot2];
}
it('should return false when params are different', () => {
expect(equalParamsAndUrlSegments(createSnapshot({a: 1}, []), createSnapshot({a: 2}, [])))
.toEqual(false);
});
it('should return false when urls are different', () => {
expect(equalParamsAndUrlSegments(
createSnapshot({a: 1}, [new UrlSegment('a', {})]),
createSnapshot({a: 1}, [new UrlSegment('b', {})])))
.toEqual(false);
});
it('should return true othewise', () => {
expect(equalParamsAndUrlSegments(
createSnapshot({a: 1}, [new UrlSegment('a', {})]),
createSnapshot({a: 1}, [new UrlSegment('a', {})])))
.toEqual(true);
});
it('should return false when upstream params are different', () => {
const [snapshot1, snapshot2] =
createSnapshotPairWithParent([{a: 1}, {a: 1}], [{b: 1}, {c: 1}], ['a', 'a']);
expect(equalParamsAndUrlSegments(snapshot1, snapshot2)).toEqual(false);
});
it('should return false when upstream urls are different', () => {
const [snapshot1, snapshot2] =
createSnapshotPairWithParent([{a: 1}, {a: 1}], [{b: 1}, {b: 1}], ['a', 'b']);
expect(equalParamsAndUrlSegments(snapshot1, snapshot2)).toEqual(false);
});
it('should return true when upstream urls and params are equal', () => {
const [snapshot1, snapshot2] =
createSnapshotPairWithParent([{a: 1}, {a: 1}], [{b: 1}, {b: 1}], ['a', 'a']);
expect(equalParamsAndUrlSegments(snapshot1, snapshot2)).toEqual(true);
});
});
describe('advanceActivatedRoute', () => {
let route: ActivatedRoute;
beforeEach(() => { route = createActivatedRoute('a'); });
function createSnapshot(params: Params, url: UrlSegment[]): ActivatedRouteSnapshot {
const queryParams = {};
const fragment = '';
const data = {};
const snapshot = new ActivatedRouteSnapshot(
url, params, queryParams, fragment, data, <any>null, <any>null, <any>null, <any>null, -1,
null);
const state = new RouterStateSnapshot('', new TreeNode(snapshot, []));
snapshot._routerState = state;
return snapshot;
}
it('should call change observers', () => {
const firstPlace = createSnapshot({a: 1}, []);
const secondPlace = createSnapshot({a: 2}, []);
route.snapshot = firstPlace;
route._futureSnapshot = secondPlace;
let hasSeenDataChange = false;
route.data.forEach((data) => { hasSeenDataChange = true; });
advanceActivatedRoute(route);
expect(hasSeenDataChange).toEqual(true);
});
});
});
function createActivatedRouteSnapshot(cmp: string) {
return new ActivatedRouteSnapshot(
<any>null, <any>null, <any>null, <any>null, <any>null, <any>null, <any>cmp, <any>null,
<any>null, -1, null);
}
function createActivatedRoute(cmp: string) {
return new ActivatedRoute(
new BehaviorSubject([new UrlSegment('', {})]), new BehaviorSubject({}), <any>null, <any>null,
new BehaviorSubject({}), <any>null, <any>cmp, <any>null);
}

View File

@ -0,0 +1,45 @@
/**
* @license
* Copyright Google Inc. 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 {fakeAsync, tick} from '@angular/core/testing';
import {SpyNgModuleFactoryLoader} from '../testing/router_testing_module';
describe('SpyNgModuleFactoryLoader', () => {
it('should invoke the compiler when the setter is called', () => {
const expected = Promise.resolve('returned');
const compiler: any = {compileModuleAsync: () => {}};
spyOn(compiler, 'compileModuleAsync').and.returnValue(expected);
const r = new SpyNgModuleFactoryLoader(<any>compiler);
r.stubbedModules = {'one': 'someModule'};
expect(compiler.compileModuleAsync).toHaveBeenCalledWith('someModule');
expect(r.stubbedModules['one']).toBe(expected);
});
it('should return the created promise', () => {
const expected = Promise.resolve('returned');
const compiler: any = {compileModuleAsync: () => expected};
const r = new SpyNgModuleFactoryLoader(<any>compiler);
r.stubbedModules = {'one': 'someModule'};
expect(r.load('one')).toBe(expected);
});
it('should return a rejected promise when given an invalid path', fakeAsync(() => {
const r = new SpyNgModuleFactoryLoader(<any>null);
let error: any = null;
r.load('two').catch(e => error = e);
tick();
expect(error).toEqual(new Error('Cannot find module two'));
}));
});

View File

@ -0,0 +1,243 @@
/**
* @license
* Copyright Google Inc. 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 {PRIMARY_OUTLET} from '../src/shared';
import {DefaultUrlSerializer, UrlSegmentGroup, encode, serializePath} from '../src/url_tree';
describe('url serializer', () => {
const url = new DefaultUrlSerializer();
it('should parse the root url', () => {
const tree = url.parse('/');
expectSegment(tree.root, '');
expect(url.serialize(tree)).toEqual('/');
});
it('should parse non-empty urls', () => {
const tree = url.parse('one/two');
expectSegment(tree.root.children[PRIMARY_OUTLET], 'one/two');
expect(url.serialize(tree)).toEqual('/one/two');
});
it('should parse multiple secondary segments', () => {
const tree = url.parse('/one/two(left:three//right:four)');
expectSegment(tree.root.children[PRIMARY_OUTLET], 'one/two');
expectSegment(tree.root.children['left'], 'three');
expectSegment(tree.root.children['right'], 'four');
expect(url.serialize(tree)).toEqual('/one/two(left:three//right:four)');
});
it('should parse top-level nodes with only secondary segment', () => {
const tree = url.parse('/(left:one)');
expect(tree.root.numberOfChildren).toEqual(1);
expectSegment(tree.root.children['left'], 'one');
expect(url.serialize(tree)).toEqual('/(left:one)');
});
it('should parse nodes with only secondary segment', () => {
const tree = url.parse('/one/(left:two)');
const one = tree.root.children[PRIMARY_OUTLET];
expectSegment(one, 'one', true);
expect(one.numberOfChildren).toEqual(1);
expectSegment(one.children['left'], 'two');
expect(url.serialize(tree)).toEqual('/one/(left:two)');
});
it('should not parse empty path segments with params', () => {
expect(() => url.parse('/one/two/(;a=1//right:;b=2)'))
.toThrowError(/Empty path url segment cannot have parameters/);
});
it('should parse scoped secondary segments', () => {
const tree = url.parse('/one/(two//left:three)');
const primary = tree.root.children[PRIMARY_OUTLET];
expectSegment(primary, 'one', true);
expectSegment(primary.children[PRIMARY_OUTLET], 'two');
expectSegment(primary.children['left'], 'three');
expect(url.serialize(tree)).toEqual('/one/(two//left:three)');
});
it('should parse scoped secondary segments with unscoped ones', () => {
const tree = url.parse('/one/(two//left:three)(right:four)');
const primary = tree.root.children[PRIMARY_OUTLET];
expectSegment(primary, 'one', true);
expectSegment(primary.children[PRIMARY_OUTLET], 'two');
expectSegment(primary.children['left'], 'three');
expectSegment(tree.root.children['right'], 'four');
expect(url.serialize(tree)).toEqual('/one/(two//left:three)(right:four)');
});
it('should parse secondary segments that have children', () => {
const tree = url.parse('/one(left:two/three)');
expectSegment(tree.root.children[PRIMARY_OUTLET], 'one');
expectSegment(tree.root.children['left'], 'two/three');
expect(url.serialize(tree)).toEqual('/one(left:two/three)');
});
it('should parse an empty secondary segment group', () => {
const tree = url.parse('/one()');
expectSegment(tree.root.children[PRIMARY_OUTLET], 'one');
expect(url.serialize(tree)).toEqual('/one');
});
it('should parse key-value matrix params', () => {
const tree = url.parse('/one;a=11a;b=11b(left:two;c=22//right:three;d=33)');
expectSegment(tree.root.children[PRIMARY_OUTLET], 'one;a=11a;b=11b');
expectSegment(tree.root.children['left'], 'two;c=22');
expectSegment(tree.root.children['right'], 'three;d=33');
expect(url.serialize(tree)).toEqual('/one;a=11a;b=11b(left:two;c=22//right:three;d=33)');
});
it('should parse key only matrix params', () => {
const tree = url.parse('/one;a');
expectSegment(tree.root.children[PRIMARY_OUTLET], 'one;a=');
expect(url.serialize(tree)).toEqual('/one;a=');
});
it('should parse query params (root)', () => {
const tree = url.parse('/?a=1&b=2');
expect(tree.root.children).toEqual({});
expect(tree.queryParams).toEqual({a: '1', b: '2'});
expect(url.serialize(tree)).toEqual('/?a=1&b=2');
});
it('should parse query params', () => {
const tree = url.parse('/one?a=1&b=2');
expect(tree.queryParams).toEqual({a: '1', b: '2'});
});
it('should parse query params when with parenthesis', () => {
const tree = url.parse('/one?a=(11)&b=(22)');
expect(tree.queryParams).toEqual({a: '(11)', b: '(22)'});
});
it('should parse query params when with slashes', () => {
const tree = url.parse('/one?a=1/2&b=3/4');
expect(tree.queryParams).toEqual({a: '1/2', b: '3/4'});
});
it('should parse key only query params', () => {
const tree = url.parse('/one?a');
expect(tree.queryParams).toEqual({a: ''});
});
it('should parse a value-empty query param', () => {
const tree = url.parse('/one?a=');
expect(tree.queryParams).toEqual({a: ''});
});
it('should parse value-empty query params', () => {
const tree = url.parse('/one?a=&b=');
expect(tree.queryParams).toEqual({a: '', b: ''});
});
it('should serializer query params', () => {
const tree = url.parse('/one?a');
expect(url.serialize(tree)).toEqual('/one?a=');
});
it('should handle multiple query params of the same name into an array', () => {
const tree = url.parse('/one?a=foo&a=bar&a=swaz');
expect(tree.queryParams).toEqual({a: ['foo', 'bar', 'swaz']});
});
it('should parse fragment', () => {
const tree = url.parse('/one#two');
expect(tree.fragment).toEqual('two');
expect(url.serialize(tree)).toEqual('/one#two');
});
it('should parse fragment (root)', () => {
const tree = url.parse('/#one');
expectSegment(tree.root, '');
expect(url.serialize(tree)).toEqual('/#one');
});
it('should parse empty fragment', () => {
const tree = url.parse('/one#');
expect(tree.fragment).toEqual('');
expect(url.serialize(tree)).toEqual('/one#');
});
describe('encoding/decoding', () => {
it('should encode/decode path segments and parameters', () => {
const u =
`/${encode("one two")};${encode("p 1")}=${encode("v 1")};${encode("p 2")}=${encode("v 2")}`;
const tree = url.parse(u);
expect(tree.root.children[PRIMARY_OUTLET].segments[0].path).toEqual('one two');
expect(tree.root.children[PRIMARY_OUTLET].segments[0].parameters)
.toEqual({['p 1']: 'v 1', ['p 2']: 'v 2'});
expect(url.serialize(tree)).toEqual(u);
});
it('should encode/decode "slash" in path segments and parameters', () => {
const u = `/${encode("one/two")};${encode("p/1")}=${encode("v/1")}/three`;
const tree = url.parse(u);
expect(tree.root.children[PRIMARY_OUTLET].segments[0].path).toEqual('one/two');
expect(tree.root.children[PRIMARY_OUTLET].segments[0].parameters).toEqual({['p/1']: 'v/1'});
expect(url.serialize(tree)).toEqual(u);
});
it('should encode/decode query params', () => {
const u = `/one?${encode("p 1")}=${encode("v 1")}&${encode("p 2")}=${encode("v 2")}`;
const tree = url.parse(u);
expect(tree.queryParams).toEqual({['p 1']: 'v 1', ['p 2']: 'v 2'});
expect(url.serialize(tree)).toEqual(u);
});
it('should encode/decode fragment', () => {
const u = `/one#${encodeURI("one two=three four")}`;
const tree = url.parse(u);
expect(tree.fragment).toEqual('one two=three four');
expect(url.serialize(tree)).toEqual(u);
});
});
describe('error handling', () => {
it('should throw when invalid characters inside children', () => {
expect(() => url.parse('/one/(left#one)'))
.toThrowError('Cannot parse url \'/one/(left#one)\'');
});
it('should throw when missing closing )', () => {
expect(() => url.parse('/one/(left')).toThrowError('Cannot parse url \'/one/(left\'');
});
});
});
function expectSegment(
segment: UrlSegmentGroup, expected: string, hasChildren: boolean = false): void {
if (segment.segments.filter(s => s.path === '').length > 0) {
throw new Error(`UrlSegments cannot be empty ${segment.segments}`);
}
const p = segment.segments.map(p => serializePath(p)).join('/');
expect(p).toEqual(expected);
expect(Object.keys(segment.children).length > 0).toEqual(hasChildren);
}

View File

@ -0,0 +1,135 @@
/**
* @license
* Copyright Google Inc. 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 {DefaultUrlSerializer, containsTree} from '../src/url_tree';
describe('UrlTree', () => {
const serializer = new DefaultUrlSerializer();
describe('containsTree', () => {
describe('exact = true', () => {
it('should return true when two tree are the same', () => {
const url = '/one/(one//left:three)(right:four)';
const t1 = serializer.parse(url);
const t2 = serializer.parse(url);
expect(containsTree(t1, t2, true)).toBe(true);
expect(containsTree(t2, t1, true)).toBe(true);
});
it('should return true when queryParams are the same', () => {
const t1 = serializer.parse('/one/two?test=1&page=5');
const t2 = serializer.parse('/one/two?test=1&page=5');
expect(containsTree(t1, t2, true)).toBe(true);
});
it('should return false when queryParams are not the same', () => {
const t1 = serializer.parse('/one/two?test=1&page=5');
const t2 = serializer.parse('/one/two?test=1');
expect(containsTree(t1, t2, true)).toBe(false);
});
it('should return false when containee is missing queryParams', () => {
const t1 = serializer.parse('/one/two?page=5');
const t2 = serializer.parse('/one/two');
expect(containsTree(t1, t2, true)).toBe(false);
});
it('should return false when paths are not the same', () => {
const t1 = serializer.parse('/one/two(right:three)');
const t2 = serializer.parse('/one/two2(right:three)');
expect(containsTree(t1, t2, true)).toBe(false);
});
it('should return false when container has an extra child', () => {
const t1 = serializer.parse('/one/two(right:three)');
const t2 = serializer.parse('/one/two');
expect(containsTree(t1, t2, true)).toBe(false);
});
it('should return false when containee has an extra child', () => {
const t1 = serializer.parse('/one/two');
const t2 = serializer.parse('/one/two(right:three)');
expect(containsTree(t1, t2, true)).toBe(false);
});
});
describe('exact = false', () => {
it('should return true when containee is missing a segment', () => {
const t1 = serializer.parse('/one/(two//left:three)(right:four)');
const t2 = serializer.parse('/one/(two//left:three)');
expect(containsTree(t1, t2, false)).toBe(true);
});
it('should return true when containee is missing some paths', () => {
const t1 = serializer.parse('/one/two/three');
const t2 = serializer.parse('/one/two');
expect(containsTree(t1, t2, false)).toBe(true);
});
it('should return true container has its paths splitted into multiple segments', () => {
const t1 = serializer.parse('/one/(two//left:three)');
const t2 = serializer.parse('/one/two');
expect(containsTree(t1, t2, false)).toBe(true);
});
it('should return false when containee has extra segments', () => {
const t1 = serializer.parse('/one/two');
const t2 = serializer.parse('/one/(two//left:three)');
expect(containsTree(t1, t2, false)).toBe(false);
});
it('should return false containee has segments that the container does not have', () => {
const t1 = serializer.parse('/one/(two//left:three)');
const t2 = serializer.parse('/one/(two//right:four)');
expect(containsTree(t1, t2, false)).toBe(false);
});
it('should return false when containee has extra paths', () => {
const t1 = serializer.parse('/one');
const t2 = serializer.parse('/one/two');
expect(containsTree(t1, t2, false)).toBe(false);
});
it('should return true when queryParams are the same', () => {
const t1 = serializer.parse('/one/two?test=1&page=5');
const t2 = serializer.parse('/one/two?test=1&page=5');
expect(containsTree(t1, t2, false)).toBe(true);
});
it('should return true when container contains containees queryParams', () => {
const t1 = serializer.parse('/one/two?test=1&u=5');
const t2 = serializer.parse('/one/two?u=5');
expect(containsTree(t1, t2, false)).toBe(true);
});
it('should return true when containee does not have queryParams', () => {
const t1 = serializer.parse('/one/two?page=5');
const t2 = serializer.parse('/one/two');
expect(containsTree(t1, t2, false)).toBe(true);
});
it('should return false when containee has but container does not have queryParams', () => {
const t1 = serializer.parse('/one/two');
const t2 = serializer.parse('/one/two?page=1');
expect(containsTree(t1, t2, false)).toBe(false);
});
it('should return false when containee has different queryParams', () => {
const t1 = serializer.parse('/one/two?page=5');
const t2 = serializer.parse('/one/two?test=1');
expect(containsTree(t1, t2, false)).toBe(false);
});
it('should return false when containee has more queryParams than container', () => {
const t1 = serializer.parse('/one/two?page=5');
const t2 = serializer.parse('/one/two?page=5&test=1');
expect(containsTree(t1, t2, false)).toBe(false);
});
});
});
});

View File

@ -0,0 +1,53 @@
/**
* @license
* Copyright Google Inc. 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 {Tree, TreeNode} from '../../src/utils/tree';
describe('tree', () => {
it('should return the root of the tree', () => {
const t = new Tree<any>(new TreeNode<number>(1, []));
expect(t.root).toEqual(1);
});
it('should return the parent of a node', () => {
const t = new Tree<any>(new TreeNode<number>(1, [new TreeNode<number>(2, [])]));
expect(t.parent(1)).toEqual(null);
expect(t.parent(2)).toEqual(1);
});
it('should return the parent of a node (second child)', () => {
const t = new Tree<any>(
new TreeNode<number>(1, [new TreeNode<number>(2, []), new TreeNode<number>(3, [])]));
expect(t.parent(1)).toEqual(null);
expect(t.parent(3)).toEqual(1);
});
it('should return the children of a node', () => {
const t = new Tree<any>(new TreeNode<number>(1, [new TreeNode<number>(2, [])]));
expect(t.children(1)).toEqual([2]);
expect(t.children(2)).toEqual([]);
});
it('should return the first child of a node', () => {
const t = new Tree<any>(new TreeNode<number>(1, [new TreeNode<number>(2, [])]));
expect(t.firstChild(1)).toEqual(2);
expect(t.firstChild(2)).toEqual(null);
});
it('should return the siblings of a node', () => {
const t = new Tree<any>(
new TreeNode<number>(1, [new TreeNode<number>(2, []), new TreeNode<number>(3, [])]));
expect(t.siblings(2)).toEqual([3]);
expect(t.siblings(1)).toEqual([]);
});
it('should return the path to the root', () => {
const t = new Tree<any>(new TreeNode<number>(1, [new TreeNode<number>(2, [])]));
expect(t.pathFromRoot(2)).toEqual([1, 2]);
});
});