1+ /**
2+ * @license
3+ * Copyright Google Inc. All Rights Reserved.
4+ *
5+ * Use of this source code is governed by an MIT-style license that can be
6+ * found in the LICENSE file at https://angular.io/license
7+ */
8+
9+ import { LocationChangeEvent , LocationChangeListener , PlatformLocation } from '@angular/common' ;
10+ import { Injectable , InjectionToken , Optional } from '@angular/core' ;
11+ import { Subject } from 'rxjs' ;
12+
13+ function parseUrl ( urlStr : string , baseHref : string ) {
14+ const verifyProtocol = / ^ ( ( h t t p [ s ] ? | f t p ) : \/ \/ ) / ;
15+ let serverBase = '' ;
16+
17+ // URL class requires full URL. If the URL string doesn't start with protocol, we need to add an
18+ // arbitrary base URL which can be removed afterward.
19+ if ( ! verifyProtocol . test ( urlStr ) ) {
20+ serverBase = 'http://empty.com/' ;
21+ }
22+ const parsedUrl = new URL ( urlStr , serverBase ) ;
23+ if ( parsedUrl . pathname && parsedUrl . pathname . indexOf ( baseHref ) === 0 ) {
24+ parsedUrl . pathname = parsedUrl . pathname . substring ( baseHref . length ) ;
25+ }
26+ return {
27+ hostname : ! serverBase && parsedUrl . hostname || '' ,
28+ protocol : ! serverBase && parsedUrl . protocol || '' ,
29+ port : ! serverBase && parsedUrl . port || '' ,
30+ pathname : parsedUrl . pathname || '/' ,
31+ search : parsedUrl . search || '' ,
32+ hash : parsedUrl . hash || '' ,
33+ } ;
34+ }
35+
36+ export interface MockPlatformLocationConfig {
37+ startUrl ?: string ;
38+ appBaseHref ?: string ;
39+ }
40+
41+ export const MOCK_PLATFORM_LOCATION_CONFIG = new InjectionToken ( 'MOCK_PLATFORM_LOCATION_CONFIG' ) ;
42+
43+ /**
44+ * Mock implementation of URL state.
45+ */
46+ @Injectable ( )
47+ export class MockPlatformLocation implements PlatformLocation {
48+ private baseHref : string = '' ;
49+ private hashUpdate = new Subject < LocationChangeEvent > ( ) ;
50+ private urlChanges : {
51+ hostname : string ,
52+ protocol : string ,
53+ port : string ,
54+ pathname : string ,
55+ search : string ,
56+ hash : string ,
57+ state : unknown
58+ } [ ] = [ { hostname : '' , protocol : '' , port : '' , pathname : '/' , search : '' , hash : '' , state : null } ] ;
59+
60+ constructor ( @Optional ( ) config ?: MockPlatformLocationConfig ) {
61+ if ( config ) {
62+ this . baseHref = config . appBaseHref || '' ;
63+
64+ const parsedChanges =
65+ this . parseChanges ( null , config . startUrl || 'http://<empty>' , this . baseHref ) ;
66+ this . urlChanges [ 0 ] = { ...parsedChanges } ;
67+ }
68+ }
69+
70+ get hostname ( ) { return this . urlChanges [ 0 ] . hostname ; }
71+ get protocol ( ) { return this . urlChanges [ 0 ] . protocol ; }
72+ get port ( ) { return this . urlChanges [ 0 ] . port ; }
73+ get pathname ( ) { return this . urlChanges [ 0 ] . pathname ; }
74+ get search ( ) { return this . urlChanges [ 0 ] . search ; }
75+ get hash ( ) { return this . urlChanges [ 0 ] . hash ; }
76+ get state ( ) { return this . urlChanges [ 0 ] . state ; }
77+
78+
79+ getBaseHrefFromDOM ( ) : string { return this . baseHref ; }
80+
81+ onPopState ( fn : LocationChangeListener ) : void {
82+ // No-op: a state stack is not implemented, so
83+ // no events will ever come.
84+ }
85+
86+ onHashChange ( fn : LocationChangeListener ) : void { this . hashUpdate . subscribe ( fn ) ; }
87+
88+ get href ( ) : string {
89+ return `${ this . protocol } //${ this . hostname } ${ this . baseHref } ${ this . pathname === '/' ? '' : this . pathname } ${ this . search } ${ this . hash } ` ;
90+ }
91+
92+ get url ( ) : string { return `${ this . pathname } ${ this . search } ${ this . hash } ` ; }
93+
94+ private setHash ( value : string , oldUrl : string ) {
95+ if ( this . hash === value ) {
96+ // Don't fire events if the hash has not changed.
97+ return ;
98+ }
99+ ( this as { hash : string } ) . hash = value ;
100+ const newUrl = this . url ;
101+ scheduleMicroTask ( ( ) => this . hashUpdate . next ( {
102+ type : 'hashchange' , state : null , oldUrl, newUrl
103+ } as LocationChangeEvent ) ) ;
104+ }
105+
106+ private parseChanges ( state : unknown , url : string , baseHref : string = '' ) {
107+ return { ...parseUrl ( url , baseHref ) , state} ;
108+ }
109+
110+ replaceState ( state : any , title : string , newUrl : string ) : void {
111+ const oldUrl = this . url ;
112+
113+ const { pathname, search, state : parsedState , hash} = this . parseChanges ( state , newUrl ) ;
114+
115+ this . urlChanges [ 0 ] = { ...this . urlChanges [ 0 ] , pathname, search, state : parsedState } ;
116+ this . setHash ( hash , oldUrl ) ;
117+ }
118+
119+ pushState ( state : any , title : string , newUrl : string ) : void {
120+ const { pathname, search, state : parsedState , hash} = this . parseChanges ( state , newUrl ) ;
121+ this . urlChanges . unshift ( { ...this . urlChanges [ 0 ] , pathname, search, state : parsedState } ) ;
122+ }
123+
124+ forward ( ) : void { throw new Error ( 'Not implemented' ) ; }
125+
126+ back ( ) : void { this . urlChanges . shift ( ) ; }
127+
128+ // History API isn't available on server, therefore return undefined
129+ getState ( ) : unknown { return this . state ; }
130+ }
131+
132+ export function scheduleMicroTask ( cb : ( ) => any ) {
133+ Promise . resolve ( null ) . then ( cb ) ;
134+ }
0 commit comments