package.fesm2022.testing.mjs Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of common Show documentation
Show all versions of common Show documentation
Angular - commonly needed directives and services
/**
* @license Angular v18.2.6
* (c) 2010-2024 Google LLC. https://angular.io/
* License: MIT
*/
import { ɵPlatformNavigation, DOCUMENT, PlatformLocation, ɵnormalizeQueryParams, LocationStrategy, Location } from '@angular/common';
import * as i0 from '@angular/core';
import { Injectable, InjectionToken, Inject, Optional, inject, EventEmitter } from '@angular/core';
import { Subject } from 'rxjs';
/**
* This class wraps the platform Navigation API which allows server-specific and test
* implementations.
*/
class PlatformNavigation {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.6", ngImport: i0, type: PlatformNavigation, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.6", ngImport: i0, type: PlatformNavigation, providedIn: 'platform', useFactory: () => window.navigation }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.6", ngImport: i0, type: PlatformNavigation, decorators: [{
type: Injectable,
args: [{ providedIn: 'platform', useFactory: () => window.navigation }]
}] });
/**
* Fake implementation of user agent history and navigation behavior. This is a
* high-fidelity implementation of browser behavior that attempts to emulate
* things like traversal delay.
*/
class FakeNavigation {
/** Equivalent to `navigation.currentEntry`. */
get currentEntry() {
return this.entriesArr[this.currentEntryIndex];
}
get canGoBack() {
return this.currentEntryIndex > 0;
}
get canGoForward() {
return this.currentEntryIndex < this.entriesArr.length - 1;
}
constructor(window, startURL) {
this.window = window;
/**
* The fake implementation of an entries array. Only same-document entries
* allowed.
*/
this.entriesArr = [];
/**
* The current active entry index into `entriesArr`.
*/
this.currentEntryIndex = 0;
/**
* The current navigate event.
*/
this.navigateEvent = undefined;
/**
* A Map of pending traversals, so that traversals to the same entry can be
* re-used.
*/
this.traversalQueue = new Map();
/**
* A Promise that resolves when the previous traversals have finished. Used to
* simulate the cross-process communication necessary for traversals.
*/
this.nextTraversal = Promise.resolve();
/**
* A prospective current active entry index, which includes unresolved
* traversals. Used by `go` to determine where navigations are intended to go.
*/
this.prospectiveEntryIndex = 0;
/**
* A test-only option to make traversals synchronous, rather than emulate
* cross-process communication.
*/
this.synchronousTraversals = false;
/** Whether to allow a call to setInitialEntryForTesting. */
this.canSetInitialEntry = true;
/** `EventTarget` to dispatch events. */
this.eventTarget = this.window.document.createElement('div');
/** The next unique id for created entries. Replace recreates this id. */
this.nextId = 0;
/** The next unique key for created entries. Replace inherits this id. */
this.nextKey = 0;
/** Whether this fake is disposed. */
this.disposed = false;
// First entry.
this.setInitialEntryForTesting(startURL);
}
/**
* Sets the initial entry.
*/
setInitialEntryForTesting(url, options = { historyState: null }) {
if (!this.canSetInitialEntry) {
throw new Error('setInitialEntryForTesting can only be called before any ' + 'navigation has occurred');
}
const currentInitialEntry = this.entriesArr[0];
this.entriesArr[0] = new FakeNavigationHistoryEntry(new URL(url).toString(), {
index: 0,
key: currentInitialEntry?.key ?? String(this.nextKey++),
id: currentInitialEntry?.id ?? String(this.nextId++),
sameDocument: true,
historyState: options?.historyState,
state: options.state,
});
}
/** Returns whether the initial entry is still eligible to be set. */
canSetInitialEntryForTesting() {
return this.canSetInitialEntry;
}
/**
* Sets whether to emulate traversals as synchronous rather than
* asynchronous.
*/
setSynchronousTraversalsForTesting(synchronousTraversals) {
this.synchronousTraversals = synchronousTraversals;
}
/** Equivalent to `navigation.entries()`. */
entries() {
return this.entriesArr.slice();
}
/** Equivalent to `navigation.navigate()`. */
navigate(url, options) {
const fromUrl = new URL(this.currentEntry.url);
const toUrl = new URL(url, this.currentEntry.url);
let navigationType;
if (!options?.history || options.history === 'auto') {
// Auto defaults to push, but if the URLs are the same, is a replace.
if (fromUrl.toString() === toUrl.toString()) {
navigationType = 'replace';
}
else {
navigationType = 'push';
}
}
else {
navigationType = options.history;
}
const hashChange = isHashChange(fromUrl, toUrl);
const destination = new FakeNavigationDestination({
url: toUrl.toString(),
state: options?.state,
sameDocument: hashChange,
historyState: null,
});
const result = new InternalNavigationResult();
this.userAgentNavigate(destination, result, {
navigationType,
cancelable: true,
canIntercept: true,
// Always false for navigate().
userInitiated: false,
hashChange,
info: options?.info,
});
return {
committed: result.committed,
finished: result.finished,
};
}
/** Equivalent to `history.pushState()`. */
pushState(data, title, url) {
this.pushOrReplaceState('push', data, title, url);
}
/** Equivalent to `history.replaceState()`. */
replaceState(data, title, url) {
this.pushOrReplaceState('replace', data, title, url);
}
pushOrReplaceState(navigationType, data, _title, url) {
const fromUrl = new URL(this.currentEntry.url);
const toUrl = url ? new URL(url, this.currentEntry.url) : fromUrl;
const hashChange = isHashChange(fromUrl, toUrl);
const destination = new FakeNavigationDestination({
url: toUrl.toString(),
sameDocument: true,
historyState: data,
});
const result = new InternalNavigationResult();
this.userAgentNavigate(destination, result, {
navigationType,
cancelable: true,
canIntercept: true,
// Always false for pushState() or replaceState().
userInitiated: false,
hashChange,
skipPopState: true,
});
}
/** Equivalent to `navigation.traverseTo()`. */
traverseTo(key, options) {
const fromUrl = new URL(this.currentEntry.url);
const entry = this.findEntry(key);
if (!entry) {
const domException = new DOMException('Invalid key', 'InvalidStateError');
const committed = Promise.reject(domException);
const finished = Promise.reject(domException);
committed.catch(() => { });
finished.catch(() => { });
return {
committed,
finished,
};
}
if (entry === this.currentEntry) {
return {
committed: Promise.resolve(this.currentEntry),
finished: Promise.resolve(this.currentEntry),
};
}
if (this.traversalQueue.has(entry.key)) {
const existingResult = this.traversalQueue.get(entry.key);
return {
committed: existingResult.committed,
finished: existingResult.finished,
};
}
const hashChange = isHashChange(fromUrl, new URL(entry.url, this.currentEntry.url));
const destination = new FakeNavigationDestination({
url: entry.url,
state: entry.getState(),
historyState: entry.getHistoryState(),
key: entry.key,
id: entry.id,
index: entry.index,
sameDocument: entry.sameDocument,
});
this.prospectiveEntryIndex = entry.index;
const result = new InternalNavigationResult();
this.traversalQueue.set(entry.key, result);
this.runTraversal(() => {
this.traversalQueue.delete(entry.key);
this.userAgentNavigate(destination, result, {
navigationType: 'traverse',
cancelable: true,
canIntercept: true,
// Always false for traverseTo().
userInitiated: false,
hashChange,
info: options?.info,
});
});
return {
committed: result.committed,
finished: result.finished,
};
}
/** Equivalent to `navigation.back()`. */
back(options) {
if (this.currentEntryIndex === 0) {
const domException = new DOMException('Cannot go back', 'InvalidStateError');
const committed = Promise.reject(domException);
const finished = Promise.reject(domException);
committed.catch(() => { });
finished.catch(() => { });
return {
committed,
finished,
};
}
const entry = this.entriesArr[this.currentEntryIndex - 1];
return this.traverseTo(entry.key, options);
}
/** Equivalent to `navigation.forward()`. */
forward(options) {
if (this.currentEntryIndex === this.entriesArr.length - 1) {
const domException = new DOMException('Cannot go forward', 'InvalidStateError');
const committed = Promise.reject(domException);
const finished = Promise.reject(domException);
committed.catch(() => { });
finished.catch(() => { });
return {
committed,
finished,
};
}
const entry = this.entriesArr[this.currentEntryIndex + 1];
return this.traverseTo(entry.key, options);
}
/**
* Equivalent to `history.go()`.
* Note that this method does not actually work precisely to how Chrome
* does, instead choosing a simpler model with less unexpected behavior.
* Chrome has a few edge case optimizations, for instance with repeated
* `back(); forward()` chains it collapses certain traversals.
*/
go(direction) {
const targetIndex = this.prospectiveEntryIndex + direction;
if (targetIndex >= this.entriesArr.length || targetIndex < 0) {
return;
}
this.prospectiveEntryIndex = targetIndex;
this.runTraversal(() => {
// Check again that destination is in the entries array.
if (targetIndex >= this.entriesArr.length || targetIndex < 0) {
return;
}
const fromUrl = new URL(this.currentEntry.url);
const entry = this.entriesArr[targetIndex];
const hashChange = isHashChange(fromUrl, new URL(entry.url, this.currentEntry.url));
const destination = new FakeNavigationDestination({
url: entry.url,
state: entry.getState(),
historyState: entry.getHistoryState(),
key: entry.key,
id: entry.id,
index: entry.index,
sameDocument: entry.sameDocument,
});
const result = new InternalNavigationResult();
this.userAgentNavigate(destination, result, {
navigationType: 'traverse',
cancelable: true,
canIntercept: true,
// Always false for go().
userInitiated: false,
hashChange,
});
});
}
/** Runs a traversal synchronously or asynchronously */
runTraversal(traversal) {
if (this.synchronousTraversals) {
traversal();
return;
}
// Each traversal occupies a single timeout resolution.
// This means that Promises added to commit and finish should resolve
// before the next traversal.
this.nextTraversal = this.nextTraversal.then(() => {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
traversal();
});
});
});
}
/** Equivalent to `navigation.addEventListener()`. */
addEventListener(type, callback, options) {
this.eventTarget.addEventListener(type, callback, options);
}
/** Equivalent to `navigation.removeEventListener()`. */
removeEventListener(type, callback, options) {
this.eventTarget.removeEventListener(type, callback, options);
}
/** Equivalent to `navigation.dispatchEvent()` */
dispatchEvent(event) {
return this.eventTarget.dispatchEvent(event);
}
/** Cleans up resources. */
dispose() {
// Recreate eventTarget to release current listeners.
// `document.createElement` because NodeJS `EventTarget` is incompatible with Domino's `Event`.
this.eventTarget = this.window.document.createElement('div');
this.disposed = true;
}
/** Returns whether this fake is disposed. */
isDisposed() {
return this.disposed;
}
/** Implementation for all navigations and traversals. */
userAgentNavigate(destination, result, options) {
// The first navigation should disallow any future calls to set the initial
// entry.
this.canSetInitialEntry = false;
if (this.navigateEvent) {
this.navigateEvent.cancel(new DOMException('Navigation was aborted', 'AbortError'));
this.navigateEvent = undefined;
}
const navigateEvent = createFakeNavigateEvent({
navigationType: options.navigationType,
cancelable: options.cancelable,
canIntercept: options.canIntercept,
userInitiated: options.userInitiated,
hashChange: options.hashChange,
signal: result.signal,
destination,
info: options.info,
sameDocument: destination.sameDocument,
skipPopState: options.skipPopState,
result,
userAgentCommit: () => {
this.userAgentCommit();
},
});
this.navigateEvent = navigateEvent;
this.eventTarget.dispatchEvent(navigateEvent);
navigateEvent.dispatchedNavigateEvent();
if (navigateEvent.commitOption === 'immediate') {
navigateEvent.commit(/* internal= */ true);
}
}
/** Implementation to commit a navigation. */
userAgentCommit() {
if (!this.navigateEvent) {
return;
}
const from = this.currentEntry;
if (!this.navigateEvent.sameDocument) {
const error = new Error('Cannot navigate to a non-same-document URL.');
this.navigateEvent.cancel(error);
throw error;
}
if (this.navigateEvent.navigationType === 'push' ||
this.navigateEvent.navigationType === 'replace') {
this.userAgentPushOrReplace(this.navigateEvent.destination, {
navigationType: this.navigateEvent.navigationType,
});
}
else if (this.navigateEvent.navigationType === 'traverse') {
this.userAgentTraverse(this.navigateEvent.destination);
}
this.navigateEvent.userAgentNavigated(this.currentEntry);
const currentEntryChangeEvent = createFakeNavigationCurrentEntryChangeEvent({
from,
navigationType: this.navigateEvent.navigationType,
});
this.eventTarget.dispatchEvent(currentEntryChangeEvent);
if (!this.navigateEvent.skipPopState) {
const popStateEvent = createPopStateEvent({
state: this.navigateEvent.destination.getHistoryState(),
});
this.window.dispatchEvent(popStateEvent);
}
}
/** Implementation for a push or replace navigation. */
userAgentPushOrReplace(destination, { navigationType }) {
if (navigationType === 'push') {
this.currentEntryIndex++;
this.prospectiveEntryIndex = this.currentEntryIndex;
}
const index = this.currentEntryIndex;
const key = navigationType === 'push' ? String(this.nextKey++) : this.currentEntry.key;
const entry = new FakeNavigationHistoryEntry(destination.url, {
id: String(this.nextId++),
key,
index,
sameDocument: true,
state: destination.getState(),
historyState: destination.getHistoryState(),
});
if (navigationType === 'push') {
this.entriesArr.splice(index, Infinity, entry);
}
else {
this.entriesArr[index] = entry;
}
}
/** Implementation for a traverse navigation. */
userAgentTraverse(destination) {
this.currentEntryIndex = destination.index;
}
/** Utility method for finding entries with the given `key`. */
findEntry(key) {
for (const entry of this.entriesArr) {
if (entry.key === key)
return entry;
}
return undefined;
}
set onnavigate(_handler) {
throw new Error('unimplemented');
}
get onnavigate() {
throw new Error('unimplemented');
}
set oncurrententrychange(_handler) {
throw new Error('unimplemented');
}
get oncurrententrychange() {
throw new Error('unimplemented');
}
set onnavigatesuccess(_handler) {
throw new Error('unimplemented');
}
get onnavigatesuccess() {
throw new Error('unimplemented');
}
set onnavigateerror(_handler) {
throw new Error('unimplemented');
}
get onnavigateerror() {
throw new Error('unimplemented');
}
get transition() {
throw new Error('unimplemented');
}
updateCurrentEntry(_options) {
throw new Error('unimplemented');
}
reload(_options) {
throw new Error('unimplemented');
}
}
/**
* Fake equivalent of `NavigationHistoryEntry`.
*/
class FakeNavigationHistoryEntry {
constructor(url, { id, key, index, sameDocument, state, historyState, }) {
this.url = url;
// tslint:disable-next-line:no-any
this.ondispose = null;
this.id = id;
this.key = key;
this.index = index;
this.sameDocument = sameDocument;
this.state = state;
this.historyState = historyState;
}
getState() {
// Budget copy.
return this.state ? JSON.parse(JSON.stringify(this.state)) : this.state;
}
getHistoryState() {
// Budget copy.
return this.historyState ? JSON.parse(JSON.stringify(this.historyState)) : this.historyState;
}
addEventListener(type, callback, options) {
throw new Error('unimplemented');
}
removeEventListener(type, callback, options) {
throw new Error('unimplemented');
}
dispatchEvent(event) {
throw new Error('unimplemented');
}
}
/**
* Create a fake equivalent of `NavigateEvent`. This is not a class because ES5
* transpiled JavaScript cannot extend native Event.
*/
function createFakeNavigateEvent({ cancelable, canIntercept, userInitiated, hashChange, navigationType, signal, destination, info, sameDocument, skipPopState, result, userAgentCommit, }) {
const event = new Event('navigate', { bubbles: false, cancelable });
event.canIntercept = canIntercept;
event.userInitiated = userInitiated;
event.hashChange = hashChange;
event.navigationType = navigationType;
event.signal = signal;
event.destination = destination;
event.info = info;
event.downloadRequest = null;
event.formData = null;
event.sameDocument = sameDocument;
event.skipPopState = skipPopState;
event.commitOption = 'immediate';
let handlerFinished = undefined;
let interceptCalled = false;
let dispatchedNavigateEvent = false;
let commitCalled = false;
event.intercept = function (options) {
interceptCalled = true;
event.sameDocument = true;
const handler = options?.handler;
if (handler) {
handlerFinished = handler();
}
if (options?.commit) {
event.commitOption = options.commit;
}
if (options?.focusReset !== undefined || options?.scroll !== undefined) {
throw new Error('unimplemented');
}
};
event.scroll = function () {
throw new Error('unimplemented');
};
event.commit = function (internal = false) {
if (!internal && !interceptCalled) {
throw new DOMException(`Failed to execute 'commit' on 'NavigateEvent': intercept() must be ` +
`called before commit().`, 'InvalidStateError');
}
if (!dispatchedNavigateEvent) {
throw new DOMException(`Failed to execute 'commit' on 'NavigateEvent': commit() may not be ` +
`called during event dispatch.`, 'InvalidStateError');
}
if (commitCalled) {
throw new DOMException(`Failed to execute 'commit' on 'NavigateEvent': commit() already ` + `called.`, 'InvalidStateError');
}
commitCalled = true;
userAgentCommit();
};
// Internal only.
event.cancel = function (reason) {
result.committedReject(reason);
result.finishedReject(reason);
};
// Internal only.
event.dispatchedNavigateEvent = function () {
dispatchedNavigateEvent = true;
if (event.commitOption === 'after-transition') {
// If handler finishes before commit, call commit.
handlerFinished?.then(() => {
if (!commitCalled) {
event.commit(/* internal */ true);
}
}, () => { });
}
Promise.all([result.committed, handlerFinished]).then(([entry]) => {
result.finishedResolve(entry);
}, (reason) => {
result.finishedReject(reason);
});
};
// Internal only.
event.userAgentNavigated = function (entry) {
result.committedResolve(entry);
};
return event;
}
/**
* Create a fake equivalent of `NavigationCurrentEntryChange`. This does not use
* a class because ES5 transpiled JavaScript cannot extend native Event.
*/
function createFakeNavigationCurrentEntryChangeEvent({ from, navigationType, }) {
const event = new Event('currententrychange', {
bubbles: false,
cancelable: false,
});
event.from = from;
event.navigationType = navigationType;
return event;
}
/**
* Create a fake equivalent of `PopStateEvent`. This does not use a class
* because ES5 transpiled JavaScript cannot extend native Event.
*/
function createPopStateEvent({ state }) {
const event = new Event('popstate', {
bubbles: false,
cancelable: false,
});
event.state = state;
return event;
}
/**
* Fake equivalent of `NavigationDestination`.
*/
class FakeNavigationDestination {
constructor({ url, sameDocument, historyState, state, key = null, id = null, index = -1, }) {
this.url = url;
this.sameDocument = sameDocument;
this.state = state;
this.historyState = historyState;
this.key = key;
this.id = id;
this.index = index;
}
getState() {
return this.state;
}
getHistoryState() {
return this.historyState;
}
}
/** Utility function to determine whether two UrlLike have the same hash. */
function isHashChange(from, to) {
return (to.hash !== from.hash &&
to.hostname === from.hostname &&
to.pathname === from.pathname &&
to.search === from.search);
}
/** Internal utility class for representing the result of a navigation. */
class InternalNavigationResult {
get signal() {
return this.abortController.signal;
}
constructor() {
this.abortController = new AbortController();
this.committed = new Promise((resolve, reject) => {
this.committedResolve = resolve;
this.committedReject = reject;
});
this.finished = new Promise(async (resolve, reject) => {
this.finishedResolve = resolve;
this.finishedReject = (reason) => {
reject(reason);
this.abortController.abort(reason);
};
});
// All rejections are handled.
this.committed.catch(() => { });
this.finished.catch(() => { });
}
}
/**
* Parser from https://tools.ietf.org/html/rfc3986#appendix-B
* ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?
* 12 3 4 5 6 7 8 9
*
* Example: http://www.ics.uci.edu/pub/ietf/uri/#Related
*
* Results in:
*
* $1 = http:
* $2 = http
* $3 = //www.ics.uci.edu
* $4 = www.ics.uci.edu
* $5 = /pub/ietf/uri/
* $6 =
* $7 =
* $8 = #Related
* $9 = Related
*/
const urlParse = /^(([^:\/?#]+):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/;
function parseUrl(urlStr, baseHref) {
const verifyProtocol = /^((http[s]?|ftp):\/\/)/;
let serverBase;
// URL class requires full URL. If the URL string doesn't start with protocol, we need to add
// an arbitrary base URL which can be removed afterward.
if (!verifyProtocol.test(urlStr)) {
serverBase = 'http://empty.com/';
}
let parsedUrl;
try {
parsedUrl = new URL(urlStr, serverBase);
}
catch (e) {
const result = urlParse.exec(serverBase || '' + urlStr);
if (!result) {
throw new Error(`Invalid URL: ${urlStr} with base: ${baseHref}`);
}
const hostSplit = result[4].split(':');
parsedUrl = {
protocol: result[1],
hostname: hostSplit[0],
port: hostSplit[1] || '',
pathname: result[5],
search: result[6],
hash: result[8],
};
}
if (parsedUrl.pathname && parsedUrl.pathname.indexOf(baseHref) === 0) {
parsedUrl.pathname = parsedUrl.pathname.substring(baseHref.length);
}
return {
hostname: (!serverBase && parsedUrl.hostname) || '',
protocol: (!serverBase && parsedUrl.protocol) || '',
port: (!serverBase && parsedUrl.port) || '',
pathname: parsedUrl.pathname || '/',
search: parsedUrl.search || '',
hash: parsedUrl.hash || '',
};
}
/**
* Provider for mock platform location config
*
* @publicApi
*/
const MOCK_PLATFORM_LOCATION_CONFIG = new InjectionToken('MOCK_PLATFORM_LOCATION_CONFIG');
/**
* Mock implementation of URL state.
*
* @publicApi
*/
class MockPlatformLocation {
constructor(config) {
this.baseHref = '';
this.hashUpdate = new Subject();
this.popStateSubject = new Subject();
this.urlChangeIndex = 0;
this.urlChanges = [{ hostname: '', protocol: '', port: '', pathname: '/', search: '', hash: '', state: null }];
if (config) {
this.baseHref = config.appBaseHref || '';
const parsedChanges = this.parseChanges(null, config.startUrl || 'http://_empty_/', this.baseHref);
this.urlChanges[0] = { ...parsedChanges };
}
}
get hostname() {
return this.urlChanges[this.urlChangeIndex].hostname;
}
get protocol() {
return this.urlChanges[this.urlChangeIndex].protocol;
}
get port() {
return this.urlChanges[this.urlChangeIndex].port;
}
get pathname() {
return this.urlChanges[this.urlChangeIndex].pathname;
}
get search() {
return this.urlChanges[this.urlChangeIndex].search;
}
get hash() {
return this.urlChanges[this.urlChangeIndex].hash;
}
get state() {
return this.urlChanges[this.urlChangeIndex].state;
}
getBaseHrefFromDOM() {
return this.baseHref;
}
onPopState(fn) {
const subscription = this.popStateSubject.subscribe(fn);
return () => subscription.unsubscribe();
}
onHashChange(fn) {
const subscription = this.hashUpdate.subscribe(fn);
return () => subscription.unsubscribe();
}
get href() {
let url = `${this.protocol}//${this.hostname}${this.port ? ':' + this.port : ''}`;
url += `${this.pathname === '/' ? '' : this.pathname}${this.search}${this.hash}`;
return url;
}
get url() {
return `${this.pathname}${this.search}${this.hash}`;
}
parseChanges(state, url, baseHref = '') {
// When the `history.state` value is stored, it is always copied.
state = JSON.parse(JSON.stringify(state));
return { ...parseUrl(url, baseHref), state };
}
replaceState(state, title, newUrl) {
const { pathname, search, state: parsedState, hash } = this.parseChanges(state, newUrl);
this.urlChanges[this.urlChangeIndex] = {
...this.urlChanges[this.urlChangeIndex],
pathname,
search,
hash,
state: parsedState,
};
}
pushState(state, title, newUrl) {
const { pathname, search, state: parsedState, hash } = this.parseChanges(state, newUrl);
if (this.urlChangeIndex > 0) {
this.urlChanges.splice(this.urlChangeIndex + 1);
}
this.urlChanges.push({
...this.urlChanges[this.urlChangeIndex],
pathname,
search,
hash,
state: parsedState,
});
this.urlChangeIndex = this.urlChanges.length - 1;
}
forward() {
const oldUrl = this.url;
const oldHash = this.hash;
if (this.urlChangeIndex < this.urlChanges.length) {
this.urlChangeIndex++;
}
this.emitEvents(oldHash, oldUrl);
}
back() {
const oldUrl = this.url;
const oldHash = this.hash;
if (this.urlChangeIndex > 0) {
this.urlChangeIndex--;
}
this.emitEvents(oldHash, oldUrl);
}
historyGo(relativePosition = 0) {
const oldUrl = this.url;
const oldHash = this.hash;
const nextPageIndex = this.urlChangeIndex + relativePosition;
if (nextPageIndex >= 0 && nextPageIndex < this.urlChanges.length) {
this.urlChangeIndex = nextPageIndex;
}
this.emitEvents(oldHash, oldUrl);
}
getState() {
return this.state;
}
/**
* Browsers are inconsistent in when they fire events and perform the state updates
* The most easiest thing to do in our mock is synchronous and that happens to match
* Firefox and Chrome, at least somewhat closely
*
* https://github.com/WICG/navigation-api#watching-for-navigations
* https://docs.google.com/document/d/1Pdve-DJ1JCGilj9Yqf5HxRJyBKSel5owgOvUJqTauwU/edit#heading=h.3ye4v71wsz94
* popstate is always sent before hashchange:
* https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event#when_popstate_is_sent
*/
emitEvents(oldHash, oldUrl) {
this.popStateSubject.next({
type: 'popstate',
state: this.getState(),
oldUrl,
newUrl: this.url,
});
if (oldHash !== this.hash) {
this.hashUpdate.next({
type: 'hashchange',
state: null,
oldUrl,
newUrl: this.url,
});
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.6", ngImport: i0, type: MockPlatformLocation, deps: [{ token: MOCK_PLATFORM_LOCATION_CONFIG, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.6", ngImport: i0, type: MockPlatformLocation }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.6", ngImport: i0, type: MockPlatformLocation, decorators: [{
type: Injectable
}], ctorParameters: () => [{ type: undefined, decorators: [{
type: Inject,
args: [MOCK_PLATFORM_LOCATION_CONFIG]
}, {
type: Optional
}] }] });
/**
* Mock implementation of URL state.
*/
class FakeNavigationPlatformLocation {
constructor() {
this._platformNavigation = inject(ɵPlatformNavigation);
this.window = inject(DOCUMENT).defaultView;
this.config = inject(MOCK_PLATFORM_LOCATION_CONFIG, { optional: true });
if (!(this._platformNavigation instanceof FakeNavigation)) {
throw new Error('FakePlatformNavigation cannot be used without FakeNavigation. Use ' +
'`provideFakeNavigation` to have all these services provided together.');
}
}
getBaseHrefFromDOM() {
return this.config?.appBaseHref ?? '';
}
onPopState(fn) {
this.window.addEventListener('popstate', fn);
return () => this.window.removeEventListener('popstate', fn);
}
onHashChange(fn) {
this.window.addEventListener('hashchange', fn);
return () => this.window.removeEventListener('hashchange', fn);
}
get href() {
return this._platformNavigation.currentEntry.url;
}
get protocol() {
return new URL(this._platformNavigation.currentEntry.url).protocol;
}
get hostname() {
return new URL(this._platformNavigation.currentEntry.url).hostname;
}
get port() {
return new URL(this._platformNavigation.currentEntry.url).port;
}
get pathname() {
return new URL(this._platformNavigation.currentEntry.url).pathname;
}
get search() {
return new URL(this._platformNavigation.currentEntry.url).search;
}
get hash() {
return new URL(this._platformNavigation.currentEntry.url).hash;
}
pushState(state, title, url) {
this._platformNavigation.pushState(state, title, url);
}
replaceState(state, title, url) {
this._platformNavigation.replaceState(state, title, url);
}
forward() {
this._platformNavigation.forward();
}
back() {
this._platformNavigation.back();
}
historyGo(relativePosition = 0) {
this._platformNavigation.go(relativePosition);
}
getState() {
return this._platformNavigation.currentEntry.getHistoryState();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.6", ngImport: i0, type: FakeNavigationPlatformLocation, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.6", ngImport: i0, type: FakeNavigationPlatformLocation }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.6", ngImport: i0, type: FakeNavigationPlatformLocation, decorators: [{
type: Injectable
}], ctorParameters: () => [] });
/**
* Return a provider for the `FakeNavigation` in place of the real Navigation API.
*/
function provideFakePlatformNavigation() {
return [
{
provide: PlatformNavigation,
useFactory: () => {
const config = inject(MOCK_PLATFORM_LOCATION_CONFIG, { optional: true });
return new FakeNavigation(inject(DOCUMENT).defaultView, config?.startUrl ?? 'http://_empty_/');
},
},
{ provide: PlatformLocation, useClass: FakeNavigationPlatformLocation },
];
}
/**
* A spy for {@link Location} that allows tests to fire simulated location events.
*
* @publicApi
*/
class SpyLocation {
constructor() {
this.urlChanges = [];
this._history = [new LocationState('', '', null)];
this._historyIndex = 0;
/** @internal */
this._subject = new EventEmitter();
/** @internal */
this._basePath = '';
/** @internal */
this._locationStrategy = null;
/** @internal */
this._urlChangeListeners = [];
/** @internal */
this._urlChangeSubscription = null;
}
/** @nodoc */
ngOnDestroy() {
this._urlChangeSubscription?.unsubscribe();
this._urlChangeListeners = [];
}
setInitialPath(url) {
this._history[this._historyIndex].path = url;
}
setBaseHref(url) {
this._basePath = url;
}
path() {
return this._history[this._historyIndex].path;
}
getState() {
return this._history[this._historyIndex].state;
}
isCurrentPathEqualTo(path, query = '') {
const givenPath = path.endsWith('/') ? path.substring(0, path.length - 1) : path;
const currPath = this.path().endsWith('/')
? this.path().substring(0, this.path().length - 1)
: this.path();
return currPath == givenPath + (query.length > 0 ? '?' + query : '');
}
simulateUrlPop(pathname) {
this._subject.emit({ 'url': pathname, 'pop': true, 'type': 'popstate' });
}
simulateHashChange(pathname) {
const path = this.prepareExternalUrl(pathname);
this.pushHistory(path, '', null);
this.urlChanges.push('hash: ' + pathname);
// the browser will automatically fire popstate event before each `hashchange` event, so we need
// to simulate it.
this._subject.emit({ 'url': pathname, 'pop': true, 'type': 'popstate' });
this._subject.emit({ 'url': pathname, 'pop': true, 'type': 'hashchange' });
}
prepareExternalUrl(url) {
if (url.length > 0 && !url.startsWith('/')) {
url = '/' + url;
}
return this._basePath + url;
}
go(path, query = '', state = null) {
path = this.prepareExternalUrl(path);
this.pushHistory(path, query, state);
const locationState = this._history[this._historyIndex - 1];
if (locationState.path == path && locationState.query == query) {
return;
}
const url = path + (query.length > 0 ? '?' + query : '');
this.urlChanges.push(url);
this._notifyUrlChangeListeners(path + ɵnormalizeQueryParams(query), state);
}
replaceState(path, query = '', state = null) {
path = this.prepareExternalUrl(path);
const history = this._history[this._historyIndex];
history.state = state;
if (history.path == path && history.query == query) {
return;
}
history.path = path;
history.query = query;
const url = path + (query.length > 0 ? '?' + query : '');
this.urlChanges.push('replace: ' + url);
this._notifyUrlChangeListeners(path + ɵnormalizeQueryParams(query), state);
}
forward() {
if (this._historyIndex < this._history.length - 1) {
this._historyIndex++;
this._subject.emit({
'url': this.path(),
'state': this.getState(),
'pop': true,
'type': 'popstate',
});
}
}
back() {
if (this._historyIndex > 0) {
this._historyIndex--;
this._subject.emit({
'url': this.path(),
'state': this.getState(),
'pop': true,
'type': 'popstate',
});
}
}
historyGo(relativePosition = 0) {
const nextPageIndex = this._historyIndex + relativePosition;
if (nextPageIndex >= 0 && nextPageIndex < this._history.length) {
this._historyIndex = nextPageIndex;
this._subject.emit({
'url': this.path(),
'state': this.getState(),
'pop': true,
'type': 'popstate',
});
}
}
onUrlChange(fn) {
this._urlChangeListeners.push(fn);
this._urlChangeSubscription ??= this.subscribe((v) => {
this._notifyUrlChangeListeners(v.url, v.state);
});
return () => {
const fnIndex = this._urlChangeListeners.indexOf(fn);
this._urlChangeListeners.splice(fnIndex, 1);
if (this._urlChangeListeners.length === 0) {
this._urlChangeSubscription?.unsubscribe();
this._urlChangeSubscription = null;
}
};
}
/** @internal */
_notifyUrlChangeListeners(url = '', state) {
this._urlChangeListeners.forEach((fn) => fn(url, state));
}
subscribe(onNext, onThrow, onReturn) {
return this._subject.subscribe({ next: onNext, error: onThrow, complete: onReturn });
}
normalize(url) {
return null;
}
pushHistory(path, query, state) {
if (this._historyIndex > 0) {
this._history.splice(this._historyIndex + 1);
}
this._history.push(new LocationState(path, query, state));
this._historyIndex = this._history.length - 1;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.6", ngImport: i0, type: SpyLocation, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.6", ngImport: i0, type: SpyLocation }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.6", ngImport: i0, type: SpyLocation, decorators: [{
type: Injectable
}] });
class LocationState {
constructor(path, query, state) {
this.path = path;
this.query = query;
this.state = state;
}
}
/**
* A mock implementation of {@link LocationStrategy} that allows tests to fire simulated
* location events.
*
* @publicApi
*/
class MockLocationStrategy extends LocationStrategy {
constructor() {
super();
this.internalBaseHref = '/';
this.internalPath = '/';
this.internalTitle = '';
this.urlChanges = [];
/** @internal */
this._subject = new EventEmitter();
this.stateChanges = [];
}
simulatePopState(url) {
this.internalPath = url;
this._subject.emit(new _MockPopStateEvent(this.path()));
}
path(includeHash = false) {
return this.internalPath;
}
prepareExternalUrl(internal) {
if (internal.startsWith('/') && this.internalBaseHref.endsWith('/')) {
return this.internalBaseHref + internal.substring(1);
}
return this.internalBaseHref + internal;
}
pushState(ctx, title, path, query) {
// Add state change to changes array
this.stateChanges.push(ctx);
this.internalTitle = title;
const url = path + (query.length > 0 ? '?' + query : '');
this.internalPath = url;
const externalUrl = this.prepareExternalUrl(url);
this.urlChanges.push(externalUrl);
}
replaceState(ctx, title, path, query) {
// Reset the last index of stateChanges to the ctx (state) object
this.stateChanges[(this.stateChanges.length || 1) - 1] = ctx;
this.internalTitle = title;
const url = path + (query.length > 0 ? '?' + query : '');
this.internalPath = url;
const externalUrl = this.prepareExternalUrl(url);
this.urlChanges.push('replace: ' + externalUrl);
}
onPopState(fn) {
this._subject.subscribe({ next: fn });
}
getBaseHref() {
return this.internalBaseHref;
}
back() {
if (this.urlChanges.length > 0) {
this.urlChanges.pop();
this.stateChanges.pop();
const nextUrl = this.urlChanges.length > 0 ? this.urlChanges[this.urlChanges.length - 1] : '';
this.simulatePopState(nextUrl);
}
}
forward() {
throw 'not implemented';
}
getState() {
return this.stateChanges[(this.stateChanges.length || 1) - 1];
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.6", ngImport: i0, type: MockLocationStrategy, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.6", ngImport: i0, type: MockLocationStrategy }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.6", ngImport: i0, type: MockLocationStrategy, decorators: [{
type: Injectable
}], ctorParameters: () => [] });
class _MockPopStateEvent {
constructor(newUrl) {
this.newUrl = newUrl;
this.pop = true;
this.type = 'popstate';
}
}
/**
* Returns mock providers for the `Location` and `LocationStrategy` classes.
* The mocks are helpful in tests to fire simulated location events.
*
* @publicApi
*/
function provideLocationMocks() {
return [
{ provide: Location, useClass: SpyLocation },
{ provide: LocationStrategy, useClass: MockLocationStrategy },
];
}
/**
* @module
* @description
* Entry point for all public APIs of the common/testing package.
*/
/**
* @module
* @description
* Entry point for all public APIs of this package.
*/
// This file only reexports content of the `src` folder. Keep it that way.
// This file is not used to build this module. It is only used during editing
/**
* Generated bundle index. Do not edit.
*/
export { MOCK_PLATFORM_LOCATION_CONFIG, MockLocationStrategy, MockPlatformLocation, SpyLocation, provideLocationMocks, provideFakePlatformNavigation as ɵprovideFakePlatformNavigation };
//# sourceMappingURL=testing.mjs.map