diff --git a/modules/angular2/test/router/integration/lifecycle_hook_spec.ts b/modules/angular2/test/router/integration/lifecycle_hook_spec.ts new file mode 100644 index 0000000000..748b6d1cf4 --- /dev/null +++ b/modules/angular2/test/router/integration/lifecycle_hook_spec.ts @@ -0,0 +1,593 @@ +import { + AsyncTestCompleter, + TestComponentBuilder, + asNativeElements, + beforeEach, + ddescribe, + xdescribe, + describe, + el, + expect, + iit, + inject, + beforeEachBindings, + it, + xit +} from 'angular2/test_lib'; + +import {Injector, Inject, bind} from 'angular2/di'; +import {Component, View} from 'angular2/metadata'; +import {isPresent} from 'angular2/src/facade/lang'; +import { + Promise, + PromiseWrapper, + PromiseCompleter, + EventEmitter, + ObservableWrapper +} from 'angular2/src/facade/async'; + +import {RootRouter} from 'angular2/src/router/router'; +import {Pipeline} from 'angular2/src/router/pipeline'; +import {Router, RouterOutlet, RouterLink, RouteParams, ROUTE_DATA} from 'angular2/router'; +import { + RouteConfig, + Route, + AuxRoute, + AsyncRoute, + Redirect +} from 'angular2/src/router/route_config_decorator'; + +import {SpyLocation} from 'angular2/src/mock/location_mock'; +import {Location} from 'angular2/src/router/location'; +import {RouteRegistry} from 'angular2/src/router/route_registry'; +import { + OnActivate, + OnDeactivate, + OnReuse, + CanDeactivate, + CanReuse +} from 'angular2/src/router/interfaces'; +import {CanActivate} from 'angular2/src/router/lifecycle_annotations'; +import {ComponentInstruction} from 'angular2/src/router/instruction'; +import {DirectiveResolver} from 'angular2/src/core/compiler/directive_resolver'; + +var cmpInstanceCount; +var log: List; +var eventBus: EventEmitter; +var completer: PromiseCompleter; + +export function main() { + describe('Router lifecycle hooks', () => { + + var tcb: TestComponentBuilder; + var rootTC, rtr; + + beforeEachBindings(() => [ + Pipeline, + RouteRegistry, + DirectiveResolver, + bind(Location).toClass(SpyLocation), + bind(Router) + .toFactory((registry, pipeline, + location) => { return new RootRouter(registry, pipeline, location, MyComp); }, + [RouteRegistry, Pipeline, Location]) + ]); + + beforeEach(inject([TestComponentBuilder, Router], (tcBuilder, router) => { + tcb = tcBuilder; + rtr = router; + cmpInstanceCount = 0; + log = []; + eventBus = new EventEmitter(); + })); + + function compile(template: string = "") { + return tcb.overrideView(MyComp, new View({ + template: ('
' + template + '
'), + directives: [RouterOutlet, RouterLink] + })) + .createAsync(MyComp) + .then((tc) => { rootTC = tc; }); + } + + it('should call the onActivate hook', inject([AsyncTestCompleter], (async) => { + compile() + .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) + .then((_) => rtr.navigate('/on-activate')) + .then((_) => { + rootTC.detectChanges(); + expect(rootTC.nativeElement).toHaveText('activate cmp'); + expect(log).toEqual(['activate: null -> /on-activate']); + async.done(); + }); + })); + + it('should wait for a parent component\'s onActivate hook to resolve before calling its child\'s', + inject([AsyncTestCompleter], (async) => { + compile() + .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) + .then((_) => { + ObservableWrapper.subscribe(eventBus, (ev) => { + if (ev.startsWith('parent activate')) { + completer.resolve(true); + } + }); + rtr.navigate('/parent-activate/child-activate') + .then((_) => { + rootTC.detectChanges(); + expect(rootTC.nativeElement).toHaveText('parent {activate cmp}'); + expect(log).toEqual([ + 'parent activate: null -> /parent-activate', + 'activate: null -> /child-activate' + ]); + async.done(); + }); + }); + })); + + it('should call the onDeactivate hook', inject([AsyncTestCompleter], (async) => { + compile() + .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) + .then((_) => rtr.navigate('/on-deactivate')) + .then((_) => rtr.navigate('/a')) + .then((_) => { + rootTC.detectChanges(); + expect(rootTC.nativeElement).toHaveText('A'); + expect(log).toEqual(['deactivate: /on-deactivate -> /a']); + async.done(); + }); + })); + + it('should wait for a child component\'s onDeactivate hook to resolve before calling its parent\'s', + inject([AsyncTestCompleter], (async) => { + compile() + .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) + .then((_) => rtr.navigate('/parent-deactivate/child-deactivate')) + .then((_) => { + ObservableWrapper.subscribe(eventBus, (ev) => { + if (ev.startsWith('deactivate')) { + completer.resolve(true); + rootTC.detectChanges(); + expect(rootTC.nativeElement).toHaveText('parent {deactivate cmp}'); + } + }); + rtr.navigate('/a').then((_) => { + rootTC.detectChanges(); + expect(rootTC.nativeElement).toHaveText('A'); + expect(log).toEqual([ + 'deactivate: /child-deactivate -> null', + 'parent deactivate: /parent-deactivate -> /a' + ]); + async.done(); + }); + }); + })); + + it('should reuse a component when the canReuse hook returns true', + inject([AsyncTestCompleter], (async) => { + compile() + .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) + .then((_) => rtr.navigate('/on-reuse/1/a')) + .then((_) => { + rootTC.detectChanges(); + expect(log).toEqual([]); + expect(rootTC.nativeElement).toHaveText('reuse {A}'); + expect(cmpInstanceCount).toBe(1); + }) + .then((_) => rtr.navigate('/on-reuse/2/b')) + .then((_) => { + rootTC.detectChanges(); + expect(log).toEqual(['reuse: /on-reuse/1 -> /on-reuse/2']); + expect(rootTC.nativeElement).toHaveText('reuse {B}'); + expect(cmpInstanceCount).toBe(1); + async.done(); + }); + })); + + + it('should not reuse a component when the canReuse hook returns false', + inject([AsyncTestCompleter], (async) => { + compile() + .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) + .then((_) => rtr.navigate('/never-reuse/1/a')) + .then((_) => { + rootTC.detectChanges(); + expect(log).toEqual([]); + expect(rootTC.nativeElement).toHaveText('reuse {A}'); + expect(cmpInstanceCount).toBe(1); + }) + .then((_) => rtr.navigate('/never-reuse/2/b')) + .then((_) => { + rootTC.detectChanges(); + expect(log).toEqual([]); + expect(rootTC.nativeElement).toHaveText('reuse {B}'); + expect(cmpInstanceCount).toBe(2); + async.done(); + }); + })); + + + it('should navigate when canActivate returns true', inject([AsyncTestCompleter], (async) => { + compile() + .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) + .then((_) => { + ObservableWrapper.subscribe(eventBus, (ev) => { + if (ev.startsWith('canActivate')) { + completer.resolve(true); + } + }); + rtr.navigate('/can-activate/a') + .then((_) => { + rootTC.detectChanges(); + expect(rootTC.nativeElement).toHaveText('canActivate {A}'); + expect(log).toEqual(['canActivate: null -> /can-activate']); + async.done(); + }); + }); + })); + + it('should not navigate when canActivate returns false', + inject([AsyncTestCompleter], (async) => { + compile() + .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) + .then((_) => { + ObservableWrapper.subscribe(eventBus, (ev) => { + if (ev.startsWith('canActivate')) { + completer.resolve(false); + } + }); + rtr.navigate('/can-activate/a') + .then((_) => { + rootTC.detectChanges(); + expect(rootTC.nativeElement).toHaveText(''); + expect(log).toEqual(['canActivate: null -> /can-activate']); + async.done(); + }); + }); + })); + + it('should navigate away when canDeactivate returns true', + inject([AsyncTestCompleter], (async) => { + compile() + .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) + .then((_) => rtr.navigate('/can-deactivate/a')) + .then((_) => { + rootTC.detectChanges(); + expect(rootTC.nativeElement).toHaveText('canDeactivate {A}'); + expect(log).toEqual([]); + + ObservableWrapper.subscribe(eventBus, (ev) => { + if (ev.startsWith('canDeactivate')) { + completer.resolve(true); + } + }); + + rtr.navigate('/a').then((_) => { + rootTC.detectChanges(); + expect(rootTC.nativeElement).toHaveText('A'); + expect(log).toEqual(['canDeactivate: /can-deactivate -> /a']); + async.done(); + }); + }); + })); + + it('should not navigate away when canDeactivate returns false', + inject([AsyncTestCompleter], (async) => { + compile() + .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) + .then((_) => rtr.navigate('/can-deactivate/a')) + .then((_) => { + rootTC.detectChanges(); + expect(rootTC.nativeElement).toHaveText('canDeactivate {A}'); + expect(log).toEqual([]); + + ObservableWrapper.subscribe(eventBus, (ev) => { + if (ev.startsWith('canDeactivate')) { + completer.resolve(false); + } + }); + + rtr.navigate('/a').then((_) => { + rootTC.detectChanges(); + expect(rootTC.nativeElement).toHaveText('canDeactivate {A}'); + expect(log).toEqual(['canDeactivate: /can-deactivate -> /a']); + async.done(); + }); + }); + })); + + + it('should run activation and deactivation hooks in the correct order', + inject([AsyncTestCompleter], (async) => { + compile() + .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) + .then((_) => rtr.navigate('/activation-hooks/child')) + .then((_) => { + expect(log).toEqual([ + 'canActivate child: null -> /child', + 'canActivate parent: null -> /activation-hooks', + 'onActivate parent: null -> /activation-hooks', + 'onActivate child: null -> /child' + ]); + + log = []; + return rtr.navigate('/a'); + }) + .then((_) => { + expect(log).toEqual([ + 'canDeactivate parent: /activation-hooks -> /a', + 'canDeactivate child: /child -> null', + 'onDeactivate child: /child -> null', + 'onDeactivate parent: /activation-hooks -> /a' + ]); + async.done(); + }); + })); + + it('should only run reuse hooks when reusing', inject([AsyncTestCompleter], (async) => { + compile() + .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) + .then((_) => rtr.navigate('/reuse-hooks/1')) + .then((_) => { + expect(log).toEqual( + ['canActivate: null -> /reuse-hooks/1', 'onActivate: null -> /reuse-hooks/1']); + + ObservableWrapper.subscribe(eventBus, (ev) => { + if (ev.startsWith('canReuse')) { + completer.resolve(true); + } + }); + + + log = []; + return rtr.navigate('/reuse-hooks/2'); + }) + .then((_) => { + expect(log).toEqual([ + 'canReuse: /reuse-hooks/1 -> /reuse-hooks/2', + 'onReuse: /reuse-hooks/1 -> /reuse-hooks/2' + ]); + async.done(); + }); + })); + + it('should not run reuse hooks when not reusing', inject([AsyncTestCompleter], (async) => { + compile() + .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) + .then((_) => rtr.navigate('/reuse-hooks/1')) + .then((_) => { + expect(log).toEqual( + ['canActivate: null -> /reuse-hooks/1', 'onActivate: null -> /reuse-hooks/1']); + + ObservableWrapper.subscribe(eventBus, (ev) => { + if (ev.startsWith('canReuse')) { + completer.resolve(false); + } + }); + + log = []; + return rtr.navigate('/reuse-hooks/2'); + }) + .then((_) => { + expect(log).toEqual([ + 'canReuse: /reuse-hooks/1 -> /reuse-hooks/2', + 'canActivate: /reuse-hooks/1 -> /reuse-hooks/2', + 'canDeactivate: /reuse-hooks/1 -> /reuse-hooks/2', + 'onDeactivate: /reuse-hooks/1 -> /reuse-hooks/2', + 'onActivate: /reuse-hooks/1 -> /reuse-hooks/2' + ]); + async.done(); + }); + })); + }); +} + + +@Component({selector: 'a-cmp'}) +@View({template: "A"}) +class A { +} + + +@Component({selector: 'b-cmp'}) +@View({template: "B"}) +class B { +} + + +@Component({selector: 'my-comp'}) +class MyComp { + name; +} + +function logHook(name: string, next: ComponentInstruction, prev: ComponentInstruction) { + var message = name + ': ' + (isPresent(prev) ? ('/' + prev.urlPath) : 'null') + ' -> ' + + (isPresent(next) ? ('/' + next.urlPath) : 'null'); + log.push(message); + ObservableWrapper.callNext(eventBus, message); +} + +@Component({selector: 'activate-cmp'}) +@View({template: 'activate cmp'}) +class ActivateCmp implements OnActivate { + onActivate(next: ComponentInstruction, prev: ComponentInstruction) { + logHook('activate', next, prev); + } +} + +@Component({selector: 'parent-activate-cmp'}) +@View({template: `parent {}`, directives: [RouterOutlet]}) +@RouteConfig([new Route({path: '/child-activate', component: ActivateCmp})]) +class ParentActivateCmp implements OnActivate { + onActivate(next: ComponentInstruction, prev: ComponentInstruction): Promise { + completer = PromiseWrapper.completer(); + logHook('parent activate', next, prev); + return completer.promise; + } +} + +@Component({selector: 'deactivate-cmp'}) +@View({template: 'deactivate cmp'}) +class DeactivateCmp implements OnDeactivate { + onDeactivate(next: ComponentInstruction, prev: ComponentInstruction) { + logHook('deactivate', next, prev); + } +} + +@Component({selector: 'deactivate-cmp'}) +@View({template: 'deactivate cmp'}) +class WaitDeactivateCmp implements OnDeactivate { + onDeactivate(next: ComponentInstruction, prev: ComponentInstruction): Promise { + completer = PromiseWrapper.completer(); + logHook('deactivate', next, prev); + return completer.promise; + } +} + +@Component({selector: 'parent-deactivate-cmp'}) +@View({template: `parent {}`, directives: [RouterOutlet]}) +@RouteConfig([new Route({path: '/child-deactivate', component: WaitDeactivateCmp})]) +class ParentDeactivateCmp implements OnDeactivate { + onDeactivate(next: ComponentInstruction, prev: ComponentInstruction) { + logHook('parent deactivate', next, prev); + } +} + +@Component({selector: 'reuse-cmp'}) +@View({template: `reuse {}`, directives: [RouterOutlet]}) +@RouteConfig([new Route({path: '/a', component: A}), new Route({path: '/b', component: B})]) +class ReuseCmp implements OnReuse, CanReuse { + constructor() { cmpInstanceCount += 1; } + canReuse(next: ComponentInstruction, prev: ComponentInstruction) { return true; } + onReuse(next: ComponentInstruction, prev: ComponentInstruction) { logHook('reuse', next, prev); } +} + +@Component({selector: 'never-reuse-cmp'}) +@View({template: `reuse {}`, directives: [RouterOutlet]}) +@RouteConfig([new Route({path: '/a', component: A}), new Route({path: '/b', component: B})]) +class NeverReuseCmp implements OnReuse, CanReuse { + constructor() { cmpInstanceCount += 1; } + canReuse(next: ComponentInstruction, prev: ComponentInstruction) { return false; } + onReuse(next: ComponentInstruction, prev: ComponentInstruction) { logHook('reuse', next, prev); } +} + +@Component({selector: 'can-activate-cmp'}) +@View({template: `canActivate {}`, directives: [RouterOutlet]}) +@RouteConfig([new Route({path: '/a', component: A}), new Route({path: '/b', component: B})]) +@CanActivate(CanActivateCmp.canActivate) +class CanActivateCmp { + static canActivate(next: ComponentInstruction, prev: ComponentInstruction): Promise { + completer = PromiseWrapper.completer(); + logHook('canActivate', next, prev); + return completer.promise; + } +} + +@Component({selector: 'can-deactivate-cmp'}) +@View({template: `canDeactivate {}`, directives: [RouterOutlet]}) +@RouteConfig([new Route({path: '/a', component: A}), new Route({path: '/b', component: B})]) +class CanDeactivateCmp implements CanDeactivate { + canDeactivate(next: ComponentInstruction, prev: ComponentInstruction): Promise { + completer = PromiseWrapper.completer(); + logHook('canDeactivate', next, prev); + return completer.promise; + } +} + +@Component({selector: 'all-hooks-child-cmp'}) +@View({template: `child`}) +@CanActivate(AllHooksChildCmp.canActivate) +class AllHooksChildCmp implements CanDeactivate, OnDeactivate, OnActivate { + canDeactivate(next: ComponentInstruction, prev: ComponentInstruction) { + logHook('canDeactivate child', next, prev); + return true; + } + + onDeactivate(next: ComponentInstruction, prev: ComponentInstruction) { + logHook('onDeactivate child', next, prev); + } + + static canActivate(next: ComponentInstruction, prev: ComponentInstruction) { + logHook('canActivate child', next, prev); + return true; + } + + onActivate(next: ComponentInstruction, prev: ComponentInstruction) { + logHook('onActivate child', next, prev); + } +} + +@Component({selector: 'all-hooks-parent-cmp'}) +@View({template: ``, directives: [RouterOutlet]}) +@RouteConfig([new Route({path: '/child', component: AllHooksChildCmp})]) +@CanActivate(AllHooksParentCmp.canActivate) +class AllHooksParentCmp implements CanDeactivate, OnDeactivate, OnActivate { + canDeactivate(next: ComponentInstruction, prev: ComponentInstruction) { + logHook('canDeactivate parent', next, prev); + return true; + } + + onDeactivate(next: ComponentInstruction, prev: ComponentInstruction) { + logHook('onDeactivate parent', next, prev); + } + + static canActivate(next: ComponentInstruction, prev: ComponentInstruction) { + logHook('canActivate parent', next, prev); + return true; + } + + onActivate(next: ComponentInstruction, prev: ComponentInstruction) { + logHook('onActivate parent', next, prev); + } +} + +@Component({selector: 'reuse-hooks-cmp'}) +@View({template: 'reuse hooks cmp'}) +@CanActivate(ReuseHooksCmp.canActivate) +class ReuseHooksCmp implements OnActivate, OnReuse, OnDeactivate, CanReuse, CanDeactivate { + canReuse(next: ComponentInstruction, prev: ComponentInstruction): Promise { + completer = PromiseWrapper.completer(); + logHook('canReuse', next, prev); + return completer.promise; + } + + onReuse(next: ComponentInstruction, prev: ComponentInstruction) { + logHook('onReuse', next, prev); + } + + canDeactivate(next: ComponentInstruction, prev: ComponentInstruction) { + logHook('canDeactivate', next, prev); + return true; + } + + onDeactivate(next: ComponentInstruction, prev: ComponentInstruction) { + logHook('onDeactivate', next, prev); + } + + static canActivate(next: ComponentInstruction, prev: ComponentInstruction) { + logHook('canActivate', next, prev); + return true; + } + + onActivate(next: ComponentInstruction, prev: ComponentInstruction) { + logHook('onActivate', next, prev); + } +} + +@Component({selector: 'lifecycle-cmp'}) +@View({template: ``, directives: [RouterOutlet]}) +@RouteConfig([ + new Route({path: '/a', component: A}), + new Route({path: '/on-activate', component: ActivateCmp}), + new Route({path: '/parent-activate/...', component: ParentActivateCmp}), + new Route({path: '/on-deactivate', component: DeactivateCmp}), + new Route({path: '/parent-deactivate/...', component: ParentDeactivateCmp}), + new Route({path: '/on-reuse/:number/...', component: ReuseCmp}), + new Route({path: '/never-reuse/:number/...', component: NeverReuseCmp}), + new Route({path: '/can-activate/...', component: CanActivateCmp}), + new Route({path: '/can-deactivate/...', component: CanDeactivateCmp}), + new Route({path: '/activation-hooks/...', component: AllHooksParentCmp}), + new Route({path: '/reuse-hooks/:number', component: ReuseHooksCmp}) +]) +class LifecycleCmp { +} diff --git a/modules/angular2/test/router/integration/outlet_spec.ts b/modules/angular2/test/router/integration/outlet_spec.ts new file mode 100644 index 0000000000..76e3e9b1e8 --- /dev/null +++ b/modules/angular2/test/router/integration/outlet_spec.ts @@ -0,0 +1,306 @@ +import { + AsyncTestCompleter, + TestComponentBuilder, + asNativeElements, + beforeEach, + ddescribe, + xdescribe, + describe, + el, + expect, + iit, + inject, + beforeEachBindings, + it, + xit +} from 'angular2/test_lib'; + +import {Injector, Inject, bind} from 'angular2/di'; +import {Component, View} from 'angular2/metadata'; +import {CONST, NumberWrapper, isPresent, Json} from 'angular2/src/facade/lang'; +import {Promise, PromiseWrapper} from 'angular2/src/facade/async'; + +import {RootRouter} from 'angular2/src/router/router'; +import {Pipeline} from 'angular2/src/router/pipeline'; +import {Router, RouterOutlet, RouterLink, RouteParams, ROUTE_DATA} from 'angular2/router'; +import { + RouteConfig, + Route, + AuxRoute, + AsyncRoute, + Redirect +} from 'angular2/src/router/route_config_decorator'; + +import {SpyLocation} from 'angular2/src/mock/location_mock'; +import {Location} from 'angular2/src/router/location'; +import {RouteRegistry} from 'angular2/src/router/route_registry'; +import {DirectiveResolver} from 'angular2/src/core/compiler/directive_resolver'; + +var cmpInstanceCount; +var log: List; + +export function main() { + describe('Outlet Directive', () => { + + var tcb: TestComponentBuilder; + var rootTC, rtr; + + beforeEachBindings(() => [ + Pipeline, + RouteRegistry, + DirectiveResolver, + bind(Location).toClass(SpyLocation), + bind(Router) + .toFactory((registry, pipeline, + location) => { return new RootRouter(registry, pipeline, location, MyComp); }, + [RouteRegistry, Pipeline, Location]) + ]); + + beforeEach(inject([TestComponentBuilder, Router], (tcBuilder, router) => { + tcb = tcBuilder; + rtr = router; + cmpInstanceCount = 0; + log = []; + })); + + function compile(template: string = "") { + return tcb.overrideView(MyComp, new View({ + template: ('
' + template + '
'), + directives: [RouterOutlet, RouterLink] + })) + .createAsync(MyComp) + .then((tc) => { rootTC = tc; }); + } + + it('should work in a simple case', inject([AsyncTestCompleter], (async) => { + compile() + .then((_) => rtr.config([new Route({path: '/test', component: HelloCmp})])) + .then((_) => rtr.navigate('/test')) + .then((_) => { + rootTC.detectChanges(); + expect(rootTC.nativeElement).toHaveText('hello'); + async.done(); + }); + })); + + + it('should navigate between components with different parameters', + inject([AsyncTestCompleter], (async) => { + compile() + .then((_) => rtr.config([new Route({path: '/user/:name', component: UserCmp})])) + .then((_) => rtr.navigate('/user/brian')) + .then((_) => { + rootTC.detectChanges(); + expect(rootTC.nativeElement).toHaveText('hello brian'); + }) + .then((_) => rtr.navigate('/user/igor')) + .then((_) => { + rootTC.detectChanges(); + expect(rootTC.nativeElement).toHaveText('hello igor'); + async.done(); + }); + })); + + + it('should navigate to child routes', inject([AsyncTestCompleter], (async) => { + compile('outer { }') + .then((_) => rtr.config([new Route({path: '/a/...', component: ParentCmp})])) + .then((_) => rtr.navigate('/a/b')) + .then((_) => { + rootTC.detectChanges(); + expect(rootTC.nativeElement).toHaveText('outer { inner { hello } }'); + async.done(); + }); + })); + + + it('should recognize and apply redirects', + inject([AsyncTestCompleter, Location], (async, location) => { + compile() + .then((_) => rtr.config([ + new Redirect({path: '/original', redirectTo: '/redirected'}), + new Route({path: '/redirected', component: HelloCmp}) + ])) + .then((_) => rtr.navigate('/original')) + .then((_) => { + rootTC.detectChanges(); + expect(rootTC.nativeElement).toHaveText('hello'); + expect(location.urlChanges).toEqual(['/redirected']); + async.done(); + }); + })); + + + it('should reuse common parent components', inject([AsyncTestCompleter], (async) => { + compile() + .then((_) => rtr.config([new Route({path: '/team/:id/...', component: TeamCmp})])) + .then((_) => rtr.navigate('/team/angular/user/rado')) + .then((_) => { + rootTC.detectChanges(); + expect(cmpInstanceCount).toBe(1); + expect(rootTC.nativeElement).toHaveText('team angular { hello rado }'); + }) + .then((_) => rtr.navigate('/team/angular/user/victor')) + .then((_) => { + rootTC.detectChanges(); + expect(cmpInstanceCount).toBe(1); + expect(rootTC.nativeElement).toHaveText('team angular { hello victor }'); + async.done(); + }); + })); + + it('should inject route data into component', inject([AsyncTestCompleter], (async) => { + compile() + .then((_) => rtr.config([ + new Route({path: '/route-data', component: RouteDataCmp, data: {'isAdmin': true}}) + ])) + .then((_) => rtr.navigate('/route-data')) + .then((_) => { + rootTC.detectChanges(); + expect(rootTC.nativeElement).toHaveText(Json.stringify({'isAdmin': true})); + async.done(); + }); + })); + + it('should inject route data into component with AsyncRoute', + inject([AsyncTestCompleter], (async) => { + compile() + .then((_) => rtr.config([ + new AsyncRoute( + {path: '/route-data', loader: AsyncRouteDataCmp, data: {isAdmin: true}}) + ])) + .then((_) => rtr.navigate('/route-data')) + .then((_) => { + rootTC.detectChanges(); + expect(rootTC.nativeElement).toHaveText(Json.stringify({'isAdmin': true})); + async.done(); + }); + })); + + it('should inject null if the route has no data property', + inject([AsyncTestCompleter], (async) => { + compile() + .then((_) => rtr.config( + [new Route({path: '/route-data-default', component: RouteDataCmp})])) + .then((_) => rtr.navigate('/route-data-default')) + .then((_) => { + rootTC.detectChanges(); + expect(rootTC.nativeElement).toHaveText('null'); + async.done(); + }); + })); + + it('should allow an array as the route data', inject([AsyncTestCompleter], (async) => { + compile() + .then((_) => rtr.config([ + new Route({path: '/route-data-array', component: RouteDataCmp, data: [1, 2, 3]}) + ])) + .then((_) => rtr.navigate('/route-data-array')) + .then((_) => { + rootTC.detectChanges(); + expect(rootTC.nativeElement).toHaveText(Json.stringify([1, 2, 3])); + async.done(); + }); + })); + + it('should allow a string as the route data', inject([AsyncTestCompleter], (async) => { + compile() + .then((_) => rtr.config([ + new Route( + {path: '/route-data-string', component: RouteDataCmp, data: 'hello world'}) + ])) + .then((_) => rtr.navigate('/route-data-string')) + .then((_) => { + rootTC.detectChanges(); + expect(rootTC.nativeElement).toHaveText(Json.stringify('hello world')); + async.done(); + }); + })); + + describe('auxiliary routes', () => { + it('should recognize a simple case', inject([AsyncTestCompleter], (async) => { + compile() + .then((_) => rtr.config([new Route({path: '/...', component: AuxCmp})])) + .then((_) => rtr.navigate('/hello(modal)')) + .then((_) => { + rootTC.detectChanges(); + expect(rootTC.nativeElement).toHaveText('main {hello} | aux {modal}'); + async.done(); + }); + })); + }); + }); +} + + +@Component({selector: 'hello-cmp'}) +@View({template: "{{greeting}}"}) +class HelloCmp { + greeting: string; + constructor() { this.greeting = "hello"; } +} + + +function AsyncRouteDataCmp() { + return PromiseWrapper.resolve(RouteDataCmp); +} + +@Component({selector: 'data-cmp'}) +@View({template: "{{myData}}"}) +class RouteDataCmp { + myData: string; + constructor(@Inject(ROUTE_DATA) data: any) { + this.myData = isPresent(data) ? Json.stringify(data) : 'null'; + } +} + +@Component({selector: 'user-cmp'}) +@View({template: "hello {{user}}"}) +class UserCmp { + user: string; + constructor(params: RouteParams) { this.user = params.get('name'); } +} + + +@Component({selector: 'parent-cmp'}) +@View({template: "inner { }", directives: [RouterOutlet]}) +@RouteConfig([new Route({path: '/b', component: HelloCmp})]) +class ParentCmp { + constructor() {} +} + + +@Component({selector: 'team-cmp'}) +@View({template: "team {{id}} { }", directives: [RouterOutlet]}) +@RouteConfig([new Route({path: '/user/:name', component: UserCmp})]) +class TeamCmp { + id: string; + constructor(params: RouteParams) { + this.id = params.get('id'); + cmpInstanceCount += 1; + } +} + + +@Component({selector: 'my-comp'}) +class MyComp { + name; +} + +@Component({selector: 'modal-cmp'}) +@View({template: "modal"}) +class ModalCmp { +} + +@Component({selector: 'aux-cmp'}) +@View({ + template: + `main {} | aux {}`, + directives: [RouterOutlet] +}) +@RouteConfig([ + new Route({path: '/hello', component: HelloCmp}), + new AuxRoute({path: '/modal', component: ModalCmp}) +]) +class AuxCmp { +} diff --git a/modules/angular2/test/router/router_integration_spec.ts b/modules/angular2/test/router/integration/router_integration_spec.ts similarity index 100% rename from modules/angular2/test/router/router_integration_spec.ts rename to modules/angular2/test/router/integration/router_integration_spec.ts diff --git a/modules/angular2/test/router/integration/router_link_spec.ts b/modules/angular2/test/router/integration/router_link_spec.ts new file mode 100644 index 0000000000..488be5c79e --- /dev/null +++ b/modules/angular2/test/router/integration/router_link_spec.ts @@ -0,0 +1,251 @@ +import { + AsyncTestCompleter, + beforeEach, + ddescribe, + xdescribe, + describe, + dispatchEvent, + expect, + iit, + inject, + beforeEachBindings, + it, + xit, + TestComponentBuilder, + proxy, + SpyObject, + By +} from 'angular2/test_lib'; + +import {IMPLEMENTS, NumberWrapper} from 'angular2/src/facade/lang'; + +import {bind, Component, View} from 'angular2/angular2'; + +import {SpyLocation} from 'angular2/src/mock/location_mock'; +import { + Location, + Router, + RootRouter, + RouteRegistry, + Pipeline, + RouterLink, + RouterOutlet, + Route, + RouteParams, + RouteConfig +} from 'angular2/router'; +import {DirectiveResolver} from 'angular2/src/core/compiler/directive_resolver'; + +import {DOM} from 'angular2/src/dom/dom_adapter'; + +export function main() { + describe('router-link directive', function() { + var tcb: TestComponentBuilder; + var rootTC, router, location; + + beforeEachBindings(() => [ + Pipeline, + RouteRegistry, + DirectiveResolver, + bind(Location).toClass(SpyLocation), + bind(Router) + .toFactory((registry, pipeline, + location) => { return new RootRouter(registry, pipeline, location, AppCmp); }, + [RouteRegistry, Pipeline, Location]) + ]); + + beforeEach(inject([TestComponentBuilder, Router, Location], (tcBuilder, rtr, loc) => { + tcb = tcBuilder; + router = rtr; + location = loc; + })); + + function compile(template: string = "") { + return tcb.overrideView(MyComp, new View({ + template: ('
' + template + '
'), + directives: [RouterOutlet, RouterLink] + })) + .createAsync(MyComp) + .then((tc) => { rootTC = tc; }); + } + + it('should generate absolute hrefs that include the base href', + inject([AsyncTestCompleter], (async) => { + location.setBaseHref('/my/base'); + compile('') + .then((_) => + router.config([new Route({path: '/user', component: UserCmp, as: 'user'})])) + .then((_) => router.navigate('/a/b')) + .then((_) => { + rootTC.detectChanges(); + expect(getHref(rootTC)).toEqual('/my/base/user'); + async.done(); + }); + })); + + + it('should generate link hrefs without params', inject([AsyncTestCompleter], (async) => { + compile('') + .then((_) => + router.config([new Route({path: '/user', component: UserCmp, as: 'user'})])) + .then((_) => router.navigate('/a/b')) + .then((_) => { + rootTC.detectChanges(); + expect(getHref(rootTC)).toEqual('/user'); + async.done(); + }); + })); + + + + it('should generate link hrefs with params', inject([AsyncTestCompleter], (async) => { + compile('{{name}}') + .then((_) => router.config( + [new Route({path: '/user/:name', component: UserCmp, as: 'user'})])) + .then((_) => router.navigate('/a/b')) + .then((_) => { + rootTC.componentInstance.name = 'brian'; + rootTC.detectChanges(); + expect(rootTC.nativeElement).toHaveText('brian'); + expect(DOM.getAttribute(rootTC.componentViewChildren[0].nativeElement, 'href')) + .toEqual('/user/brian'); + async.done(); + }); + })); + + it('should generate link hrefs from a child to its sibling', + inject([AsyncTestCompleter], (async) => { + compile() + .then((_) => router.config( + [new Route({path: '/page/:number', component: SiblingPageCmp, as: 'page'})])) + .then((_) => router.navigate('/page/1')) + .then((_) => { + rootTC.detectChanges(); + expect(DOM.getAttribute( + rootTC.componentViewChildren[1].componentViewChildren[0].nativeElement, + 'href')) + .toEqual('/page/2'); + async.done(); + }); + })); + + it('should generate relative links preserving the existing parent route', + inject([AsyncTestCompleter], (async) => { + compile() + .then((_) => router.config( + [new Route({path: '/book/:title/...', component: BookCmp, as: 'book'})])) + .then((_) => router.navigate('/book/1984/page/1')) + .then((_) => { + rootTC.detectChanges(); + expect(DOM.getAttribute( + rootTC.componentViewChildren[1].componentViewChildren[0].nativeElement, + 'href')) + .toEqual('/book/1984/page/100'); + + expect(DOM.getAttribute(rootTC.componentViewChildren[1] + .componentViewChildren[2] + .componentViewChildren[0] + .nativeElement, + 'href')) + .toEqual('/book/1984/page/2'); + async.done(); + }); + })); + + + describe('when clicked', () => { + + var clickOnElement = function(view) { + var anchorEl = rootTC.componentViewChildren[0].nativeElement; + var dispatchedEvent = DOM.createMouseEvent('click'); + DOM.dispatchEvent(anchorEl, dispatchedEvent); + return dispatchedEvent; + }; + + it('should navigate to link hrefs without params', inject([AsyncTestCompleter], (async) => { + compile('') + .then((_) => router.config( + [new Route({path: '/user', component: UserCmp, as: 'user'})])) + .then((_) => router.navigate('/a/b')) + .then((_) => { + rootTC.detectChanges(); + + var dispatchedEvent = clickOnElement(rootTC); + expect(DOM.isPrevented(dispatchedEvent)).toBe(true); + + // router navigation is async. + router.subscribe((_) => { + expect(location.urlChanges).toEqual(['/user']); + async.done(); + }); + }); + })); + + it('should navigate to link hrefs in presence of base href', + inject([AsyncTestCompleter], (async) => { + location.setBaseHref('/base'); + compile('') + .then((_) => router.config( + [new Route({path: '/user', component: UserCmp, as: 'user'})])) + .then((_) => router.navigate('/a/b')) + .then((_) => { + rootTC.detectChanges(); + + var dispatchedEvent = clickOnElement(rootTC); + expect(DOM.isPrevented(dispatchedEvent)).toBe(true); + + // router navigation is async. + router.subscribe((_) => { + expect(location.urlChanges).toEqual(['/base/user']); + async.done(); + }); + }); + })); + }); + }); +} + +function getHref(tc) { + return DOM.getAttribute(tc.componentViewChildren[0].nativeElement, 'href'); +} + +class AppCmp {} + +@Component({selector: 'my-comp'}) +class MyComp { + name; +} + +@Component({selector: 'user-cmp'}) +@View({template: "hello {{user}}"}) +class UserCmp { + user: string; + constructor(params: RouteParams) { this.user = params.get('name'); } +} + +@Component({selector: 'page-cmp'}) +@View({ + template: + `page #{{pageNumber}} | next`, + directives: [RouterLink] +}) +class SiblingPageCmp { + pageNumber: number; + nextPage: number; + constructor(params: RouteParams) { + this.pageNumber = NumberWrapper.parseInt(params.get('number'), 10); + this.nextPage = this.pageNumber + 1; + } +} + +@Component({selector: 'book-cmp'}) +@View({ + template: `{{title}} | + `, + directives: [RouterLink, RouterOutlet] +}) +@RouteConfig([new Route({path: '/page/:number', component: SiblingPageCmp, as: 'page'})]) +class BookCmp { + title: string; + constructor(params: RouteParams) { this.title = params.get('title'); } +} diff --git a/modules/angular2/test/router/outlet_spec.ts b/modules/angular2/test/router/outlet_spec.ts deleted file mode 100644 index a47605b30d..0000000000 --- a/modules/angular2/test/router/outlet_spec.ts +++ /dev/null @@ -1,1003 +0,0 @@ -import { - AsyncTestCompleter, - TestComponentBuilder, - asNativeElements, - beforeEach, - ddescribe, - xdescribe, - describe, - el, - expect, - iit, - inject, - beforeEachBindings, - it, - xit -} from 'angular2/test_lib'; - -import {Injector, Inject, bind} from 'angular2/di'; -import {Component, View, ViewMetadata} from 'angular2/metadata'; -import {CONST, NumberWrapper, isPresent, Json} from 'angular2/src/facade/lang'; -import { - Promise, - PromiseWrapper, - PromiseCompleter, - EventEmitter, - ObservableWrapper -} from 'angular2/src/facade/async'; - -import {RootRouter} from 'angular2/src/router/router'; -import {Pipeline} from 'angular2/src/router/pipeline'; -import {Router, RouterOutlet, RouterLink, RouteParams, ROUTE_DATA} from 'angular2/router'; -import { - RouteConfig, - Route, - AuxRoute, - AsyncRoute, - Redirect -} from 'angular2/src/router/route_config_decorator'; - -import {DOM} from 'angular2/src/dom/dom_adapter'; - -import {SpyLocation} from 'angular2/src/mock/location_mock'; -import {Location} from 'angular2/src/router/location'; -import {RouteRegistry} from 'angular2/src/router/route_registry'; -import { - OnActivate, - OnDeactivate, - OnReuse, - CanDeactivate, - CanReuse -} from 'angular2/src/router/interfaces'; -import {CanActivate} from 'angular2/src/router/lifecycle_annotations'; -import {ComponentInstruction} from 'angular2/src/router/instruction'; -import {DirectiveResolver} from 'angular2/src/core/compiler/directive_resolver'; - -var cmpInstanceCount; -var log: List; -var eventBus: EventEmitter; -var completer: PromiseCompleter; - -export function main() { - describe('Outlet Directive', () => { - - var tcb: TestComponentBuilder; - var rootTC, rtr, location; - - beforeEachBindings(() => [ - Pipeline, - RouteRegistry, - DirectiveResolver, - bind(Location).toClass(SpyLocation), - bind(Router) - .toFactory((registry, pipeline, - location) => { return new RootRouter(registry, pipeline, location, MyComp); }, - [RouteRegistry, Pipeline, Location]) - ]); - - beforeEach(inject([TestComponentBuilder, Router, Location], (tcBuilder, router, loc) => { - tcb = tcBuilder; - rtr = router; - location = loc; - cmpInstanceCount = 0; - log = []; - eventBus = new EventEmitter(); - })); - - function compile(template: string = "") { - return tcb.overrideView(MyComp, new ViewMetadata({ - template: ('
' + template + '
'), - directives: [RouterOutlet, RouterLink] - })) - .createAsync(MyComp) - .then((tc) => { rootTC = tc; }); - } - - it('should work in a simple case', inject([AsyncTestCompleter], (async) => { - compile() - .then((_) => rtr.config([new Route({path: '/test', component: HelloCmp})])) - .then((_) => rtr.navigate('/test')) - .then((_) => { - rootTC.detectChanges(); - expect(rootTC.nativeElement).toHaveText('hello'); - async.done(); - }); - }), 1000); - - - it('should navigate between components with different parameters', - inject([AsyncTestCompleter], (async) => { - compile() - .then((_) => rtr.config([new Route({path: '/user/:name', component: UserCmp})])) - .then((_) => rtr.navigate('/user/brian')) - .then((_) => { - rootTC.detectChanges(); - expect(rootTC.nativeElement).toHaveText('hello brian'); - }) - .then((_) => rtr.navigate('/user/igor')) - .then((_) => { - rootTC.detectChanges(); - expect(rootTC.nativeElement).toHaveText('hello igor'); - async.done(); - }); - }), 1000); - - - it('should work with child routers', inject([AsyncTestCompleter], (async) => { - compile('outer { }') - .then((_) => rtr.config([new Route({path: '/a/...', component: ParentCmp})])) - .then((_) => rtr.navigate('/a/b')) - .then((_) => { - rootTC.detectChanges(); - expect(rootTC.nativeElement).toHaveText('outer { inner { hello } }'); - async.done(); - }); - }), 1000); - - - it('should work with redirects', inject([AsyncTestCompleter, Location], (async, location) => { - compile() - .then((_) => rtr.config([ - new Redirect({path: '/original', redirectTo: '/redirected'}), - new Route({path: '/redirected', component: A}) - ])) - .then((_) => rtr.navigate('/original')) - .then((_) => { - rootTC.detectChanges(); - expect(rootTC.nativeElement).toHaveText('A'); - expect(location.urlChanges).toEqual(['/redirected']); - async.done(); - }); - }), 1000); - - function getHref(tc) { - return DOM.getAttribute(tc.componentViewChildren[0].nativeElement, 'href'); - } - - it('should generate absolute hrefs that include the base href', - inject([AsyncTestCompleter], (async) => { - location.setBaseHref('/my/base'); - compile('') - .then((_) => rtr.config([new Route({path: '/user', component: UserCmp, as: 'user'})])) - .then((_) => rtr.navigate('/a/b')) - .then((_) => { - rootTC.detectChanges(); - expect(getHref(rootTC)).toEqual('/my/base/user'); - async.done(); - }); - }), 1000); - - - it('should generate link hrefs without params', inject([AsyncTestCompleter], (async) => { - compile('') - .then((_) => rtr.config([new Route({path: '/user', component: UserCmp, as: 'user'})])) - .then((_) => rtr.navigate('/a/b')) - .then((_) => { - rootTC.detectChanges(); - expect(getHref(rootTC)).toEqual('/user'); - async.done(); - }); - }), 1000); - - - it('should reuse common parent components', inject([AsyncTestCompleter], (async) => { - compile() - .then((_) => rtr.config([new Route({path: '/team/:id/...', component: TeamCmp})])) - .then((_) => rtr.navigate('/team/angular/user/rado')) - .then((_) => { - rootTC.detectChanges(); - expect(cmpInstanceCount).toBe(1); - expect(rootTC.nativeElement).toHaveText('team angular { hello rado }'); - }) - .then((_) => rtr.navigate('/team/angular/user/victor')) - .then((_) => { - rootTC.detectChanges(); - expect(cmpInstanceCount).toBe(1); - expect(rootTC.nativeElement).toHaveText('team angular { hello victor }'); - async.done(); - }); - }), 1000); - - - it('should generate link hrefs with params', inject([AsyncTestCompleter], (async) => { - compile('{{name}}') - .then((_) => rtr.config( - [new Route({path: '/user/:name', component: UserCmp, as: 'user'})])) - .then((_) => rtr.navigate('/a/b')) - .then((_) => { - rootTC.componentInstance.name = 'brian'; - rootTC.detectChanges(); - expect(rootTC.nativeElement).toHaveText('brian'); - expect(DOM.getAttribute(rootTC.componentViewChildren[0].nativeElement, 'href')) - .toEqual('/user/brian'); - async.done(); - }); - }), 1000); - - it('should generate link hrefs from a child to its sibling', - inject([AsyncTestCompleter], (async) => { - compile() - .then((_) => rtr.config( - [new Route({path: '/page/:number', component: SiblingPageCmp, as: 'page'})])) - .then((_) => rtr.navigate('/page/1')) - .then((_) => { - rootTC.detectChanges(); - expect(DOM.getAttribute( - rootTC.componentViewChildren[1].componentViewChildren[0].nativeElement, - 'href')) - .toEqual('/page/2'); - async.done(); - }); - }), 1000); - - it('should generate relative links preserving the existing parent route', - inject([AsyncTestCompleter], (async) => { - compile() - .then((_) => rtr.config( - [new Route({path: '/book/:title/...', component: BookCmp, as: 'book'})])) - .then((_) => rtr.navigate('/book/1984/page/1')) - .then((_) => { - rootTC.detectChanges(); - expect(DOM.getAttribute( - rootTC.componentViewChildren[1].componentViewChildren[0].nativeElement, - 'href')) - .toEqual('/book/1984/page/100'); - - expect(DOM.getAttribute(rootTC.componentViewChildren[1] - .componentViewChildren[2] - .componentViewChildren[0] - .nativeElement, - 'href')) - .toEqual('/book/1984/page/2'); - async.done(); - }); - }), 1000); - - it('should inject RouteData into component', inject([AsyncTestCompleter], (async) => { - compile() - .then((_) => rtr.config([ - new Route({path: '/route-data', component: RouteDataCmp, data: {'isAdmin': true}}) - ])) - .then((_) => rtr.navigate('/route-data')) - .then((_) => { - rootTC.detectChanges(); - expect(rootTC.nativeElement).toHaveText(Json.stringify({'isAdmin': true})); - async.done(); - }); - }), 1000); - - it('should inject RouteData into component with AsyncRoute', - inject([AsyncTestCompleter], (async) => { - compile() - .then((_) => rtr.config([ - new AsyncRoute( - {path: '/route-data', loader: AsyncRouteDataCmp, data: {isAdmin: true}}) - ])) - .then((_) => rtr.navigate('/route-data')) - .then((_) => { - rootTC.detectChanges(); - expect(rootTC.nativeElement).toHaveText(Json.stringify({'isAdmin': true})); - async.done(); - }); - }), 1000); - - it('should inject nested RouteData into component', inject([AsyncTestCompleter], (async) => { - compile() - .then((_) => rtr.config([ - new Route({ - path: '/route-data-nested', - component: RouteDataCmp, - data: {'isAdmin': true, 'test': {'moreData': 'testing'}} - }) - ])) - .then((_) => rtr.navigate('/route-data-nested')) - .then((_) => { - rootTC.detectChanges(); - expect(rootTC.nativeElement) - .toHaveText(Json.stringify({'isAdmin': true, 'test': {'moreData': 'testing'}})); - async.done(); - }); - }), 1000); - - it('should inject null if the route has no data property', - inject([AsyncTestCompleter], (async) => { - compile() - .then((_) => rtr.config( - [new Route({path: '/route-data-default', component: RouteDataCmp})])) - .then((_) => rtr.navigate('/route-data-default')) - .then((_) => { - rootTC.detectChanges(); - expect(rootTC.nativeElement).toHaveText('null'); - async.done(); - }); - }), 1000); - - it('should allow an array as the route data', inject([AsyncTestCompleter], (async) => { - compile() - .then((_) => rtr.config([ - new Route({path: '/route-data-array', component: RouteDataCmp, data: [1, 2, 3]}) - ])) - .then((_) => rtr.navigate('/route-data-array')) - .then((_) => { - rootTC.detectChanges(); - expect(rootTC.nativeElement).toHaveText(Json.stringify([1, 2, 3])); - async.done(); - }); - }), 1000); - - it('should allow a string as the route data', inject([AsyncTestCompleter], (async) => { - compile() - .then((_) => rtr.config([ - new Route( - {path: '/route-data-string', component: RouteDataCmp, data: 'hello world'}) - ])) - .then((_) => rtr.navigate('/route-data-string')) - .then((_) => { - rootTC.detectChanges(); - expect(rootTC.nativeElement).toHaveText(Json.stringify('hello world')); - async.done(); - }); - }), 1000); - - describe('lifecycle hooks', () => { - it('should call the onActivate hook', inject([AsyncTestCompleter], (async) => { - compile() - .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) - .then((_) => rtr.navigate('/on-activate')) - .then((_) => { - rootTC.detectChanges(); - expect(rootTC.nativeElement).toHaveText('activate cmp'); - expect(log).toEqual(['activate: null -> /on-activate']); - async.done(); - }); - }), 1000); - - it('should wait for a parent component\'s onActivate hook to resolve before calling its child\'s', - inject([AsyncTestCompleter], (async) => { - compile() - .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) - .then((_) => { - ObservableWrapper.subscribe(eventBus, (ev) => { - if (ev.startsWith('parent activate')) { - completer.resolve(true); - } - }); - rtr.navigate('/parent-activate/child-activate') - .then((_) => { - rootTC.detectChanges(); - expect(rootTC.nativeElement).toHaveText('parent {activate cmp}'); - expect(log).toEqual([ - 'parent activate: null -> /parent-activate', - 'activate: null -> /child-activate' - ]); - async.done(); - }); - }); - }), 1000); - - it('should call the onDeactivate hook', inject([AsyncTestCompleter], (async) => { - compile() - .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) - .then((_) => rtr.navigate('/on-deactivate')) - .then((_) => rtr.navigate('/a')) - .then((_) => { - rootTC.detectChanges(); - expect(rootTC.nativeElement).toHaveText('A'); - expect(log).toEqual(['deactivate: /on-deactivate -> /a']); - async.done(); - }); - }), 1000); - - it('should wait for a child component\'s onDeactivate hook to resolve before calling its parent\'s', - inject([AsyncTestCompleter], (async) => { - compile() - .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) - .then((_) => rtr.navigate('/parent-deactivate/child-deactivate')) - .then((_) => { - ObservableWrapper.subscribe(eventBus, (ev) => { - if (ev.startsWith('deactivate')) { - completer.resolve(true); - rootTC.detectChanges(); - expect(rootTC.nativeElement).toHaveText('parent {deactivate cmp}'); - } - }); - rtr.navigate('/a').then((_) => { - rootTC.detectChanges(); - expect(rootTC.nativeElement).toHaveText('A'); - expect(log).toEqual([ - 'deactivate: /child-deactivate -> null', - 'parent deactivate: /parent-deactivate -> /a' - ]); - async.done(); - }); - }); - }), 1000); - - it('should reuse a component when the canReuse hook returns true', - inject([AsyncTestCompleter], (async) => { - compile() - .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) - .then((_) => rtr.navigate('/on-reuse/1/a')) - .then((_) => { - rootTC.detectChanges(); - expect(log).toEqual([]); - expect(rootTC.nativeElement).toHaveText('reuse {A}'); - expect(cmpInstanceCount).toBe(1); - }) - .then((_) => rtr.navigate('/on-reuse/2/b')) - .then((_) => { - rootTC.detectChanges(); - expect(log).toEqual(['reuse: /on-reuse/1 -> /on-reuse/2']); - expect(rootTC.nativeElement).toHaveText('reuse {B}'); - expect(cmpInstanceCount).toBe(1); - async.done(); - }); - }), 1000); - - - it('should not reuse a component when the canReuse hook returns false', - inject([AsyncTestCompleter], (async) => { - compile() - .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) - .then((_) => rtr.navigate('/never-reuse/1/a')) - .then((_) => { - rootTC.detectChanges(); - expect(log).toEqual([]); - expect(rootTC.nativeElement).toHaveText('reuse {A}'); - expect(cmpInstanceCount).toBe(1); - }) - .then((_) => rtr.navigate('/never-reuse/2/b')) - .then((_) => { - rootTC.detectChanges(); - expect(log).toEqual([]); - expect(rootTC.nativeElement).toHaveText('reuse {B}'); - expect(cmpInstanceCount).toBe(2); - async.done(); - }); - }), 1000); - - - it('should navigate when canActivate returns true', inject([AsyncTestCompleter], (async) => { - compile() - .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) - .then((_) => { - ObservableWrapper.subscribe(eventBus, (ev) => { - if (ev.startsWith('canActivate')) { - completer.resolve(true); - } - }); - rtr.navigate('/can-activate/a') - .then((_) => { - rootTC.detectChanges(); - expect(rootTC.nativeElement).toHaveText('canActivate {A}'); - expect(log).toEqual(['canActivate: null -> /can-activate']); - async.done(); - }); - }); - }), 1000); - - it('should not navigate when canActivate returns false', - inject([AsyncTestCompleter], (async) => { - compile() - .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) - .then((_) => { - ObservableWrapper.subscribe(eventBus, (ev) => { - if (ev.startsWith('canActivate')) { - completer.resolve(false); - } - }); - rtr.navigate('/can-activate/a') - .then((_) => { - rootTC.detectChanges(); - expect(rootTC.nativeElement).toHaveText(''); - expect(log).toEqual(['canActivate: null -> /can-activate']); - async.done(); - }); - }); - }), 1000); - - it('should navigate away when canDeactivate returns true', - inject([AsyncTestCompleter], (async) => { - compile() - .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) - .then((_) => rtr.navigate('/can-deactivate/a')) - .then((_) => { - rootTC.detectChanges(); - expect(rootTC.nativeElement).toHaveText('canDeactivate {A}'); - expect(log).toEqual([]); - - ObservableWrapper.subscribe(eventBus, (ev) => { - if (ev.startsWith('canDeactivate')) { - completer.resolve(true); - } - }); - - rtr.navigate('/a').then((_) => { - rootTC.detectChanges(); - expect(rootTC.nativeElement).toHaveText('A'); - expect(log).toEqual(['canDeactivate: /can-deactivate -> /a']); - async.done(); - }); - }); - }), 1000); - - it('should not navigate away when canDeactivate returns false', - inject([AsyncTestCompleter], (async) => { - compile() - .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) - .then((_) => rtr.navigate('/can-deactivate/a')) - .then((_) => { - rootTC.detectChanges(); - expect(rootTC.nativeElement).toHaveText('canDeactivate {A}'); - expect(log).toEqual([]); - - ObservableWrapper.subscribe(eventBus, (ev) => { - if (ev.startsWith('canDeactivate')) { - completer.resolve(false); - } - }); - - rtr.navigate('/a').then((_) => { - rootTC.detectChanges(); - expect(rootTC.nativeElement).toHaveText('canDeactivate {A}'); - expect(log).toEqual(['canDeactivate: /can-deactivate -> /a']); - async.done(); - }); - }); - }), 1000); - - - it('should run activation and deactivation hooks in the correct order', - inject([AsyncTestCompleter], (async) => { - compile() - .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) - .then((_) => rtr.navigate('/activation-hooks/child')) - .then((_) => { - expect(log).toEqual([ - 'canActivate child: null -> /child', - 'canActivate parent: null -> /activation-hooks', - 'onActivate parent: null -> /activation-hooks', - 'onActivate child: null -> /child' - ]); - - log = []; - return rtr.navigate('/a'); - }) - .then((_) => { - expect(log).toEqual([ - 'canDeactivate parent: /activation-hooks -> /a', - 'canDeactivate child: /child -> null', - 'onDeactivate child: /child -> null', - 'onDeactivate parent: /activation-hooks -> /a' - ]); - async.done(); - }); - }), 1000); - - it('should only run reuse hooks when reusing', inject([AsyncTestCompleter], (async) => { - compile() - .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) - .then((_) => rtr.navigate('/reuse-hooks/1')) - .then((_) => { - expect(log).toEqual( - ['canActivate: null -> /reuse-hooks/1', 'onActivate: null -> /reuse-hooks/1']); - - ObservableWrapper.subscribe(eventBus, (ev) => { - if (ev.startsWith('canReuse')) { - completer.resolve(true); - } - }); - - - log = []; - return rtr.navigate('/reuse-hooks/2'); - }) - .then((_) => { - expect(log).toEqual([ - 'canReuse: /reuse-hooks/1 -> /reuse-hooks/2', - 'onReuse: /reuse-hooks/1 -> /reuse-hooks/2' - ]); - async.done(); - }); - }), 1000); - - it('should not run reuse hooks when not reusing', inject([AsyncTestCompleter], (async) => { - compile() - .then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})])) - .then((_) => rtr.navigate('/reuse-hooks/1')) - .then((_) => { - expect(log).toEqual( - ['canActivate: null -> /reuse-hooks/1', 'onActivate: null -> /reuse-hooks/1']); - - ObservableWrapper.subscribe(eventBus, (ev) => { - if (ev.startsWith('canReuse')) { - completer.resolve(false); - } - }); - - log = []; - return rtr.navigate('/reuse-hooks/2'); - }) - .then((_) => { - expect(log).toEqual([ - 'canReuse: /reuse-hooks/1 -> /reuse-hooks/2', - 'canActivate: /reuse-hooks/1 -> /reuse-hooks/2', - 'canDeactivate: /reuse-hooks/1 -> /reuse-hooks/2', - 'onDeactivate: /reuse-hooks/1 -> /reuse-hooks/2', - 'onActivate: /reuse-hooks/1 -> /reuse-hooks/2' - ]); - async.done(); - }); - }), 1000); - - }); - - describe('when clicked', () => { - - var clickOnElement = function(view) { - var anchorEl = rootTC.componentViewChildren[0].nativeElement; - var dispatchedEvent = DOM.createMouseEvent('click'); - DOM.dispatchEvent(anchorEl, dispatchedEvent); - return dispatchedEvent; - }; - - it('should navigate to link hrefs without params', inject([AsyncTestCompleter], (async) => { - compile('') - .then((_) => - rtr.config([new Route({path: '/user', component: UserCmp, as: 'user'})])) - .then((_) => rtr.navigate('/a/b')) - .then((_) => { - rootTC.detectChanges(); - - var dispatchedEvent = clickOnElement(rootTC); - expect(DOM.isPrevented(dispatchedEvent)).toBe(true); - - // router navigation is async. - rtr.subscribe((_) => { - expect(location.urlChanges).toEqual(['/user']); - async.done(); - }); - }); - })); - - it('should navigate to link hrefs in presence of base href', - inject([AsyncTestCompleter], (async) => { - location.setBaseHref('/base'); - compile('') - .then((_) => - rtr.config([new Route({path: '/user', component: UserCmp, as: 'user'})])) - .then((_) => rtr.navigate('/a/b')) - .then((_) => { - rootTC.detectChanges(); - - var dispatchedEvent = clickOnElement(rootTC); - expect(DOM.isPrevented(dispatchedEvent)).toBe(true); - - // router navigation is async. - rtr.subscribe((_) => { - expect(location.urlChanges).toEqual(['/base/user']); - async.done(); - }); - }); - })); - }); - - describe('auxillary routes', () => { - it('should recognize a simple case', inject([AsyncTestCompleter], (async) => { - compile() - .then((_) => rtr.config([new Route({path: '/...', component: AuxCmp})])) - .then((_) => rtr.navigate('/hello(modal)')) - .then((_) => { - rootTC.detectChanges(); - expect(rootTC.nativeElement).toHaveText('main {hello} | aux {modal}'); - async.done(); - }); - })); - }); - }); -} - - -@Component({selector: 'hello-cmp'}) -@View({template: "{{greeting}}"}) -class HelloCmp { - greeting: string; - constructor() { this.greeting = "hello"; } -} - - -@Component({selector: 'a-cmp'}) -@View({template: "A"}) -class A { -} - - -@Component({selector: 'b-cmp'}) -@View({template: "B"}) -class B { -} - - -function AsyncRouteDataCmp() { - return PromiseWrapper.resolve(RouteDataCmp); -} - -@Component({selector: 'data-cmp'}) -@View({template: "{{myData}}"}) -class RouteDataCmp { - myData: string; - constructor(@Inject(ROUTE_DATA) data: any) { - this.myData = isPresent(data) ? Json.stringify(data) : 'null'; - } -} - -@Component({selector: 'user-cmp'}) -@View({template: "hello {{user}}"}) -class UserCmp { - user: string; - constructor(params: RouteParams) { this.user = params.get('name'); } -} - - -@Component({selector: 'page-cmp'}) -@View({ - template: - `page #{{pageNumber}} | next`, - directives: [RouterLink] -}) -class SiblingPageCmp { - pageNumber: number; - nextPage: number; - constructor(params: RouteParams) { - this.pageNumber = NumberWrapper.parseInt(params.get('number'), 10); - this.nextPage = this.pageNumber + 1; - } -} - -@Component({selector: 'book-cmp'}) -@View({ - template: `{{title}} | - `, - directives: [RouterLink, RouterOutlet] -}) -@RouteConfig([new Route({path: '/page/:number', component: SiblingPageCmp, as: 'page'})]) -class BookCmp { - title: string; - constructor(params: RouteParams) { this.title = params.get('title'); } -} - - -@Component({selector: 'parent-cmp'}) -@View({template: "inner { }", directives: [RouterOutlet]}) -@RouteConfig([new Route({path: '/b', component: HelloCmp})]) -class ParentCmp { - constructor() {} -} - - -@Component({selector: 'team-cmp'}) -@View({template: "team {{id}} { }", directives: [RouterOutlet]}) -@RouteConfig([new Route({path: '/user/:name', component: UserCmp})]) -class TeamCmp { - id: string; - constructor(params: RouteParams) { - this.id = params.get('id'); - cmpInstanceCount += 1; - } -} - - -@Component({selector: 'my-comp'}) -class MyComp { - name; -} - -function logHook(name: string, next: ComponentInstruction, prev: ComponentInstruction) { - var message = name + ': ' + (isPresent(prev) ? ('/' + prev.urlPath) : 'null') + ' -> ' + - (isPresent(next) ? ('/' + next.urlPath) : 'null'); - log.push(message); - ObservableWrapper.callNext(eventBus, message); -} - -@Component({selector: 'activate-cmp'}) -@View({template: 'activate cmp'}) -class ActivateCmp implements OnActivate { - onActivate(next: ComponentInstruction, prev: ComponentInstruction) { - logHook('activate', next, prev); - } -} - -@Component({selector: 'parent-activate-cmp'}) -@View({template: `parent {}`, directives: [RouterOutlet]}) -@RouteConfig([new Route({path: '/child-activate', component: ActivateCmp})]) -class ParentActivateCmp implements OnActivate { - onActivate(next: ComponentInstruction, prev: ComponentInstruction): Promise { - completer = PromiseWrapper.completer(); - logHook('parent activate', next, prev); - return completer.promise; - } -} - -@Component({selector: 'deactivate-cmp'}) -@View({template: 'deactivate cmp'}) -class DeactivateCmp implements OnDeactivate { - onDeactivate(next: ComponentInstruction, prev: ComponentInstruction) { - logHook('deactivate', next, prev); - } -} - -@Component({selector: 'deactivate-cmp'}) -@View({template: 'deactivate cmp'}) -class WaitDeactivateCmp implements OnDeactivate { - onDeactivate(next: ComponentInstruction, prev: ComponentInstruction): Promise { - completer = PromiseWrapper.completer(); - logHook('deactivate', next, prev); - return completer.promise; - } -} - -@Component({selector: 'parent-deactivate-cmp'}) -@View({template: `parent {}`, directives: [RouterOutlet]}) -@RouteConfig([new Route({path: '/child-deactivate', component: WaitDeactivateCmp})]) -class ParentDeactivateCmp implements OnDeactivate { - onDeactivate(next: ComponentInstruction, prev: ComponentInstruction) { - logHook('parent deactivate', next, prev); - } -} - -@Component({selector: 'reuse-cmp'}) -@View({template: `reuse {}`, directives: [RouterOutlet]}) -@RouteConfig([new Route({path: '/a', component: A}), new Route({path: '/b', component: B})]) -class ReuseCmp implements OnReuse, CanReuse { - constructor() { cmpInstanceCount += 1; } - canReuse(next: ComponentInstruction, prev: ComponentInstruction) { return true; } - onReuse(next: ComponentInstruction, prev: ComponentInstruction) { logHook('reuse', next, prev); } -} - -@Component({selector: 'never-reuse-cmp'}) -@View({template: `reuse {}`, directives: [RouterOutlet]}) -@RouteConfig([new Route({path: '/a', component: A}), new Route({path: '/b', component: B})]) -class NeverReuseCmp implements OnReuse, CanReuse { - constructor() { cmpInstanceCount += 1; } - canReuse(next: ComponentInstruction, prev: ComponentInstruction) { return false; } - onReuse(next: ComponentInstruction, prev: ComponentInstruction) { logHook('reuse', next, prev); } -} - -@Component({selector: 'can-activate-cmp'}) -@View({template: `canActivate {}`, directives: [RouterOutlet]}) -@RouteConfig([new Route({path: '/a', component: A}), new Route({path: '/b', component: B})]) -@CanActivate(CanActivateCmp.canActivate) -class CanActivateCmp { - static canActivate(next: ComponentInstruction, prev: ComponentInstruction): Promise { - completer = PromiseWrapper.completer(); - logHook('canActivate', next, prev); - return completer.promise; - } -} - -@Component({selector: 'can-deactivate-cmp'}) -@View({template: `canDeactivate {}`, directives: [RouterOutlet]}) -@RouteConfig([new Route({path: '/a', component: A}), new Route({path: '/b', component: B})]) -class CanDeactivateCmp implements CanDeactivate { - canDeactivate(next: ComponentInstruction, prev: ComponentInstruction): Promise { - completer = PromiseWrapper.completer(); - logHook('canDeactivate', next, prev); - return completer.promise; - } -} - -@Component({selector: 'all-hooks-child-cmp'}) -@View({template: `child`}) -@CanActivate(AllHooksChildCmp.canActivate) -class AllHooksChildCmp implements CanDeactivate, OnDeactivate, OnActivate { - canDeactivate(next: ComponentInstruction, prev: ComponentInstruction) { - logHook('canDeactivate child', next, prev); - return true; - } - - onDeactivate(next: ComponentInstruction, prev: ComponentInstruction) { - logHook('onDeactivate child', next, prev); - } - - static canActivate(next: ComponentInstruction, prev: ComponentInstruction) { - logHook('canActivate child', next, prev); - return true; - } - - onActivate(next: ComponentInstruction, prev: ComponentInstruction) { - logHook('onActivate child', next, prev); - } -} - -@Component({selector: 'all-hooks-parent-cmp'}) -@View({template: ``, directives: [RouterOutlet]}) -@RouteConfig([new Route({path: '/child', component: AllHooksChildCmp})]) -@CanActivate(AllHooksParentCmp.canActivate) -class AllHooksParentCmp implements CanDeactivate, OnDeactivate, OnActivate { - canDeactivate(next: ComponentInstruction, prev: ComponentInstruction) { - logHook('canDeactivate parent', next, prev); - return true; - } - - onDeactivate(next: ComponentInstruction, prev: ComponentInstruction) { - logHook('onDeactivate parent', next, prev); - } - - static canActivate(next: ComponentInstruction, prev: ComponentInstruction) { - logHook('canActivate parent', next, prev); - return true; - } - - onActivate(next: ComponentInstruction, prev: ComponentInstruction) { - logHook('onActivate parent', next, prev); - } -} - -@Component({selector: 'reuse-hooks-cmp'}) -@View({template: 'reuse hooks cmp'}) -@CanActivate(ReuseHooksCmp.canActivate) -class ReuseHooksCmp implements OnActivate, OnReuse, OnDeactivate, CanReuse, CanDeactivate { - canReuse(next: ComponentInstruction, prev: ComponentInstruction): Promise { - completer = PromiseWrapper.completer(); - logHook('canReuse', next, prev); - return completer.promise; - } - - onReuse(next: ComponentInstruction, prev: ComponentInstruction) { - logHook('onReuse', next, prev); - } - - canDeactivate(next: ComponentInstruction, prev: ComponentInstruction) { - logHook('canDeactivate', next, prev); - return true; - } - - onDeactivate(next: ComponentInstruction, prev: ComponentInstruction) { - logHook('onDeactivate', next, prev); - } - - static canActivate(next: ComponentInstruction, prev: ComponentInstruction) { - logHook('canActivate', next, prev); - return true; - } - - onActivate(next: ComponentInstruction, prev: ComponentInstruction) { - logHook('onActivate', next, prev); - } -} - -@Component({selector: 'lifecycle-cmp'}) -@View({template: ``, directives: [RouterOutlet]}) -@RouteConfig([ - new Route({path: '/a', component: A}), - new Route({path: '/on-activate', component: ActivateCmp}), - new Route({path: '/parent-activate/...', component: ParentActivateCmp}), - new Route({path: '/on-deactivate', component: DeactivateCmp}), - new Route({path: '/parent-deactivate/...', component: ParentDeactivateCmp}), - new Route({path: '/on-reuse/:number/...', component: ReuseCmp}), - new Route({path: '/never-reuse/:number/...', component: NeverReuseCmp}), - new Route({path: '/can-activate/...', component: CanActivateCmp}), - new Route({path: '/can-deactivate/...', component: CanDeactivateCmp}), - new Route({path: '/activation-hooks/...', component: AllHooksParentCmp}), - new Route({path: '/reuse-hooks/:number', component: ReuseHooksCmp}) -]) -class LifecycleCmp { -} - -@Component({selector: 'modal-cmp'}) -@View({template: "modal"}) -class ModalCmp { -} - -@Component({selector: 'aux-cmp'}) -@View({ - template: - `main {} | aux {}`, - directives: [RouterOutlet] -}) -@RouteConfig([ - new Route({path: '/hello', component: HelloCmp}), - new AuxRoute({path: '/modal', component: ModalCmp}) -]) -class AuxCmp { -} diff --git a/modules/angular2/test/router/router_link_spec.ts b/modules/angular2/test/router/router_link_spec.ts index c525aee39d..4ab7a75753 100644 --- a/modules/angular2/test/router/router_link_spec.ts +++ b/modules/angular2/test/router/router_link_spec.ts @@ -21,7 +21,17 @@ import {IMPLEMENTS} from 'angular2/src/facade/lang'; import {bind, Component, View} from 'angular2/angular2'; -import {Location, Router, RouterLink} from 'angular2/router'; +import { + Location, + Router, + RootRouter, + RouteRegistry, + Pipeline, + RouterLink, + RouterOutlet, + Route, + RouteParams +} from 'angular2/router'; import {Instruction, ComponentInstruction} from 'angular2/src/router/instruction'; import {DOM} from 'angular2/src/dom/dom_adapter'; @@ -30,14 +40,15 @@ var dummyInstruction = new Instruction(new ComponentInstruction('detail', [], nu export function main() { describe('router-link directive', function() { + var tcb: TestComponentBuilder; beforeEachBindings( () => [bind(Location).toValue(makeDummyLocation()), bind(Router).toValue(makeDummyRouter())]); + beforeEach(inject([TestComponentBuilder], (tcBuilder) => { tcb = tcBuilder; })); - it('should update a[href] attribute', - inject([TestComponentBuilder, AsyncTestCompleter], (tcb, async) => { + it('should update a[href] attribute', inject([AsyncTestCompleter], (async) => { tcb.createAsync(TestComponent) .then((testComponent) => { @@ -50,13 +61,13 @@ export function main() { it('should call router.navigate when a link is clicked', - inject([TestComponentBuilder, AsyncTestCompleter, Router], (tcb, async, router) => { + inject([AsyncTestCompleter, Router], (async, router) => { tcb.createAsync(TestComponent) .then((testComponent) => { testComponent.detectChanges(); // TODO: shouldn't this be just 'click' rather than '^click'? - testComponent.query(By.css('a')).triggerEventHandler('^click', {}); + testComponent.query(By.css('a')).triggerEventHandler('^click', null); expect(router.spy('navigateInstruction')).toHaveBeenCalledWith(dummyInstruction); async.done(); }); @@ -64,6 +75,17 @@ export function main() { }); } +@Component({selector: 'my-comp'}) +class MyComp { + name; +} + +@Component({selector: 'user-cmp'}) +@View({template: "hello {{user}}"}) +class UserCmp { + user: string; + constructor(params: RouteParams) { this.user = params.get('name'); } +} @Component({selector: 'test-component'}) @View({