From 1bec4f6c6135d7aaccec7492d70c36e1ceeaeefa Mon Sep 17 00:00:00 2001 From: Brian Ford Date: Mon, 23 Nov 2015 10:47:06 -0800 Subject: [PATCH] feat(router): add support for APP_BASE_HREF to HashLocationStrategy Closes #4935 Closes #5368 Closes #5451 --- modules/angular2/router.ts | 3 + .../src/router/hash_location_strategy.ts | 51 ++--- .../angular2/src/router/location_strategy.ts | 23 +++ .../src/router/path_location_strategy.ts | 41 ++-- .../angular2/src/router/platform_location.ts | 46 +++++ .../router/hash_location_strategy_spec.ts | 167 ++++++++++++++++ .../router/path_location_strategy_spec.ts | 178 ++++++++++++++++++ modules/angular2/test/router/spies.dart | 5 + modules/angular2/test/router/spies.ts | 9 +- 9 files changed, 476 insertions(+), 47 deletions(-) create mode 100644 modules/angular2/src/router/platform_location.ts create mode 100644 modules/angular2/test/router/hash_location_strategy_spec.ts create mode 100644 modules/angular2/test/router/path_location_strategy_spec.ts diff --git a/modules/angular2/router.ts b/modules/angular2/router.ts index 8d2901f459..3bc054c9b7 100644 --- a/modules/angular2/router.ts +++ b/modules/angular2/router.ts @@ -9,6 +9,7 @@ export {RouterOutlet} from './src/router/router_outlet'; export {RouterLink} from './src/router/router_link'; export {RouteParams, RouteData} from './src/router/instruction'; export {RouteRegistry} from './src/router/route_registry'; +export {PlatformLocation} from './src/router/platform_location'; export {LocationStrategy, APP_BASE_HREF} from './src/router/location_strategy'; export {HashLocationStrategy} from './src/router/hash_location_strategy'; export {PathLocationStrategy} from './src/router/path_location_strategy'; @@ -20,6 +21,7 @@ export {CanActivate} from './src/router/lifecycle_annotations'; export {Instruction, ComponentInstruction} from './src/router/instruction'; export {OpaqueToken} from 'angular2/core'; +import {PlatformLocation} from './src/router/platform_location'; import {LocationStrategy} from './src/router/location_strategy'; import {PathLocationStrategy} from './src/router/path_location_strategy'; import {Router, RootRouter} from './src/router/router'; @@ -111,6 +113,7 @@ export const ROUTER_DIRECTIVES: any[] = CONST_EXPR([RouterOutlet, RouterLink]); export const ROUTER_PROVIDERS: any[] = CONST_EXPR([ RouteRegistry, CONST_EXPR(new Provider(LocationStrategy, {useClass: PathLocationStrategy})), + PlatformLocation, Location, CONST_EXPR(new Provider( Router, diff --git a/modules/angular2/src/router/hash_location_strategy.ts b/modules/angular2/src/router/hash_location_strategy.ts index 4e063a1bc2..333cd188d2 100644 --- a/modules/angular2/src/router/hash_location_strategy.ts +++ b/modules/angular2/src/router/hash_location_strategy.ts @@ -1,7 +1,13 @@ -import {DOM} from 'angular2/src/platform/dom/dom_adapter'; -import {Injectable} from 'angular2/core'; -import {LocationStrategy, normalizeQueryParams} from './location_strategy'; -import {EventListener, History, Location} from 'angular2/src/facade/browser'; +import {Injectable, Inject, Optional} from 'angular2/core'; +import { + LocationStrategy, + joinWithSlash, + APP_BASE_HREF, + normalizeQueryParams +} from './location_strategy'; +import {EventListener} from 'angular2/src/facade/browser'; +import {isPresent} from 'angular2/src/facade/lang'; +import {PlatformLocation} from './platform_location'; /** * `HashLocationStrategy` is a {@link LocationStrategy} used to configure the @@ -43,48 +49,45 @@ import {EventListener, History, Location} from 'angular2/src/facade/browser'; */ @Injectable() export class HashLocationStrategy extends LocationStrategy { - private _location: Location; - private _history: History; - - constructor() { + private _baseHref: string = ''; + constructor(private _platformLocation: PlatformLocation, + @Optional() @Inject(APP_BASE_HREF) _baseHref?: string) { super(); - this._location = DOM.getLocation(); - this._history = DOM.getHistory(); + if (isPresent(_baseHref)) { + this._baseHref = _baseHref; + } } - onPopState(fn: EventListener): void { - DOM.getGlobalEventTarget('window').addEventListener('popstate', fn, false); - } + onPopState(fn: EventListener): void { this._platformLocation.onPopState(fn); } - getBaseHref(): string { return ''; } + getBaseHref(): string { return this._baseHref; } path(): string { // the hash value is always prefixed with a `#` // and if it is empty then it will stay empty - var path = this._location.hash; + var path = this._platformLocation.hash; // Dart will complain if a call to substring is // executed with a position value that extends the // length of string. return (path.length > 0 ? path.substring(1) : path) + - normalizeQueryParams(this._location.search); + normalizeQueryParams(this._platformLocation.search); } prepareExternalUrl(internal: string): string { - return internal.length > 0 ? ('#' + internal) : internal; + var url = joinWithSlash(this._baseHref, internal); + return url.length > 0 ? ('#' + url) : url; } pushState(state: any, title: string, path: string, queryParams: string) { - var url = path + normalizeQueryParams(queryParams); + var url = this.prepareExternalUrl(path + normalizeQueryParams(queryParams)); if (url.length == 0) { - url = this._location.pathname; - } else { - url = this.prepareExternalUrl(url); + url = this._platformLocation.pathname; } - this._history.pushState(state, title, url); + this._platformLocation.pushState(state, title, url); } - forward(): void { this._history.forward(); } + forward(): void { this._platformLocation.forward(); } - back(): void { this._history.back(); } + back(): void { this._platformLocation.back(); } } diff --git a/modules/angular2/src/router/location_strategy.ts b/modules/angular2/src/router/location_strategy.ts index 2b13122b7c..6098b50d10 100644 --- a/modules/angular2/src/router/location_strategy.ts +++ b/modules/angular2/src/router/location_strategy.ts @@ -62,3 +62,26 @@ export const APP_BASE_HREF: OpaqueToken = CONST_EXPR(new OpaqueToken('appBaseHre export function normalizeQueryParams(params: string): string { return (params.length > 0 && params.substring(0, 1) != '?') ? ('?' + params) : params; } + +export function joinWithSlash(start: string, end: string): string { + if (start.length == 0) { + return end; + } + if (end.length == 0) { + return start; + } + var slashes = 0; + if (start.endsWith('/')) { + slashes++; + } + if (end.startsWith('/')) { + slashes++; + } + if (slashes == 2) { + return start + end.substring(1); + } + if (slashes == 1) { + return start + end; + } + return start + '/' + end; +} diff --git a/modules/angular2/src/router/path_location_strategy.ts b/modules/angular2/src/router/path_location_strategy.ts index d1e69bb917..d9c9c03be6 100644 --- a/modules/angular2/src/router/path_location_strategy.ts +++ b/modules/angular2/src/router/path_location_strategy.ts @@ -1,9 +1,14 @@ -import {DOM} from 'angular2/src/platform/dom/dom_adapter'; -import {Injectable, Inject} from 'angular2/core'; +import {Injectable, Inject, Optional} from 'angular2/core'; import {EventListener, History, Location} from 'angular2/src/facade/browser'; import {isBlank} from 'angular2/src/facade/lang'; import {BaseException} from 'angular2/src/facade/exceptions'; -import {LocationStrategy, APP_BASE_HREF, normalizeQueryParams} from './location_strategy'; +import { + LocationStrategy, + APP_BASE_HREF, + normalizeQueryParams, + joinWithSlash +} from './location_strategy'; +import {PlatformLocation} from './platform_location'; /** * `PathLocationStrategy` is a {@link LocationStrategy} used to configure the @@ -52,15 +57,14 @@ import {LocationStrategy, APP_BASE_HREF, normalizeQueryParams} from './location_ */ @Injectable() export class PathLocationStrategy extends LocationStrategy { - private _location: Location; - private _history: History; private _baseHref: string; - constructor(@Inject(APP_BASE_HREF) href?: string) { + constructor(private _platformLocation: PlatformLocation, + @Optional() @Inject(APP_BASE_HREF) href?: string) { super(); if (isBlank(href)) { - href = DOM.getBaseHref(); + href = this._platformLocation.getBaseHrefFromDOM(); } if (isBlank(href)) { @@ -68,33 +72,28 @@ export class PathLocationStrategy extends LocationStrategy { `No base href set. Please provide a value for the APP_BASE_HREF token or add a base element to the document.`); } - this._location = DOM.getLocation(); - this._history = DOM.getHistory(); this._baseHref = href; } onPopState(fn: EventListener): void { - DOM.getGlobalEventTarget('window').addEventListener('popstate', fn, false); - DOM.getGlobalEventTarget('window').addEventListener('hashchange', fn, false); + this._platformLocation.onPopState(fn); + this._platformLocation.onHashChange(fn); } getBaseHref(): string { return this._baseHref; } - prepareExternalUrl(internal: string): string { - if (internal.startsWith('/') && this._baseHref.endsWith('/')) { - return this._baseHref + internal.substring(1); - } - return this._baseHref + internal; - } + prepareExternalUrl(internal: string): string { return joinWithSlash(this._baseHref, internal); } - path(): string { return this._location.pathname + normalizeQueryParams(this._location.search); } + path(): string { + return this._platformLocation.pathname + normalizeQueryParams(this._platformLocation.search); + } pushState(state: any, title: string, url: string, queryParams: string) { var externalUrl = this.prepareExternalUrl(url + normalizeQueryParams(queryParams)); - this._history.pushState(state, title, externalUrl); + this._platformLocation.pushState(state, title, externalUrl); } - forward(): void { this._history.forward(); } + forward(): void { this._platformLocation.forward(); } - back(): void { this._history.back(); } + back(): void { this._platformLocation.back(); } } diff --git a/modules/angular2/src/router/platform_location.ts b/modules/angular2/src/router/platform_location.ts new file mode 100644 index 0000000000..79591b12de --- /dev/null +++ b/modules/angular2/src/router/platform_location.ts @@ -0,0 +1,46 @@ +import {DOM} from 'angular2/src/platform/dom/dom_adapter'; +import {Injectable} from 'angular2/core'; +import {EventListener, History, Location} from 'angular2/src/facade/browser'; + +/** + * `PlatformLocation` encapsulates all of the direct calls to platform APIs. + * This class should not be used directly by an application developer. Instead, use + * {@link Location}. + */ +@Injectable() +export class PlatformLocation { + private _location: Location; + private _history: History; + + constructor() { this._init(); } + + // This is moved to its own method so that `MockPlatformLocationStrategy` can overwrite it + /** @internal */ + _init() { + this._location = DOM.getLocation(); + this._history = DOM.getHistory(); + } + + getBaseHrefFromDOM(): string { return DOM.getBaseHref(); } + + onPopState(fn: EventListener): void { + DOM.getGlobalEventTarget('window').addEventListener('popstate', fn, false); + } + + onHashChange(fn: EventListener): void { + DOM.getGlobalEventTarget('window').addEventListener('hashchange', fn, false); + } + + get pathname(): string { return this._location.pathname; } + get search(): string { return this._location.search; } + get hash(): string { return this._location.hash; } + set pathname(newPath: string) { this._location.pathname = newPath; } + + pushState(state: any, title: string, url: string): void { + this._history.pushState(state, title, url); + } + + forward(): void { this._history.forward(); } + + back(): void { this._history.back(); } +} diff --git a/modules/angular2/test/router/hash_location_strategy_spec.ts b/modules/angular2/test/router/hash_location_strategy_spec.ts new file mode 100644 index 0000000000..e354cccfe0 --- /dev/null +++ b/modules/angular2/test/router/hash_location_strategy_spec.ts @@ -0,0 +1,167 @@ +import { + AsyncTestCompleter, + describe, + proxy, + it, + iit, + ddescribe, + expect, + inject, + beforeEach, + beforeEachProviders, + SpyObject +} from 'angular2/testing_internal'; + +import {Injector, provide} from 'angular2/core'; +import {CONST_EXPR} from 'angular2/src/facade/lang'; + +import {PlatformLocation} from 'angular2/src/router/platform_location'; +import {LocationStrategy, APP_BASE_HREF} from 'angular2/src/router/location_strategy'; +import {HashLocationStrategy} from 'angular2/src/router/hash_location_strategy'; +import {SpyPlatformLocation} from './spies'; + +export function main() { + describe('HashLocationStrategy', () => { + var platformLocation, locationStrategy; + + beforeEachProviders( + () => [HashLocationStrategy, provide(PlatformLocation, {useClass: SpyPlatformLocation})]); + + describe('without APP_BASE_HREF', () => { + beforeEach(inject([PlatformLocation, HashLocationStrategy], (pl, ls) => { + platformLocation = pl; + locationStrategy = ls; + platformLocation.spy('pushState'); + platformLocation.pathname = ''; + })); + + it('should prepend urls with a hash for non-empty URLs', () => { + expect(locationStrategy.prepareExternalUrl('foo')).toEqual('#foo'); + + locationStrategy.pushState(null, 'Title', 'foo', ''); + expect(platformLocation.spy('pushState')).toHaveBeenCalledWith(null, 'Title', '#foo'); + }); + + it('should prepend urls with a hash for URLs with query params', () => { + expect(locationStrategy.prepareExternalUrl('foo?bar')).toEqual('#foo?bar'); + + locationStrategy.pushState(null, 'Title', 'foo', 'bar=baz'); + expect(platformLocation.spy('pushState')) + .toHaveBeenCalledWith(null, 'Title', '#foo?bar=baz'); + }); + + it('should prepend urls with a hash for URLs with just query params', () => { + expect(locationStrategy.prepareExternalUrl('?bar')).toEqual('#?bar'); + + locationStrategy.pushState(null, 'Title', '', 'bar=baz'); + expect(platformLocation.spy('pushState')).toHaveBeenCalledWith(null, 'Title', '#?bar=baz'); + }); + + it('should not prepend a hash to external urls for an empty internal URL', () => { + expect(locationStrategy.prepareExternalUrl('')).toEqual(''); + + locationStrategy.pushState(null, 'Title', '', ''); + expect(platformLocation.spy('pushState')).toHaveBeenCalledWith(null, 'Title', ''); + }); + }); + + describe('with APP_BASE_HREF with neither leading nor trailing slash', () => { + beforeEachProviders(() => [provide(APP_BASE_HREF, {useValue: 'app'})]); + + beforeEach(inject([PlatformLocation, HashLocationStrategy], (pl, ls) => { + platformLocation = pl; + locationStrategy = ls; + platformLocation.spy('pushState'); + platformLocation.pathname = ''; + })); + + it('should prepend urls with a hash for non-empty URLs', () => { + expect(locationStrategy.prepareExternalUrl('foo')).toEqual('#app/foo'); + + locationStrategy.pushState(null, 'Title', 'foo', ''); + expect(platformLocation.spy('pushState')).toHaveBeenCalledWith(null, 'Title', '#app/foo'); + }); + + it('should prepend urls with a hash for URLs with query params', () => { + expect(locationStrategy.prepareExternalUrl('foo?bar')).toEqual('#app/foo?bar'); + + locationStrategy.pushState(null, 'Title', 'foo', 'bar=baz'); + expect(platformLocation.spy('pushState')) + .toHaveBeenCalledWith(null, 'Title', '#app/foo?bar=baz'); + }); + + it('should not prepend a hash to external urls for an empty internal URL', () => { + expect(locationStrategy.prepareExternalUrl('')).toEqual('#app'); + + locationStrategy.pushState(null, 'Title', '', ''); + expect(platformLocation.spy('pushState')).toHaveBeenCalledWith(null, 'Title', '#app'); + }); + }); + + describe('with APP_BASE_HREF with leading slash', () => { + beforeEachProviders(() => [provide(APP_BASE_HREF, {useValue: '/app'})]); + + beforeEach(inject([PlatformLocation, HashLocationStrategy], (pl, ls) => { + platformLocation = pl; + locationStrategy = ls; + platformLocation.spy('pushState'); + platformLocation.pathname = ''; + })); + + it('should prepend urls with a hash for non-empty URLs', () => { + expect(locationStrategy.prepareExternalUrl('foo')).toEqual('#/app/foo'); + + locationStrategy.pushState(null, 'Title', 'foo', ''); + expect(platformLocation.spy('pushState')).toHaveBeenCalledWith(null, 'Title', '#/app/foo'); + }); + + it('should prepend urls with a hash for URLs with query params', () => { + expect(locationStrategy.prepareExternalUrl('foo?bar')).toEqual('#/app/foo?bar'); + + locationStrategy.pushState(null, 'Title', 'foo', 'bar=baz'); + expect(platformLocation.spy('pushState')) + .toHaveBeenCalledWith(null, 'Title', '#/app/foo?bar=baz'); + }); + + it('should not prepend a hash to external urls for an empty internal URL', () => { + expect(locationStrategy.prepareExternalUrl('')).toEqual('#/app'); + + locationStrategy.pushState(null, 'Title', '', ''); + expect(platformLocation.spy('pushState')).toHaveBeenCalledWith(null, 'Title', '#/app'); + }); + }); + + describe('with APP_BASE_HREF with both leading and trailing slash', () => { + beforeEachProviders(() => [provide(APP_BASE_HREF, {useValue: '/app/'})]); + + beforeEach(inject([PlatformLocation, HashLocationStrategy], (pl, ls) => { + platformLocation = pl; + locationStrategy = ls; + platformLocation.spy('pushState'); + platformLocation.pathname = ''; + })); + + it('should prepend urls with a hash for non-empty URLs', () => { + expect(locationStrategy.prepareExternalUrl('foo')).toEqual('#/app/foo'); + + locationStrategy.pushState(null, 'Title', 'foo', ''); + expect(platformLocation.spy('pushState')).toHaveBeenCalledWith(null, 'Title', '#/app/foo'); + }); + + it('should prepend urls with a hash for URLs with query params', () => { + expect(locationStrategy.prepareExternalUrl('foo?bar')).toEqual('#/app/foo?bar'); + + locationStrategy.pushState(null, 'Title', 'foo', 'bar=baz'); + expect(platformLocation.spy('pushState')) + .toHaveBeenCalledWith(null, 'Title', '#/app/foo?bar=baz'); + }); + + it('should not prepend a hash to external urls for an empty internal URL', () => { + expect(locationStrategy.prepareExternalUrl('')).toEqual('#/app/'); + + locationStrategy.pushState(null, 'Title', '', ''); + expect(platformLocation.spy('pushState')).toHaveBeenCalledWith(null, 'Title', '#/app/'); + }); + }); + }); +} diff --git a/modules/angular2/test/router/path_location_strategy_spec.ts b/modules/angular2/test/router/path_location_strategy_spec.ts new file mode 100644 index 0000000000..d1a88389c1 --- /dev/null +++ b/modules/angular2/test/router/path_location_strategy_spec.ts @@ -0,0 +1,178 @@ +import { + AsyncTestCompleter, + describe, + proxy, + it, + iit, + ddescribe, + expect, + inject, + beforeEach, + beforeEachProviders, + SpyObject +} from 'angular2/testing_internal'; + +import {Injector, provide} from 'angular2/core'; +import {CONST_EXPR} from 'angular2/src/facade/lang'; + +import {PlatformLocation} from 'angular2/src/router/platform_location'; +import {LocationStrategy, APP_BASE_HREF} from 'angular2/src/router/location_strategy'; +import {PathLocationStrategy} from 'angular2/src/router/path_location_strategy'; +import {SpyPlatformLocation} from './spies'; + +export function main() { + describe('PathLocationStrategy', () => { + var platformLocation, locationStrategy; + + beforeEachProviders(() => [ + PathLocationStrategy, + provide(PlatformLocation, {useFactory: makeSpyPlatformLocation}) + ]); + + it('should throw without a base element or APP_BASE_HREF', () => { + platformLocation = new SpyPlatformLocation(); + platformLocation.pathname = ''; + platformLocation.spy('getBaseHrefFromDOM').andReturn(null); + + expect(() => new PathLocationStrategy(platformLocation)) + .toThrowError( + 'No base href set. Please provide a value for the APP_BASE_HREF token or add a base element to the document.'); + }); + + describe('without APP_BASE_HREF', () => { + beforeEach(inject([PlatformLocation, PathLocationStrategy], (pl, ls) => { + platformLocation = pl; + locationStrategy = ls; + })); + + it('should prepend urls with a hash for non-empty URLs', () => { + expect(locationStrategy.prepareExternalUrl('foo')).toEqual('foo'); + + locationStrategy.pushState(null, 'Title', 'foo', ''); + expect(platformLocation.spy('pushState')).toHaveBeenCalledWith(null, 'Title', 'foo'); + }); + + it('should prepend urls with a hash for URLs with query params', () => { + expect(locationStrategy.prepareExternalUrl('foo?bar')).toEqual('foo?bar'); + + locationStrategy.pushState(null, 'Title', 'foo', 'bar=baz'); + expect(platformLocation.spy('pushState')) + .toHaveBeenCalledWith(null, 'Title', 'foo?bar=baz'); + }); + + it('should not prepend a hash to external urls for an empty internal URL', () => { + expect(locationStrategy.prepareExternalUrl('')).toEqual(''); + + locationStrategy.pushState(null, 'Title', '', ''); + expect(platformLocation.spy('pushState')).toHaveBeenCalledWith(null, 'Title', ''); + }); + }); + + describe('with APP_BASE_HREF with neither leading nor trailing slash', () => { + beforeEachProviders(() => [provide(APP_BASE_HREF, {useValue: 'app'})]); + + beforeEach(inject([PlatformLocation, PathLocationStrategy], (pl, ls) => { + platformLocation = pl; + locationStrategy = ls; + platformLocation.spy('pushState'); + platformLocation.pathname = ''; + })); + + it('should prepend urls with a hash for non-empty URLs', () => { + expect(locationStrategy.prepareExternalUrl('foo')).toEqual('app/foo'); + + locationStrategy.pushState(null, 'Title', 'foo', ''); + expect(platformLocation.spy('pushState')).toHaveBeenCalledWith(null, 'Title', 'app/foo'); + }); + + it('should prepend urls with a hash for URLs with query params', () => { + expect(locationStrategy.prepareExternalUrl('foo?bar')).toEqual('app/foo?bar'); + + locationStrategy.pushState(null, 'Title', 'foo', 'bar=baz'); + expect(platformLocation.spy('pushState')) + .toHaveBeenCalledWith(null, 'Title', 'app/foo?bar=baz'); + }); + + it('should not prepend a hash to external urls for an empty internal URL', () => { + expect(locationStrategy.prepareExternalUrl('')).toEqual('app'); + + locationStrategy.pushState(null, 'Title', '', ''); + expect(platformLocation.spy('pushState')).toHaveBeenCalledWith(null, 'Title', 'app'); + }); + }); + + describe('with APP_BASE_HREF with leading slash', () => { + beforeEachProviders(() => [provide(APP_BASE_HREF, {useValue: '/app'})]); + + beforeEach(inject([PlatformLocation, PathLocationStrategy], (pl, ls) => { + platformLocation = pl; + locationStrategy = ls; + platformLocation.spy('pushState'); + platformLocation.pathname = ''; + })); + + it('should prepend urls with a hash for non-empty URLs', () => { + expect(locationStrategy.prepareExternalUrl('foo')).toEqual('/app/foo'); + + locationStrategy.pushState(null, 'Title', 'foo', ''); + expect(platformLocation.spy('pushState')).toHaveBeenCalledWith(null, 'Title', '/app/foo'); + }); + + it('should prepend urls with a hash for URLs with query params', () => { + expect(locationStrategy.prepareExternalUrl('foo?bar')).toEqual('/app/foo?bar'); + + locationStrategy.pushState(null, 'Title', 'foo', 'bar=baz'); + expect(platformLocation.spy('pushState')) + .toHaveBeenCalledWith(null, 'Title', '/app/foo?bar=baz'); + }); + + it('should not prepend a hash to external urls for an empty internal URL', () => { + expect(locationStrategy.prepareExternalUrl('')).toEqual('/app'); + + locationStrategy.pushState(null, 'Title', '', ''); + expect(platformLocation.spy('pushState')).toHaveBeenCalledWith(null, 'Title', '/app'); + }); + }); + + describe('with APP_BASE_HREF with both leading and trailing slash', () => { + beforeEachProviders(() => [provide(APP_BASE_HREF, {useValue: '/app/'})]); + + beforeEach(inject([PlatformLocation, PathLocationStrategy], (pl, ls) => { + platformLocation = pl; + locationStrategy = ls; + platformLocation.spy('pushState'); + platformLocation.pathname = ''; + })); + + it('should prepend urls with a hash for non-empty URLs', () => { + expect(locationStrategy.prepareExternalUrl('foo')).toEqual('/app/foo'); + + locationStrategy.pushState(null, 'Title', 'foo', ''); + expect(platformLocation.spy('pushState')).toHaveBeenCalledWith(null, 'Title', '/app/foo'); + }); + + it('should prepend urls with a hash for URLs with query params', () => { + expect(locationStrategy.prepareExternalUrl('foo?bar')).toEqual('/app/foo?bar'); + + locationStrategy.pushState(null, 'Title', 'foo', 'bar=baz'); + expect(platformLocation.spy('pushState')) + .toHaveBeenCalledWith(null, 'Title', '/app/foo?bar=baz'); + }); + + it('should not prepend a hash to external urls for an empty internal URL', () => { + expect(locationStrategy.prepareExternalUrl('')).toEqual('/app/'); + + locationStrategy.pushState(null, 'Title', '', ''); + expect(platformLocation.spy('pushState')).toHaveBeenCalledWith(null, 'Title', '/app/'); + }); + }); + }); +} + +function makeSpyPlatformLocation() { + var platformLocation = new SpyPlatformLocation(); + platformLocation.spy('getBaseHrefFromDOM').andReturn(''); + platformLocation.spy('pushState'); + platformLocation.pathname = ''; + return platformLocation; +} diff --git a/modules/angular2/test/router/spies.dart b/modules/angular2/test/router/spies.dart index 621c037390..2e23c9a6a3 100644 --- a/modules/angular2/test/router/spies.dart +++ b/modules/angular2/test/router/spies.dart @@ -17,3 +17,8 @@ class SpyRouter extends SpyObject implements Router { class SpyRouterOutlet extends SpyObject implements RouterOutlet { noSuchMethod(m) => super.noSuchMethod(m); } + +class SpyPlatformLocation extends SpyObject implements PlatformLocation { + String pathname; + noSuchMethod(m) => super.noSuchMethod(m); +} diff --git a/modules/angular2/test/router/spies.ts b/modules/angular2/test/router/spies.ts index 849b39a69b..81b7f21cb8 100644 --- a/modules/angular2/test/router/spies.ts +++ b/modules/angular2/test/router/spies.ts @@ -1,4 +1,4 @@ -import {Router, RouterOutlet, Location} from 'angular2/router'; +import {Router, RouterOutlet, Location, PlatformLocation} from 'angular2/router'; import {SpyObject, proxy} from 'angular2/testing_internal'; export class SpyRouter extends SpyObject { @@ -11,4 +11,9 @@ export class SpyRouterOutlet extends SpyObject { export class SpyLocation extends SpyObject { constructor() { super(Location); } -} \ No newline at end of file +} + +export class SpyPlatformLocation extends SpyObject { + pathname: string = null; + constructor() { super(SpyPlatformLocation); } +}