diff --git a/modules/@angular/router/karma-test-shim.js b/modules/@angular/router/karma-test-shim.js index b94bb2f8de..089d9c1ae9 100644 --- a/modules/@angular/router/karma-test-shim.js +++ b/modules/@angular/router/karma-test-shim.js @@ -1,5 +1,5 @@ /*global jasmine, __karma__, window*/ -Error.stackTraceLimit = 10; +Error.stackTraceLimit = 5; jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; __karma__.loaded = function () { diff --git a/modules/@angular/router/src/apply_redirects.ts b/modules/@angular/router/src/apply_redirects.ts index c5ee5541c3..cb9bf62104 100644 --- a/modules/@angular/router/src/apply_redirects.ts +++ b/modules/@angular/router/src/apply_redirects.ts @@ -13,6 +13,7 @@ import {of } from 'rxjs/observable/of'; import {Route, RouterConfig} from './config'; import {PRIMARY_OUTLET} from './shared'; import {UrlPathWithParams, UrlSegment, UrlTree, mapChildren} from './url_tree'; +import {merge} from './utils/collection'; class NoMatch { constructor(public segment: UrlSegment = null) {} @@ -38,7 +39,10 @@ export function applyRedirects(urlTree: UrlTree, config: RouterConfig): Observab } } -function createUrlTree(urlTree: UrlTree, root: UrlSegment): Observable { +function createUrlTree(urlTree: UrlTree, rootCandidate: UrlSegment): Observable { + const root = rootCandidate.pathsWithParams.length > 0 ? + new UrlSegment([], {[PRIMARY_OUTLET]: rootCandidate}) : + rootCandidate; return of (new UrlTree(root, urlTree.queryParams, urlTree.fragment)); } @@ -70,10 +74,10 @@ function expandPathsWithParams( function expandPathsWithParamsAgainstRoute( segment: UrlSegment, routes: Route[], route: Route, paths: UrlPathWithParams[], outlet: string, allowRedirects: boolean): UrlSegment { - if ((route.outlet ? route.outlet : PRIMARY_OUTLET) !== outlet) throw new NoMatch(); - if (route.redirectTo && !allowRedirects) throw new NoMatch(); + if (getOutlet(route) !== outlet) throw new NoMatch(); + if (route.redirectTo !== undefined && !allowRedirects) throw new NoMatch(); - if (route.redirectTo) { + if (route.redirectTo !== undefined) { return expandPathsWithParamsAgainstRouteUsingRedirect(segment, routes, route, paths, outlet); } else { return matchPathsWithParamsAgainstRoute(segment, route, paths); @@ -115,22 +119,23 @@ function expandRegularPathWithParamsAgainstRouteUsingRedirect( } function matchPathsWithParamsAgainstRoute( - segment: UrlSegment, route: Route, paths: UrlPathWithParams[]): UrlSegment { + rawSegment: UrlSegment, route: Route, paths: UrlPathWithParams[]): UrlSegment { if (route.path === '**') { return new UrlSegment(paths, {}); } else { - const {consumedPaths, lastChild} = match(segment, route, paths); + const {consumedPaths, lastChild} = match(rawSegment, route, paths); const childConfig = route.children ? route.children : []; - const slicedPath = paths.slice(lastChild); + const rawSlicedPath = paths.slice(lastChild); - if (childConfig.length === 0 && slicedPath.length === 0) { - return new UrlSegment(consumedPaths, {}); + const {segment, slicedPath} = split(rawSegment, consumedPaths, rawSlicedPath, childConfig); - // TODO: check that the right segment is present - } else if (slicedPath.length === 0 && segment.hasChildren()) { + if (slicedPath.length === 0 && segment.hasChildren()) { const children = expandSegmentChildren(childConfig, segment); return new UrlSegment(consumedPaths, children); + } else if (childConfig.length === 0 && slicedPath.length === 0) { + return new UrlSegment(consumedPaths, {}); + } else { const cs = expandPathsWithParams(segment, childConfig, slicedPath, PRIMARY_OUTLET, true); return new UrlSegment(consumedPaths.concat(cs.pathsWithParams), cs.children); @@ -183,12 +188,11 @@ function match(segment: UrlSegment, route: Route, paths: UrlPathWithParams[]): { function applyRedirectCommands( paths: UrlPathWithParams[], redirectTo: string, posParams: {[k: string]: UrlPathWithParams}): UrlPathWithParams[] { - if (redirectTo.startsWith('/')) { - const parts = redirectTo.substring(1).split('/'); - return createPaths(redirectTo, parts, paths, posParams); + const r = redirectTo.startsWith('/') ? redirectTo.substring(1) : redirectTo; + if (r === '') { + return []; } else { - const parts = redirectTo.split('/'); - return createPaths(redirectTo, parts, paths, posParams); + return createPaths(redirectTo, r.split('/'), paths, posParams); } } @@ -219,3 +223,72 @@ function findOrCreatePath(part: string, paths: UrlPathWithParams[]): UrlPathWith return new UrlPathWithParams(part, {}); } } + + +function split( + segment: UrlSegment, consumedPaths: UrlPathWithParams[], slicedPath: UrlPathWithParams[], + config: Route[]) { + if (slicedPath.length > 0 && + containsEmptyPathRedirectsWithNamedOutlets(segment, slicedPath, config)) { + const s = new UrlSegment( + consumedPaths, + createChildrenForEmptyPaths(config, new UrlSegment(slicedPath, segment.children))); + return {segment: s, slicedPath: []}; + + } else if (slicedPath.length === 0 && containsEmptyPathRedirects(segment, slicedPath, config)) { + const s = new UrlSegment( + segment.pathsWithParams, + addEmptyPathsToChildrenIfNeeded(segment, slicedPath, config, segment.children)); + return {segment: s, slicedPath}; + + } else { + return {segment, slicedPath}; + } +} + +function addEmptyPathsToChildrenIfNeeded( + segment: UrlSegment, slicedPath: UrlPathWithParams[], routes: Route[], + children: {[name: string]: UrlSegment}): {[name: string]: UrlSegment} { + const res: {[name: string]: UrlSegment} = {}; + for (let r of routes) { + if (emptyPathRedirect(segment, slicedPath, r) && !children[getOutlet(r)]) { + res[getOutlet(r)] = new UrlSegment([], {}); + } + } + return merge(children, res); +} + +function createChildrenForEmptyPaths( + routes: Route[], primarySegment: UrlSegment): {[name: string]: UrlSegment} { + const res: {[name: string]: UrlSegment} = {}; + res[PRIMARY_OUTLET] = primarySegment; + for (let r of routes) { + if (r.path === '') { + res[getOutlet(r)] = new UrlSegment([], {}); + } + } + return res; +} + +function containsEmptyPathRedirectsWithNamedOutlets( + segment: UrlSegment, slicedPath: UrlPathWithParams[], routes: Route[]): boolean { + return routes + .filter( + r => emptyPathRedirect(segment, slicedPath, r) && getOutlet(r) !== PRIMARY_OUTLET) + .length > 0; +} + +function containsEmptyPathRedirects( + segment: UrlSegment, slicedPath: UrlPathWithParams[], routes: Route[]): boolean { + return routes.filter(r => emptyPathRedirect(segment, slicedPath, r)).length > 0; +} + +function emptyPathRedirect( + segment: UrlSegment, slicedPath: UrlPathWithParams[], r: Route): boolean { + if ((segment.hasChildren() || slicedPath.length > 0) && r.terminal) return false; + return r.path === '' && r.redirectTo !== undefined; +} + +function getOutlet(route: Route): string { + return route.outlet ? route.outlet : PRIMARY_OUTLET; +} \ No newline at end of file diff --git a/modules/@angular/router/src/recognize.ts b/modules/@angular/router/src/recognize.ts index 38aa33e483..9350da374b 100644 --- a/modules/@angular/router/src/recognize.ts +++ b/modules/@angular/router/src/recognize.ts @@ -48,8 +48,7 @@ function processSegment(config: Route[], segment: UrlSegment, extraParams: Param if (segment.pathsWithParams.length === 0 && segment.hasChildren()) { return processSegmentChildren(config, segment, extraParams); } else { - return [processPathsWithParams( - config, segment, 0, segment.pathsWithParams, extraParams, outlet)]; + return processPathsWithParams(config, segment, 0, segment.pathsWithParams, extraParams, outlet); } } @@ -72,7 +71,7 @@ function sortActivatedRouteSnapshots(nodes: TreeNode[]): function processPathsWithParams( config: Route[], segment: UrlSegment, pathIndex: number, paths: UrlPathWithParams[], - extraParams: Params, outlet: string): TreeNode { + extraParams: Params, outlet: string): TreeNode[] { for (let r of config) { try { return processPathsWithParamsAgainstRoute(r, segment, pathIndex, paths, extraParams, outlet); @@ -84,8 +83,8 @@ function processPathsWithParams( } function processPathsWithParamsAgainstRoute( - route: Route, segment: UrlSegment, pathIndex: number, paths: UrlPathWithParams[], - parentExtraParams: Params, outlet: string): TreeNode { + route: Route, rawSegment: UrlSegment, pathIndex: number, paths: UrlPathWithParams[], + parentExtraParams: Params, outlet: string): TreeNode[] { if (route.redirectTo) throw new NoMatch(); if ((route.outlet ? route.outlet : PRIMARY_OUTLET) !== outlet) throw new NoMatch(); @@ -93,30 +92,33 @@ function processPathsWithParamsAgainstRoute( if (route.path === '**') { const params = paths.length > 0 ? last(paths).parameters : {}; const snapshot = new ActivatedRouteSnapshot( - paths, merge(parentExtraParams, params), outlet, route.component, route, segment, -1); - return new TreeNode(snapshot, []); + paths, merge(parentExtraParams, params), outlet, route.component, route, + getSourceSegment(rawSegment), getPathIndexShift(rawSegment) - 1); + return [new TreeNode(snapshot, [])]; } const {consumedPaths, parameters, extraParams, lastChild} = - match(segment, route, paths, parentExtraParams); - const snapshot = new ActivatedRouteSnapshot( - consumedPaths, parameters, outlet, route.component, route, segment, - pathIndex + lastChild - 1); - const slicedPath = paths.slice(lastChild); + match(rawSegment, route, paths, parentExtraParams); + const rawSlicedPath = paths.slice(lastChild); const childConfig = route.children ? route.children : []; - if (childConfig.length === 0 && slicedPath.length === 0) { - return new TreeNode(snapshot, []); + const {segment, slicedPath} = split(rawSegment, consumedPaths, rawSlicedPath, childConfig); - // TODO: check that the right segment is present - } else if (slicedPath.length === 0 && segment.hasChildren()) { + const snapshot = new ActivatedRouteSnapshot( + consumedPaths, parameters, outlet, route.component, route, getSourceSegment(rawSegment), + getPathIndexShift(rawSegment) + pathIndex + lastChild - 1); + + if (slicedPath.length === 0 && segment.hasChildren()) { const children = processSegmentChildren(childConfig, segment, extraParams); - return new TreeNode(snapshot, children); + return [new TreeNode(snapshot, children)]; + + } else if (childConfig.length === 0 && slicedPath.length === 0) { + return [new TreeNode(snapshot, [])]; } else { - const child = processPathsWithParams( + const children = processPathsWithParams( childConfig, segment, pathIndex + lastChild, slicedPath, extraParams, PRIMARY_OUTLET); - return new TreeNode(snapshot, [child]); + return [new TreeNode(snapshot, children)]; } } @@ -173,4 +175,103 @@ function checkOutletNameUniqueness(nodes: TreeNode[]): v } names[n.value.outlet] = n.value; }); +} + +function getSourceSegment(segment: UrlSegment): UrlSegment { + let s = segment; + while (s._sourceSegment) { + s = s._sourceSegment; + } + return s; +} + +function getPathIndexShift(segment: UrlSegment): number { + let s = segment; + let res = 0; + while (s._sourceSegment) { + s = s._sourceSegment; + res += segment._pathIndexShift; + } + return res; +} + +function split( + segment: UrlSegment, consumedPaths: UrlPathWithParams[], slicedPath: UrlPathWithParams[], + config: Route[]) { + if (slicedPath.length > 0 && + containsEmptyPathMatchesWithNamedOutlets(segment, slicedPath, config)) { + const s = new UrlSegment( + consumedPaths, + createChildrenForEmptyPaths( + segment, consumedPaths, config, new UrlSegment(slicedPath, segment.children))); + s._sourceSegment = segment; + s._pathIndexShift = 0; + return {segment: s, slicedPath: []}; + + } else if (slicedPath.length === 0 && containsEmptyPathMatches(segment, slicedPath, config)) { + const s = new UrlSegment( + segment.pathsWithParams, + addEmptyPathsToChildrenIfNeeded(segment, slicedPath, config, segment.children)); + s._sourceSegment = segment; + s._pathIndexShift = 0; + return {segment: s, slicedPath}; + + } else { + return {segment, slicedPath}; + } +} + +function addEmptyPathsToChildrenIfNeeded( + segment: UrlSegment, slicedPath: UrlPathWithParams[], routes: Route[], + children: {[name: string]: UrlSegment}): {[name: string]: UrlSegment} { + const res: {[name: string]: UrlSegment} = {}; + for (let r of routes) { + if (emptyPathMatch(segment, slicedPath, r) && !children[getOutlet(r)]) { + const s = new UrlSegment([], {}); + s._sourceSegment = segment; + s._pathIndexShift = segment.pathsWithParams.length; + res[getOutlet(r)] = s; + } + } + return merge(children, res); +} + +function createChildrenForEmptyPaths( + segment: UrlSegment, consumedPaths: UrlPathWithParams[], routes: Route[], + primarySegment: UrlSegment): {[name: string]: UrlSegment} { + const res: {[name: string]: UrlSegment} = {}; + res[PRIMARY_OUTLET] = primarySegment; + primarySegment._sourceSegment = segment; + primarySegment._pathIndexShift = consumedPaths.length; + + for (let r of routes) { + if (r.path === '') { + const s = new UrlSegment([], {}); + s._sourceSegment = segment; + s._pathIndexShift = consumedPaths.length; + res[getOutlet(r)] = s; + } + } + return res; +} + +function containsEmptyPathMatchesWithNamedOutlets( + segment: UrlSegment, slicedPath: UrlPathWithParams[], routes: Route[]): boolean { + return routes + .filter(r => emptyPathMatch(segment, slicedPath, r) && getOutlet(r) !== PRIMARY_OUTLET) + .length > 0; +} + +function containsEmptyPathMatches( + segment: UrlSegment, slicedPath: UrlPathWithParams[], routes: Route[]): boolean { + return routes.filter(r => emptyPathMatch(segment, slicedPath, r)).length > 0; +} + +function emptyPathMatch(segment: UrlSegment, slicedPath: UrlPathWithParams[], r: Route): boolean { + if ((segment.hasChildren() || slicedPath.length > 0) && r.terminal) return false; + return r.path === '' && r.redirectTo === undefined; +} + +function getOutlet(route: Route): string { + return route.outlet ? route.outlet : PRIMARY_OUTLET; } \ No newline at end of file diff --git a/modules/@angular/router/src/url_tree.ts b/modules/@angular/router/src/url_tree.ts index 847a472cc3..2631c8fc40 100644 --- a/modules/@angular/router/src/url_tree.ts +++ b/modules/@angular/router/src/url_tree.ts @@ -75,6 +75,16 @@ export class UrlTree { } export class UrlSegment { + /** + * @internal + */ + _sourceSegment: UrlSegment; + + /** + * @internal + */ + _pathIndexShift: number; + public parent: UrlSegment = null; constructor( public pathsWithParams: UrlPathWithParams[], public children: {[key: string]: UrlSegment}) { @@ -306,7 +316,11 @@ class UrlParser { } parsePathWithParams(): UrlPathWithParams { - let path = matchPathWithParams(this.remaining); + const path = matchPathWithParams(this.remaining); + if (path === '' && this.peekStartsWith(';')) { + throw new Error(`Empty path url segment cannot have parameters: '${this.remaining}'.`); + } + this.capture(path); let matrixParams: {[key: string]: any} = {}; if (this.peekStartsWith(';')) { diff --git a/modules/@angular/router/test/apply_redirects.spec.ts b/modules/@angular/router/test/apply_redirects.spec.ts index 2790f10a98..59314345ee 100644 --- a/modules/@angular/router/test/apply_redirects.spec.ts +++ b/modules/@angular/router/test/apply_redirects.spec.ts @@ -73,95 +73,37 @@ describe('applyRedirects', () => { 'c/b', (t: UrlTree) => { compareTrees(t, tree('a/b')); }); }); - it('should redirect empty path', () => { + it('should support redirects with both main and aux', () => { checkRedirect( - [ - { - path: 'a', - component: ComponentA, - children: [ - {path: 'b', component: ComponentB}, - ] - }, - {path: '', redirectTo: 'a'} - ], - 'b', (t: UrlTree) => { compareTrees(t, tree('a/b')); }); + [{ + 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 redirect empty path (global redirect)', () => { + it('should support redirects with both main and aux (with a nested redirect)', () => { checkRedirect( - [ - { - path: 'a', - component: ComponentA, - children: [ - {path: 'b', component: ComponentB}, - ] - }, - {path: '', redirectTo: '/a/b'} - ], - '', (t: UrlTree) => { compareTrees(t, tree('a/b')); }); - }); + [{ + path: 'a', + children: [ + {path: 'bb', component: ComponentB}, {path: 'b', redirectTo: 'bb'}, - xit('should support nested redirects', () => { - checkRedirect( - [ - { - path: 'a', - component: ComponentA, - children: [{path: 'b', component: ComponentB}, {path: '', redirectTo: 'b'}] - }, - {path: '', redirectTo: 'a'} - ], - '', (t: UrlTree) => { compareTrees(t, tree('a/b')); }); - }); - - xit('should support nested redirects (when redirected to an empty path)', () => { - checkRedirect( - [ - { - path: '', - component: ComponentA, - children: [{path: 'b', component: ComponentB}, {path: '', redirectTo: 'b'}] - }, - {path: 'a', redirectTo: ''} - ], - 'a', (t: UrlTree) => { compareTrees(t, tree('b')); }); - }); - - xit('should support redirects with both main and aux', () => { - checkRedirect( - [ - { - path: 'a', - children: [ - {path: 'b', component: ComponentB}, {path: '', redirectTo: 'b'}, - - {path: 'c', component: ComponentC, outlet: 'aux'}, - {path: '', redirectTo: 'c', outlet: 'aux'} - ] - }, - {path: 'a', redirectTo: ''} - ], - 'a', (t: UrlTree) => { compareTrees(t, tree('a/(b//aux:c)')); }); - }); - - it('should redirect empty path route only when terminal', () => { - const config = [ - { - path: 'a', - component: ComponentA, - children: [ - {path: 'b', component: ComponentB}, - ] - }, - {path: '', redirectTo: 'a', terminal: true} - ]; - - applyRedirects(tree('b'), config) - .subscribe( - (_) => { throw 'Should not be reached'; }, - e => { expect(e.message).toEqual('Cannot match any routes: \'b\''); }); + { + 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', () => { @@ -185,6 +127,187 @@ describe('applyRedirects', () => { ], '/a/b/1', (t: UrlTree) => { compareTrees(t, tree('/global/1')); }); }); + + 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 (global 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 = [ + { + path: 'a', + component: ComponentA, + children: [ + {path: 'b', component: ComponentB}, + ] + }, + {path: '', redirectTo: 'a', terminal: true} + ]; + + applyRedirects(tree('b'), config) + .subscribe( + (_) => { throw 'Should not be reached'; }, + e => { expect(e.message).toEqual('Cannot match any routes: \'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: '', terminal: true, 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: '', terminal: true, 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 = [{ + 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: '', terminal: true, redirectTo: 'c', outlet: 'aux'} + ] + }]; + + applyRedirects(tree('a/(d//aux:e)'), config) + .subscribe( + (_) => { throw 'Should not be reached'; }, + e => { expect(e.message).toEqual('Cannot match any routes: \'a\''); }); + }); + }); + }); }); function checkRedirect(config: RouterConfig, url: string, callback: any): void { diff --git a/modules/@angular/router/test/recognize.spec.ts b/modules/@angular/router/test/recognize.spec.ts index 3aad6a45c4..8bd5a61e10 100644 --- a/modules/@angular/router/test/recognize.spec.ts +++ b/modules/@angular/router/test/recognize.spec.ts @@ -107,20 +107,6 @@ describe('recognize', () => { }); }); - xit('should handle nested 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 handle non top-level secondary routes', () => { checkRecognize( [ @@ -168,86 +154,219 @@ describe('recognize', () => { }); }); - describe('matching empty url', () => { - it('should support root index routes', () => { - recognize(RootComponent, [{path: '', component: ComponentA}], tree(''), '') - .forEach((s: RouterStateSnapshot) => { - checkActivatedRoute(s.firstChild(s.root), '', {}, ComponentA); - }); - }); + describe('empty path', () => { + describe('root', () => { + it('should work', () => { + checkRecognize([{path: '', component: ComponentA}], '', (s: RouterStateSnapshot) => { + checkActivatedRoute(s.firstChild(s.root), '', {}, ComponentA); + }); + }); - it('should support nested root index routes', () => { - recognize( - RootComponent, - [{path: '', component: ComponentA, children: [{path: '', component: ComponentB}]}], - tree(''), '') - .forEach((s: RouterStateSnapshot) => { - checkActivatedRoute(s.firstChild(s.root), '', {}, ComponentA); - checkActivatedRoute(s.firstChild(s.firstChild(s.root)), '', {}, ComponentB); - }); - }); + it('should match when terminal', () => { + checkRecognize( + [{path: '', terminal: true, component: ComponentA}], '', (s: RouterStateSnapshot) => { + checkActivatedRoute(s.firstChild(s.root), '', {}, ComponentA); + }); + }); - 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(s.firstChild(s.root)); - expect(c2._urlSegment).toBe(url.root); - expect(c2._lastPathIndex).toBe(-1); - }); - }); - - it('should support index routes', () => { - recognize( - RootComponent, - [{path: 'a', component: ComponentA, children: [{path: '', component: ComponentB}]}], - tree('a'), 'a') - .forEach((s: RouterStateSnapshot) => { - checkActivatedRoute(s.firstChild(s.root), 'a', {}, ComponentA); - checkActivatedRoute(s.firstChild(s.firstChild(s.root)), '', {}, ComponentB); - }); - }); - - it('should support index routes with children', () => { - recognize( - RootComponent, [{ - path: '', - component: ComponentA, - children: [{ + it('should not match when terminal', () => { + recognize( + RootComponent, [{ path: '', - component: ComponentB, - children: [{path: 'c/:id', component: ComponentC}] - }] - }], - tree('c/10'), 'c/10') - .forEach((s: RouterStateSnapshot) => { - checkActivatedRoute(s.firstChild(s.root), '', {}, ComponentA); - checkActivatedRoute(s.firstChild(s.firstChild(s.root)), '', {}, ComponentB); - checkActivatedRoute( - s.firstChild(s.firstChild(s.firstChild(s.root))), 'c/10', {id: '10'}, - ComponentC); - }); + terminal: true, + component: ComponentA, + children: [{path: 'b', component: ComponentB}] + }], + tree('b'), '') + .subscribe( + () => {}, (e) => { expect(e.message).toEqual('Cannot match any routes: \'b\''); }); + }); + + it('should work (nested case)', () => { + checkRecognize( + [{path: '', component: ComponentA, children: [{path: '', component: ComponentB}]}], '', + (s: RouterStateSnapshot) => { + checkActivatedRoute(s.firstChild(s.root), '', {}, ComponentA); + checkActivatedRoute(s.firstChild(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(s.firstChild(s.root)); + expect(c2._urlSegment).toBe(url.root); + expect(c2._lastPathIndex).toBe(-1); + }); + }); }); - xit('should pass parameters to every nested index route (case with non-index route)', () => { - recognize( - RootComponent, - [{path: 'a', component: ComponentA, children: [{path: '', component: ComponentB}]}], - tree('/a;a=1'), '/a;a=1') - .forEach((s: RouterStateSnapshot) => { - checkActivatedRoute(s.firstChild(s.root), 'a', {a: '1'}, ComponentA); - checkActivatedRoute(s.firstChild(s.firstChild(s.root)), '', {a: '1'}, ComponentB); - }); + 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 (terminal)', () => { + checkRecognize( + [{ + path: 'a', + component: ComponentA, + children: [ + {path: 'b', component: ComponentB}, + {path: '', terminal: true, 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); + }); + }); + }); + + 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: '', terminal: true, component: ComponentB}, + {path: '', terminal: true, 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'); + }); + }); + }); + + 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); + }); + }); }); }); @@ -307,64 +426,6 @@ describe('recognize', () => { checkActivatedRoute(c, 'c', {}, ComponentC); }); }); - - xit('should work with empty paths', () => { - checkRecognize( - [{ - path: 'p/:id', - children: [ - {path: '', component: ComponentA}, - {path: '', component: ComponentB, outlet: 'aux'} - ] - }], - 'p/11', (s: RouterStateSnapshot) => { - const p = s.firstChild(s.root); - checkActivatedRoute(p, 'p/11', {id: '11'}, undefined); - - const c = s.children(p); - console.log('lsfs', c); - checkActivatedRoute(c[0], '', {}, ComponentA); - checkActivatedRoute(c[1], '', {}, ComponentB, 'aux'); - }); - }); - - xit('should work with empty paths and params', () => { - checkRecognize( - [{ - path: 'p/:id', - children: [ - {path: '', component: ComponentA}, - {path: '', component: ComponentB, outlet: 'aux'} - ] - }], - 'p/11/(;pa=33//aux:;pb=44)', (s: RouterStateSnapshot) => { - const p = s.firstChild(s.root); - checkActivatedRoute(p, 'p/11', {id: '11'}, undefined); - - const c = s.children(p); - checkActivatedRoute(c[0], '', {pa: '33'}, ComponentA); - checkActivatedRoute(c[1], '', {pb: '44'}, ComponentB, 'aux'); - }); - }); - - xit('should work with only aux path', () => { - checkRecognize( - [{ - path: 'p/:id', - children: [ - {path: '', component: ComponentA}, - {path: '', component: ComponentB, outlet: 'aux'} - ] - }], - 'p/11', (s: RouterStateSnapshot) => { - const p = s.firstChild(s.root); - checkActivatedRoute(p, 'p/11(aux:;pb=44)', {id: '11'}, undefined); - - const c = s.children(p); - checkActivatedRoute(c[0], '', {}, ComponentA); - checkActivatedRoute(c[1], '', {pb: '44'}, ComponentB, 'aux'); - }); - }); }); describe('query parameters', () => { @@ -441,3 +502,5 @@ class RootComponent {} class ComponentA {} class ComponentB {} class ComponentC {} +class ComponentD {} +class ComponentE {} diff --git a/modules/@angular/router/test/url_serializer.spec.ts b/modules/@angular/router/test/url_serializer.spec.ts index adf2ef179d..edf18639ec 100644 --- a/modules/@angular/router/test/url_serializer.spec.ts +++ b/modules/@angular/router/test/url_serializer.spec.ts @@ -27,31 +27,9 @@ describe('url serializer', () => { expect(url.serialize(tree)).toEqual('/one/two(left:three//right:four)'); }); - it('should parse segments with empty paths', () => { - const tree = url.parse('/one/two/(;a=1//right:;b=2)'); - - const c = tree.root.children[PRIMARY_OUTLET]; - expectSegment(tree.root.children[PRIMARY_OUTLET], 'one/two', true); - - expect(c.children[PRIMARY_OUTLET].pathsWithParams[0].path).toEqual(''); - expect(c.children[PRIMARY_OUTLET].pathsWithParams[0].parameters).toEqual({a: '1'}); - - expect(c.children['right'].pathsWithParams[0].path).toEqual(''); - expect(c.children['right'].pathsWithParams[0].parameters).toEqual({b: '2'}); - - expect(url.serialize(tree)).toEqual('/one/two/(;a=1//right:;b=2)'); - }); - - it('should parse segments with empty paths (only aux)', () => { - const tree = url.parse('/one/two/(right:;b=2)'); - - const c = tree.root.children[PRIMARY_OUTLET]; - expectSegment(tree.root.children[PRIMARY_OUTLET], 'one/two', true); - - expect(c.children['right'].pathsWithParams[0].path).toEqual(''); - expect(c.children['right'].pathsWithParams[0].parameters).toEqual({b: '2'}); - - expect(url.serialize(tree)).toEqual('/one/two/(right:;b=2)'); + 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', () => {