UNPKG

18.7 kBJavaScriptView Raw
1/**
2 * @license Angular v14.0.4
3 * (c) 2010-2022 Google LLC. https://angular.io/
4 * License: MIT
5 */
6
7import * as i0 from '@angular/core';
8import { EventEmitter, Injectable, InjectionToken, Inject, Optional } from '@angular/core';
9import { LocationStrategy } from '@angular/common';
10import { Subject } from 'rxjs';
11
12/**
13 * @license
14 * Copyright Google LLC All Rights Reserved.
15 *
16 * Use of this source code is governed by an MIT-style license that can be
17 * found in the LICENSE file at https://angular.io/license
18 */
19/**
20 * Joins two parts of a URL with a slash if needed.
21 *
22 * @param start URL string
23 * @param end URL string
24 *
25 *
26 * @returns The joined URL string.
27 */
28function joinWithSlash(start, end) {
29 if (start.length == 0) {
30 return end;
31 }
32 if (end.length == 0) {
33 return start;
34 }
35 let slashes = 0;
36 if (start.endsWith('/')) {
37 slashes++;
38 }
39 if (end.startsWith('/')) {
40 slashes++;
41 }
42 if (slashes == 2) {
43 return start + end.substring(1);
44 }
45 if (slashes == 1) {
46 return start + end;
47 }
48 return start + '/' + end;
49}
50/**
51 * Removes a trailing slash from a URL string if needed.
52 * Looks for the first occurrence of either `#`, `?`, or the end of the
53 * line as `/` characters and removes the trailing slash if one exists.
54 *
55 * @param url URL string.
56 *
57 * @returns The URL string, modified if needed.
58 */
59function stripTrailingSlash(url) {
60 const match = url.match(/#|\?|$/);
61 const pathEndIdx = match && match.index || url.length;
62 const droppedSlashIdx = pathEndIdx - (url[pathEndIdx - 1] === '/' ? 1 : 0);
63 return url.slice(0, droppedSlashIdx) + url.slice(pathEndIdx);
64}
65/**
66 * Normalizes URL parameters by prepending with `?` if needed.
67 *
68 * @param params String of URL parameters.
69 *
70 * @returns The normalized URL parameters string.
71 */
72function normalizeQueryParams(params) {
73 return params && params[0] !== '?' ? '?' + params : params;
74}
75
76/**
77 * A spy for {@link Location} that allows tests to fire simulated location events.
78 *
79 * @publicApi
80 */
81class SpyLocation {
82 constructor() {
83 this.urlChanges = [];
84 this._history = [new LocationState('', '', null)];
85 this._historyIndex = 0;
86 /** @internal */
87 this._subject = new EventEmitter();
88 /** @internal */
89 this._baseHref = '';
90 /** @internal */
91 this._locationStrategy = null;
92 /** @internal */
93 this._urlChangeListeners = [];
94 /** @internal */
95 this._urlChangeSubscription = null;
96 }
97 ngOnDestroy() {
98 this._urlChangeSubscription?.unsubscribe();
99 this._urlChangeListeners = [];
100 }
101 setInitialPath(url) {
102 this._history[this._historyIndex].path = url;
103 }
104 setBaseHref(url) {
105 this._baseHref = url;
106 }
107 path() {
108 return this._history[this._historyIndex].path;
109 }
110 getState() {
111 return this._history[this._historyIndex].state;
112 }
113 isCurrentPathEqualTo(path, query = '') {
114 const givenPath = path.endsWith('/') ? path.substring(0, path.length - 1) : path;
115 const currPath = this.path().endsWith('/') ? this.path().substring(0, this.path().length - 1) : this.path();
116 return currPath == givenPath + (query.length > 0 ? ('?' + query) : '');
117 }
118 simulateUrlPop(pathname) {
119 this._subject.emit({ 'url': pathname, 'pop': true, 'type': 'popstate' });
120 }
121 simulateHashChange(pathname) {
122 const path = this.prepareExternalUrl(pathname);
123 this.pushHistory(path, '', null);
124 this.urlChanges.push('hash: ' + pathname);
125 // the browser will automatically fire popstate event before each `hashchange` event, so we need
126 // to simulate it.
127 this._subject.emit({ 'url': pathname, 'pop': true, 'type': 'popstate' });
128 this._subject.emit({ 'url': pathname, 'pop': true, 'type': 'hashchange' });
129 }
130 prepareExternalUrl(url) {
131 if (url.length > 0 && !url.startsWith('/')) {
132 url = '/' + url;
133 }
134 return this._baseHref + url;
135 }
136 go(path, query = '', state = null) {
137 path = this.prepareExternalUrl(path);
138 this.pushHistory(path, query, state);
139 const locationState = this._history[this._historyIndex - 1];
140 if (locationState.path == path && locationState.query == query) {
141 return;
142 }
143 const url = path + (query.length > 0 ? ('?' + query) : '');
144 this.urlChanges.push(url);
145 this._notifyUrlChangeListeners(path + normalizeQueryParams(query), state);
146 }
147 replaceState(path, query = '', state = null) {
148 path = this.prepareExternalUrl(path);
149 const history = this._history[this._historyIndex];
150 if (history.path == path && history.query == query) {
151 return;
152 }
153 history.path = path;
154 history.query = query;
155 history.state = state;
156 const url = path + (query.length > 0 ? ('?' + query) : '');
157 this.urlChanges.push('replace: ' + url);
158 this._notifyUrlChangeListeners(path + normalizeQueryParams(query), state);
159 }
160 forward() {
161 if (this._historyIndex < (this._history.length - 1)) {
162 this._historyIndex++;
163 this._subject.emit({ 'url': this.path(), 'state': this.getState(), 'pop': true, 'type': 'popstate' });
164 }
165 }
166 back() {
167 if (this._historyIndex > 0) {
168 this._historyIndex--;
169 this._subject.emit({ 'url': this.path(), 'state': this.getState(), 'pop': true, 'type': 'popstate' });
170 }
171 }
172 historyGo(relativePosition = 0) {
173 const nextPageIndex = this._historyIndex + relativePosition;
174 if (nextPageIndex >= 0 && nextPageIndex < this._history.length) {
175 this._historyIndex = nextPageIndex;
176 this._subject.emit({ 'url': this.path(), 'state': this.getState(), 'pop': true, 'type': 'popstate' });
177 }
178 }
179 onUrlChange(fn) {
180 this._urlChangeListeners.push(fn);
181 if (!this._urlChangeSubscription) {
182 this._urlChangeSubscription = this.subscribe(v => {
183 this._notifyUrlChangeListeners(v.url, v.state);
184 });
185 }
186 return () => {
187 const fnIndex = this._urlChangeListeners.indexOf(fn);
188 this._urlChangeListeners.splice(fnIndex, 1);
189 if (this._urlChangeListeners.length === 0) {
190 this._urlChangeSubscription?.unsubscribe();
191 this._urlChangeSubscription = null;
192 }
193 };
194 }
195 /** @internal */
196 _notifyUrlChangeListeners(url = '', state) {
197 this._urlChangeListeners.forEach(fn => fn(url, state));
198 }
199 subscribe(onNext, onThrow, onReturn) {
200 return this._subject.subscribe({ next: onNext, error: onThrow, complete: onReturn });
201 }
202 normalize(url) {
203 return null;
204 }
205 pushHistory(path, query, state) {
206 if (this._historyIndex > 0) {
207 this._history.splice(this._historyIndex + 1);
208 }
209 this._history.push(new LocationState(path, query, state));
210 this._historyIndex = this._history.length - 1;
211 }
212}
213SpyLocation.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.0.4", ngImport: i0, type: SpyLocation, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
214SpyLocation.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "14.0.4", ngImport: i0, type: SpyLocation });
215i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.0.4", ngImport: i0, type: SpyLocation, decorators: [{
216 type: Injectable
217 }] });
218class LocationState {
219 constructor(path, query, state) {
220 this.path = path;
221 this.query = query;
222 this.state = state;
223 }
224}
225
226/**
227 * @license
228 * Copyright Google LLC All Rights Reserved.
229 *
230 * Use of this source code is governed by an MIT-style license that can be
231 * found in the LICENSE file at https://angular.io/license
232 */
233/**
234 * A mock implementation of {@link LocationStrategy} that allows tests to fire simulated
235 * location events.
236 *
237 * @publicApi
238 */
239class MockLocationStrategy extends LocationStrategy {
240 constructor() {
241 super();
242 this.internalBaseHref = '/';
243 this.internalPath = '/';
244 this.internalTitle = '';
245 this.urlChanges = [];
246 /** @internal */
247 this._subject = new EventEmitter();
248 this.stateChanges = [];
249 }
250 simulatePopState(url) {
251 this.internalPath = url;
252 this._subject.emit(new _MockPopStateEvent(this.path()));
253 }
254 path(includeHash = false) {
255 return this.internalPath;
256 }
257 prepareExternalUrl(internal) {
258 if (internal.startsWith('/') && this.internalBaseHref.endsWith('/')) {
259 return this.internalBaseHref + internal.substring(1);
260 }
261 return this.internalBaseHref + internal;
262 }
263 pushState(ctx, title, path, query) {
264 // Add state change to changes array
265 this.stateChanges.push(ctx);
266 this.internalTitle = title;
267 const url = path + (query.length > 0 ? ('?' + query) : '');
268 this.internalPath = url;
269 const externalUrl = this.prepareExternalUrl(url);
270 this.urlChanges.push(externalUrl);
271 }
272 replaceState(ctx, title, path, query) {
273 // Reset the last index of stateChanges to the ctx (state) object
274 this.stateChanges[(this.stateChanges.length || 1) - 1] = ctx;
275 this.internalTitle = title;
276 const url = path + (query.length > 0 ? ('?' + query) : '');
277 this.internalPath = url;
278 const externalUrl = this.prepareExternalUrl(url);
279 this.urlChanges.push('replace: ' + externalUrl);
280 }
281 onPopState(fn) {
282 this._subject.subscribe({ next: fn });
283 }
284 getBaseHref() {
285 return this.internalBaseHref;
286 }
287 back() {
288 if (this.urlChanges.length > 0) {
289 this.urlChanges.pop();
290 this.stateChanges.pop();
291 const nextUrl = this.urlChanges.length > 0 ? this.urlChanges[this.urlChanges.length - 1] : '';
292 this.simulatePopState(nextUrl);
293 }
294 }
295 forward() {
296 throw 'not implemented';
297 }
298 getState() {
299 return this.stateChanges[(this.stateChanges.length || 1) - 1];
300 }
301}
302MockLocationStrategy.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.0.4", ngImport: i0, type: MockLocationStrategy, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
303MockLocationStrategy.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "14.0.4", ngImport: i0, type: MockLocationStrategy });
304i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.0.4", ngImport: i0, type: MockLocationStrategy, decorators: [{
305 type: Injectable
306 }], ctorParameters: function () { return []; } });
307class _MockPopStateEvent {
308 constructor(newUrl) {
309 this.newUrl = newUrl;
310 this.pop = true;
311 this.type = 'popstate';
312 }
313}
314
315/**
316 * Parser from https://tools.ietf.org/html/rfc3986#appendix-B
317 * ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?
318 * 12 3 4 5 6 7 8 9
319 *
320 * Example: http://www.ics.uci.edu/pub/ietf/uri/#Related
321 *
322 * Results in:
323 *
324 * $1 = http:
325 * $2 = http
326 * $3 = //www.ics.uci.edu
327 * $4 = www.ics.uci.edu
328 * $5 = /pub/ietf/uri/
329 * $6 = <undefined>
330 * $7 = <undefined>
331 * $8 = #Related
332 * $9 = Related
333 */
334const urlParse = /^(([^:\/?#]+):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/;
335function parseUrl(urlStr, baseHref) {
336 const verifyProtocol = /^((http[s]?|ftp):\/\/)/;
337 let serverBase;
338 // URL class requires full URL. If the URL string doesn't start with protocol, we need to add
339 // an arbitrary base URL which can be removed afterward.
340 if (!verifyProtocol.test(urlStr)) {
341 serverBase = 'http://empty.com/';
342 }
343 let parsedUrl;
344 try {
345 parsedUrl = new URL(urlStr, serverBase);
346 }
347 catch (e) {
348 const result = urlParse.exec(serverBase || '' + urlStr);
349 if (!result) {
350 throw new Error(`Invalid URL: ${urlStr} with base: ${baseHref}`);
351 }
352 const hostSplit = result[4].split(':');
353 parsedUrl = {
354 protocol: result[1],
355 hostname: hostSplit[0],
356 port: hostSplit[1] || '',
357 pathname: result[5],
358 search: result[6],
359 hash: result[8],
360 };
361 }
362 if (parsedUrl.pathname && parsedUrl.pathname.indexOf(baseHref) === 0) {
363 parsedUrl.pathname = parsedUrl.pathname.substring(baseHref.length);
364 }
365 return {
366 hostname: !serverBase && parsedUrl.hostname || '',
367 protocol: !serverBase && parsedUrl.protocol || '',
368 port: !serverBase && parsedUrl.port || '',
369 pathname: parsedUrl.pathname || '/',
370 search: parsedUrl.search || '',
371 hash: parsedUrl.hash || '',
372 };
373}
374/**
375 * Provider for mock platform location config
376 *
377 * @publicApi
378 */
379const MOCK_PLATFORM_LOCATION_CONFIG = new InjectionToken('MOCK_PLATFORM_LOCATION_CONFIG');
380/**
381 * Mock implementation of URL state.
382 *
383 * @publicApi
384 */
385class MockPlatformLocation {
386 constructor(config) {
387 this.baseHref = '';
388 this.hashUpdate = new Subject();
389 this.urlChangeIndex = 0;
390 this.urlChanges = [{ hostname: '', protocol: '', port: '', pathname: '/', search: '', hash: '', state: null }];
391 if (config) {
392 this.baseHref = config.appBaseHref || '';
393 const parsedChanges = this.parseChanges(null, config.startUrl || 'http://_empty_/', this.baseHref);
394 this.urlChanges[0] = { ...parsedChanges };
395 }
396 }
397 get hostname() {
398 return this.urlChanges[this.urlChangeIndex].hostname;
399 }
400 get protocol() {
401 return this.urlChanges[this.urlChangeIndex].protocol;
402 }
403 get port() {
404 return this.urlChanges[this.urlChangeIndex].port;
405 }
406 get pathname() {
407 return this.urlChanges[this.urlChangeIndex].pathname;
408 }
409 get search() {
410 return this.urlChanges[this.urlChangeIndex].search;
411 }
412 get hash() {
413 return this.urlChanges[this.urlChangeIndex].hash;
414 }
415 get state() {
416 return this.urlChanges[this.urlChangeIndex].state;
417 }
418 getBaseHrefFromDOM() {
419 return this.baseHref;
420 }
421 onPopState(fn) {
422 // No-op: a state stack is not implemented, so
423 // no events will ever come.
424 return () => { };
425 }
426 onHashChange(fn) {
427 const subscription = this.hashUpdate.subscribe(fn);
428 return () => subscription.unsubscribe();
429 }
430 get href() {
431 let url = `${this.protocol}//${this.hostname}${this.port ? ':' + this.port : ''}`;
432 url += `${this.pathname === '/' ? '' : this.pathname}${this.search}${this.hash}`;
433 return url;
434 }
435 get url() {
436 return `${this.pathname}${this.search}${this.hash}`;
437 }
438 parseChanges(state, url, baseHref = '') {
439 // When the `history.state` value is stored, it is always copied.
440 state = JSON.parse(JSON.stringify(state));
441 return { ...parseUrl(url, baseHref), state };
442 }
443 replaceState(state, title, newUrl) {
444 const { pathname, search, state: parsedState, hash } = this.parseChanges(state, newUrl);
445 this.urlChanges[this.urlChangeIndex] =
446 { ...this.urlChanges[this.urlChangeIndex], pathname, search, hash, state: parsedState };
447 }
448 pushState(state, title, newUrl) {
449 const { pathname, search, state: parsedState, hash } = this.parseChanges(state, newUrl);
450 if (this.urlChangeIndex > 0) {
451 this.urlChanges.splice(this.urlChangeIndex + 1);
452 }
453 this.urlChanges.push({ ...this.urlChanges[this.urlChangeIndex], pathname, search, hash, state: parsedState });
454 this.urlChangeIndex = this.urlChanges.length - 1;
455 }
456 forward() {
457 const oldUrl = this.url;
458 const oldHash = this.hash;
459 if (this.urlChangeIndex < this.urlChanges.length) {
460 this.urlChangeIndex++;
461 }
462 this.scheduleHashUpdate(oldHash, oldUrl);
463 }
464 back() {
465 const oldUrl = this.url;
466 const oldHash = this.hash;
467 if (this.urlChangeIndex > 0) {
468 this.urlChangeIndex--;
469 }
470 this.scheduleHashUpdate(oldHash, oldUrl);
471 }
472 historyGo(relativePosition = 0) {
473 const oldUrl = this.url;
474 const oldHash = this.hash;
475 const nextPageIndex = this.urlChangeIndex + relativePosition;
476 if (nextPageIndex >= 0 && nextPageIndex < this.urlChanges.length) {
477 this.urlChangeIndex = nextPageIndex;
478 }
479 this.scheduleHashUpdate(oldHash, oldUrl);
480 }
481 getState() {
482 return this.state;
483 }
484 scheduleHashUpdate(oldHash, oldUrl) {
485 if (oldHash !== this.hash) {
486 scheduleMicroTask(() => this.hashUpdate.next({ type: 'hashchange', state: null, oldUrl, newUrl: this.url }));
487 }
488 }
489}
490MockPlatformLocation.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.0.4", ngImport: i0, type: MockPlatformLocation, deps: [{ token: MOCK_PLATFORM_LOCATION_CONFIG, optional: true }], target: i0.ɵɵFactoryTarget.Injectable });
491MockPlatformLocation.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "14.0.4", ngImport: i0, type: MockPlatformLocation });
492i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.0.4", ngImport: i0, type: MockPlatformLocation, decorators: [{
493 type: Injectable
494 }], ctorParameters: function () { return [{ type: undefined, decorators: [{
495 type: Inject,
496 args: [MOCK_PLATFORM_LOCATION_CONFIG]
497 }, {
498 type: Optional
499 }] }]; } });
500function scheduleMicroTask(cb) {
501 Promise.resolve(null).then(cb);
502}
503
504/**
505 * @license
506 * Copyright Google LLC All Rights Reserved.
507 *
508 * Use of this source code is governed by an MIT-style license that can be
509 * found in the LICENSE file at https://angular.io/license
510 */
511
512/**
513 * @license
514 * Copyright Google LLC All Rights Reserved.
515 *
516 * Use of this source code is governed by an MIT-style license that can be
517 * found in the LICENSE file at https://angular.io/license
518 */
519// This file only reexports content of the `src` folder. Keep it that way.
520
521/**
522 * @license
523 * Copyright Google LLC All Rights Reserved.
524 *
525 * Use of this source code is governed by an MIT-style license that can be
526 * found in the LICENSE file at https://angular.io/license
527 */
528
529/**
530 * Generated bundle index. Do not edit.
531 */
532
533export { MOCK_PLATFORM_LOCATION_CONFIG, MockLocationStrategy, MockPlatformLocation, SpyLocation };
534//# sourceMappingURL=testing.mjs.map