From 6db27153ef04dabdf479fed64dc797653196867b Mon Sep 17 00:00:00 2001 From: Victor Savkin Date: Wed, 10 Aug 2016 15:53:57 -0700 Subject: [PATCH] Router Fixes (#10579) * fix(router): copy over data during data resolution * fix(router): components instantiated in lazy-loaded modules should use location's injector --- .../router/src/directives/router_outlet.ts | 1 - modules/@angular/router/src/router.ts | 53 +- modules/@angular/router/src/router_state.ts | 3 +- .../@angular/router/test/integration.spec.ts | 1819 ++++++++++++++++ modules/@angular/router/test/router.spec.ts | 1836 +---------------- modules/@angular/router/tsconfig.json | 1 + 6 files changed, 1901 insertions(+), 1812 deletions(-) create mode 100644 modules/@angular/router/test/integration.spec.ts diff --git a/modules/@angular/router/src/directives/router_outlet.ts b/modules/@angular/router/src/directives/router_outlet.ts index c0cd11296a..9035e375bd 100644 --- a/modules/@angular/router/src/directives/router_outlet.ts +++ b/modules/@angular/router/src/directives/router_outlet.ts @@ -104,7 +104,6 @@ export class RouterOutlet implements OnDestroy { } const injector = loadedInjector ? loadedInjector : this.location.parentInjector; - const inj = ReflectiveInjector.fromResolvedProviders(providers, injector); this.activated = this.location.createComponent(factory, this.location.length, inj, []); this.activated.changeDetectorRef.detectChanges(); diff --git a/modules/@angular/router/src/router.ts b/modules/@angular/router/src/router.ts index a86023ad98..7957a7b417 100644 --- a/modules/@angular/router/src/router.ts +++ b/modules/@angular/router/src/router.ts @@ -447,7 +447,6 @@ export class Router { class CanActivate { constructor(public path: ActivatedRouteSnapshot[]) {} - get route(): ActivatedRouteSnapshot { return this.path[this.path.length - 1]; } } @@ -456,7 +455,7 @@ class CanDeactivate { } -class PreActivation { +export class PreActivation { private checks: Array = []; constructor( private future: RouterStateSnapshot, private curr: RouterStateSnapshot, @@ -504,6 +503,7 @@ class PreActivation { futureNode: TreeNode, currNode: TreeNode, outletMap: RouterOutletMap, futurePath: ActivatedRouteSnapshot[]): void { const prevChildren: {[key: string]: any} = nodeChildrenAsMap(currNode); + futureNode.children.forEach(c => { this.traverseRoutes(c, prevChildren[c.value.outlet], outletMap, futurePath.concat([c.value])); delete prevChildren[c.value.outlet]; @@ -524,6 +524,9 @@ class PreActivation { if (curr && future._routeConfig === curr._routeConfig) { if (!shallowEqual(future.params, curr.params)) { this.checks.push(new CanDeactivate(outlet.component, curr), new CanActivate(futurePath)); + } else { + // we need to set the data + future.data = curr.data; } // If we have a component, we need to go through an outlet. @@ -578,7 +581,7 @@ class PreActivation { const canActivate = future._routeConfig ? future._routeConfig.canActivate : null; if (!canActivate || canActivate.length === 0) return of (true); const obs = from(canActivate).map(c => { - const guard = this.getToken(c, future, this.future); + const guard = this.getToken(c, future); if (guard.canActivate) { return wrapIntoObservable(guard.canActivate(future, this.future)); } else { @@ -598,7 +601,7 @@ class PreActivation { return andObservables(from(canActivateChildGuards).map(d => { const obs = from(d.guards).map(c => { - const guard = this.getToken(c, c.node, this.future); + const guard = this.getToken(c, c.node); if (guard.canActivateChild) { return wrapIntoObservable(guard.canActivateChild(future, this.future)); } else { @@ -621,7 +624,7 @@ class PreActivation { if (!canDeactivate || canDeactivate.length === 0) return of (true); return from(canDeactivate) .map(c => { - const guard = this.getToken(c, curr, this.curr); + const guard = this.getToken(c, curr); if (guard.canDeactivate) { return wrapIntoObservable(guard.canDeactivate(component, curr, this.curr)); } else { @@ -643,14 +646,14 @@ class PreActivation { private resolveNode(resolve: ResolveData, future: ActivatedRouteSnapshot): Observable { return waitForMap(resolve, (k, v) => { - const resolver = this.getToken(v, future, this.future); + const resolver = this.getToken(v, future); return resolver.resolve ? wrapIntoObservable(resolver.resolve(future, this.future)) : wrapIntoObservable(resolver(future, this.future)); }); } - private getToken(token: any, snapshot: ActivatedRouteSnapshot, state: RouterStateSnapshot): any { - const config = closestLoadedConfig(state, snapshot); + private getToken(token: any, snapshot: ActivatedRouteSnapshot): any { + const config = closestLoadedConfig(snapshot); const injector = config ? config.injector : this.injector; return injector.get(token); } @@ -736,7 +739,8 @@ class ActivateRoutes { useValue: outletMap }]; - const config = closestLoadedConfig(this.futureState.snapshot, future.snapshot); + const config = parentLoadedConfig(future.snapshot); + let loadedFactoryResolver: ComponentFactoryResolver = null; let loadedInjector: Injector = null; @@ -744,8 +748,7 @@ class ActivateRoutes { loadedFactoryResolver = config.factoryResolver; loadedInjector = config.injector; resolved.push({provide: ComponentFactoryResolver, useValue: loadedFactoryResolver}); - }; - + } outlet.activate( future, loadedFactoryResolver, loadedInjector, ReflectiveInjector.resolve(resolved), outletMap); @@ -763,13 +766,27 @@ class ActivateRoutes { } } -function closestLoadedConfig( - state: RouterStateSnapshot, snapshot: ActivatedRouteSnapshot): LoadedRouterConfig { - const b = state.pathFromRoot(snapshot).filter(s => { - const config = (s)._routeConfig; - return config && config._loadedConfig && s !== snapshot; - }); - return b.length > 0 ? (b[b.length - 1])._routeConfig._loadedConfig : null; +function parentLoadedConfig(snapshot: ActivatedRouteSnapshot): LoadedRouterConfig { + let s = snapshot.parent; + while (s) { + const c: any = s._routeConfig; + if (c && c._loadedConfig) return c._loadedConfig; + if (c && c.component) return null; + s = s.parent; + } + return null; +} + +function closestLoadedConfig(snapshot: ActivatedRouteSnapshot): LoadedRouterConfig { + if (!snapshot) return null; + + let s = snapshot.parent; + while (s) { + const c: any = s._routeConfig; + if (c && c._loadedConfig) return c._loadedConfig; + s = s.parent; + } + return null; } function nodeChildrenAsMap(node: TreeNode) { diff --git a/modules/@angular/router/src/router_state.ts b/modules/@angular/router/src/router_state.ts index e5ff64da21..d573cd7113 100644 --- a/modules/@angular/router/src/router_state.ts +++ b/modules/@angular/router/src/router_state.ts @@ -70,7 +70,8 @@ export function createEmptyState(urlTree: UrlTree, rootComponent: Type): RouterS return new RouterState(new TreeNode(activated, []), snapshot); } -function createEmptyStateSnapshot(urlTree: UrlTree, rootComponent: Type): RouterStateSnapshot { +export function createEmptyStateSnapshot( + urlTree: UrlTree, rootComponent: Type): RouterStateSnapshot { const emptyParams = {}; const emptyData = {}; const emptyQueryParams = {}; diff --git a/modules/@angular/router/test/integration.spec.ts b/modules/@angular/router/test/integration.spec.ts new file mode 100644 index 0000000000..1b7fe696a0 --- /dev/null +++ b/modules/@angular/router/test/integration.spec.ts @@ -0,0 +1,1819 @@ +/** + * @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 'rxjs/add/operator/map'; + +import {Location} from '@angular/common'; +import {Component, NgModule, NgModuleFactoryLoader} from '@angular/core'; +import {ComponentFixture, TestBed, TestComponentBuilder, addProviders, fakeAsync, inject, tick} from '@angular/core/testing'; +import {expect} from '@angular/platform-browser/testing/matchers'; +import {Observable} from 'rxjs/Observable'; +import {of } from 'rxjs/observable/of'; + +import {ActivatedRoute, ActivatedRouteSnapshot, CanActivate, CanDeactivate, Event, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, Params, ROUTER_DIRECTIVES, Resolve, Router, RouterModule, RouterStateSnapshot, RoutesRecognized, provideRoutes} from '../index'; +import {RouterTestingModule, SpyNgModuleFactoryLoader} from '../testing'; + +describe('Integration', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [RouterTestingModule], + providers: [provideRoutes( + [{path: '', component: BlankCmp}, {path: 'simple', component: SimpleCmp}])], + declarations: [ + BlankCmp, SimpleCmp, TeamCmp, UserCmp, StringLinkCmp, DummyLinkCmp, AbsoluteLinkCmp, + RelativeLinkCmp, DummyLinkWithParentCmp, LinkWithQueryParamsAndFragment, CollectParamsCmp, + QueryParamsAndFragmentCmp, StringLinkButtonCmp, WrapperCmp, LinkInNgIf, + ComponentRecordingRoutePathAndUrl, RouteCmp + ] + }); + }); + + it('should navigate with a provided config', + fakeAsync(inject( + [Router, TestComponentBuilder, Location], + (router: Router, tcb: TestComponentBuilder, location: Location) => { + const fixture = createRoot(tcb, router, RootCmp); + + router.navigateByUrl('/simple'); + advance(fixture); + + expect(location.path()).toEqual('/simple'); + }))); + + it('should work when an outlet is in an ngIf', + fakeAsync(inject( + [Router, TestComponentBuilder, Location], + (router: Router, tcb: TestComponentBuilder, location: Location) => { + const fixture = createRoot(tcb, router, RootCmp); + + router.resetConfig([{ + path: 'child', + component: LinkInNgIf, + children: [{path: 'simple', component: SimpleCmp}] + }]); + + router.navigateByUrl('/child/simple'); + advance(fixture); + + expect(location.path()).toEqual('/child/simple'); + }))); + + it('should work when an outlet is in an ngIf (and is removed)', + fakeAsync(inject( + [Router, TestComponentBuilder, Location], + (router: Router, tcb: TestComponentBuilder, location: Location) => { + @Component({ + selector: 'someRoot', + template: `
`, + entryComponents: [BlankCmp, SimpleCmp] + }) + class RootCmpWithLink { + cond: boolean = true; + } + + const fixture = createRoot(tcb, router, RootCmpWithLink); + + router.resetConfig( + [{path: 'simple', component: SimpleCmp}, {path: 'blank', component: BlankCmp}]); + + router.navigateByUrl('/simple'); + advance(fixture); + expect(location.path()).toEqual('/simple'); + + const instance = fixture.componentInstance; + instance.cond = false; + advance(fixture); + + let recordedError: any = null; + router.navigateByUrl('/blank').catch(e => recordedError = e); + advance(fixture); + expect(recordedError.message).toEqual('Cannot find primary outlet to load \'BlankCmp\''); + }))); + + it('should update location when navigating', + fakeAsync(inject( + [Router, TestComponentBuilder, Location], + (router: Router, tcb: TestComponentBuilder, location: Location) => { + const fixture = createRoot(tcb, router, RootCmp); + + router.resetConfig([{path: 'team/:id', component: TeamCmp}]); + + router.navigateByUrl('/team/22'); + advance(fixture); + expect(location.path()).toEqual('/team/22'); + + router.navigateByUrl('/team/33'); + advance(fixture); + + expect(location.path()).toEqual('/team/33'); + }))); + + it('should skip location update when using NavigationExtras.skipLocationChange with navigateByUrl', + fakeAsync(inject( + [Router, TestComponentBuilder, Location], + (router: Router, tcb: TestComponentBuilder, location: Location) => { + const fixture = tcb.createFakeAsync(RootCmp); + advance(fixture); + + router.resetConfig([{path: 'team/:id', component: TeamCmp}]); + + router.navigateByUrl('/team/22'); + advance(fixture); + expect(location.path()).toEqual('/team/22'); + + expect(fixture.debugElement.nativeElement).toHaveText('team 22 [ , right: ]'); + + router.navigateByUrl('/team/33', {skipLocationChange: true}); + advance(fixture); + + expect(location.path()).toEqual('/team/22'); + + expect(fixture.debugElement.nativeElement).toHaveText('team 33 [ , right: ]'); + }))); + + it('should skip location update when using NavigationExtras.skipLocationChange with navigate', + fakeAsync(inject( + [Router, TestComponentBuilder, Location], + (router: Router, tcb: TestComponentBuilder, location: Location) => { + const fixture = tcb.createFakeAsync(RootCmp); + advance(fixture); + + router.resetConfig([{path: 'team/:id', component: TeamCmp}]); + + router.navigate(['/team/22']); + advance(fixture); + expect(location.path()).toEqual('/team/22'); + + expect(fixture.debugElement.nativeElement).toHaveText('team 22 [ , right: ]'); + + router.navigate(['/team/33'], {skipLocationChange: true}); + advance(fixture); + + expect(location.path()).toEqual('/team/22'); + + expect(fixture.debugElement.nativeElement).toHaveText('team 33 [ , right: ]'); + }))); + + it('should navigate back and forward', + fakeAsync(inject( + [Router, TestComponentBuilder, Location], + (router: Router, tcb: TestComponentBuilder, location: Location) => { + const fixture = createRoot(tcb, router, RootCmp); + + router.resetConfig([{ + path: 'team/:id', + component: TeamCmp, + children: [ + {path: 'simple', component: SimpleCmp}, {path: 'user/:name', component: UserCmp} + ] + }]); + + + router.navigateByUrl('/team/33/simple'); + advance(fixture); + expect(location.path()).toEqual('/team/33/simple'); + + router.navigateByUrl('/team/22/user/victor'); + advance(fixture); + + location.back(); + advance(fixture); + expect(location.path()).toEqual('/team/33/simple'); + + location.forward(); + advance(fixture); + expect(location.path()).toEqual('/team/22/user/victor'); + }))); + + it('should navigate when locations changes', + fakeAsync(inject( + [Router, TestComponentBuilder, Location], + (router: Router, tcb: TestComponentBuilder, location: Location) => { + const fixture = createRoot(tcb, router, RootCmp); + + router.resetConfig([{ + path: 'team/:id', + component: TeamCmp, + children: [{path: 'user/:name', component: UserCmp}] + }]); + + const recordedEvents: any[] = []; + router.events.forEach(e => recordedEvents.push(e)); + + router.navigateByUrl('/team/22/user/victor'); + advance(fixture); + + (location).simulateHashChange('/team/22/user/fedor'); + advance(fixture); + + (location).simulateUrlPop('/team/22/user/fedor'); + advance(fixture); + + expect(fixture.debugElement.nativeElement).toHaveText('team 22 [ user fedor, right: ]'); + + expectEvents(recordedEvents, [ + [NavigationStart, '/team/22/user/victor'], [RoutesRecognized, '/team/22/user/victor'], + [NavigationEnd, '/team/22/user/victor'], + + [NavigationStart, '/team/22/user/fedor'], [RoutesRecognized, '/team/22/user/fedor'], + [NavigationEnd, '/team/22/user/fedor'] + ]); + }))); + + it('should update the location when the matched route does not change', + fakeAsync(inject( + [Router, TestComponentBuilder, Location], + (router: Router, tcb: TestComponentBuilder, location: Location) => { + const fixture = createRoot(tcb, router, RootCmp); + + router.resetConfig([{path: '**', component: CollectParamsCmp}]); + + router.navigateByUrl('/one/two'); + advance(fixture); + const cmp = fixture.debugElement.children[1].componentInstance; + expect(location.path()).toEqual('/one/two'); + expect(fixture.debugElement.nativeElement).toHaveText('collect-params'); + + expect(cmp.recordedUrls()).toEqual(['one/two']); + + router.navigateByUrl('/three/four'); + advance(fixture); + expect(location.path()).toEqual('/three/four'); + expect(fixture.debugElement.nativeElement).toHaveText('collect-params'); + expect(cmp.recordedUrls()).toEqual(['one/two', 'three/four']); + }))); + + it('should support secondary routes', + fakeAsync( + inject([Router, TestComponentBuilder], (router: Router, tcb: TestComponentBuilder) => { + const fixture = createRoot(tcb, router, RootCmp); + + router.resetConfig([{ + path: 'team/:id', + component: TeamCmp, + children: [ + {path: 'user/:name', component: UserCmp}, + {path: 'simple', component: SimpleCmp, outlet: 'right'} + ] + }]); + + router.navigateByUrl('/team/22/(user/victor//right:simple)'); + advance(fixture); + + expect(fixture.debugElement.nativeElement) + .toHaveText('team 22 [ user victor, right: simple ]'); + }))); + + it('should deactivate outlets', + fakeAsync( + inject([Router, TestComponentBuilder], (router: Router, tcb: TestComponentBuilder) => { + const fixture = createRoot(tcb, router, RootCmp); + + router.resetConfig([{ + path: 'team/:id', + component: TeamCmp, + children: [ + {path: 'user/:name', component: UserCmp}, + {path: 'simple', component: SimpleCmp, outlet: 'right'} + ] + }]); + + router.navigateByUrl('/team/22/(user/victor//right:simple)'); + advance(fixture); + + router.navigateByUrl('/team/22/user/victor'); + advance(fixture); + + expect(fixture.debugElement.nativeElement) + .toHaveText('team 22 [ user victor, right: ]'); + }))); + + it('should deactivate nested outlets', + fakeAsync( + inject([Router, TestComponentBuilder], (router: Router, tcb: TestComponentBuilder) => { + const fixture = createRoot(tcb, router, RootCmp); + + router.resetConfig([ + { + path: 'team/:id', + component: TeamCmp, + children: [ + {path: 'user/:name', component: UserCmp}, + {path: 'simple', component: SimpleCmp, outlet: 'right'} + ] + }, + {path: '', component: BlankCmp} + ]); + + router.navigateByUrl('/team/22/(user/victor//right:simple)'); + advance(fixture); + + router.navigateByUrl('/'); + advance(fixture); + + expect(fixture.debugElement.nativeElement).toHaveText(''); + }))); + + it('should set query params and fragment', + fakeAsync( + inject([Router, TestComponentBuilder], (router: Router, tcb: TestComponentBuilder) => { + const fixture = createRoot(tcb, router, RootCmp); + + router.resetConfig([{path: 'query', component: QueryParamsAndFragmentCmp}]); + + router.navigateByUrl('/query?name=1#fragment1'); + advance(fixture); + expect(fixture.debugElement.nativeElement).toHaveText('query: 1 fragment: fragment1'); + + router.navigateByUrl('/query?name=2#fragment2'); + advance(fixture); + expect(fixture.debugElement.nativeElement).toHaveText('query: 2 fragment: fragment2'); + }))); + + it('should push params only when they change', + fakeAsync( + inject([Router, TestComponentBuilder], (router: Router, tcb: TestComponentBuilder) => { + const fixture = createRoot(tcb, router, RootCmp); + + router.resetConfig([{ + path: 'team/:id', + component: TeamCmp, + children: [{path: 'user/:name', component: UserCmp}] + }]); + + router.navigateByUrl('/team/22/user/victor'); + advance(fixture); + const team = fixture.debugElement.children[1].componentInstance; + const user = fixture.debugElement.children[1].children[1].componentInstance; + + expect(team.recordedParams).toEqual([{id: '22'}]); + expect(user.recordedParams).toEqual([{name: 'victor'}]); + + router.navigateByUrl('/team/22/user/fedor'); + advance(fixture); + + expect(team.recordedParams).toEqual([{id: '22'}]); + expect(user.recordedParams).toEqual([{name: 'victor'}, {name: 'fedor'}]); + }))); + + it('should work when navigating to /', + fakeAsync( + inject([Router, TestComponentBuilder], (router: Router, tcb: TestComponentBuilder) => { + const fixture = createRoot(tcb, router, RootCmp); + + router.resetConfig([ + {path: '', pathMatch: 'full', component: SimpleCmp}, + {path: 'user/:name', component: UserCmp} + ]); + + router.navigateByUrl('/user/victor'); + advance(fixture); + + expect(fixture.debugElement.nativeElement).toHaveText('user victor'); + + router.navigateByUrl('/'); + advance(fixture); + + expect(fixture.debugElement.nativeElement).toHaveText('simple'); + }))); + + it('should cancel in-flight navigations', + fakeAsync( + inject([Router, TestComponentBuilder], (router: Router, tcb: TestComponentBuilder) => { + const fixture = createRoot(tcb, router, RootCmp); + + router.resetConfig([{path: 'user/:name', component: UserCmp}]); + + const recordedEvents: any[] = []; + router.events.forEach(e => recordedEvents.push(e)); + + router.navigateByUrl('/user/init'); + advance(fixture); + + const user = fixture.debugElement.children[1].componentInstance; + + let r1: any, r2: any; + router.navigateByUrl('/user/victor').then(_ => r1 = _); + router.navigateByUrl('/user/fedor').then(_ => r2 = _); + advance(fixture); + + expect(r1).toEqual(false); // returns false because it was canceled + expect(r2).toEqual(true); // returns true because it was successful + + expect(fixture.debugElement.nativeElement).toHaveText('user fedor'); + expect(user.recordedParams).toEqual([{name: 'init'}, {name: 'fedor'}]); + + expectEvents(recordedEvents, [ + [NavigationStart, '/user/init'], [RoutesRecognized, '/user/init'], + [NavigationEnd, '/user/init'], + + [NavigationStart, '/user/victor'], [NavigationStart, '/user/fedor'], + + [NavigationCancel, '/user/victor'], [RoutesRecognized, '/user/fedor'], + [NavigationEnd, '/user/fedor'] + ]); + }))); + + it('should handle failed navigations gracefully', + fakeAsync( + inject([Router, TestComponentBuilder], (router: Router, tcb: TestComponentBuilder) => { + const fixture = createRoot(tcb, router, RootCmp); + + router.resetConfig([{path: 'user/:name', component: UserCmp}]); + + const recordedEvents: any[] = []; + router.events.forEach(e => recordedEvents.push(e)); + + let e: any; + router.navigateByUrl('/invalid').catch(_ => e = _); + advance(fixture); + expect(e.message).toContain('Cannot match any routes'); + + router.navigateByUrl('/user/fedor'); + advance(fixture); + + expect(fixture.debugElement.nativeElement).toHaveText('user fedor'); + + expectEvents(recordedEvents, [ + [NavigationStart, '/invalid'], [NavigationError, '/invalid'], + + [NavigationStart, '/user/fedor'], [RoutesRecognized, '/user/fedor'], + [NavigationEnd, '/user/fedor'] + ]); + }))); + + it('should replace state when path is equal to current path', + fakeAsync(inject( + [Router, TestComponentBuilder, Location], + (router: Router, tcb: TestComponentBuilder, location: Location) => { + const fixture = createRoot(tcb, router, RootCmp); + + router.resetConfig([{ + path: 'team/:id', + component: TeamCmp, + children: [ + {path: 'simple', component: SimpleCmp}, {path: 'user/:name', component: UserCmp} + ] + }]); + + router.navigateByUrl('/team/33/simple'); + advance(fixture); + + router.navigateByUrl('/team/22/user/victor'); + advance(fixture); + + router.navigateByUrl('/team/22/user/victor'); + advance(fixture); + + location.back(); + advance(fixture); + expect(location.path()).toEqual('/team/33/simple'); + }))); + + it('should handle componentless paths', + fakeAsync(inject( + [Router, TestComponentBuilder, Location], + (router: Router, tcb: TestComponentBuilder, location: Location) => { + const fixture = createRoot(tcb, router, RootCmpWithTwoOutlets); + + router.resetConfig([ + { + path: 'parent/:id', + children: [ + {path: 'simple', component: SimpleCmp}, + {path: 'user/:name', component: UserCmp, outlet: 'right'} + ] + }, + {path: 'user/:name', component: UserCmp} + ]); + + + // navigate to a componentless route + router.navigateByUrl('/parent/11/(simple//right:user/victor)'); + advance(fixture); + expect(location.path()).toEqual('/parent/11/(simple//right:user/victor)'); + expect(fixture.debugElement.nativeElement) + .toHaveText('primary [simple] right [user victor]'); + + // navigate to the same route with different params (reuse) + router.navigateByUrl('/parent/22/(simple//right:user/fedor)'); + advance(fixture); + expect(location.path()).toEqual('/parent/22/(simple//right:user/fedor)'); + expect(fixture.debugElement.nativeElement) + .toHaveText('primary [simple] right [user fedor]'); + + // navigate to a normal route (check deactivation) + router.navigateByUrl('/user/victor'); + advance(fixture); + expect(location.path()).toEqual('/user/victor'); + expect(fixture.debugElement.nativeElement).toHaveText('primary [user victor] right []'); + + // navigate back to a componentless route + router.navigateByUrl('/parent/11/(simple//right:user/victor)'); + advance(fixture); + expect(location.path()).toEqual('/parent/11/(simple//right:user/victor)'); + expect(fixture.debugElement.nativeElement) + .toHaveText('primary [simple] right [user victor]'); + }))); + + it('should emit an event when an outlet gets activated', + fakeAsync(inject( + [Router, TestComponentBuilder, Location], + (router: Router, tcb: TestComponentBuilder, location: Location) => { + @Component({ + selector: 'container', + template: + `` + }) + class Container { + activations: any[] = []; + deactivations: any[] = []; + + recordActivate(component: any): void { this.activations.push(component); } + + recordDeactivate(component: any): void { this.deactivations.push(component); } + } + + const fixture = createRoot(tcb, router, Container); + const cmp = fixture.debugElement.componentInstance; + + router.resetConfig( + [{path: 'blank', component: BlankCmp}, {path: 'simple', component: SimpleCmp}]); + + cmp.activations = []; + cmp.deactivations = []; + + router.navigateByUrl('/blank'); + advance(fixture); + + expect(cmp.activations.length).toEqual(1); + expect(cmp.activations[0] instanceof BlankCmp).toBe(true); + + router.navigateByUrl('/simple'); + advance(fixture); + + expect(cmp.activations.length).toEqual(2); + expect(cmp.activations[1] instanceof SimpleCmp).toBe(true); + expect(cmp.deactivations.length).toEqual(2); + expect(cmp.deactivations[1] instanceof BlankCmp).toBe(true); + }))); + + it('should update url and router state before activating components', + fakeAsync(inject( + [Router, TestComponentBuilder, Location], + (router: Router, tcb: TestComponentBuilder, location: Location) => { + + const fixture = createRoot(tcb, router, RootCmp); + + router.resetConfig([{path: 'cmp', component: ComponentRecordingRoutePathAndUrl}]); + + router.navigateByUrl('/cmp'); + advance(fixture); + + const cmp = fixture.debugElement.children[1].componentInstance; + + expect(cmp.url).toBe('/cmp'); + expect(cmp.path.length).toEqual(2); + }))); + + describe('data', () => { + class ResolveSix implements Resolve { + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): number { return 6; } + } + + beforeEach(() => { + addProviders([ + {provide: 'resolveTwo', useValue: (a: any, b: any) => 2}, + {provide: 'resolveFour', useValue: (a: any, b: any) => 4}, + {provide: 'resolveSix', useClass: ResolveSix} + ]); + }); + + it('should provide resolved data', + fakeAsync(inject( + [Router, TestComponentBuilder, Location], + (router: Router, tcb: TestComponentBuilder, location: Location) => { + const fixture = createRoot(tcb, router, RootCmpWithTwoOutlets); + + router.resetConfig([{ + path: 'parent/:id', + data: {one: 1}, + resolve: {two: 'resolveTwo'}, + children: [ + {path: '', data: {three: 3}, resolve: {four: 'resolveFour'}, component: RouteCmp}, + { + path: '', + data: {five: 5}, + resolve: {six: 'resolveSix'}, + component: RouteCmp, + outlet: 'right' + } + ] + }]); + + router.navigateByUrl('/parent/1'); + advance(fixture); + + const primaryCmp = fixture.debugElement.children[1].componentInstance; + const rightCmp = fixture.debugElement.children[3].componentInstance; + + expect(primaryCmp.route.snapshot.data).toEqual({one: 1, two: 2, three: 3, four: 4}); + expect(rightCmp.route.snapshot.data).toEqual({one: 1, two: 2, five: 5, six: 6}); + + let primaryRecorded: any[] = []; + primaryCmp.route.data.forEach((rec: any) => primaryRecorded.push(rec)); + + let rightRecorded: any[] = []; + rightCmp.route.data.forEach((rec: any) => rightRecorded.push(rec)); + + router.navigateByUrl('/parent/2'); + advance(fixture); + + expect(primaryRecorded).toEqual([ + {one: 1, three: 3, two: 2, four: 4}, {one: 1, three: 3, two: 2, four: 4} + ]); + expect(rightRecorded).toEqual([ + {one: 1, five: 5, two: 2, six: 6}, {one: 1, five: 5, two: 2, six: 6} + ]); + }))); + }); + + describe('router links', () => { + it('should support string router links', + fakeAsync( + inject([Router, TestComponentBuilder], (router: Router, tcb: TestComponentBuilder) => { + const fixture = createRoot(tcb, router, RootCmp); + + router.resetConfig([{ + path: 'team/:id', + component: TeamCmp, + children: [ + {path: 'link', component: StringLinkCmp}, + {path: 'simple', component: SimpleCmp} + ] + }]); + + router.navigateByUrl('/team/22/link'); + advance(fixture); + expect(fixture.debugElement.nativeElement).toHaveText('team 22 [ link, right: ]'); + + const native = fixture.debugElement.nativeElement.querySelector('a'); + expect(native.getAttribute('href')).toEqual('/team/33/simple'); + native.click(); + advance(fixture); + + expect(fixture.debugElement.nativeElement).toHaveText('team 33 [ simple, right: ]'); + }))); + + it('should not preserve query params and fragment by default', + fakeAsync( + inject([Router, TestComponentBuilder], (router: Router, tcb: TestComponentBuilder) => { + @Component({ + selector: 'someRoot', + template: `Link`, + directives: ROUTER_DIRECTIVES + }) + class RootCmpWithLink { + } + + const fixture = createRoot(tcb, router, RootCmpWithLink); + + router.resetConfig([{path: 'home', component: SimpleCmp}]); + + const native = fixture.debugElement.nativeElement.querySelector('a'); + + router.navigateByUrl('/home?q=123#fragment'); + advance(fixture); + expect(native.getAttribute('href')).toEqual('/home'); + }))); + + it('should update hrefs when query params or fragment change', + fakeAsync(inject([Router, TestComponentBuilder], (router: Router, tcb: TestComponentBuilder) => { + + @Component({ + selector: 'someRoot', + template: + `Link`, + directives: ROUTER_DIRECTIVES + }) + class RootCmpWithLink { + } + + const fixture = createRoot(tcb, router, RootCmpWithLink); + + router.resetConfig([{path: 'home', component: SimpleCmp}]); + + const native = fixture.debugElement.nativeElement.querySelector('a'); + + router.navigateByUrl('/home?q=123'); + advance(fixture); + expect(native.getAttribute('href')).toEqual('/home?q=123'); + + router.navigateByUrl('/home?q=456'); + advance(fixture); + expect(native.getAttribute('href')).toEqual('/home?q=456'); + + router.navigateByUrl('/home?q=456#1'); + advance(fixture); + expect(native.getAttribute('href')).toEqual('/home?q=456#1'); + }))); + + it('should support using links on non-a tags', + fakeAsync( + inject([Router, TestComponentBuilder], (router: Router, tcb: TestComponentBuilder) => { + const fixture = createRoot(tcb, router, RootCmp); + + router.resetConfig([{ + path: 'team/:id', + component: TeamCmp, + children: [ + {path: 'link', component: StringLinkButtonCmp}, + {path: 'simple', component: SimpleCmp} + ] + }]); + + router.navigateByUrl('/team/22/link'); + advance(fixture); + expect(fixture.debugElement.nativeElement).toHaveText('team 22 [ link, right: ]'); + + const native = fixture.debugElement.nativeElement.querySelector('button'); + native.click(); + advance(fixture); + + expect(fixture.debugElement.nativeElement).toHaveText('team 33 [ simple, right: ]'); + }))); + + it('should support absolute router links', + fakeAsync( + inject([Router, TestComponentBuilder], (router: Router, tcb: TestComponentBuilder) => { + const fixture = createRoot(tcb, router, RootCmp); + + router.resetConfig([{ + path: 'team/:id', + component: TeamCmp, + children: [ + {path: 'link', component: AbsoluteLinkCmp}, + {path: 'simple', component: SimpleCmp} + ] + }]); + + router.navigateByUrl('/team/22/link'); + advance(fixture); + expect(fixture.debugElement.nativeElement).toHaveText('team 22 [ link, right: ]'); + + const native = fixture.debugElement.nativeElement.querySelector('a'); + expect(native.getAttribute('href')).toEqual('/team/33/simple'); + native.click(); + advance(fixture); + + expect(fixture.debugElement.nativeElement).toHaveText('team 33 [ simple, right: ]'); + }))); + + it('should support relative router links', + fakeAsync( + inject([Router, TestComponentBuilder], (router: Router, tcb: TestComponentBuilder) => { + const fixture = createRoot(tcb, router, RootCmp); + + router.resetConfig([{ + path: 'team/:id', + component: TeamCmp, + children: [ + {path: 'link', component: RelativeLinkCmp}, + {path: 'simple', component: SimpleCmp} + ] + }]); + + router.navigateByUrl('/team/22/link'); + advance(fixture); + expect(fixture.debugElement.nativeElement).toHaveText('team 22 [ link, right: ]'); + + const native = fixture.debugElement.nativeElement.querySelector('a'); + expect(native.getAttribute('href')).toEqual('/team/22/simple'); + native.click(); + advance(fixture); + + expect(fixture.debugElement.nativeElement).toHaveText('team 22 [ simple, right: ]'); + }))); + + it('should support top-level link', + fakeAsync( + inject([Router, TestComponentBuilder], (router: Router, tcb: TestComponentBuilder) => { + const fixture = createRoot(tcb, router, RelativeLinkInIfCmp); + advance(fixture); + + router.resetConfig( + [{path: 'simple', component: SimpleCmp}, {path: '', component: BlankCmp}]); + + router.navigateByUrl('/'); + advance(fixture); + expect(fixture.debugElement.nativeElement).toHaveText(' '); + const cmp = fixture.debugElement.componentInstance; + + cmp.show = true; + advance(fixture); + + expect(fixture.debugElement.nativeElement).toHaveText('link '); + const native = fixture.debugElement.nativeElement.querySelector('a'); + + expect(native.getAttribute('href')).toEqual('/simple'); + native.click(); + advance(fixture); + + expect(fixture.debugElement.nativeElement).toHaveText('link simple'); + }))); + + it('should support query params and fragments', + fakeAsync(inject( + [Router, Location, TestComponentBuilder], + (router: Router, location: Location, tcb: TestComponentBuilder) => { + const fixture = createRoot(tcb, router, RootCmp); + + router.resetConfig([{ + path: 'team/:id', + component: TeamCmp, + children: [ + {path: 'link', component: LinkWithQueryParamsAndFragment}, + {path: 'simple', component: SimpleCmp} + ] + }]); + + router.navigateByUrl('/team/22/link'); + advance(fixture); + + const native = fixture.debugElement.nativeElement.querySelector('a'); + expect(native.getAttribute('href')).toEqual('/team/22/simple?q=1#f'); + native.click(); + advance(fixture); + + expect(fixture.debugElement.nativeElement).toHaveText('team 22 [ simple, right: ]'); + + expect(location.path()).toEqual('/team/22/simple?q=1#f'); + }))); + }); + + describe('redirects', () => { + it('should work', fakeAsync(inject( + [Router, TestComponentBuilder, Location], + (router: Router, tcb: TestComponentBuilder, location: Location) => { + const fixture = createRoot(tcb, router, RootCmp); + + router.resetConfig([ + {path: 'old/team/:id', redirectTo: 'team/:id'}, + {path: 'team/:id', component: TeamCmp} + ]); + + router.navigateByUrl('old/team/22'); + advance(fixture); + + expect(location.path()).toEqual('/team/22'); + }))); + }); + + describe('guards', () => { + describe('CanActivate', () => { + describe('should not activate a route when CanActivate returns false', () => { + beforeEach(() => { + addProviders([{provide: 'alwaysFalse', useValue: (a: any, b: any) => false}]); + }); + + it('works', + fakeAsync(inject( + [Router, TestComponentBuilder, Location], + (router: Router, tcb: TestComponentBuilder, location: Location) => { + const fixture = createRoot(tcb, router, RootCmp); + + router.resetConfig( + [{path: 'team/:id', component: TeamCmp, canActivate: ['alwaysFalse']}]); + + router.navigateByUrl('/team/22'); + advance(fixture); + + expect(location.path()).toEqual('/'); + }))); + }); + + describe( + 'should not activate a route when CanActivate returns false (componentless route)', + () => { + beforeEach(() => { + addProviders([{provide: 'alwaysFalse', useValue: (a: any, b: any) => false}]); + }); + + it('works', fakeAsync(inject( + [Router, TestComponentBuilder, Location], + (router: Router, tcb: TestComponentBuilder, location: Location) => { + const fixture = createRoot(tcb, router, RootCmp); + + router.resetConfig([{ + path: 'parent', + canActivate: ['alwaysFalse'], + children: [{path: 'team/:id', component: TeamCmp}] + }]); + + router.navigateByUrl('parent/team/22'); + advance(fixture); + + expect(location.path()).toEqual('/'); + }))); + }); + + describe('should activate a route when CanActivate returns true', () => { + beforeEach(() => { + addProviders([{ + provide: 'alwaysTrue', + useValue: (a: ActivatedRouteSnapshot, s: RouterStateSnapshot) => true + }]); + }); + + it('works', + fakeAsync(inject( + [Router, TestComponentBuilder, Location], + (router: Router, tcb: TestComponentBuilder, location: Location) => { + const fixture = createRoot(tcb, router, RootCmp); + + router.resetConfig( + [{path: 'team/:id', component: TeamCmp, canActivate: ['alwaysTrue']}]); + + router.navigateByUrl('/team/22'); + advance(fixture); + + expect(location.path()).toEqual('/team/22'); + }))); + }); + + describe('should work when given a class', () => { + class AlwaysTrue implements CanActivate { + canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { + return true; + } + } + + beforeEach(() => { addProviders([AlwaysTrue]); }); + + it('works', fakeAsync(inject( + [Router, TestComponentBuilder, Location], + (router: Router, tcb: TestComponentBuilder, location: Location) => { + const fixture = createRoot(tcb, router, RootCmp); + + router.resetConfig( + [{path: 'team/:id', component: TeamCmp, canActivate: [AlwaysTrue]}]); + + router.navigateByUrl('/team/22'); + advance(fixture); + + expect(location.path()).toEqual('/team/22'); + }))); + }); + + describe('should work when returns an observable', () => { + beforeEach(() => { + addProviders([{ + provide: 'CanActivate', + useValue: (a: ActivatedRouteSnapshot, b: RouterStateSnapshot) => { return of (false); } + }]); + }); + + + it('works', + fakeAsync(inject( + [Router, TestComponentBuilder, Location], + (router: Router, tcb: TestComponentBuilder, location: Location) => { + const fixture = createRoot(tcb, router, RootCmp); + + router.resetConfig( + [{path: 'team/:id', component: TeamCmp, canActivate: ['CanActivate']}]); + + router.navigateByUrl('/team/22'); + advance(fixture); + expect(location.path()).toEqual('/'); + }))); + }); + + describe('should work when returns a promise', () => { + beforeEach(() => { + addProviders([{ + provide: 'CanActivate', + useValue: (a: ActivatedRouteSnapshot, b: RouterStateSnapshot) => { + if (a.params['id'] == '22') { + return Promise.resolve(true); + } else { + return Promise.resolve(false); + } + } + }]); + }); + + + it('works', + fakeAsync(inject( + [Router, TestComponentBuilder, Location], + (router: Router, tcb: TestComponentBuilder, location: Location) => { + const fixture = createRoot(tcb, router, RootCmp); + + router.resetConfig( + [{path: 'team/:id', component: TeamCmp, canActivate: ['CanActivate']}]); + + router.navigateByUrl('/team/22'); + advance(fixture); + expect(location.path()).toEqual('/team/22'); + + router.navigateByUrl('/team/33'); + advance(fixture); + expect(location.path()).toEqual('/team/22'); + }))); + }); + }); + + describe('CanDeactivate', () => { + describe('should not deactivate a route when CanDeactivate returns false', () => { + beforeEach(() => { + addProviders([ + { + provide: 'CanDeactivateParent', + useValue: (c: any, a: ActivatedRouteSnapshot, b: RouterStateSnapshot) => { + return a.params['id'] === '22'; + } + }, + { + provide: 'CanDeactivateTeam', + useValue: (c: any, a: ActivatedRouteSnapshot, b: RouterStateSnapshot) => { + return c.route.snapshot.params['id'] === '22'; + } + }, + { + provide: 'CanDeactivateUser', + useValue: (c: any, a: ActivatedRouteSnapshot, b: RouterStateSnapshot) => { + return a.params['name'] === 'victor'; + } + } + ]); + }); + + it('works', + fakeAsync(inject( + [Router, TestComponentBuilder, Location], + (router: Router, tcb: TestComponentBuilder, location: Location) => { + const fixture = createRoot(tcb, router, RootCmp); + + router.resetConfig([ + {path: 'team/:id', component: TeamCmp, canDeactivate: ['CanDeactivateTeam']} + ]); + + router.navigateByUrl('/team/22'); + advance(fixture); + expect(location.path()).toEqual('/team/22'); + + let successStatus: boolean; + router.navigateByUrl('/team/33').then(res => successStatus = res); + advance(fixture); + expect(location.path()).toEqual('/team/33'); + expect(successStatus).toEqual(true); + + let canceledStatus: boolean; + router.navigateByUrl('/team/44').then(res => canceledStatus = res); + advance(fixture); + expect(location.path()).toEqual('/team/33'); + expect(canceledStatus).toEqual(false); + }))); + + it('works (componentless route)', + fakeAsync(inject( + [Router, TestComponentBuilder, Location], + (router: Router, tcb: TestComponentBuilder, location: Location) => { + const fixture = createRoot(tcb, router, RootCmp); + + router.resetConfig([{ + path: 'parent/:id', + canDeactivate: ['CanDeactivateParent'], + children: [{path: 'simple', component: SimpleCmp}] + }]); + + router.navigateByUrl('/parent/22/simple'); + advance(fixture); + expect(location.path()).toEqual('/parent/22/simple'); + + router.navigateByUrl('/parent/33/simple'); + advance(fixture); + expect(location.path()).toEqual('/parent/33/simple'); + + router.navigateByUrl('/parent/44/simple'); + advance(fixture); + expect(location.path()).toEqual('/parent/33/simple'); + }))); + + it('works with a nested route', + fakeAsync(inject( + [Router, TestComponentBuilder, Location], + (router: Router, tcb: TestComponentBuilder, location: Location) => { + const fixture = createRoot(tcb, router, RootCmp); + + router.resetConfig([{ + path: 'team/:id', + component: TeamCmp, + children: [ + {path: '', pathMatch: 'full', component: SimpleCmp}, { + path: 'user/:name', + component: UserCmp, + canDeactivate: ['CanDeactivateUser'] + } + ] + }]); + + router.navigateByUrl('/team/22/user/victor'); + advance(fixture); + + // this works because we can deactivate victor + router.navigateByUrl('/team/33'); + advance(fixture); + expect(location.path()).toEqual('/team/33'); + + router.navigateByUrl('/team/33/user/fedor'); + advance(fixture); + + // this doesn't work cause we cannot deactivate fedor + router.navigateByUrl('/team/44'); + advance(fixture); + expect(location.path()).toEqual('/team/33/user/fedor'); + }))); + }); + + describe('should work when given a class', () => { + class AlwaysTrue implements CanDeactivate { + canDeactivate( + component: TeamCmp, route: ActivatedRouteSnapshot, + state: RouterStateSnapshot): boolean { + return true; + } + } + + beforeEach(() => { addProviders([AlwaysTrue]); }); + + it('works', + fakeAsync(inject( + [Router, TestComponentBuilder, Location], + (router: Router, tcb: TestComponentBuilder, location: Location) => { + const fixture = createRoot(tcb, router, RootCmp); + + router.resetConfig( + [{path: 'team/:id', component: TeamCmp, canDeactivate: [AlwaysTrue]}]); + + router.navigateByUrl('/team/22'); + advance(fixture); + expect(location.path()).toEqual('/team/22'); + + router.navigateByUrl('/team/33'); + advance(fixture); + expect(location.path()).toEqual('/team/33'); + }))); + }); + + + describe('should work when returns an observable', () => { + beforeEach(() => { + addProviders([{ + provide: 'CanDeactivate', + useValue: (c: TeamCmp, a: ActivatedRouteSnapshot, b: RouterStateSnapshot) => { + return of (false); + } + }]); + }); + + it('works', + fakeAsync(inject( + [Router, TestComponentBuilder, Location], + (router: Router, tcb: TestComponentBuilder, location: Location) => { + const fixture = createRoot(tcb, router, RootCmp); + + router.resetConfig( + [{path: 'team/:id', component: TeamCmp, canDeactivate: ['CanDeactivate']}]); + + router.navigateByUrl('/team/22'); + advance(fixture); + expect(location.path()).toEqual('/team/22'); + + router.navigateByUrl('/team/33'); + advance(fixture); + expect(location.path()).toEqual('/team/22'); + }))); + }); + }); + + describe('CanActivateChild', () => { + describe('should be invoked when activating a child', () => { + beforeEach(() => { + addProviders([{ + provide: 'alwaysFalse', + useValue: (a: any, b: any) => { return a.params.id === '22'; } + }]); + }); + + it('works', fakeAsync(inject( + [Router, TestComponentBuilder, Location], + (router: Router, tcb: TestComponentBuilder, location: Location) => { + const fixture = createRoot(tcb, router, RootCmp); + + router.resetConfig([{ + path: '', + canActivateChild: ['alwaysFalse'], + children: [{path: 'team/:id', component: TeamCmp}] + }]); + + router.navigateByUrl('/team/22'); + advance(fixture); + + expect(location.path()).toEqual('/team/22'); + + router.navigateByUrl('/team/33').catch(() => {}); + advance(fixture); + + expect(location.path()).toEqual('/team/22'); + }))); + }); + }); + + describe('CanLoad', () => { + describe('should not load children when CanLoad returns false', () => { + beforeEach(() => { + addProviders([ + {provide: 'alwaysFalse', useValue: (a: any) => false}, + {provide: 'alwaysTrue', useValue: (a: any) => true} + ]); + }); + + it('works', + fakeAsync(inject( + [Router, TestComponentBuilder, Location, NgModuleFactoryLoader], + (router: Router, tcb: TestComponentBuilder, location: Location, + loader: SpyNgModuleFactoryLoader) => { + + @Component({selector: 'lazy', template: 'lazy-loaded'}) + class LazyLoadedComponent { + } + + @NgModule({ + declarations: [LazyLoadedComponent], + imports: + [RouterModule.forChild([{path: 'loaded', component: LazyLoadedComponent}])], + entryComponents: [LazyLoadedComponent] + }) + class LoadedModule { + } + + loader.stubbedModules = {lazyFalse: LoadedModule, lazyTrue: LoadedModule}; + const fixture = createRoot(tcb, router, RootCmp); + + router.resetConfig([ + {path: 'lazyFalse', canLoad: ['alwaysFalse'], loadChildren: 'lazyFalse'}, + {path: 'lazyTrue', canLoad: ['alwaysTrue'], loadChildren: 'lazyTrue'} + ]); + + const recordedEvents: any[] = []; + router.events.forEach(e => recordedEvents.push(e)); + + + // failed navigation + router.navigateByUrl('/lazyFalse/loaded').catch(s => {}); + advance(fixture); + + expect(location.path()).toEqual('/'); + + expectEvents(recordedEvents, [ + [NavigationStart, '/lazyFalse/loaded'], [NavigationError, '/lazyFalse/loaded'] + ]); + + recordedEvents.splice(0); + + + // successful navigation + router.navigateByUrl('/lazyTrue/loaded'); + advance(fixture); + + expect(location.path()).toEqual('/lazyTrue/loaded'); + + expectEvents(recordedEvents, [ + [NavigationStart, '/lazyTrue/loaded'], [RoutesRecognized, '/lazyTrue/loaded'], + [NavigationEnd, '/lazyTrue/loaded'] + ]); + }))); + }); + }); + }); + + describe('routerActiveLink', () => { + it('should set the class when the link is active (a tag)', + fakeAsync(inject( + [Router, TestComponentBuilder, Location], + (router: Router, tcb: TestComponentBuilder, location: Location) => { + const fixture = createRoot(tcb, router, RootCmp); + + router.resetConfig([{ + path: 'team/:id', + component: TeamCmp, + children: [{ + path: 'link', + component: DummyLinkCmp, + children: + [{path: 'simple', component: SimpleCmp}, {path: '', component: BlankCmp}] + }] + }]); + + router.navigateByUrl('/team/22/link;exact=true'); + advance(fixture); + expect(location.path()).toEqual('/team/22/link;exact=true'); + + const nativeLink = fixture.debugElement.nativeElement.querySelector('a'); + const nativeButton = fixture.debugElement.nativeElement.querySelector('button'); + expect(nativeLink.className).toEqual('active'); + expect(nativeButton.className).toEqual('active'); + + router.navigateByUrl('/team/22/link/simple'); + advance(fixture); + expect(location.path()).toEqual('/team/22/link/simple'); + expect(nativeLink.className).toEqual(''); + expect(nativeButton.className).toEqual(''); + }))); + + it('should not set the class until the first navigation succeeds', + fakeAsync(inject( + [Router, TestComponentBuilder, Location], + (router: Router, tcb: TestComponentBuilder, location: Location) => { + @Component({ + template: + '' + }) + class RootCmpWithLink { + } + + const f = tcb.createFakeAsync(RootCmpWithLink); + advance(f); + + const link = f.debugElement.nativeElement.querySelector('a'); + expect(link.className).toEqual(''); + + router.initialNavigation(); + advance(f); + + expect(link.className).toEqual('active'); + }))); + + + it('should set the class on a parent element when the link is active', + fakeAsync(inject( + [Router, TestComponentBuilder, Location], + (router: Router, tcb: TestComponentBuilder, location: Location) => { + const fixture = createRoot(tcb, router, RootCmp); + + router.resetConfig([{ + path: 'team/:id', + component: TeamCmp, + children: [{ + path: 'link', + component: DummyLinkWithParentCmp, + children: + [{path: 'simple', component: SimpleCmp}, {path: '', component: BlankCmp}] + }] + }]); + + router.navigateByUrl('/team/22/link;exact=true'); + advance(fixture); + expect(location.path()).toEqual('/team/22/link;exact=true'); + + const native = fixture.debugElement.nativeElement.querySelector('link-parent'); + expect(native.className).toEqual('active'); + + router.navigateByUrl('/team/22/link/simple'); + advance(fixture); + expect(location.path()).toEqual('/team/22/link/simple'); + expect(native.className).toEqual(''); + }))); + + it('should set the class when the link is active', + fakeAsync(inject( + [Router, TestComponentBuilder, Location], + (router: Router, tcb: TestComponentBuilder, location: Location) => { + const fixture = createRoot(tcb, router, RootCmp); + + router.resetConfig([{ + path: 'team/:id', + component: TeamCmp, + children: [{ + path: 'link', + component: DummyLinkCmp, + children: + [{path: 'simple', component: SimpleCmp}, {path: '', component: BlankCmp}] + }] + }]); + + router.navigateByUrl('/team/22/link'); + advance(fixture); + expect(location.path()).toEqual('/team/22/link'); + + const native = fixture.debugElement.nativeElement.querySelector('a'); + expect(native.className).toEqual('active'); + + router.navigateByUrl('/team/22/link/simple'); + advance(fixture); + expect(location.path()).toEqual('/team/22/link/simple'); + expect(native.className).toEqual('active'); + }))); + + }); + + describe('lazy loading', () => { + it('works', fakeAsync(inject( + [Router, TestComponentBuilder, Location, NgModuleFactoryLoader], + (router: Router, tcb: TestComponentBuilder, location: Location, + loader: SpyNgModuleFactoryLoader) => { + @Component({ + selector: 'lazy', + template: 'lazy-loaded-parent []', + directives: ROUTER_DIRECTIVES + }) + class ParentLazyLoadedComponent { + } + + @Component({selector: 'lazy', template: 'lazy-loaded-child'}) + class ChildLazyLoadedComponent { + } + + @NgModule({ + declarations: [ParentLazyLoadedComponent, ChildLazyLoadedComponent], + imports: [RouterModule.forChild([{ + path: 'loaded', + component: ParentLazyLoadedComponent, + children: [{path: 'child', component: ChildLazyLoadedComponent}] + }])], + entryComponents: [ParentLazyLoadedComponent, ChildLazyLoadedComponent] + }) + class LoadedModule { + } + + + loader.stubbedModules = {expected: LoadedModule}; + + const fixture = createRoot(tcb, router, RootCmp); + + router.resetConfig([{path: 'lazy', loadChildren: 'expected'}]); + + router.navigateByUrl('/lazy/loaded/child'); + advance(fixture); + + expect(location.path()).toEqual('/lazy/loaded/child'); + expect(fixture.debugElement.nativeElement) + .toHaveText('lazy-loaded-parent [lazy-loaded-child]'); + }))); + + it('should combine routes from multiple modules into a single configuration', + fakeAsync(inject( + [Router, TestComponentBuilder, Location, NgModuleFactoryLoader], + (router: Router, tcb: TestComponentBuilder, location: Location, + loader: SpyNgModuleFactoryLoader) => { + @Component({selector: 'lazy', template: 'lazy-loaded-2'}) + class LazyComponent2 { + } + + @NgModule({ + declarations: [LazyComponent2], + imports: [RouterModule.forChild([{path: 'loaded', component: LazyComponent2}])], + entryComponents: [LazyComponent2] + }) + class SiblingOfLoadedModule { + } + + @Component( + {selector: 'lazy', template: 'lazy-loaded-1', directives: ROUTER_DIRECTIVES}) + class LazyComponent1 { + } + + @NgModule({ + declarations: [LazyComponent1], + imports: [ + RouterModule.forChild([{path: 'loaded', component: LazyComponent1}]), + SiblingOfLoadedModule + ], + entryComponents: [LazyComponent1] + }) + class LoadedModule { + } + + loader.stubbedModules = {expected1: LoadedModule, expected2: SiblingOfLoadedModule}; + + const fixture = createRoot(tcb, router, RootCmp); + + router.resetConfig([ + {path: 'lazy1', loadChildren: 'expected1'}, + {path: 'lazy2', loadChildren: 'expected2'} + ]); + + router.navigateByUrl('/lazy1/loaded'); + advance(fixture); + expect(location.path()).toEqual('/lazy1/loaded'); + + router.navigateByUrl('/lazy2/loaded'); + advance(fixture); + expect(location.path()).toEqual('/lazy2/loaded'); + }))); + + it('should use the injector of the lazily-loaded configuration', + fakeAsync(inject( + [Router, TestComponentBuilder, Location, NgModuleFactoryLoader], + (router: Router, tcb: TestComponentBuilder, location: Location, + loader: SpyNgModuleFactoryLoader) => { + class LazyLoadedServiceDefinedInModule {} + class LazyLoadedServiceDefinedInCmp {} + + @Component({selector: 'lazy', template: 'lazy-loaded'}) + class LazyLoadedChildComponent { + constructor(service: LazyLoadedServiceDefinedInCmp) {} + } + + @Component({ + selector: 'lazy', + template: '', + providers: [LazyLoadedServiceDefinedInCmp] + }) + class LazyLoadedParentComponent { + constructor(service: LazyLoadedServiceDefinedInModule) {} + } + + @NgModule({ + entryComponents: [LazyLoadedParentComponent], + declarations: [LazyLoadedParentComponent, LazyLoadedChildComponent], + imports: [RouterModule.forChild([{ + path: '', + children: [{ + path: 'loaded', + component: LazyLoadedParentComponent, + children: [{path: 'child', component: LazyLoadedChildComponent}] + }] + }])], + providers: [LazyLoadedServiceDefinedInModule] + }) + class LoadedModule { + } + + loader.stubbedModules = {expected: LoadedModule}; + + const fixture = createRoot(tcb, router, RootCmp); + + router.resetConfig([{path: 'lazy', loadChildren: 'expected'}]); + + router.navigateByUrl('/lazy/loaded/child'); + advance(fixture); + + expect(location.path()).toEqual('/lazy/loaded/child'); + expect(fixture.debugElement.nativeElement).toHaveText('lazy-loaded'); + }))); + + it('error emit an error when cannot load a config', + fakeAsync(inject( + [Router, TestComponentBuilder, Location, NgModuleFactoryLoader], + (router: Router, tcb: TestComponentBuilder, location: Location, + loader: SpyNgModuleFactoryLoader) => { + loader.stubbedModules = {}; + const fixture = createRoot(tcb, router, RootCmp); + + router.resetConfig([{path: 'lazy', loadChildren: 'invalid'}]); + + const recordedEvents: any[] = []; + router.events.forEach(e => recordedEvents.push(e)); + + router.navigateByUrl('/lazy/loaded').catch(s => {}); + advance(fixture); + + expect(location.path()).toEqual('/'); + + expectEvents( + recordedEvents, + [[NavigationStart, '/lazy/loaded'], [NavigationError, '/lazy/loaded']]); + }))); + }); +}); + +function expectEvents(events: Event[], pairs: any[]) { + for (let i = 0; i < events.length; ++i) { + expect((events[i].constructor).name).toBe(pairs[i][0].name); + expect((events[i]).url).toBe(pairs[i][1]); + } +} + +@Component({ + selector: 'link-cmp', + template: `link`, + directives: ROUTER_DIRECTIVES +}) +class StringLinkCmp { +} + +@Component({ + selector: 'link-cmp', + template: ``, + directives: ROUTER_DIRECTIVES +}) +class StringLinkButtonCmp { +} + +@Component({ + selector: 'link-cmp', + template: `link`, + directives: ROUTER_DIRECTIVES +}) +class AbsoluteLinkCmp { +} + +@Component({ + selector: 'link-cmp', + template: + `link + +`, + directives: ROUTER_DIRECTIVES +}) +class DummyLinkCmp { + private exact: boolean; + constructor(route: ActivatedRoute) { this.exact = (route.snapshot.params).exact === 'true'; } +} + +@Component({ + selector: 'link-cmp', + template: `link`, + directives: ROUTER_DIRECTIVES +}) +class RelativeLinkCmp { +} + +@Component({ + selector: 'link-cmp', + template: `link`, + directives: ROUTER_DIRECTIVES +}) +class LinkWithQueryParamsAndFragment { +} + +@Component({selector: 'simple-cmp', template: `simple`, directives: ROUTER_DIRECTIVES}) +class SimpleCmp { +} + +@Component( + {selector: 'collect-params-cmp', template: `collect-params`, directives: ROUTER_DIRECTIVES}) +class CollectParamsCmp { + private params: any = []; + private urls: any = []; + + constructor(a: ActivatedRoute) { + a.params.forEach(p => this.params.push(p)); + a.url.forEach(u => this.urls.push(u)); + } + + recordedUrls(): string[] { + return this.urls.map((a: any) => a.map((p: any) => p.path).join('/')); + } +} + +@Component({selector: 'blank-cmp', template: ``, directives: ROUTER_DIRECTIVES}) +class BlankCmp { +} + +@Component({ + selector: 'team-cmp', + template: + `team {{id | async}} [ , right: ]`, + directives: ROUTER_DIRECTIVES +}) +class TeamCmp { + id: Observable; + recordedParams: Params[] = []; + + constructor(public route: ActivatedRoute) { + this.id = route.params.map(p => p['id']); + route.params.forEach(_ => this.recordedParams.push(_)); + } +} + +@Component( + {selector: 'user-cmp', template: `user {{name | async}}`, directives: [ROUTER_DIRECTIVES]}) +class UserCmp { + name: Observable; + recordedParams: Params[] = []; + + constructor(route: ActivatedRoute) { + this.name = route.params.map(p => p['name']); + route.params.forEach(_ => this.recordedParams.push(_)); + } +} + +@Component({ + selector: 'wrapper', + template: ``, + directives: [ROUTER_DIRECTIVES] +}) +class WrapperCmp { +} + +@Component({ + selector: 'query-cmp', + template: `query: {{name | async}} fragment: {{fragment | async}}`, + directives: [ROUTER_DIRECTIVES] +}) +class QueryParamsAndFragmentCmp { + name: Observable; + fragment: Observable; + + constructor(router: Router) { + this.name = router.routerState.queryParams.map(p => p['name']); + this.fragment = router.routerState.fragment; + } +} + +@Component({selector: 'route-cmp', template: `route`, directives: ROUTER_DIRECTIVES}) +class RouteCmp { + constructor(public route: ActivatedRoute) {} +} + +@Component({ + selector: 'link-cmp', + template: + ` `, + directives: ROUTER_DIRECTIVES, + entryComponents: [BlankCmp, SimpleCmp] +}) +class RelativeLinkInIfCmp { + show: boolean = false; +} + +@Component({ + selector: 'child', + template: '
', + directives: ROUTER_DIRECTIVES +}) +class LinkInNgIf { + alwaysTrue = true; +} + +@Component({ + selector: 'link-cmp', + template: ` + + + `, + directives: ROUTER_DIRECTIVES +}) +class DummyLinkWithParentCmp { + private exact: boolean; + constructor(route: ActivatedRoute) { this.exact = (route.snapshot.params).exact === 'true'; } +} + +@Component({selector: 'cmp', template: ''}) +class ComponentRecordingRoutePathAndUrl { + private path: any; + private url: any; + + constructor(router: Router, route: ActivatedRoute) { + this.path = router.routerState.pathFromRoot(route); + this.url = router.url.toString(); + } +} + +@Component({ + selector: 'root-cmp', + template: ``, + directives: [ROUTER_DIRECTIVES], + entryComponents: [ + BlankCmp, SimpleCmp, TeamCmp, UserCmp, StringLinkCmp, DummyLinkCmp, AbsoluteLinkCmp, + RelativeLinkCmp, DummyLinkWithParentCmp, LinkWithQueryParamsAndFragment, CollectParamsCmp, + QueryParamsAndFragmentCmp, StringLinkButtonCmp, WrapperCmp, LinkInNgIf, + ComponentRecordingRoutePathAndUrl + ] +}) +class RootCmp { +} + +@Component({ + selector: 'root-cmp', + template: + `primary [] right []`, + directives: [ROUTER_DIRECTIVES], + entryComponents: [BlankCmp, SimpleCmp, RouteCmp, UserCmp] +}) +class RootCmpWithTwoOutlets { +} + + +function advance(fixture: ComponentFixture): void { + tick(); + fixture.detectChanges(); +} + +function createRoot(tcb: TestComponentBuilder, router: Router, type: any): ComponentFixture { + const f = tcb.createFakeAsync(type); + advance(f); + router.initialNavigation(); + advance(f); + return f; +} diff --git a/modules/@angular/router/test/router.spec.ts b/modules/@angular/router/test/router.spec.ts index e07003063f..42ea05b27b 100644 --- a/modules/@angular/router/test/router.spec.ts +++ b/modules/@angular/router/test/router.spec.ts @@ -6,1807 +6,59 @@ * found in the LICENSE file at https://angular.io/license */ -import 'rxjs/add/operator/map'; - -import {Location} from '@angular/common'; -import {Component, NgModule, NgModuleFactoryLoader} from '@angular/core'; -import {ComponentFixture, TestBed, TestComponentBuilder, addProviders, fakeAsync, inject, tick} from '@angular/core/testing'; -import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; -import {expect} from '@angular/platform-browser/testing/matchers'; -import {Observable} from 'rxjs/Observable'; -import {of } from 'rxjs/observable/of'; - -import {ActivatedRoute, ActivatedRouteSnapshot, CanActivate, CanDeactivate, Event, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, Params, ROUTER_DIRECTIVES, Resolve, Router, RouterModule, RouterStateSnapshot, RoutesRecognized, provideRoutes} from '../index'; -import {RouterTestingModule, SpyNgModuleFactoryLoader} from '../testing'; - -describe('Integration', () => { - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [RouterTestingModule], - providers: [provideRoutes( - [{path: '', component: BlankCmp}, {path: 'simple', component: SimpleCmp}])], - declarations: [ - BlankCmp, SimpleCmp, TeamCmp, UserCmp, StringLinkCmp, DummyLinkCmp, AbsoluteLinkCmp, - RelativeLinkCmp, DummyLinkWithParentCmp, LinkWithQueryParamsAndFragment, CollectParamsCmp, - QueryParamsAndFragmentCmp, StringLinkButtonCmp, WrapperCmp, LinkInNgIf, - ComponentRecordingRoutePathAndUrl, RouteCmp - ] - }); - }); - - it('should navigate with a provided config', - fakeAsync(inject( - [Router, TestComponentBuilder, Location], - (router: Router, tcb: TestComponentBuilder, location: Location) => { - const fixture = createRoot(tcb, router, RootCmp); - - router.navigateByUrl('/simple'); - advance(fixture); - - expect(location.path()).toEqual('/simple'); - }))); - - it('should work when an outlet is in an ngIf', - fakeAsync(inject( - [Router, TestComponentBuilder, Location], - (router: Router, tcb: TestComponentBuilder, location: Location) => { - const fixture = createRoot(tcb, router, RootCmp); - - router.resetConfig([{ - path: 'child', - component: LinkInNgIf, - children: [{path: 'simple', component: SimpleCmp}] - }]); - - router.navigateByUrl('/child/simple'); - advance(fixture); - - expect(location.path()).toEqual('/child/simple'); - }))); - - it('should work when an outlet is in an ngIf (and is removed)', - fakeAsync(inject( - [Router, TestComponentBuilder, Location], - (router: Router, tcb: TestComponentBuilder, location: Location) => { - @Component({ - selector: 'someRoot', - template: `
`, - entryComponents: [BlankCmp, SimpleCmp] - }) - class RootCmpWithLink { - cond: boolean = true; - } - - const fixture = createRoot(tcb, router, RootCmpWithLink); - - router.resetConfig( - [{path: 'simple', component: SimpleCmp}, {path: 'blank', component: BlankCmp}]); - - router.navigateByUrl('/simple'); - advance(fixture); - expect(location.path()).toEqual('/simple'); - - const instance = fixture.componentInstance; - instance.cond = false; - advance(fixture); - - let recordedError: any = null; - router.navigateByUrl('/blank').catch(e => recordedError = e); - advance(fixture); - expect(recordedError.message).toEqual('Cannot find primary outlet to load \'BlankCmp\''); - }))); - - it('should update location when navigating', - fakeAsync(inject( - [Router, TestComponentBuilder, Location], - (router: Router, tcb: TestComponentBuilder, location: Location) => { - const fixture = createRoot(tcb, router, RootCmp); - - router.resetConfig([{path: 'team/:id', component: TeamCmp}]); - - router.navigateByUrl('/team/22'); - advance(fixture); - expect(location.path()).toEqual('/team/22'); - - router.navigateByUrl('/team/33'); - advance(fixture); - - expect(location.path()).toEqual('/team/33'); - }))); - - it('should skip location update when using NavigationExtras.skipLocationChange with navigateByUrl', - fakeAsync(inject( - [Router, TestComponentBuilder, Location], - (router: Router, tcb: TestComponentBuilder, location: Location) => { - const fixture = tcb.createFakeAsync(RootCmp); - advance(fixture); - - router.resetConfig([{path: 'team/:id', component: TeamCmp}]); - - router.navigateByUrl('/team/22'); - advance(fixture); - expect(location.path()).toEqual('/team/22'); - - expect(fixture.debugElement.nativeElement).toHaveText('team 22 [ , right: ]'); - - router.navigateByUrl('/team/33', {skipLocationChange: true}); - advance(fixture); - - expect(location.path()).toEqual('/team/22'); - - expect(fixture.debugElement.nativeElement).toHaveText('team 33 [ , right: ]'); - }))); - - it('should skip location update when using NavigationExtras.skipLocationChange with navigate', - fakeAsync(inject( - [Router, TestComponentBuilder, Location], - (router: Router, tcb: TestComponentBuilder, location: Location) => { - const fixture = tcb.createFakeAsync(RootCmp); - advance(fixture); - - router.resetConfig([{path: 'team/:id', component: TeamCmp}]); - - router.navigate(['/team/22']); - advance(fixture); - expect(location.path()).toEqual('/team/22'); - - expect(fixture.debugElement.nativeElement).toHaveText('team 22 [ , right: ]'); - - router.navigate(['/team/33'], {skipLocationChange: true}); - advance(fixture); - - expect(location.path()).toEqual('/team/22'); - - expect(fixture.debugElement.nativeElement).toHaveText('team 33 [ , right: ]'); - }))); - - it('should navigate back and forward', - fakeAsync(inject( - [Router, TestComponentBuilder, Location], - (router: Router, tcb: TestComponentBuilder, location: Location) => { - const fixture = createRoot(tcb, router, RootCmp); - - router.resetConfig([{ - path: 'team/:id', - component: TeamCmp, - children: [ - {path: 'simple', component: SimpleCmp}, {path: 'user/:name', component: UserCmp} - ] - }]); - - - router.navigateByUrl('/team/33/simple'); - advance(fixture); - expect(location.path()).toEqual('/team/33/simple'); - - router.navigateByUrl('/team/22/user/victor'); - advance(fixture); - - location.back(); - advance(fixture); - expect(location.path()).toEqual('/team/33/simple'); - - location.forward(); - advance(fixture); - expect(location.path()).toEqual('/team/22/user/victor'); - }))); - - it('should navigate when locations changes', - fakeAsync(inject( - [Router, TestComponentBuilder, Location], - (router: Router, tcb: TestComponentBuilder, location: Location) => { - const fixture = createRoot(tcb, router, RootCmp); - - router.resetConfig([{ - path: 'team/:id', - component: TeamCmp, - children: [{path: 'user/:name', component: UserCmp}] - }]); - - const recordedEvents: any[] = []; - router.events.forEach(e => recordedEvents.push(e)); - - router.navigateByUrl('/team/22/user/victor'); - advance(fixture); - - (location).simulateHashChange('/team/22/user/fedor'); - advance(fixture); - - (location).simulateUrlPop('/team/22/user/fedor'); - advance(fixture); - - expect(fixture.debugElement.nativeElement).toHaveText('team 22 [ user fedor, right: ]'); - - expectEvents(recordedEvents, [ - [NavigationStart, '/team/22/user/victor'], [RoutesRecognized, '/team/22/user/victor'], - [NavigationEnd, '/team/22/user/victor'], - - [NavigationStart, '/team/22/user/fedor'], [RoutesRecognized, '/team/22/user/fedor'], - [NavigationEnd, '/team/22/user/fedor'] - ]); - }))); - - it('should update the location when the matched route does not change', - fakeAsync(inject( - [Router, TestComponentBuilder, Location], - (router: Router, tcb: TestComponentBuilder, location: Location) => { - const fixture = createRoot(tcb, router, RootCmp); - - router.resetConfig([{path: '**', component: CollectParamsCmp}]); - - router.navigateByUrl('/one/two'); - advance(fixture); - const cmp = fixture.debugElement.children[1].componentInstance; - expect(location.path()).toEqual('/one/two'); - expect(fixture.debugElement.nativeElement).toHaveText('collect-params'); - - expect(cmp.recordedUrls()).toEqual(['one/two']); - - router.navigateByUrl('/three/four'); - advance(fixture); - expect(location.path()).toEqual('/three/four'); - expect(fixture.debugElement.nativeElement).toHaveText('collect-params'); - expect(cmp.recordedUrls()).toEqual(['one/two', 'three/four']); - }))); - - it('should support secondary routes', - fakeAsync( - inject([Router, TestComponentBuilder], (router: Router, tcb: TestComponentBuilder) => { - const fixture = createRoot(tcb, router, RootCmp); - - router.resetConfig([{ - path: 'team/:id', - component: TeamCmp, - children: [ - {path: 'user/:name', component: UserCmp}, - {path: 'simple', component: SimpleCmp, outlet: 'right'} - ] - }]); - - router.navigateByUrl('/team/22/(user/victor//right:simple)'); - advance(fixture); - - expect(fixture.debugElement.nativeElement) - .toHaveText('team 22 [ user victor, right: simple ]'); - }))); - - it('should deactivate outlets', - fakeAsync( - inject([Router, TestComponentBuilder], (router: Router, tcb: TestComponentBuilder) => { - const fixture = createRoot(tcb, router, RootCmp); - - router.resetConfig([{ - path: 'team/:id', - component: TeamCmp, - children: [ - {path: 'user/:name', component: UserCmp}, - {path: 'simple', component: SimpleCmp, outlet: 'right'} - ] - }]); - - router.navigateByUrl('/team/22/(user/victor//right:simple)'); - advance(fixture); - - router.navigateByUrl('/team/22/user/victor'); - advance(fixture); - - expect(fixture.debugElement.nativeElement) - .toHaveText('team 22 [ user victor, right: ]'); - }))); - - it('should deactivate nested outlets', - fakeAsync( - inject([Router, TestComponentBuilder], (router: Router, tcb: TestComponentBuilder) => { - const fixture = createRoot(tcb, router, RootCmp); - - router.resetConfig([ - { - path: 'team/:id', - component: TeamCmp, - children: [ - {path: 'user/:name', component: UserCmp}, - {path: 'simple', component: SimpleCmp, outlet: 'right'} - ] - }, - {path: '', component: BlankCmp} - ]); - - router.navigateByUrl('/team/22/(user/victor//right:simple)'); - advance(fixture); - - router.navigateByUrl('/'); - advance(fixture); - - expect(fixture.debugElement.nativeElement).toHaveText(''); - }))); - - it('should set query params and fragment', - fakeAsync( - inject([Router, TestComponentBuilder], (router: Router, tcb: TestComponentBuilder) => { - const fixture = createRoot(tcb, router, RootCmp); - - router.resetConfig([{path: 'query', component: QueryParamsAndFragmentCmp}]); - - router.navigateByUrl('/query?name=1#fragment1'); - advance(fixture); - expect(fixture.debugElement.nativeElement).toHaveText('query: 1 fragment: fragment1'); - - router.navigateByUrl('/query?name=2#fragment2'); - advance(fixture); - expect(fixture.debugElement.nativeElement).toHaveText('query: 2 fragment: fragment2'); - }))); - - it('should push params only when they change', - fakeAsync( - inject([Router, TestComponentBuilder], (router: Router, tcb: TestComponentBuilder) => { - const fixture = createRoot(tcb, router, RootCmp); - - router.resetConfig([{ - path: 'team/:id', - component: TeamCmp, - children: [{path: 'user/:name', component: UserCmp}] - }]); - - router.navigateByUrl('/team/22/user/victor'); - advance(fixture); - const team = fixture.debugElement.children[1].componentInstance; - const user = fixture.debugElement.children[1].children[1].componentInstance; - - expect(team.recordedParams).toEqual([{id: '22'}]); - expect(user.recordedParams).toEqual([{name: 'victor'}]); - - router.navigateByUrl('/team/22/user/fedor'); - advance(fixture); - - expect(team.recordedParams).toEqual([{id: '22'}]); - expect(user.recordedParams).toEqual([{name: 'victor'}, {name: 'fedor'}]); - }))); - - it('should work when navigating to /', - fakeAsync( - inject([Router, TestComponentBuilder], (router: Router, tcb: TestComponentBuilder) => { - const fixture = createRoot(tcb, router, RootCmp); - - router.resetConfig([ - {path: '', pathMatch: 'full', component: SimpleCmp}, - {path: 'user/:name', component: UserCmp} - ]); - - router.navigateByUrl('/user/victor'); - advance(fixture); - - expect(fixture.debugElement.nativeElement).toHaveText('user victor'); - - router.navigateByUrl('/'); - advance(fixture); - - expect(fixture.debugElement.nativeElement).toHaveText('simple'); - }))); - - it('should cancel in-flight navigations', - fakeAsync( - inject([Router, TestComponentBuilder], (router: Router, tcb: TestComponentBuilder) => { - const fixture = createRoot(tcb, router, RootCmp); - - router.resetConfig([{path: 'user/:name', component: UserCmp}]); - - const recordedEvents: any[] = []; - router.events.forEach(e => recordedEvents.push(e)); - - router.navigateByUrl('/user/init'); - advance(fixture); - - const user = fixture.debugElement.children[1].componentInstance; - - let r1: any, r2: any; - router.navigateByUrl('/user/victor').then(_ => r1 = _); - router.navigateByUrl('/user/fedor').then(_ => r2 = _); - advance(fixture); - - expect(r1).toEqual(false); // returns false because it was canceled - expect(r2).toEqual(true); // returns true because it was successful - - expect(fixture.debugElement.nativeElement).toHaveText('user fedor'); - expect(user.recordedParams).toEqual([{name: 'init'}, {name: 'fedor'}]); - - expectEvents(recordedEvents, [ - [NavigationStart, '/user/init'], [RoutesRecognized, '/user/init'], - [NavigationEnd, '/user/init'], - - [NavigationStart, '/user/victor'], [NavigationStart, '/user/fedor'], - - [NavigationCancel, '/user/victor'], [RoutesRecognized, '/user/fedor'], - [NavigationEnd, '/user/fedor'] - ]); - }))); - - it('should handle failed navigations gracefully', - fakeAsync( - inject([Router, TestComponentBuilder], (router: Router, tcb: TestComponentBuilder) => { - const fixture = createRoot(tcb, router, RootCmp); - - router.resetConfig([{path: 'user/:name', component: UserCmp}]); - - const recordedEvents: any[] = []; - router.events.forEach(e => recordedEvents.push(e)); - - let e: any; - router.navigateByUrl('/invalid').catch(_ => e = _); - advance(fixture); - expect(e.message).toContain('Cannot match any routes'); - - router.navigateByUrl('/user/fedor'); - advance(fixture); - - expect(fixture.debugElement.nativeElement).toHaveText('user fedor'); - - expectEvents(recordedEvents, [ - [NavigationStart, '/invalid'], [NavigationError, '/invalid'], - - [NavigationStart, '/user/fedor'], [RoutesRecognized, '/user/fedor'], - [NavigationEnd, '/user/fedor'] - ]); - }))); - - it('should replace state when path is equal to current path', - fakeAsync(inject( - [Router, TestComponentBuilder, Location], - (router: Router, tcb: TestComponentBuilder, location: Location) => { - const fixture = createRoot(tcb, router, RootCmp); - - router.resetConfig([{ - path: 'team/:id', - component: TeamCmp, - children: [ - {path: 'simple', component: SimpleCmp}, {path: 'user/:name', component: UserCmp} - ] - }]); - - router.navigateByUrl('/team/33/simple'); - advance(fixture); - - router.navigateByUrl('/team/22/user/victor'); - advance(fixture); - - router.navigateByUrl('/team/22/user/victor'); - advance(fixture); - - location.back(); - advance(fixture); - expect(location.path()).toEqual('/team/33/simple'); - }))); - - it('should handle componentless paths', - fakeAsync(inject( - [Router, TestComponentBuilder, Location], - (router: Router, tcb: TestComponentBuilder, location: Location) => { - const fixture = createRoot(tcb, router, RootCmpWithTwoOutlets); - - router.resetConfig([ - { - path: 'parent/:id', - children: [ - {path: 'simple', component: SimpleCmp}, - {path: 'user/:name', component: UserCmp, outlet: 'right'} - ] - }, - {path: 'user/:name', component: UserCmp} - ]); - - - // navigate to a componentless route - router.navigateByUrl('/parent/11/(simple//right:user/victor)'); - advance(fixture); - expect(location.path()).toEqual('/parent/11/(simple//right:user/victor)'); - expect(fixture.debugElement.nativeElement) - .toHaveText('primary [simple] right [user victor]'); - - // navigate to the same route with different params (reuse) - router.navigateByUrl('/parent/22/(simple//right:user/fedor)'); - advance(fixture); - expect(location.path()).toEqual('/parent/22/(simple//right:user/fedor)'); - expect(fixture.debugElement.nativeElement) - .toHaveText('primary [simple] right [user fedor]'); - - // navigate to a normal route (check deactivation) - router.navigateByUrl('/user/victor'); - advance(fixture); - expect(location.path()).toEqual('/user/victor'); - expect(fixture.debugElement.nativeElement).toHaveText('primary [user victor] right []'); - - // navigate back to a componentless route - router.navigateByUrl('/parent/11/(simple//right:user/victor)'); - advance(fixture); - expect(location.path()).toEqual('/parent/11/(simple//right:user/victor)'); - expect(fixture.debugElement.nativeElement) - .toHaveText('primary [simple] right [user victor]'); - }))); - - it('should emit an event when an outlet gets activated', - fakeAsync(inject( - [Router, TestComponentBuilder, Location], - (router: Router, tcb: TestComponentBuilder, location: Location) => { - @Component({ - selector: 'container', - template: - `` - }) - class Container { - activations: any[] = []; - deactivations: any[] = []; - - recordActivate(component: any): void { this.activations.push(component); } - - recordDeactivate(component: any): void { this.deactivations.push(component); } - } - - const fixture = createRoot(tcb, router, Container); - const cmp = fixture.debugElement.componentInstance; - - router.resetConfig( - [{path: 'blank', component: BlankCmp}, {path: 'simple', component: SimpleCmp}]); - - cmp.activations = []; - cmp.deactivations = []; - - router.navigateByUrl('/blank'); - advance(fixture); - - expect(cmp.activations.length).toEqual(1); - expect(cmp.activations[0] instanceof BlankCmp).toBe(true); - - router.navigateByUrl('/simple'); - advance(fixture); - - expect(cmp.activations.length).toEqual(2); - expect(cmp.activations[1] instanceof SimpleCmp).toBe(true); - expect(cmp.deactivations.length).toEqual(2); - expect(cmp.deactivations[1] instanceof BlankCmp).toBe(true); - }))); - - it('should update url and router state before activating components', - fakeAsync(inject( - [Router, TestComponentBuilder, Location], - (router: Router, tcb: TestComponentBuilder, location: Location) => { - - const fixture = createRoot(tcb, router, RootCmp); - - router.resetConfig([{path: 'cmp', component: ComponentRecordingRoutePathAndUrl}]); - - router.navigateByUrl('/cmp'); - advance(fixture); - - const cmp = fixture.debugElement.children[1].componentInstance; - - expect(cmp.url).toBe('/cmp'); - expect(cmp.path.length).toEqual(2); - }))); - - describe('data', () => { - class ResolveSix implements Resolve { - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): number { return 6; } - } - - beforeEach(() => { - addProviders([ - {provide: 'resolveTwo', useValue: (a: any, b: any) => 2}, - {provide: 'resolveFour', useValue: (a: any, b: any) => 4}, - {provide: 'resolveSix', useClass: ResolveSix} - ]); - }); - - it('should provide resolved data', - fakeAsync(inject( - [Router, TestComponentBuilder, Location], - (router: Router, tcb: TestComponentBuilder, location: Location) => { - const fixture = createRoot(tcb, router, RootCmpWithTwoOutlets); - - router.resetConfig([{ - path: 'parent/:id', - data: {one: 1}, - resolve: {two: 'resolveTwo'}, - children: [ - {path: '', data: {three: 3}, resolve: {four: 'resolveFour'}, component: RouteCmp}, - { - path: '', - data: {five: 5}, - resolve: {six: 'resolveSix'}, - component: RouteCmp, - outlet: 'right' - } - ] - }]); - - router.navigateByUrl('/parent/1'); - advance(fixture); - - const primaryCmp = fixture.debugElement.children[1].componentInstance; - const rightCmp = fixture.debugElement.children[3].componentInstance; - - expect(primaryCmp.route.snapshot.data).toEqual({one: 1, two: 2, three: 3, four: 4}); - expect(rightCmp.route.snapshot.data).toEqual({one: 1, two: 2, five: 5, six: 6}); - - let primaryRecorded: any[] = []; - primaryCmp.route.data.forEach((rec: any) => primaryRecorded.push(rec)); - - let rightRecorded: any[] = []; - rightCmp.route.data.forEach((rec: any) => rightRecorded.push(rec)); - - router.navigateByUrl('/parent/2'); - advance(fixture); - - expect(primaryRecorded).toEqual([ - {one: 1, three: 3, two: 2, four: 4}, {one: 1, three: 3, two: 2, four: 4} - ]); - expect(rightRecorded).toEqual([ - {one: 1, five: 5, two: 2, six: 6}, {one: 1, five: 5, two: 2, six: 6} - ]); - }))); - }); - - describe('router links', () => { - it('should support string router links', - fakeAsync( - inject([Router, TestComponentBuilder], (router: Router, tcb: TestComponentBuilder) => { - const fixture = createRoot(tcb, router, RootCmp); - - router.resetConfig([{ - path: 'team/:id', - component: TeamCmp, - children: [ - {path: 'link', component: StringLinkCmp}, - {path: 'simple', component: SimpleCmp} - ] - }]); - - router.navigateByUrl('/team/22/link'); - advance(fixture); - expect(fixture.debugElement.nativeElement).toHaveText('team 22 [ link, right: ]'); - - const native = fixture.debugElement.nativeElement.querySelector('a'); - expect(native.getAttribute('href')).toEqual('/team/33/simple'); - dispatchClick(native); - advance(fixture); - - expect(fixture.debugElement.nativeElement).toHaveText('team 33 [ simple, right: ]'); - }))); - - it('should not preserve query params and fragment by default', - fakeAsync( - inject([Router, TestComponentBuilder], (router: Router, tcb: TestComponentBuilder) => { - @Component({ - selector: 'someRoot', - template: `Link`, - directives: ROUTER_DIRECTIVES - }) - class RootCmpWithLink { - } - - const fixture = createRoot(tcb, router, RootCmpWithLink); - - router.resetConfig([{path: 'home', component: SimpleCmp}]); - - const native = fixture.debugElement.nativeElement.querySelector('a'); - - router.navigateByUrl('/home?q=123#fragment'); - advance(fixture); - expect(native.getAttribute('href')).toEqual('/home'); - }))); - - it('should update hrefs when query params or fragment change', - fakeAsync(inject([Router, TestComponentBuilder], (router: Router, tcb: TestComponentBuilder) => { - - @Component({ - selector: 'someRoot', - template: - `Link`, - directives: ROUTER_DIRECTIVES - }) - class RootCmpWithLink { - } - - const fixture = createRoot(tcb, router, RootCmpWithLink); - - router.resetConfig([{path: 'home', component: SimpleCmp}]); - - const native = fixture.debugElement.nativeElement.querySelector('a'); - - router.navigateByUrl('/home?q=123'); - advance(fixture); - expect(native.getAttribute('href')).toEqual('/home?q=123'); - - router.navigateByUrl('/home?q=456'); - advance(fixture); - expect(native.getAttribute('href')).toEqual('/home?q=456'); - - router.navigateByUrl('/home?q=456#1'); - advance(fixture); - expect(native.getAttribute('href')).toEqual('/home?q=456#1'); - }))); - - it('should support using links on non-a tags', - fakeAsync( - inject([Router, TestComponentBuilder], (router: Router, tcb: TestComponentBuilder) => { - const fixture = createRoot(tcb, router, RootCmp); - - router.resetConfig([{ - path: 'team/:id', - component: TeamCmp, - children: [ - {path: 'link', component: StringLinkButtonCmp}, - {path: 'simple', component: SimpleCmp} - ] - }]); - - router.navigateByUrl('/team/22/link'); - advance(fixture); - expect(fixture.debugElement.nativeElement).toHaveText('team 22 [ link, right: ]'); - - const native = fixture.debugElement.nativeElement.querySelector('button'); - dispatchClick(native); - advance(fixture); - - expect(fixture.debugElement.nativeElement).toHaveText('team 33 [ simple, right: ]'); - }))); - - it('should support absolute router links', - fakeAsync( - inject([Router, TestComponentBuilder], (router: Router, tcb: TestComponentBuilder) => { - const fixture = createRoot(tcb, router, RootCmp); - - router.resetConfig([{ - path: 'team/:id', - component: TeamCmp, - children: [ - {path: 'link', component: AbsoluteLinkCmp}, - {path: 'simple', component: SimpleCmp} - ] - }]); - - router.navigateByUrl('/team/22/link'); - advance(fixture); - expect(fixture.debugElement.nativeElement).toHaveText('team 22 [ link, right: ]'); - - const native = fixture.debugElement.nativeElement.querySelector('a'); - expect(native.getAttribute('href')).toEqual('/team/33/simple'); - dispatchClick(native); - advance(fixture); - - expect(fixture.debugElement.nativeElement).toHaveText('team 33 [ simple, right: ]'); - }))); - - it('should support relative router links', - fakeAsync( - inject([Router, TestComponentBuilder], (router: Router, tcb: TestComponentBuilder) => { - const fixture = createRoot(tcb, router, RootCmp); - - router.resetConfig([{ - path: 'team/:id', - component: TeamCmp, - children: [ - {path: 'link', component: RelativeLinkCmp}, - {path: 'simple', component: SimpleCmp} - ] - }]); - - router.navigateByUrl('/team/22/link'); - advance(fixture); - expect(fixture.debugElement.nativeElement).toHaveText('team 22 [ link, right: ]'); - - const native = fixture.debugElement.nativeElement.querySelector('a'); - expect(native.getAttribute('href')).toEqual('/team/22/simple'); - dispatchClick(native); - advance(fixture); - - expect(fixture.debugElement.nativeElement).toHaveText('team 22 [ simple, right: ]'); - }))); - - it('should support top-level link', - fakeAsync( - inject([Router, TestComponentBuilder], (router: Router, tcb: TestComponentBuilder) => { - const fixture = createRoot(tcb, router, RelativeLinkInIfCmp); - advance(fixture); - - router.resetConfig( - [{path: 'simple', component: SimpleCmp}, {path: '', component: BlankCmp}]); - - router.navigateByUrl('/'); - advance(fixture); - expect(fixture.debugElement.nativeElement).toHaveText(' '); - const cmp = fixture.debugElement.componentInstance; - - cmp.show = true; - advance(fixture); - - expect(fixture.debugElement.nativeElement).toHaveText('link '); - const native = fixture.debugElement.nativeElement.querySelector('a'); - - expect(native.getAttribute('href')).toEqual('/simple'); - dispatchClick(native); - advance(fixture); - - expect(fixture.debugElement.nativeElement).toHaveText('link simple'); - }))); - - it('should support query params and fragments', - fakeAsync(inject( - [Router, Location, TestComponentBuilder], - (router: Router, location: Location, tcb: TestComponentBuilder) => { - const fixture = createRoot(tcb, router, RootCmp); - - router.resetConfig([{ - path: 'team/:id', - component: TeamCmp, - children: [ - {path: 'link', component: LinkWithQueryParamsAndFragment}, - {path: 'simple', component: SimpleCmp} - ] - }]); - - router.navigateByUrl('/team/22/link'); - advance(fixture); - - const native = fixture.debugElement.nativeElement.querySelector('a'); - expect(native.getAttribute('href')).toEqual('/team/22/simple?q=1#f'); - dispatchClick(native); - advance(fixture); - - expect(fixture.debugElement.nativeElement).toHaveText('team 22 [ simple, right: ]'); - - expect(location.path()).toEqual('/team/22/simple?q=1#f'); - }))); - }); - - describe('redirects', () => { - it('should work', fakeAsync(inject( - [Router, TestComponentBuilder, Location], - (router: Router, tcb: TestComponentBuilder, location: Location) => { - const fixture = createRoot(tcb, router, RootCmp); - - router.resetConfig([ - {path: 'old/team/:id', redirectTo: 'team/:id'}, - {path: 'team/:id', component: TeamCmp} - ]); - - router.navigateByUrl('old/team/22'); - advance(fixture); - - expect(location.path()).toEqual('/team/22'); - }))); - }); - - describe('guards', () => { - describe('CanActivate', () => { - describe('should not activate a route when CanActivate returns false', () => { - beforeEach(() => { - addProviders([{provide: 'alwaysFalse', useValue: (a: any, b: any) => false}]); - }); - - it('works', - fakeAsync(inject( - [Router, TestComponentBuilder, Location], - (router: Router, tcb: TestComponentBuilder, location: Location) => { - const fixture = createRoot(tcb, router, RootCmp); - - router.resetConfig( - [{path: 'team/:id', component: TeamCmp, canActivate: ['alwaysFalse']}]); - - router.navigateByUrl('/team/22'); - advance(fixture); - - expect(location.path()).toEqual('/'); - }))); - }); - - describe( - 'should not activate a route when CanActivate returns false (componentless route)', - () => { - beforeEach(() => { - addProviders([{provide: 'alwaysFalse', useValue: (a: any, b: any) => false}]); - }); - - it('works', fakeAsync(inject( - [Router, TestComponentBuilder, Location], - (router: Router, tcb: TestComponentBuilder, location: Location) => { - const fixture = createRoot(tcb, router, RootCmp); - - router.resetConfig([{ - path: 'parent', - canActivate: ['alwaysFalse'], - children: [{path: 'team/:id', component: TeamCmp}] - }]); - - router.navigateByUrl('parent/team/22'); - advance(fixture); - - expect(location.path()).toEqual('/'); - }))); - }); - - describe('should activate a route when CanActivate returns true', () => { - beforeEach(() => { - addProviders([{ - provide: 'alwaysTrue', - useValue: (a: ActivatedRouteSnapshot, s: RouterStateSnapshot) => true - }]); - }); - - it('works', - fakeAsync(inject( - [Router, TestComponentBuilder, Location], - (router: Router, tcb: TestComponentBuilder, location: Location) => { - const fixture = createRoot(tcb, router, RootCmp); - - router.resetConfig( - [{path: 'team/:id', component: TeamCmp, canActivate: ['alwaysTrue']}]); - - router.navigateByUrl('/team/22'); - advance(fixture); - - expect(location.path()).toEqual('/team/22'); - }))); - }); - - describe('should work when given a class', () => { - class AlwaysTrue implements CanActivate { - canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { - return true; - } - } - - beforeEach(() => { addProviders([AlwaysTrue]); }); - - it('works', fakeAsync(inject( - [Router, TestComponentBuilder, Location], - (router: Router, tcb: TestComponentBuilder, location: Location) => { - const fixture = createRoot(tcb, router, RootCmp); - - router.resetConfig( - [{path: 'team/:id', component: TeamCmp, canActivate: [AlwaysTrue]}]); - - router.navigateByUrl('/team/22'); - advance(fixture); - - expect(location.path()).toEqual('/team/22'); - }))); - }); - - describe('should work when returns an observable', () => { - beforeEach(() => { - addProviders([{ - provide: 'CanActivate', - useValue: (a: ActivatedRouteSnapshot, b: RouterStateSnapshot) => { return of (false); } - }]); - }); - - - it('works', - fakeAsync(inject( - [Router, TestComponentBuilder, Location], - (router: Router, tcb: TestComponentBuilder, location: Location) => { - const fixture = createRoot(tcb, router, RootCmp); - - router.resetConfig( - [{path: 'team/:id', component: TeamCmp, canActivate: ['CanActivate']}]); - - router.navigateByUrl('/team/22'); - advance(fixture); - expect(location.path()).toEqual('/'); - }))); - }); - - describe('should work when returns a promise', () => { - beforeEach(() => { - addProviders([{ - provide: 'CanActivate', - useValue: (a: ActivatedRouteSnapshot, b: RouterStateSnapshot) => { - if (a.params['id'] == '22') { - return Promise.resolve(true); - } else { - return Promise.resolve(false); - } - } - }]); - }); - - - it('works', - fakeAsync(inject( - [Router, TestComponentBuilder, Location], - (router: Router, tcb: TestComponentBuilder, location: Location) => { - const fixture = createRoot(tcb, router, RootCmp); - - router.resetConfig( - [{path: 'team/:id', component: TeamCmp, canActivate: ['CanActivate']}]); - - router.navigateByUrl('/team/22'); - advance(fixture); - expect(location.path()).toEqual('/team/22'); - - router.navigateByUrl('/team/33'); - advance(fixture); - expect(location.path()).toEqual('/team/22'); - }))); +import {PreActivation} from '../src/router'; +import {RouterOutletMap} from '../src/router_outlet_map'; +import {ActivatedRouteSnapshot, InheritedResolve, RouterStateSnapshot, createEmptyStateSnapshot} from '../src/router_state'; +import {DefaultUrlSerializer} from '../src/url_tree'; +import {TreeNode} from '../src/utils/tree'; + +describe('Router', () => { + 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 = new InheritedResolve(InheritedResolve.empty, {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'}); }); }); - describe('CanDeactivate', () => { - describe('should not deactivate a route when CanDeactivate returns false', () => { - beforeEach(() => { - addProviders([ - { - provide: 'CanDeactivateParent', - useValue: (c: any, a: ActivatedRouteSnapshot, b: RouterStateSnapshot) => { - return a.params['id'] === '22'; - } - }, - { - provide: 'CanDeactivateTeam', - useValue: (c: any, a: ActivatedRouteSnapshot, b: RouterStateSnapshot) => { - return c.route.snapshot.params['id'] === '22'; - } - }, - { - provide: 'CanDeactivateUser', - useValue: (c: any, a: ActivatedRouteSnapshot, b: RouterStateSnapshot) => { - return a.params['name'] === 'victor'; - } - } - ]); - }); + it('should copy over data when creating a snapshot', () => { + const r1 = new InheritedResolve(InheritedResolve.empty, {data: 'resolver1'}); + const r2 = new InheritedResolve(InheritedResolve.empty, {data: 'resolver2'}); - it('works', - fakeAsync(inject( - [Router, TestComponentBuilder, Location], - (router: Router, tcb: TestComponentBuilder, location: Location) => { - const fixture = createRoot(tcb, router, RootCmp); + const n1 = createActivatedRouteSnapshot('a', {resolve: r1}); + const s1 = new RouterStateSnapshot('url', new TreeNode(empty.root, [new TreeNode(n1, [])])); + checkResolveData(s1, empty, inj, () => {}); - router.resetConfig([ - {path: 'team/:id', component: TeamCmp, canDeactivate: ['CanDeactivateTeam']} - ]); - - router.navigateByUrl('/team/22'); - advance(fixture); - expect(location.path()).toEqual('/team/22'); - - let successStatus: boolean; - router.navigateByUrl('/team/33').then(res => successStatus = res); - advance(fixture); - expect(location.path()).toEqual('/team/33'); - expect(successStatus).toEqual(true); - - let canceledStatus: boolean; - router.navigateByUrl('/team/44').then(res => canceledStatus = res); - advance(fixture); - expect(location.path()).toEqual('/team/33'); - expect(canceledStatus).toEqual(false); - }))); - - it('works (componentless route)', - fakeAsync(inject( - [Router, TestComponentBuilder, Location], - (router: Router, tcb: TestComponentBuilder, location: Location) => { - const fixture = createRoot(tcb, router, RootCmp); - - router.resetConfig([{ - path: 'parent/:id', - canDeactivate: ['CanDeactivateParent'], - children: [{path: 'simple', component: SimpleCmp}] - }]); - - router.navigateByUrl('/parent/22/simple'); - advance(fixture); - expect(location.path()).toEqual('/parent/22/simple'); - - router.navigateByUrl('/parent/33/simple'); - advance(fixture); - expect(location.path()).toEqual('/parent/33/simple'); - - router.navigateByUrl('/parent/44/simple'); - advance(fixture); - expect(location.path()).toEqual('/parent/33/simple'); - }))); - - it('works with a nested route', - fakeAsync(inject( - [Router, TestComponentBuilder, Location], - (router: Router, tcb: TestComponentBuilder, location: Location) => { - const fixture = createRoot(tcb, router, RootCmp); - - router.resetConfig([{ - path: 'team/:id', - component: TeamCmp, - children: [ - {path: '', pathMatch: 'full', component: SimpleCmp}, { - path: 'user/:name', - component: UserCmp, - canDeactivate: ['CanDeactivateUser'] - } - ] - }]); - - router.navigateByUrl('/team/22/user/victor'); - advance(fixture); - - // this works because we can deactivate victor - router.navigateByUrl('/team/33'); - advance(fixture); - expect(location.path()).toEqual('/team/33'); - - router.navigateByUrl('/team/33/user/fedor'); - advance(fixture); - - // this doesn't work cause we cannot deactivate fedor - router.navigateByUrl('/team/44'); - advance(fixture); - expect(location.path()).toEqual('/team/33/user/fedor'); - }))); - }); - - describe('should work when given a class', () => { - class AlwaysTrue implements CanDeactivate { - canDeactivate( - component: TeamCmp, route: ActivatedRouteSnapshot, - state: RouterStateSnapshot): boolean { - return true; - } - } - - beforeEach(() => { addProviders([AlwaysTrue]); }); - - it('works', - fakeAsync(inject( - [Router, TestComponentBuilder, Location], - (router: Router, tcb: TestComponentBuilder, location: Location) => { - const fixture = createRoot(tcb, router, RootCmp); - - router.resetConfig( - [{path: 'team/:id', component: TeamCmp, canDeactivate: [AlwaysTrue]}]); - - router.navigateByUrl('/team/22'); - advance(fixture); - expect(location.path()).toEqual('/team/22'); - - router.navigateByUrl('/team/33'); - advance(fixture); - expect(location.path()).toEqual('/team/33'); - }))); - }); - - - describe('should work when returns an observable', () => { - beforeEach(() => { - addProviders([{ - provide: 'CanDeactivate', - useValue: (c: TeamCmp, a: ActivatedRouteSnapshot, b: RouterStateSnapshot) => { - return of (false); - } - }]); - }); - - it('works', - fakeAsync(inject( - [Router, TestComponentBuilder, Location], - (router: Router, tcb: TestComponentBuilder, location: Location) => { - const fixture = createRoot(tcb, router, RootCmp); - - router.resetConfig( - [{path: 'team/:id', component: TeamCmp, canDeactivate: ['CanDeactivate']}]); - - router.navigateByUrl('/team/22'); - advance(fixture); - expect(location.path()).toEqual('/team/22'); - - router.navigateByUrl('/team/33'); - advance(fixture); - expect(location.path()).toEqual('/team/22'); - }))); + 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'}); }); }); - - describe('CanActivateChild', () => { - describe('should be invoked when activating a child', () => { - beforeEach(() => { - addProviders([{ - provide: 'alwaysFalse', - useValue: (a: any, b: any) => { return a.params.id === '22'; } - }]); - }); - - it('works', fakeAsync(inject( - [Router, TestComponentBuilder, Location], - (router: Router, tcb: TestComponentBuilder, location: Location) => { - const fixture = createRoot(tcb, router, RootCmp); - - router.resetConfig([{ - path: '', - canActivateChild: ['alwaysFalse'], - children: [{path: 'team/:id', component: TeamCmp}] - }]); - - router.navigateByUrl('/team/22'); - advance(fixture); - - expect(location.path()).toEqual('/team/22'); - - router.navigateByUrl('/team/33').catch(() => {}); - advance(fixture); - - expect(location.path()).toEqual('/team/22'); - }))); - }); - }); - - describe('CanLoad', () => { - describe('should not load children when CanLoad returns false', () => { - beforeEach(() => { - addProviders([ - {provide: 'alwaysFalse', useValue: (a: any) => false}, - {provide: 'alwaysTrue', useValue: (a: any) => true} - ]); - }); - - it('works', - fakeAsync(inject( - [Router, TestComponentBuilder, Location, NgModuleFactoryLoader], - (router: Router, tcb: TestComponentBuilder, location: Location, - loader: SpyNgModuleFactoryLoader) => { - - @Component({selector: 'lazy', template: 'lazy-loaded'}) - class LazyLoadedComponent { - } - - @NgModule({ - declarations: [LazyLoadedComponent], - imports: - [RouterModule.forChild([{path: 'loaded', component: LazyLoadedComponent}])], - entryComponents: [LazyLoadedComponent] - }) - class LoadedModule { - } - - loader.stubbedModules = {lazyFalse: LoadedModule, lazyTrue: LoadedModule}; - const fixture = createRoot(tcb, router, RootCmp); - - router.resetConfig([ - {path: 'lazyFalse', canLoad: ['alwaysFalse'], loadChildren: 'lazyFalse'}, - {path: 'lazyTrue', canLoad: ['alwaysTrue'], loadChildren: 'lazyTrue'} - ]); - - const recordedEvents: any[] = []; - router.events.forEach(e => recordedEvents.push(e)); - - - // failed navigation - router.navigateByUrl('/lazyFalse/loaded').catch(s => {}); - advance(fixture); - - expect(location.path()).toEqual('/'); - - expectEvents(recordedEvents, [ - [NavigationStart, '/lazyFalse/loaded'], [NavigationError, '/lazyFalse/loaded'] - ]); - - recordedEvents.splice(0); - - - // successful navigation - router.navigateByUrl('/lazyTrue/loaded'); - advance(fixture); - - expect(location.path()).toEqual('/lazyTrue/loaded'); - - expectEvents(recordedEvents, [ - [NavigationStart, '/lazyTrue/loaded'], [RoutesRecognized, '/lazyTrue/loaded'], - [NavigationEnd, '/lazyTrue/loaded'] - ]); - }))); - }); - }); - }); - - describe('routerActiveLink', () => { - it('should set the class when the link is active (a tag)', - fakeAsync(inject( - [Router, TestComponentBuilder, Location], - (router: Router, tcb: TestComponentBuilder, location: Location) => { - const fixture = createRoot(tcb, router, RootCmp); - - router.resetConfig([{ - path: 'team/:id', - component: TeamCmp, - children: [{ - path: 'link', - component: DummyLinkCmp, - children: - [{path: 'simple', component: SimpleCmp}, {path: '', component: BlankCmp}] - }] - }]); - - router.navigateByUrl('/team/22/link;exact=true'); - advance(fixture); - expect(location.path()).toEqual('/team/22/link;exact=true'); - - const nativeLink = fixture.debugElement.nativeElement.querySelector('a'); - const nativeButton = fixture.debugElement.nativeElement.querySelector('button'); - expect(nativeLink.className).toEqual('active'); - expect(nativeButton.className).toEqual('active'); - - router.navigateByUrl('/team/22/link/simple'); - advance(fixture); - expect(location.path()).toEqual('/team/22/link/simple'); - expect(nativeLink.className).toEqual(''); - expect(nativeButton.className).toEqual(''); - }))); - - it('should not set the class until the first navigation succeeds', - fakeAsync(inject( - [Router, TestComponentBuilder, Location], - (router: Router, tcb: TestComponentBuilder, location: Location) => { - @Component({ - template: - '' - }) - class RootCmpWithLink { - } - - const f = tcb.createFakeAsync(RootCmpWithLink); - advance(f); - - const link = f.debugElement.nativeElement.querySelector('a'); - expect(link.className).toEqual(''); - - router.initialNavigation(); - advance(f); - - expect(link.className).toEqual('active'); - }))); - - - it('should set the class on a parent element when the link is active', - fakeAsync(inject( - [Router, TestComponentBuilder, Location], - (router: Router, tcb: TestComponentBuilder, location: Location) => { - const fixture = createRoot(tcb, router, RootCmp); - - router.resetConfig([{ - path: 'team/:id', - component: TeamCmp, - children: [{ - path: 'link', - component: DummyLinkWithParentCmp, - children: - [{path: 'simple', component: SimpleCmp}, {path: '', component: BlankCmp}] - }] - }]); - - router.navigateByUrl('/team/22/link;exact=true'); - advance(fixture); - expect(location.path()).toEqual('/team/22/link;exact=true'); - - const native = fixture.debugElement.nativeElement.querySelector('link-parent'); - expect(native.className).toEqual('active'); - - router.navigateByUrl('/team/22/link/simple'); - advance(fixture); - expect(location.path()).toEqual('/team/22/link/simple'); - expect(native.className).toEqual(''); - }))); - - it('should set the class when the link is active', - fakeAsync(inject( - [Router, TestComponentBuilder, Location], - (router: Router, tcb: TestComponentBuilder, location: Location) => { - const fixture = createRoot(tcb, router, RootCmp); - - router.resetConfig([{ - path: 'team/:id', - component: TeamCmp, - children: [{ - path: 'link', - component: DummyLinkCmp, - children: - [{path: 'simple', component: SimpleCmp}, {path: '', component: BlankCmp}] - }] - }]); - - router.navigateByUrl('/team/22/link'); - advance(fixture); - expect(location.path()).toEqual('/team/22/link'); - - const native = fixture.debugElement.nativeElement.querySelector('a'); - expect(native.className).toEqual('active'); - - router.navigateByUrl('/team/22/link/simple'); - advance(fixture); - expect(location.path()).toEqual('/team/22/link/simple'); - expect(native.className).toEqual('active'); - }))); - - }); - - describe('lazy loading', () => { - it('works', fakeAsync(inject( - [Router, TestComponentBuilder, Location, NgModuleFactoryLoader], - (router: Router, tcb: TestComponentBuilder, location: Location, - loader: SpyNgModuleFactoryLoader) => { - @Component({ - selector: 'lazy', - template: 'lazy-loaded-parent []', - directives: ROUTER_DIRECTIVES - }) - class ParentLazyLoadedComponent { - } - - @Component({selector: 'lazy', template: 'lazy-loaded-child'}) - class ChildLazyLoadedComponent { - } - - @NgModule({ - declarations: [ParentLazyLoadedComponent, ChildLazyLoadedComponent], - imports: [RouterModule.forChild([{ - path: 'loaded', - component: ParentLazyLoadedComponent, - children: [{path: 'child', component: ChildLazyLoadedComponent}] - }])], - entryComponents: [ParentLazyLoadedComponent, ChildLazyLoadedComponent] - }) - class LoadedModule { - } - - - loader.stubbedModules = {expected: LoadedModule}; - - const fixture = createRoot(tcb, router, RootCmp); - - router.resetConfig([{path: 'lazy', loadChildren: 'expected'}]); - - router.navigateByUrl('/lazy/loaded/child'); - advance(fixture); - - expect(location.path()).toEqual('/lazy/loaded/child'); - expect(fixture.debugElement.nativeElement) - .toHaveText('lazy-loaded-parent [lazy-loaded-child]'); - }))); - - it('should combine routes from multiple modules into a single configuration', - fakeAsync(inject( - [Router, TestComponentBuilder, Location, NgModuleFactoryLoader], - (router: Router, tcb: TestComponentBuilder, location: Location, - loader: SpyNgModuleFactoryLoader) => { - @Component({selector: 'lazy', template: 'lazy-loaded-2'}) - class LazyComponent2 { - } - - @NgModule({ - declarations: [LazyComponent2], - imports: [RouterModule.forChild([{path: 'loaded', component: LazyComponent2}])], - entryComponents: [LazyComponent2] - }) - class SiblingOfLoadedModule { - } - - @Component( - {selector: 'lazy', template: 'lazy-loaded-1', directives: ROUTER_DIRECTIVES}) - class LazyComponent1 { - } - - @NgModule({ - declarations: [LazyComponent1], - imports: [ - RouterModule.forChild([{path: 'loaded', component: LazyComponent1}]), - SiblingOfLoadedModule - ], - entryComponents: [LazyComponent1] - }) - class LoadedModule { - } - - loader.stubbedModules = {expected1: LoadedModule, expected2: SiblingOfLoadedModule}; - - const fixture = createRoot(tcb, router, RootCmp); - - router.resetConfig([ - {path: 'lazy1', loadChildren: 'expected1'}, - {path: 'lazy2', loadChildren: 'expected2'} - ]); - - router.navigateByUrl('/lazy1/loaded'); - advance(fixture); - expect(location.path()).toEqual('/lazy1/loaded'); - - router.navigateByUrl('/lazy2/loaded'); - advance(fixture); - expect(location.path()).toEqual('/lazy2/loaded'); - }))); - - it('should use the injector of the lazily-loaded configuration', - fakeAsync(inject( - [Router, TestComponentBuilder, Location, NgModuleFactoryLoader], - (router: Router, tcb: TestComponentBuilder, location: Location, - loader: SpyNgModuleFactoryLoader) => { - class LazyLoadedService {} - - @Component({selector: 'lazy', template: 'lazy-loaded', directives: ROUTER_DIRECTIVES}) - class LazyLoadedComponent { - constructor(service: LazyLoadedService) {} - } - - @NgModule({ - entryComponents: [LazyLoadedComponent], - declarations: [LazyLoadedComponent], - imports: [RouterModule.forChild([{ - path: '', - canActivate: ['alwaysTrue'], - children: [{path: 'loaded', component: LazyLoadedComponent}] - }])], - providers: [LazyLoadedService, {provide: 'alwaysTrue', useValue: () => true}] - }) - class LoadedModule { - } - - loader.stubbedModules = {expected: LoadedModule}; - - const fixture = createRoot(tcb, router, RootCmp); - - router.resetConfig([{path: 'lazy', loadChildren: 'expected'}]); - - router.navigateByUrl('/lazy/loaded'); - advance(fixture); - - expect(location.path()).toEqual('/lazy/loaded'); - expect(fixture.debugElement.nativeElement).toHaveText('lazy-loaded'); - }))); - - it('error emit an error when cannot load a config', - fakeAsync(inject( - [Router, TestComponentBuilder, Location, NgModuleFactoryLoader], - (router: Router, tcb: TestComponentBuilder, location: Location, - loader: SpyNgModuleFactoryLoader) => { - loader.stubbedModules = {}; - const fixture = createRoot(tcb, router, RootCmp); - - router.resetConfig([{path: 'lazy', loadChildren: 'invalid'}]); - - const recordedEvents: any[] = []; - router.events.forEach(e => recordedEvents.push(e)); - - router.navigateByUrl('/lazy/loaded').catch(s => {}); - advance(fixture); - - expect(location.path()).toEqual('/'); - - expectEvents( - recordedEvents, - [[NavigationStart, '/lazy/loaded'], [NavigationError, '/lazy/loaded']]); - }))); }); }); -function expectEvents(events: Event[], pairs: any[]) { - for (let i = 0; i < events.length; ++i) { - expect((events[i].constructor).name).toBe(pairs[i][0].name); - expect((events[i]).url).toBe(pairs[i][1]); - } +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; }); } -@Component({ - selector: 'link-cmp', - template: `link`, - directives: ROUTER_DIRECTIVES -}) -class StringLinkCmp { -} - -@Component({ - selector: 'link-cmp', - template: ``, - directives: ROUTER_DIRECTIVES -}) -class StringLinkButtonCmp { -} - -@Component({ - selector: 'link-cmp', - template: `link`, - directives: ROUTER_DIRECTIVES -}) -class AbsoluteLinkCmp { -} - -@Component({ - selector: 'link-cmp', - template: - `link - -`, - directives: ROUTER_DIRECTIVES -}) -class DummyLinkCmp { - private exact: boolean; - constructor(route: ActivatedRoute) { this.exact = (route.snapshot.params).exact === 'true'; } -} - -@Component({ - selector: 'link-cmp', - template: `link`, - directives: ROUTER_DIRECTIVES -}) -class RelativeLinkCmp { -} - -@Component({ - selector: 'link-cmp', - template: `link`, - directives: ROUTER_DIRECTIVES -}) -class LinkWithQueryParamsAndFragment { -} - -@Component({selector: 'simple-cmp', template: `simple`, directives: ROUTER_DIRECTIVES}) -class SimpleCmp { -} - -@Component( - {selector: 'collect-params-cmp', template: `collect-params`, directives: ROUTER_DIRECTIVES}) -class CollectParamsCmp { - private params: any = []; - private urls: any = []; - - constructor(a: ActivatedRoute) { - a.params.forEach(p => this.params.push(p)); - a.url.forEach(u => this.urls.push(u)); - } - - recordedUrls(): string[] { - return this.urls.map((a: any) => a.map((p: any) => p.path).join('/')); - } -} - -@Component({selector: 'blank-cmp', template: ``, directives: ROUTER_DIRECTIVES}) -class BlankCmp { -} - -@Component({ - selector: 'team-cmp', - template: - `team {{id | async}} [ , right: ]`, - directives: ROUTER_DIRECTIVES -}) -class TeamCmp { - id: Observable; - recordedParams: Params[] = []; - - constructor(public route: ActivatedRoute) { - this.id = route.params.map(p => p['id']); - route.params.forEach(_ => this.recordedParams.push(_)); - } -} - -@Component( - {selector: 'user-cmp', template: `user {{name | async}}`, directives: [ROUTER_DIRECTIVES]}) -class UserCmp { - name: Observable; - recordedParams: Params[] = []; - - constructor(route: ActivatedRoute) { - this.name = route.params.map(p => p['name']); - route.params.forEach(_ => this.recordedParams.push(_)); - } -} - -@Component({ - selector: 'wrapper', - template: ``, - directives: [ROUTER_DIRECTIVES] -}) -class WrapperCmp { -} - -@Component({ - selector: 'query-cmp', - template: `query: {{name | async}} fragment: {{fragment | async}}`, - directives: [ROUTER_DIRECTIVES] -}) -class QueryParamsAndFragmentCmp { - name: Observable; - fragment: Observable; - - constructor(router: Router) { - this.name = router.routerState.queryParams.map(p => p['name']); - this.fragment = router.routerState.fragment; - } -} - -@Component({selector: 'route-cmp', template: `route`, directives: ROUTER_DIRECTIVES}) -class RouteCmp { - constructor(public route: ActivatedRoute) {} -} - -@Component({ - selector: 'link-cmp', - template: - ` `, - directives: ROUTER_DIRECTIVES, - entryComponents: [BlankCmp, SimpleCmp] -}) -class RelativeLinkInIfCmp { - show: boolean = false; -} - -@Component({ - selector: 'child', - template: '
', - directives: ROUTER_DIRECTIVES -}) -class LinkInNgIf { - alwaysTrue = true; -} - -@Component({ - selector: 'link-cmp', - template: ` - - - `, - directives: ROUTER_DIRECTIVES -}) -class DummyLinkWithParentCmp { - private exact: boolean; - constructor(route: ActivatedRoute) { this.exact = (route.snapshot.params).exact === 'true'; } -} - -@Component({selector: 'cmp', template: ''}) -class ComponentRecordingRoutePathAndUrl { - private path: any; - private url: any; - - constructor(router: Router, route: ActivatedRoute) { - this.path = router.routerState.pathFromRoot(route); - this.url = router.url.toString(); - } -} - -@Component({ - selector: 'root-cmp', - template: ``, - directives: [ROUTER_DIRECTIVES], - entryComponents: [ - BlankCmp, SimpleCmp, TeamCmp, UserCmp, StringLinkCmp, DummyLinkCmp, AbsoluteLinkCmp, - RelativeLinkCmp, DummyLinkWithParentCmp, LinkWithQueryParamsAndFragment, CollectParamsCmp, - QueryParamsAndFragmentCmp, StringLinkButtonCmp, WrapperCmp, LinkInNgIf, - ComponentRecordingRoutePathAndUrl - ] -}) -class RootCmp { -} - -@Component({ - selector: 'root-cmp', - template: - `primary [] right []`, - directives: [ROUTER_DIRECTIVES], - entryComponents: [BlankCmp, SimpleCmp, RouteCmp, UserCmp] -}) -class RootCmpWithTwoOutlets { -} - - -function advance(fixture: ComponentFixture): void { - tick(); - fixture.detectChanges(); -} - -function createRoot(tcb: TestComponentBuilder, router: Router, type: any): ComponentFixture { - const f = tcb.createFakeAsync(type); - advance(f); - router.initialNavigation(); - advance(f); - return f; -} - -function dispatchClick(target: HTMLElement): void { - const dispatchedEvent = getDOM().createMouseEvent('click'); - getDOM().dispatchEvent(target, dispatchedEvent); -} +function createActivatedRouteSnapshot(cmp: string, extra: any = {}): ActivatedRouteSnapshot { + return new ActivatedRouteSnapshot( + null, {}, null, null, null, null, cmp, null, null, -1, + extra.resolve); +} \ No newline at end of file diff --git a/modules/@angular/router/tsconfig.json b/modules/@angular/router/tsconfig.json index f7e3ba2e35..338ceffc14 100644 --- a/modules/@angular/router/tsconfig.json +++ b/modules/@angular/router/tsconfig.json @@ -34,6 +34,7 @@ "test/config.spec.ts", "test/router_state.spec.ts", "test/router.spec.ts", + "test/integration.spec.ts", "../../../node_modules/@types/jasmine/index.d.ts" ] }