angular-cn/modules/angular2/test/router/integration/router_link_spec.ts

464 lines
18 KiB
TypeScript
Raw Normal View History

import {ComponentFixture, AsyncTestCompleter, beforeEach, ddescribe, xdescribe, describe, dispatchEvent, expect, iit, inject, beforeEachProviders, it, xit, TestComponentBuilder, proxy, SpyObject} from 'angular2/testing_internal';
2015-08-24 14:24:53 -04:00
import {By} from 'angular2/platform/common_dom';
import {NumberWrapper} from 'angular2/src/facade/lang';
import {PromiseWrapper} from 'angular2/src/facade/async';
import {ListWrapper} from 'angular2/src/facade/collection';
2015-08-24 14:24:53 -04:00
import {provide, Component, DirectiveResolver} from 'angular2/core';
2015-08-24 14:24:53 -04:00
import {SpyLocation} from 'angular2/src/mock/location_mock';
import {Location, Router, RouteRegistry, RouterLink, RouterOutlet, AsyncRoute, AuxRoute, Route, RouteParams, RouteConfig, ROUTER_DIRECTIVES, ROUTER_PRIMARY_COMPONENT} from 'angular2/router';
import {RootRouter} from 'angular2/src/router/router';
2015-08-24 14:24:53 -04:00
import {DOM} from 'angular2/src/platform/dom/dom_adapter';
import {TEMPLATE_TRANSFORMS} from 'angular2/compiler';
import {RouterLinkTransform} from 'angular2/src/router/directives/router_link_transform';
2015-08-24 14:24:53 -04:00
export function main() {
describe('routerLink directive', function() {
2015-08-24 14:24:53 -04:00
var tcb: TestComponentBuilder;
var fixture: ComponentFixture;
var router: Router;
var location: Location;
2015-08-24 14:24:53 -04:00
beforeEachProviders(
() =>
[RouteRegistry, DirectiveResolver, provide(Location, {useClass: SpyLocation}),
provide(ROUTER_PRIMARY_COMPONENT, {useValue: MyComp}),
provide(Router, {useClass: RootRouter}),
provide(TEMPLATE_TRANSFORMS, {useClass: RouterLinkTransform, multi: true})]);
beforeEach(inject(
[TestComponentBuilder, Router, Location], (tcBuilder, rtr: Router, loc: Location) => {
tcb = tcBuilder;
router = rtr;
location = loc;
}));
function compile(template: string = '<router-outlet></router-outlet>') {
return tcb.overrideTemplate(MyComp, ('<div>' + template + '</div>'))
2015-08-24 14:24:53 -04:00
.createAsync(MyComp)
.then((tc) => { fixture = tc; });
2015-08-24 14:24:53 -04:00
}
it('should generate absolute hrefs that include the base href',
inject([AsyncTestCompleter], (async) => {
(<SpyLocation>location).setBaseHref('/my/base');
compile('<a href="hello" [routerLink]="[\'./User\']"></a>')
.then(
(_) =>
router.config([new Route({path: '/user', component: UserCmp, name: 'User'})]))
.then((_) => router.navigateByUrl('/a/b'))
2015-08-24 14:24:53 -04:00
.then((_) => {
fixture.detectChanges();
expect(getHref(fixture)).toEqual('/my/base/user');
2015-08-24 14:24:53 -04:00
async.done();
});
}));
it('should generate link hrefs without params', inject([AsyncTestCompleter], (async) => {
compile('<a href="hello" [routerLink]="[\'./User\']"></a>')
.then(
(_) =>
router.config([new Route({path: '/user', component: UserCmp, name: 'User'})]))
.then((_) => router.navigateByUrl('/a/b'))
2015-08-24 14:24:53 -04:00
.then((_) => {
fixture.detectChanges();
expect(getHref(fixture)).toEqual('/user');
2015-08-24 14:24:53 -04:00
async.done();
});
}));
it('should generate link hrefs with params', inject([AsyncTestCompleter], (async) => {
compile('<a href="hello" [routerLink]="[\'./User\', {name: name}]">{{name}}</a>')
.then((_) => router.config([new Route(
{path: '/user/:name', component: UserCmp, name: 'User'})]))
.then((_) => router.navigateByUrl('/a/b'))
2015-08-24 14:24:53 -04:00
.then((_) => {
fixture.debugElement.componentInstance.name = 'brian';
fixture.detectChanges();
expect(fixture.debugElement.nativeElement).toHaveText('brian');
expect(getHref(fixture)).toEqual('/user/brian');
2015-08-24 14:24:53 -04:00
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, name: 'Page'})]))
.then((_) => router.navigateByUrl('/page/1'))
2015-08-24 14:24:53 -04:00
.then((_) => {
fixture.detectChanges();
expect(getHref(fixture)).toEqual('/page/2');
2015-08-24 14:24:53 -04:00
async.done();
});
}));
it('should generate link hrefs from a child to its sibling with no leading slash',
inject([AsyncTestCompleter], (async) => {
compile()
.then((_) => router.config([new Route(
{path: '/page/:number', component: NoPrefixSiblingPageCmp, name: 'Page'})]))
.then((_) => router.navigateByUrl('/page/1'))
.then((_) => {
fixture.detectChanges();
expect(getHref(fixture)).toEqual('/page/2');
async.done();
});
}));
it('should generate link hrefs to a child with no leading slash',
inject([AsyncTestCompleter], (async) => {
compile()
.then((_) => router.config([new Route(
{path: '/book/:title/...', component: NoPrefixBookCmp, name: 'Book'})]))
.then((_) => router.navigateByUrl('/book/1984/page/1'))
.then((_) => {
fixture.detectChanges();
expect(getHref(fixture)).toEqual('/book/1984/page/100');
async.done();
});
}));
it('should throw when links without a leading slash are ambiguous',
inject([AsyncTestCompleter], (async) => {
compile()
.then((_) => router.config([new Route(
{path: '/book/:title/...', component: AmbiguousBookCmp, name: 'Book'})]))
.then((_) => router.navigateByUrl('/book/1984/page/1'))
.then((_) => {
var link = ListWrapper.toJSON(['Book', {number: 100}]);
expect(() => fixture.detectChanges())
.toThrowErrorWith(
`Link "${link}" is ambiguous, use "./" or "../" to disambiguate.`);
async.done();
});
}));
it('should generate link hrefs when asynchronously loaded',
inject([AsyncTestCompleter], (async) => {
compile()
.then((_) => router.config([new AsyncRoute({
path: '/child-with-grandchild/...',
loader: parentCmpLoader,
name: 'ChildWithGrandchild'
})]))
.then((_) => router.navigateByUrl('/child-with-grandchild/grandchild'))
.then((_) => {
fixture.detectChanges();
expect(getHref(fixture)).toEqual('/child-with-grandchild/grandchild');
async.done();
});
}));
2015-08-24 14:24:53 -04:00
it('should generate relative links preserving the existing parent route',
inject([AsyncTestCompleter], (async) => {
compile()
.then((_) => router.config([new Route(
{path: '/book/:title/...', component: BookCmp, name: 'Book'})]))
.then((_) => router.navigateByUrl('/book/1984/page/1'))
2015-08-24 14:24:53 -04:00
.then((_) => {
fixture.detectChanges();
// TODO(juliemr): This should be one By.css('book-cmp a') query, but the parse5
// adapter
// can't handle css child selectors.
expect(DOM.getAttribute(
fixture.debugElement.query(By.css('book-cmp'))
.query(By.css('a'))
.nativeElement,
'href'))
2015-08-24 14:24:53 -04:00
.toEqual('/book/1984/page/100');
expect(DOM.getAttribute(
fixture.debugElement.query(By.css('page-cmp'))
.query(By.css('a'))
.nativeElement,
'href'))
2015-08-24 14:24:53 -04:00
.toEqual('/book/1984/page/2');
async.done();
});
}));
it('should generate links to auxiliary routes', inject([AsyncTestCompleter], (async) => {
compile()
.then((_) => router.config([new Route({path: '/...', component: AuxLinkCmp})]))
.then((_) => router.navigateByUrl('/'))
.then((_) => {
refactor(router): improve recognition and generation pipeline This is a big change. @matsko also deserves much of the credit for the implementation. Previously, `ComponentInstruction`s held all the state for async components. Now, we introduce several subclasses for `Instruction` to describe each type of navigation. BREAKING CHANGE: Redirects now use the Link DSL syntax. Before: ``` @RouteConfig([ { path: '/foo', redirectTo: '/bar' }, { path: '/bar', component: BarCmp } ]) ``` After: ``` @RouteConfig([ { path: '/foo', redirectTo: ['Bar'] }, { path: '/bar', component: BarCmp, name: 'Bar' } ]) ``` BREAKING CHANGE: This also introduces `useAsDefault` in the RouteConfig, which makes cases like lazy-loading and encapsulating large routes with sub-routes easier. Previously, you could use `redirectTo` like this to expand a URL like `/tab` to `/tab/posts`: @RouteConfig([ { path: '/tab', redirectTo: '/tab/users' } { path: '/tab', component: TabsCmp, name: 'Tab' } ]) AppCmp { ... } Now the recommended way to handle this is case is to use `useAsDefault` like so: ``` @RouteConfig([ { path: '/tab', component: TabsCmp, name: 'Tab' } ]) AppCmp { ... } @RouteConfig([ { path: '/posts', component: PostsCmp, useAsDefault: true, name: 'Posts' }, { path: '/users', component: UsersCmp, name: 'Users' } ]) TabsCmp { ... } ``` In the above example, you can write just `['/Tab']` and the route `Users` is automatically selected as a child route. Closes #4728 Closes #4228 Closes #4170 Closes #4490 Closes #4694 Closes #5200 Closes #5475
2015-11-23 21:07:37 -05:00
fixture.detectChanges();
expect(getHref(fixture)).toEqual('/(aside)');
async.done();
});
}));
2015-08-24 14:24:53 -04:00
describe('router-link-active CSS class', () => {
it('should be added to the associated element', inject([AsyncTestCompleter], (async) => {
router
.config([
new Route({path: '/child', component: HelloCmp, name: 'Child'}),
new Route({path: '/better-child', component: Hello2Cmp, name: 'BetterChild'})
])
.then((_) => compile(`<a [routerLink]="['./Child']" class="child-link">Child</a>
<a [routerLink]="['./BetterChild']" class="better-child-link">Better Child</a>
<router-outlet></router-outlet>`))
.then((_) => {
var element = fixture.debugElement.nativeElement;
fixture.detectChanges();
var link1 = DOM.querySelector(element, '.child-link');
var link2 = DOM.querySelector(element, '.better-child-link');
expect(link1).not.toHaveCssClass('router-link-active');
expect(link2).not.toHaveCssClass('router-link-active');
router.subscribe((_) => {
fixture.detectChanges();
expect(link1).not.toHaveCssClass('router-link-active');
expect(link2).toHaveCssClass('router-link-active');
async.done();
});
router.navigateByUrl('/better-child?extra=0');
});
}));
it('should be added to links in child routes', inject([AsyncTestCompleter], (async) => {
router
.config([
new Route({path: '/child', component: HelloCmp, name: 'Child'}), new Route({
path: '/child-with-grandchild/...',
component: ParentCmp,
name: 'ChildWithGrandchild'
})
])
.then((_) => compile(`<a [routerLink]="['./Child']" class="child-link">Child</a>
<a [routerLink]="['./ChildWithGrandchild/Grandchild']" class="child-with-grandchild-link">Better Child</a>
<router-outlet></router-outlet>`))
.then((_) => {
var element = fixture.debugElement.nativeElement;
fixture.detectChanges();
var link1 = DOM.querySelector(element, '.child-link');
var link2 = DOM.querySelector(element, '.child-with-grandchild-link');
expect(link1).not.toHaveCssClass('router-link-active');
expect(link2).not.toHaveCssClass('router-link-active');
router.subscribe((_) => {
fixture.detectChanges();
expect(link1).not.toHaveCssClass('router-link-active');
expect(link2).toHaveCssClass('router-link-active');
var link3 = DOM.querySelector(element, '.grandchild-link');
var link4 = DOM.querySelector(element, '.better-grandchild-link');
expect(link3).toHaveCssClass('router-link-active');
expect(link4).not.toHaveCssClass('router-link-active');
async.done();
});
router.navigateByUrl('/child-with-grandchild/grandchild?extra=0');
});
}));
describe('router link dsl', () => {
it('should generate link hrefs with params', inject([AsyncTestCompleter], (async) => {
compile('<a href="hello" [routerLink]="route:./User(name: name)">{{name}}</a>')
.then((_) => router.config([new Route(
{path: '/user/:name', component: UserCmp, name: 'User'})]))
.then((_) => router.navigateByUrl('/a/b'))
.then((_) => {
fixture.debugElement.componentInstance.name = 'brian';
fixture.detectChanges();
expect(fixture.debugElement.nativeElement).toHaveText('brian');
expect(getHref(fixture)).toEqual('/user/brian');
async.done();
});
}));
});
});
2015-08-24 14:24:53 -04:00
describe('when clicked', () => {
var clickOnElement = function(view) {
var anchorEl = fixture.debugElement.query(By.css('a')).nativeElement;
2015-08-24 14:24:53 -04:00
var dispatchedEvent = DOM.createMouseEvent('click');
DOM.dispatchEvent(anchorEl, dispatchedEvent);
return dispatchedEvent;
};
it('should navigate to link hrefs without params', inject([AsyncTestCompleter], (async) => {
compile('<a href="hello" [routerLink]="[\'./User\']"></a>')
.then((_) => router.config([new Route(
{path: '/user', component: UserCmp, name: 'User'})]))
.then((_) => router.navigateByUrl('/a/b'))
2015-08-24 14:24:53 -04:00
.then((_) => {
fixture.detectChanges();
2015-08-24 14:24:53 -04:00
var dispatchedEvent = clickOnElement(fixture);
2015-08-24 14:24:53 -04:00
expect(DOM.isPrevented(dispatchedEvent)).toBe(true);
// router navigation is async.
router.subscribe((_) => {
expect((<SpyLocation>location).urlChanges).toEqual(['/user']);
2015-08-24 14:24:53 -04:00
async.done();
});
});
}));
2015-08-24 14:24:53 -04:00
it('should navigate to link hrefs in presence of base href',
inject([AsyncTestCompleter], (async) => {
(<SpyLocation>location).setBaseHref('/base');
compile('<a href="hello" [routerLink]="[\'./User\']"></a>')
.then((_) => router.config([new Route(
{path: '/user', component: UserCmp, name: 'User'})]))
.then((_) => router.navigateByUrl('/a/b'))
2015-08-24 14:24:53 -04:00
.then((_) => {
fixture.detectChanges();
2015-08-24 14:24:53 -04:00
var dispatchedEvent = clickOnElement(fixture);
2015-08-24 14:24:53 -04:00
expect(DOM.isPrevented(dispatchedEvent)).toBe(true);
// router navigation is async.
router.subscribe((_) => {
expect((<SpyLocation>location).urlChanges).toEqual(['/base/user']);
2015-08-24 14:24:53 -04:00
async.done();
});
});
}));
2015-08-24 14:24:53 -04:00
});
});
}
function getHref(tc: ComponentFixture) {
return DOM.getAttribute(tc.debugElement.query(By.css('a')).nativeElement, 'href');
2015-08-24 14:24:53 -04:00
}
@Component({selector: 'my-comp', template: '', directives: [ROUTER_DIRECTIVES]})
2015-08-24 14:24:53 -04:00
class MyComp {
name;
}
@Component({selector: 'user-cmp', template: 'hello {{user}}'})
2015-08-24 14:24:53 -04:00
class UserCmp {
user: string;
constructor(params: RouteParams) { this.user = params.get('name'); }
}
@Component({
selector: 'page-cmp',
2015-08-24 14:24:53 -04:00
template:
`page #{{pageNumber}} | <a href="hello" [routerLink]="[\'../Page\', {number: nextPage}]">next</a>`,
2015-08-24 14:24:53 -04:00
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: 'page-cmp',
template:
`page #{{pageNumber}} | <a href="hello" [routerLink]="[\'Page\', {number: nextPage}]">next</a>`,
directives: [RouterLink]
})
class NoPrefixSiblingPageCmp {
pageNumber: number;
nextPage: number;
constructor(params: RouteParams) {
this.pageNumber = NumberWrapper.parseInt(params.get('number'), 10);
this.nextPage = this.pageNumber + 1;
}
}
refactor(router): improve recognition and generation pipeline This is a big change. @matsko also deserves much of the credit for the implementation. Previously, `ComponentInstruction`s held all the state for async components. Now, we introduce several subclasses for `Instruction` to describe each type of navigation. BREAKING CHANGE: Redirects now use the Link DSL syntax. Before: ``` @RouteConfig([ { path: '/foo', redirectTo: '/bar' }, { path: '/bar', component: BarCmp } ]) ``` After: ``` @RouteConfig([ { path: '/foo', redirectTo: ['Bar'] }, { path: '/bar', component: BarCmp, name: 'Bar' } ]) ``` BREAKING CHANGE: This also introduces `useAsDefault` in the RouteConfig, which makes cases like lazy-loading and encapsulating large routes with sub-routes easier. Previously, you could use `redirectTo` like this to expand a URL like `/tab` to `/tab/posts`: @RouteConfig([ { path: '/tab', redirectTo: '/tab/users' } { path: '/tab', component: TabsCmp, name: 'Tab' } ]) AppCmp { ... } Now the recommended way to handle this is case is to use `useAsDefault` like so: ``` @RouteConfig([ { path: '/tab', component: TabsCmp, name: 'Tab' } ]) AppCmp { ... } @RouteConfig([ { path: '/posts', component: PostsCmp, useAsDefault: true, name: 'Posts' }, { path: '/users', component: UsersCmp, name: 'Users' } ]) TabsCmp { ... } ``` In the above example, you can write just `['/Tab']` and the route `Users` is automatically selected as a child route. Closes #4728 Closes #4228 Closes #4170 Closes #4490 Closes #4694 Closes #5200 Closes #5475
2015-11-23 21:07:37 -05:00
@Component({selector: 'hello-cmp', template: 'hello'})
class HelloCmp {
}
refactor(router): improve recognition and generation pipeline This is a big change. @matsko also deserves much of the credit for the implementation. Previously, `ComponentInstruction`s held all the state for async components. Now, we introduce several subclasses for `Instruction` to describe each type of navigation. BREAKING CHANGE: Redirects now use the Link DSL syntax. Before: ``` @RouteConfig([ { path: '/foo', redirectTo: '/bar' }, { path: '/bar', component: BarCmp } ]) ``` After: ``` @RouteConfig([ { path: '/foo', redirectTo: ['Bar'] }, { path: '/bar', component: BarCmp, name: 'Bar' } ]) ``` BREAKING CHANGE: This also introduces `useAsDefault` in the RouteConfig, which makes cases like lazy-loading and encapsulating large routes with sub-routes easier. Previously, you could use `redirectTo` like this to expand a URL like `/tab` to `/tab/posts`: @RouteConfig([ { path: '/tab', redirectTo: '/tab/users' } { path: '/tab', component: TabsCmp, name: 'Tab' } ]) AppCmp { ... } Now the recommended way to handle this is case is to use `useAsDefault` like so: ``` @RouteConfig([ { path: '/tab', component: TabsCmp, name: 'Tab' } ]) AppCmp { ... } @RouteConfig([ { path: '/posts', component: PostsCmp, useAsDefault: true, name: 'Posts' }, { path: '/users', component: UsersCmp, name: 'Users' } ]) TabsCmp { ... } ``` In the above example, you can write just `['/Tab']` and the route `Users` is automatically selected as a child route. Closes #4728 Closes #4228 Closes #4170 Closes #4490 Closes #4694 Closes #5200 Closes #5475
2015-11-23 21:07:37 -05:00
@Component({selector: 'hello2-cmp', template: 'hello2'})
class Hello2Cmp {
}
function parentCmpLoader() {
return PromiseWrapper.resolve(ParentCmp);
}
@Component({
selector: 'parent-cmp',
template: `{ <a [routerLink]="['./Grandchild']" class="grandchild-link">Grandchild</a>
<a [routerLink]="['./BetterGrandchild']" class="better-grandchild-link">Better Grandchild</a>
<router-outlet></router-outlet> }`,
directives: ROUTER_DIRECTIVES
})
@RouteConfig([
new Route({path: '/grandchild', component: HelloCmp, name: 'Grandchild'}),
new Route({path: '/better-grandchild', component: Hello2Cmp, name: 'BetterGrandchild'})
])
class ParentCmp {
}
@Component({
selector: 'book-cmp',
template: `<a href="hello" [routerLink]="[\'./Page\', {number: 100}]">{{title}}</a> |
2015-08-24 14:24:53 -04:00
<router-outlet></router-outlet>`,
directives: ROUTER_DIRECTIVES
2015-08-24 14:24:53 -04:00
})
@RouteConfig([new Route({path: '/page/:number', component: SiblingPageCmp, name: 'Page'})])
2015-08-24 14:24:53 -04:00
class BookCmp {
title: string;
constructor(params: RouteParams) { this.title = params.get('title'); }
}
@Component({
selector: 'book-cmp',
template: `<a href="hello" [routerLink]="[\'Page\', {number: 100}]">{{title}}</a> |
<router-outlet></router-outlet>`,
directives: ROUTER_DIRECTIVES
})
@RouteConfig([new Route({path: '/page/:number', component: SiblingPageCmp, name: 'Page'})])
class NoPrefixBookCmp {
title: string;
constructor(params: RouteParams) { this.title = params.get('title'); }
}
@Component({
selector: 'book-cmp',
template: `<a href="hello" [routerLink]="[\'Book\', {number: 100}]">{{title}}</a> |
<router-outlet></router-outlet>`,
directives: ROUTER_DIRECTIVES
})
@RouteConfig([new Route({path: '/page/:number', component: SiblingPageCmp, name: 'Book'})])
class AmbiguousBookCmp {
title: string;
constructor(params: RouteParams) { this.title = params.get('title'); }
}
@Component({
selector: 'aux-cmp',
template:
`<a [routerLink]="[\'./Hello\', [ \'Aside\' ] ]">aside</a> |
<router-outlet></router-outlet> | aside <router-outlet name="aside"></router-outlet>`,
directives: ROUTER_DIRECTIVES
})
@RouteConfig([
new Route({path: '/', component: HelloCmp, name: 'Hello'}),
new AuxRoute({path: '/aside', component: Hello2Cmp, name: 'Aside'})
])
class AuxLinkCmp {
}