UNPKG

@angular/common

Version:

Angular - commonly needed directives and services

1 lines 94 kB
{"version":3,"file":"testing.mjs","sources":["../../../../../../packages/common/src/navigation/platform_navigation.ts","../../../../../../packages/common/testing/src/navigation/fake_navigation.ts","../../../../../../packages/common/testing/src/mock_platform_location.ts","../../../../../../packages/common/testing/src/navigation/provide_fake_platform_navigation.ts","../../../../../../packages/common/testing/src/location_mock.ts","../../../../../../packages/common/testing/src/mock_location_strategy.ts","../../../../../../packages/common/testing/src/provide_location_mocks.ts","../../../../../../packages/common/testing/src/testing.ts","../../../../../../packages/common/testing/public_api.ts","../../../../../../packages/common/testing/index.ts","../../../../../../packages/common/testing/testing.ts"],"sourcesContent":["/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.io/license\n */\n\nimport {Injectable} from '@angular/core';\n\nimport {\n NavigateEvent,\n Navigation,\n NavigationCurrentEntryChangeEvent,\n NavigationHistoryEntry,\n NavigationNavigateOptions,\n NavigationOptions,\n NavigationReloadOptions,\n NavigationResult,\n NavigationTransition,\n NavigationUpdateCurrentEntryOptions,\n} from './navigation_types';\n\n/**\n * This class wraps the platform Navigation API which allows server-specific and test\n * implementations.\n */\n@Injectable({providedIn: 'platform', useFactory: () => (window as any).navigation})\nexport abstract class PlatformNavigation implements Navigation {\n abstract entries(): NavigationHistoryEntry[];\n abstract currentEntry: NavigationHistoryEntry | null;\n abstract updateCurrentEntry(options: NavigationUpdateCurrentEntryOptions): void;\n abstract transition: NavigationTransition | null;\n abstract canGoBack: boolean;\n abstract canGoForward: boolean;\n abstract navigate(url: string, options?: NavigationNavigateOptions | undefined): NavigationResult;\n abstract reload(options?: NavigationReloadOptions | undefined): NavigationResult;\n abstract traverseTo(key: string, options?: NavigationOptions | undefined): NavigationResult;\n abstract back(options?: NavigationOptions | undefined): NavigationResult;\n abstract forward(options?: NavigationOptions | undefined): NavigationResult;\n abstract onnavigate: ((this: Navigation, ev: NavigateEvent) => any) | null;\n abstract onnavigatesuccess: ((this: Navigation, ev: Event) => any) | null;\n abstract onnavigateerror: ((this: Navigation, ev: ErrorEvent) => any) | null;\n abstract oncurrententrychange:\n | ((this: Navigation, ev: NavigationCurrentEntryChangeEvent) => any)\n | null;\n abstract addEventListener(type: unknown, listener: unknown, options?: unknown): void;\n abstract removeEventListener(type: unknown, listener: unknown, options?: unknown): void;\n abstract dispatchEvent(event: Event): boolean;\n}\n","/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.io/license\n */\n\nimport {\n NavigateEvent,\n Navigation,\n NavigationCurrentEntryChangeEvent,\n NavigationDestination,\n NavigationHistoryEntry,\n NavigationInterceptOptions,\n NavigationNavigateOptions,\n NavigationOptions,\n NavigationReloadOptions,\n NavigationResult,\n NavigationTransition,\n NavigationTypeString,\n NavigationUpdateCurrentEntryOptions,\n} from './navigation_types';\n\n/**\n * Fake implementation of user agent history and navigation behavior. This is a\n * high-fidelity implementation of browser behavior that attempts to emulate\n * things like traversal delay.\n */\nexport class FakeNavigation implements Navigation {\n /**\n * The fake implementation of an entries array. Only same-document entries\n * allowed.\n */\n private readonly entriesArr: FakeNavigationHistoryEntry[] = [];\n\n /**\n * The current active entry index into `entriesArr`.\n */\n private currentEntryIndex = 0;\n\n /**\n * The current navigate event.\n */\n private navigateEvent: InternalFakeNavigateEvent | undefined = undefined;\n\n /**\n * A Map of pending traversals, so that traversals to the same entry can be\n * re-used.\n */\n private readonly traversalQueue = new Map<string, InternalNavigationResult>();\n\n /**\n * A Promise that resolves when the previous traversals have finished. Used to\n * simulate the cross-process communication necessary for traversals.\n */\n private nextTraversal = Promise.resolve();\n\n /**\n * A prospective current active entry index, which includes unresolved\n * traversals. Used by `go` to determine where navigations are intended to go.\n */\n private prospectiveEntryIndex = 0;\n\n /**\n * A test-only option to make traversals synchronous, rather than emulate\n * cross-process communication.\n */\n private synchronousTraversals = false;\n\n /** Whether to allow a call to setInitialEntryForTesting. */\n private canSetInitialEntry = true;\n\n /** `EventTarget` to dispatch events. */\n private eventTarget: EventTarget = this.window.document.createElement('div');\n\n /** The next unique id for created entries. Replace recreates this id. */\n private nextId = 0;\n\n /** The next unique key for created entries. Replace inherits this id. */\n private nextKey = 0;\n\n /** Whether this fake is disposed. */\n private disposed = false;\n\n /** Equivalent to `navigation.currentEntry`. */\n get currentEntry(): FakeNavigationHistoryEntry {\n return this.entriesArr[this.currentEntryIndex];\n }\n\n get canGoBack(): boolean {\n return this.currentEntryIndex > 0;\n }\n\n get canGoForward(): boolean {\n return this.currentEntryIndex < this.entriesArr.length - 1;\n }\n\n constructor(\n private readonly window: Window,\n startURL: `http${string}`,\n ) {\n // First entry.\n this.setInitialEntryForTesting(startURL);\n }\n\n /**\n * Sets the initial entry.\n */\n private setInitialEntryForTesting(\n url: `http${string}`,\n options: {historyState: unknown; state?: unknown} = {historyState: null},\n ) {\n if (!this.canSetInitialEntry) {\n throw new Error(\n 'setInitialEntryForTesting can only be called before any ' + 'navigation has occurred',\n );\n }\n const currentInitialEntry = this.entriesArr[0];\n this.entriesArr[0] = new FakeNavigationHistoryEntry(new URL(url).toString(), {\n index: 0,\n key: currentInitialEntry?.key ?? String(this.nextKey++),\n id: currentInitialEntry?.id ?? String(this.nextId++),\n sameDocument: true,\n historyState: options?.historyState,\n state: options.state,\n });\n }\n\n /** Returns whether the initial entry is still eligible to be set. */\n canSetInitialEntryForTesting(): boolean {\n return this.canSetInitialEntry;\n }\n\n /**\n * Sets whether to emulate traversals as synchronous rather than\n * asynchronous.\n */\n setSynchronousTraversalsForTesting(synchronousTraversals: boolean) {\n this.synchronousTraversals = synchronousTraversals;\n }\n\n /** Equivalent to `navigation.entries()`. */\n entries(): FakeNavigationHistoryEntry[] {\n return this.entriesArr.slice();\n }\n\n /** Equivalent to `navigation.navigate()`. */\n navigate(url: string, options?: NavigationNavigateOptions): FakeNavigationResult {\n const fromUrl = new URL(this.currentEntry.url!);\n const toUrl = new URL(url, this.currentEntry.url!);\n\n let navigationType: NavigationTypeString;\n if (!options?.history || options.history === 'auto') {\n // Auto defaults to push, but if the URLs are the same, is a replace.\n if (fromUrl.toString() === toUrl.toString()) {\n navigationType = 'replace';\n } else {\n navigationType = 'push';\n }\n } else {\n navigationType = options.history;\n }\n\n const hashChange = isHashChange(fromUrl, toUrl);\n\n const destination = new FakeNavigationDestination({\n url: toUrl.toString(),\n state: options?.state,\n sameDocument: hashChange,\n historyState: null,\n });\n const result = new InternalNavigationResult();\n\n this.userAgentNavigate(destination, result, {\n navigationType,\n cancelable: true,\n canIntercept: true,\n // Always false for navigate().\n userInitiated: false,\n hashChange,\n info: options?.info,\n });\n\n return {\n committed: result.committed,\n finished: result.finished,\n };\n }\n\n /** Equivalent to `history.pushState()`. */\n pushState(data: unknown, title: string, url?: string): void {\n this.pushOrReplaceState('push', data, title, url);\n }\n\n /** Equivalent to `history.replaceState()`. */\n replaceState(data: unknown, title: string, url?: string): void {\n this.pushOrReplaceState('replace', data, title, url);\n }\n\n private pushOrReplaceState(\n navigationType: NavigationTypeString,\n data: unknown,\n _title: string,\n url?: string,\n ): void {\n const fromUrl = new URL(this.currentEntry.url!);\n const toUrl = url ? new URL(url, this.currentEntry.url!) : fromUrl;\n\n const hashChange = isHashChange(fromUrl, toUrl);\n\n const destination = new FakeNavigationDestination({\n url: toUrl.toString(),\n sameDocument: true,\n historyState: data,\n });\n const result = new InternalNavigationResult();\n\n this.userAgentNavigate(destination, result, {\n navigationType,\n cancelable: true,\n canIntercept: true,\n // Always false for pushState() or replaceState().\n userInitiated: false,\n hashChange,\n skipPopState: true,\n });\n }\n\n /** Equivalent to `navigation.traverseTo()`. */\n traverseTo(key: string, options?: NavigationOptions): FakeNavigationResult {\n const fromUrl = new URL(this.currentEntry.url!);\n const entry = this.findEntry(key);\n if (!entry) {\n const domException = new DOMException('Invalid key', 'InvalidStateError');\n const committed = Promise.reject(domException);\n const finished = Promise.reject(domException);\n committed.catch(() => {});\n finished.catch(() => {});\n return {\n committed,\n finished,\n };\n }\n if (entry === this.currentEntry) {\n return {\n committed: Promise.resolve(this.currentEntry),\n finished: Promise.resolve(this.currentEntry),\n };\n }\n if (this.traversalQueue.has(entry.key)) {\n const existingResult = this.traversalQueue.get(entry.key)!;\n return {\n committed: existingResult.committed,\n finished: existingResult.finished,\n };\n }\n\n const hashChange = isHashChange(fromUrl, new URL(entry.url!, this.currentEntry.url!));\n const destination = new FakeNavigationDestination({\n url: entry.url!,\n state: entry.getState(),\n historyState: entry.getHistoryState(),\n key: entry.key,\n id: entry.id,\n index: entry.index,\n sameDocument: entry.sameDocument,\n });\n this.prospectiveEntryIndex = entry.index;\n const result = new InternalNavigationResult();\n this.traversalQueue.set(entry.key, result);\n this.runTraversal(() => {\n this.traversalQueue.delete(entry.key);\n this.userAgentNavigate(destination, result, {\n navigationType: 'traverse',\n cancelable: true,\n canIntercept: true,\n // Always false for traverseTo().\n userInitiated: false,\n hashChange,\n info: options?.info,\n });\n });\n return {\n committed: result.committed,\n finished: result.finished,\n };\n }\n\n /** Equivalent to `navigation.back()`. */\n back(options?: NavigationOptions): FakeNavigationResult {\n if (this.currentEntryIndex === 0) {\n const domException = new DOMException('Cannot go back', 'InvalidStateError');\n const committed = Promise.reject(domException);\n const finished = Promise.reject(domException);\n committed.catch(() => {});\n finished.catch(() => {});\n return {\n committed,\n finished,\n };\n }\n const entry = this.entriesArr[this.currentEntryIndex - 1];\n return this.traverseTo(entry.key, options);\n }\n\n /** Equivalent to `navigation.forward()`. */\n forward(options?: NavigationOptions): FakeNavigationResult {\n if (this.currentEntryIndex === this.entriesArr.length - 1) {\n const domException = new DOMException('Cannot go forward', 'InvalidStateError');\n const committed = Promise.reject(domException);\n const finished = Promise.reject(domException);\n committed.catch(() => {});\n finished.catch(() => {});\n return {\n committed,\n finished,\n };\n }\n const entry = this.entriesArr[this.currentEntryIndex + 1];\n return this.traverseTo(entry.key, options);\n }\n\n /**\n * Equivalent to `history.go()`.\n * Note that this method does not actually work precisely to how Chrome\n * does, instead choosing a simpler model with less unexpected behavior.\n * Chrome has a few edge case optimizations, for instance with repeated\n * `back(); forward()` chains it collapses certain traversals.\n */\n go(direction: number): void {\n const targetIndex = this.prospectiveEntryIndex + direction;\n if (targetIndex >= this.entriesArr.length || targetIndex < 0) {\n return;\n }\n this.prospectiveEntryIndex = targetIndex;\n this.runTraversal(() => {\n // Check again that destination is in the entries array.\n if (targetIndex >= this.entriesArr.length || targetIndex < 0) {\n return;\n }\n const fromUrl = new URL(this.currentEntry.url!);\n const entry = this.entriesArr[targetIndex];\n const hashChange = isHashChange(fromUrl, new URL(entry.url!, this.currentEntry.url!));\n const destination = new FakeNavigationDestination({\n url: entry.url!,\n state: entry.getState(),\n historyState: entry.getHistoryState(),\n key: entry.key,\n id: entry.id,\n index: entry.index,\n sameDocument: entry.sameDocument,\n });\n const result = new InternalNavigationResult();\n this.userAgentNavigate(destination, result, {\n navigationType: 'traverse',\n cancelable: true,\n canIntercept: true,\n // Always false for go().\n userInitiated: false,\n hashChange,\n });\n });\n }\n\n /** Runs a traversal synchronously or asynchronously */\n private runTraversal(traversal: () => void) {\n if (this.synchronousTraversals) {\n traversal();\n return;\n }\n\n // Each traversal occupies a single timeout resolution.\n // This means that Promises added to commit and finish should resolve\n // before the next traversal.\n this.nextTraversal = this.nextTraversal.then(() => {\n return new Promise<void>((resolve) => {\n setTimeout(() => {\n resolve();\n traversal();\n });\n });\n });\n }\n\n /** Equivalent to `navigation.addEventListener()`. */\n addEventListener(\n type: string,\n callback: EventListenerOrEventListenerObject,\n options?: AddEventListenerOptions | boolean,\n ) {\n this.eventTarget.addEventListener(type, callback, options);\n }\n\n /** Equivalent to `navigation.removeEventListener()`. */\n removeEventListener(\n type: string,\n callback: EventListenerOrEventListenerObject,\n options?: EventListenerOptions | boolean,\n ) {\n this.eventTarget.removeEventListener(type, callback, options);\n }\n\n /** Equivalent to `navigation.dispatchEvent()` */\n dispatchEvent(event: Event): boolean {\n return this.eventTarget.dispatchEvent(event);\n }\n\n /** Cleans up resources. */\n dispose() {\n // Recreate eventTarget to release current listeners.\n // `document.createElement` because NodeJS `EventTarget` is incompatible with Domino's `Event`.\n this.eventTarget = this.window.document.createElement('div');\n this.disposed = true;\n }\n\n /** Returns whether this fake is disposed. */\n isDisposed() {\n return this.disposed;\n }\n\n /** Implementation for all navigations and traversals. */\n private userAgentNavigate(\n destination: FakeNavigationDestination,\n result: InternalNavigationResult,\n options: InternalNavigateOptions,\n ) {\n // The first navigation should disallow any future calls to set the initial\n // entry.\n this.canSetInitialEntry = false;\n if (this.navigateEvent) {\n this.navigateEvent.cancel(new DOMException('Navigation was aborted', 'AbortError'));\n this.navigateEvent = undefined;\n }\n\n const navigateEvent = createFakeNavigateEvent({\n navigationType: options.navigationType,\n cancelable: options.cancelable,\n canIntercept: options.canIntercept,\n userInitiated: options.userInitiated,\n hashChange: options.hashChange,\n signal: result.signal,\n destination,\n info: options.info,\n sameDocument: destination.sameDocument,\n skipPopState: options.skipPopState,\n result,\n userAgentCommit: () => {\n this.userAgentCommit();\n },\n });\n\n this.navigateEvent = navigateEvent;\n this.eventTarget.dispatchEvent(navigateEvent);\n navigateEvent.dispatchedNavigateEvent();\n if (navigateEvent.commitOption === 'immediate') {\n navigateEvent.commit(/* internal= */ true);\n }\n }\n\n /** Implementation to commit a navigation. */\n private userAgentCommit() {\n if (!this.navigateEvent) {\n return;\n }\n const from = this.currentEntry;\n if (!this.navigateEvent.sameDocument) {\n const error = new Error('Cannot navigate to a non-same-document URL.');\n this.navigateEvent.cancel(error);\n throw error;\n }\n if (\n this.navigateEvent.navigationType === 'push' ||\n this.navigateEvent.navigationType === 'replace'\n ) {\n this.userAgentPushOrReplace(this.navigateEvent.destination, {\n navigationType: this.navigateEvent.navigationType,\n });\n } else if (this.navigateEvent.navigationType === 'traverse') {\n this.userAgentTraverse(this.navigateEvent.destination);\n }\n this.navigateEvent.userAgentNavigated(this.currentEntry);\n const currentEntryChangeEvent = createFakeNavigationCurrentEntryChangeEvent({\n from,\n navigationType: this.navigateEvent.navigationType,\n });\n this.eventTarget.dispatchEvent(currentEntryChangeEvent);\n if (!this.navigateEvent.skipPopState) {\n const popStateEvent = createPopStateEvent({\n state: this.navigateEvent.destination.getHistoryState(),\n });\n this.window.dispatchEvent(popStateEvent);\n }\n }\n\n /** Implementation for a push or replace navigation. */\n private userAgentPushOrReplace(\n destination: FakeNavigationDestination,\n {navigationType}: {navigationType: NavigationTypeString},\n ) {\n if (navigationType === 'push') {\n this.currentEntryIndex++;\n this.prospectiveEntryIndex = this.currentEntryIndex;\n }\n const index = this.currentEntryIndex;\n const key = navigationType === 'push' ? String(this.nextKey++) : this.currentEntry.key;\n const entry = new FakeNavigationHistoryEntry(destination.url, {\n id: String(this.nextId++),\n key,\n index,\n sameDocument: true,\n state: destination.getState(),\n historyState: destination.getHistoryState(),\n });\n if (navigationType === 'push') {\n this.entriesArr.splice(index, Infinity, entry);\n } else {\n this.entriesArr[index] = entry;\n }\n }\n\n /** Implementation for a traverse navigation. */\n private userAgentTraverse(destination: FakeNavigationDestination) {\n this.currentEntryIndex = destination.index;\n }\n\n /** Utility method for finding entries with the given `key`. */\n private findEntry(key: string) {\n for (const entry of this.entriesArr) {\n if (entry.key === key) return entry;\n }\n return undefined;\n }\n\n set onnavigate(_handler: ((this: Navigation, ev: NavigateEvent) => any) | null) {\n throw new Error('unimplemented');\n }\n\n get onnavigate(): ((this: Navigation, ev: NavigateEvent) => any) | null {\n throw new Error('unimplemented');\n }\n\n set oncurrententrychange(\n _handler: ((this: Navigation, ev: NavigationCurrentEntryChangeEvent) => any) | null,\n ) {\n throw new Error('unimplemented');\n }\n\n get oncurrententrychange():\n | ((this: Navigation, ev: NavigationCurrentEntryChangeEvent) => any)\n | null {\n throw new Error('unimplemented');\n }\n\n set onnavigatesuccess(_handler: ((this: Navigation, ev: Event) => any) | null) {\n throw new Error('unimplemented');\n }\n\n get onnavigatesuccess(): ((this: Navigation, ev: Event) => any) | null {\n throw new Error('unimplemented');\n }\n\n set onnavigateerror(_handler: ((this: Navigation, ev: ErrorEvent) => any) | null) {\n throw new Error('unimplemented');\n }\n\n get onnavigateerror(): ((this: Navigation, ev: ErrorEvent) => any) | null {\n throw new Error('unimplemented');\n }\n\n get transition(): NavigationTransition | null {\n throw new Error('unimplemented');\n }\n\n updateCurrentEntry(_options: NavigationUpdateCurrentEntryOptions): void {\n throw new Error('unimplemented');\n }\n\n reload(_options?: NavigationReloadOptions): NavigationResult {\n throw new Error('unimplemented');\n }\n}\n\n/**\n * Fake equivalent of the `NavigationResult` interface with\n * `FakeNavigationHistoryEntry`.\n */\ninterface FakeNavigationResult extends NavigationResult {\n readonly committed: Promise<FakeNavigationHistoryEntry>;\n readonly finished: Promise<FakeNavigationHistoryEntry>;\n}\n\n/**\n * Fake equivalent of `NavigationHistoryEntry`.\n */\nexport class FakeNavigationHistoryEntry implements NavigationHistoryEntry {\n readonly sameDocument;\n\n readonly id: string;\n readonly key: string;\n readonly index: number;\n private readonly state: unknown;\n private readonly historyState: unknown;\n\n // tslint:disable-next-line:no-any\n ondispose: ((this: NavigationHistoryEntry, ev: Event) => any) | null = null;\n\n constructor(\n readonly url: string | null,\n {\n id,\n key,\n index,\n sameDocument,\n state,\n historyState,\n }: {\n id: string;\n key: string;\n index: number;\n sameDocument: boolean;\n historyState: unknown;\n state?: unknown;\n },\n ) {\n this.id = id;\n this.key = key;\n this.index = index;\n this.sameDocument = sameDocument;\n this.state = state;\n this.historyState = historyState;\n }\n\n getState(): unknown {\n // Budget copy.\n return this.state ? JSON.parse(JSON.stringify(this.state)) : this.state;\n }\n\n getHistoryState(): unknown {\n // Budget copy.\n return this.historyState ? JSON.parse(JSON.stringify(this.historyState)) : this.historyState;\n }\n\n addEventListener(\n type: string,\n callback: EventListenerOrEventListenerObject,\n options?: AddEventListenerOptions | boolean,\n ) {\n throw new Error('unimplemented');\n }\n\n removeEventListener(\n type: string,\n callback: EventListenerOrEventListenerObject,\n options?: EventListenerOptions | boolean,\n ) {\n throw new Error('unimplemented');\n }\n\n dispatchEvent(event: Event): boolean {\n throw new Error('unimplemented');\n }\n}\n\n/** `NavigationInterceptOptions` with experimental commit option. */\nexport interface ExperimentalNavigationInterceptOptions extends NavigationInterceptOptions {\n commit?: 'immediate' | 'after-transition';\n}\n\n/** `NavigateEvent` with experimental commit function. */\nexport interface ExperimentalNavigateEvent extends NavigateEvent {\n intercept(options?: ExperimentalNavigationInterceptOptions): void;\n\n commit(): void;\n}\n\n/**\n * Fake equivalent of `NavigateEvent`.\n */\nexport interface FakeNavigateEvent extends ExperimentalNavigateEvent {\n readonly destination: FakeNavigationDestination;\n}\n\ninterface InternalFakeNavigateEvent extends FakeNavigateEvent {\n readonly sameDocument: boolean;\n readonly skipPopState?: boolean;\n readonly commitOption: 'after-transition' | 'immediate';\n readonly result: InternalNavigationResult;\n\n commit(internal?: boolean): void;\n cancel(reason: Error): void;\n dispatchedNavigateEvent(): void;\n userAgentNavigated(entry: FakeNavigationHistoryEntry): void;\n}\n\n/**\n * Create a fake equivalent of `NavigateEvent`. This is not a class because ES5\n * transpiled JavaScript cannot extend native Event.\n */\nfunction createFakeNavigateEvent({\n cancelable,\n canIntercept,\n userInitiated,\n hashChange,\n navigationType,\n signal,\n destination,\n info,\n sameDocument,\n skipPopState,\n result,\n userAgentCommit,\n}: {\n cancelable: boolean;\n canIntercept: boolean;\n userInitiated: boolean;\n hashChange: boolean;\n navigationType: NavigationTypeString;\n signal: AbortSignal;\n destination: FakeNavigationDestination;\n info: unknown;\n sameDocument: boolean;\n skipPopState?: boolean;\n result: InternalNavigationResult;\n userAgentCommit: () => void;\n}) {\n const event = new Event('navigate', {bubbles: false, cancelable}) as {\n -readonly [P in keyof InternalFakeNavigateEvent]: InternalFakeNavigateEvent[P];\n };\n event.canIntercept = canIntercept;\n event.userInitiated = userInitiated;\n event.hashChange = hashChange;\n event.navigationType = navigationType;\n event.signal = signal;\n event.destination = destination;\n event.info = info;\n event.downloadRequest = null;\n event.formData = null;\n\n event.sameDocument = sameDocument;\n event.skipPopState = skipPopState;\n event.commitOption = 'immediate';\n\n let handlerFinished: Promise<void> | undefined = undefined;\n let interceptCalled = false;\n let dispatchedNavigateEvent = false;\n let commitCalled = false;\n\n event.intercept = function (\n this: InternalFakeNavigateEvent,\n options?: ExperimentalNavigationInterceptOptions,\n ): void {\n interceptCalled = true;\n event.sameDocument = true;\n const handler = options?.handler;\n if (handler) {\n handlerFinished = handler();\n }\n if (options?.commit) {\n event.commitOption = options.commit;\n }\n if (options?.focusReset !== undefined || options?.scroll !== undefined) {\n throw new Error('unimplemented');\n }\n };\n\n event.scroll = function (this: InternalFakeNavigateEvent): void {\n throw new Error('unimplemented');\n };\n\n event.commit = function (this: InternalFakeNavigateEvent, internal = false) {\n if (!internal && !interceptCalled) {\n throw new DOMException(\n `Failed to execute 'commit' on 'NavigateEvent': intercept() must be ` +\n `called before commit().`,\n 'InvalidStateError',\n );\n }\n if (!dispatchedNavigateEvent) {\n throw new DOMException(\n `Failed to execute 'commit' on 'NavigateEvent': commit() may not be ` +\n `called during event dispatch.`,\n 'InvalidStateError',\n );\n }\n if (commitCalled) {\n throw new DOMException(\n `Failed to execute 'commit' on 'NavigateEvent': commit() already ` + `called.`,\n 'InvalidStateError',\n );\n }\n commitCalled = true;\n\n userAgentCommit();\n };\n\n // Internal only.\n event.cancel = function (this: InternalFakeNavigateEvent, reason: Error) {\n result.committedReject(reason);\n result.finishedReject(reason);\n };\n\n // Internal only.\n event.dispatchedNavigateEvent = function (this: InternalFakeNavigateEvent) {\n dispatchedNavigateEvent = true;\n if (event.commitOption === 'after-transition') {\n // If handler finishes before commit, call commit.\n handlerFinished?.then(\n () => {\n if (!commitCalled) {\n event.commit(/* internal */ true);\n }\n },\n () => {},\n );\n }\n Promise.all([result.committed, handlerFinished]).then(\n ([entry]) => {\n result.finishedResolve(entry);\n },\n (reason) => {\n result.finishedReject(reason);\n },\n );\n };\n\n // Internal only.\n event.userAgentNavigated = function (\n this: InternalFakeNavigateEvent,\n entry: FakeNavigationHistoryEntry,\n ) {\n result.committedResolve(entry);\n };\n\n return event as InternalFakeNavigateEvent;\n}\n\n/** Fake equivalent of `NavigationCurrentEntryChangeEvent`. */\nexport interface FakeNavigationCurrentEntryChangeEvent extends NavigationCurrentEntryChangeEvent {\n readonly from: FakeNavigationHistoryEntry;\n}\n\n/**\n * Create a fake equivalent of `NavigationCurrentEntryChange`. This does not use\n * a class because ES5 transpiled JavaScript cannot extend native Event.\n */\nfunction createFakeNavigationCurrentEntryChangeEvent({\n from,\n navigationType,\n}: {\n from: FakeNavigationHistoryEntry;\n navigationType: NavigationTypeString;\n}) {\n const event = new Event('currententrychange', {\n bubbles: false,\n cancelable: false,\n }) as {\n -readonly [P in keyof NavigationCurrentEntryChangeEvent]: NavigationCurrentEntryChangeEvent[P];\n };\n event.from = from;\n event.navigationType = navigationType;\n return event as FakeNavigationCurrentEntryChangeEvent;\n}\n\n/**\n * Create a fake equivalent of `PopStateEvent`. This does not use a class\n * because ES5 transpiled JavaScript cannot extend native Event.\n */\nfunction createPopStateEvent({state}: {state: unknown}) {\n const event = new Event('popstate', {\n bubbles: false,\n cancelable: false,\n }) as {-readonly [P in keyof PopStateEvent]: PopStateEvent[P]};\n event.state = state;\n return event as PopStateEvent;\n}\n\n/**\n * Fake equivalent of `NavigationDestination`.\n */\nexport class FakeNavigationDestination implements NavigationDestination {\n readonly url: string;\n readonly sameDocument: boolean;\n readonly key: string | null;\n readonly id: string | null;\n readonly index: number;\n\n private readonly state?: unknown;\n private readonly historyState: unknown;\n\n constructor({\n url,\n sameDocument,\n historyState,\n state,\n key = null,\n id = null,\n index = -1,\n }: {\n url: string;\n sameDocument: boolean;\n historyState: unknown;\n state?: unknown;\n key?: string | null;\n id?: string | null;\n index?: number;\n }) {\n this.url = url;\n this.sameDocument = sameDocument;\n this.state = state;\n this.historyState = historyState;\n this.key = key;\n this.id = id;\n this.index = index;\n }\n\n getState(): unknown {\n return this.state;\n }\n\n getHistoryState(): unknown {\n return this.historyState;\n }\n}\n\n/** Utility function to determine whether two UrlLike have the same hash. */\nfunction isHashChange(from: URL, to: URL): boolean {\n return (\n to.hash !== from.hash &&\n to.hostname === from.hostname &&\n to.pathname === from.pathname &&\n to.search === from.search\n );\n}\n\n/** Internal utility class for representing the result of a navigation. */\nclass InternalNavigationResult {\n committedResolve!: (entry: FakeNavigationHistoryEntry) => void;\n committedReject!: (reason: Error) => void;\n finishedResolve!: (entry: FakeNavigationHistoryEntry) => void;\n finishedReject!: (reason: Error) => void;\n readonly committed: Promise<FakeNavigationHistoryEntry>;\n readonly finished: Promise<FakeNavigationHistoryEntry>;\n get signal(): AbortSignal {\n return this.abortController.signal;\n }\n private readonly abortController = new AbortController();\n\n constructor() {\n this.committed = new Promise<FakeNavigationHistoryEntry>((resolve, reject) => {\n this.committedResolve = resolve;\n this.committedReject = reject;\n });\n\n this.finished = new Promise<FakeNavigationHistoryEntry>(async (resolve, reject) => {\n this.finishedResolve = resolve;\n this.finishedReject = (reason: Error) => {\n reject(reason);\n this.abortController.abort(reason);\n };\n });\n // All rejections are handled.\n this.committed.catch(() => {});\n this.finished.catch(() => {});\n }\n}\n\n/** Internal options for performing a navigate. */\ninterface InternalNavigateOptions {\n navigationType: NavigationTypeString;\n cancelable: boolean;\n canIntercept: boolean;\n userInitiated: boolean;\n hashChange: boolean;\n info?: unknown;\n skipPopState?: boolean;\n}\n","/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.io/license\n */\n\nimport {\n DOCUMENT,\n LocationChangeEvent,\n LocationChangeListener,\n PlatformLocation,\n ɵPlatformNavigation as PlatformNavigation,\n} from '@angular/common';\nimport {Inject, inject, Injectable, InjectionToken, Optional} from '@angular/core';\nimport {Subject} from 'rxjs';\n\nimport {FakeNavigation} from './navigation/fake_navigation';\n\n/**\n * Parser from https://tools.ietf.org/html/rfc3986#appendix-B\n * ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?\n * 12 3 4 5 6 7 8 9\n *\n * Example: http://www.ics.uci.edu/pub/ietf/uri/#Related\n *\n * Results in:\n *\n * $1 = http:\n * $2 = http\n * $3 = //www.ics.uci.edu\n * $4 = www.ics.uci.edu\n * $5 = /pub/ietf/uri/\n * $6 = <undefined>\n * $7 = <undefined>\n * $8 = #Related\n * $9 = Related\n */\nconst urlParse = /^(([^:\\/?#]+):)?(\\/\\/([^\\/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?/;\n\nfunction parseUrl(urlStr: string, baseHref: string) {\n const verifyProtocol = /^((http[s]?|ftp):\\/\\/)/;\n let serverBase: string | undefined;\n\n // URL class requires full URL. If the URL string doesn't start with protocol, we need to add\n // an arbitrary base URL which can be removed afterward.\n if (!verifyProtocol.test(urlStr)) {\n serverBase = 'http://empty.com/';\n }\n let parsedUrl: {\n protocol: string;\n hostname: string;\n port: string;\n pathname: string;\n search: string;\n hash: string;\n };\n try {\n parsedUrl = new URL(urlStr, serverBase);\n } catch (e) {\n const result = urlParse.exec(serverBase || '' + urlStr);\n if (!result) {\n throw new Error(`Invalid URL: ${urlStr} with base: ${baseHref}`);\n }\n const hostSplit = result[4].split(':');\n parsedUrl = {\n protocol: result[1],\n hostname: hostSplit[0],\n port: hostSplit[1] || '',\n pathname: result[5],\n search: result[6],\n hash: result[8],\n };\n }\n if (parsedUrl.pathname && parsedUrl.pathname.indexOf(baseHref) === 0) {\n parsedUrl.pathname = parsedUrl.pathname.substring(baseHref.length);\n }\n return {\n hostname: (!serverBase && parsedUrl.hostname) || '',\n protocol: (!serverBase && parsedUrl.protocol) || '',\n port: (!serverBase && parsedUrl.port) || '',\n pathname: parsedUrl.pathname || '/',\n search: parsedUrl.search || '',\n hash: parsedUrl.hash || '',\n };\n}\n\n/**\n * Mock platform location config\n *\n * @publicApi\n */\nexport interface MockPlatformLocationConfig {\n startUrl?: string;\n appBaseHref?: string;\n}\n\n/**\n * Provider for mock platform location config\n *\n * @publicApi\n */\nexport const MOCK_PLATFORM_LOCATION_CONFIG = new InjectionToken<MockPlatformLocationConfig>(\n 'MOCK_PLATFORM_LOCATION_CONFIG',\n);\n\n/**\n * Mock implementation of URL state.\n *\n * @publicApi\n */\n@Injectable()\nexport class MockPlatformLocation implements PlatformLocation {\n private baseHref: string = '';\n private hashUpdate = new Subject<LocationChangeEvent>();\n private popStateSubject = new Subject<LocationChangeEvent>();\n private urlChangeIndex: number = 0;\n private urlChanges: {\n hostname: string;\n protocol: string;\n port: string;\n pathname: string;\n search: string;\n hash: string;\n state: unknown;\n }[] = [{hostname: '', protocol: '', port: '', pathname: '/', search: '', hash: '', state: null}];\n\n constructor(\n @Inject(MOCK_PLATFORM_LOCATION_CONFIG) @Optional() config?: MockPlatformLocationConfig,\n ) {\n if (config) {\n this.baseHref = config.appBaseHref || '';\n\n const parsedChanges = this.parseChanges(\n null,\n config.startUrl || 'http://_empty_/',\n this.baseHref,\n );\n this.urlChanges[0] = {...parsedChanges};\n }\n }\n\n get hostname() {\n return this.urlChanges[this.urlChangeIndex].hostname;\n }\n get protocol() {\n return this.urlChanges[this.urlChangeIndex].protocol;\n }\n get port() {\n return this.urlChanges[this.urlChangeIndex].port;\n }\n get pathname() {\n return this.urlChanges[this.urlChangeIndex].pathname;\n }\n get search() {\n return this.urlChanges[this.urlChangeIndex].search;\n }\n get hash() {\n return this.urlChanges[this.urlChangeIndex].hash;\n }\n get state() {\n return this.urlChanges[this.urlChangeIndex].state;\n }\n\n getBaseHrefFromDOM(): string {\n return this.baseHref;\n }\n\n onPopState(fn: LocationChangeListener): VoidFunction {\n const subscription = this.popStateSubject.subscribe(fn);\n return () => subscription.unsubscribe();\n }\n\n onHashChange(fn: LocationChangeListener): VoidFunction {\n const subscription = this.hashUpdate.subscribe(fn);\n return () => subscription.unsubscribe();\n }\n\n get href(): string {\n let url = `${this.protocol}//${this.hostname}${this.port ? ':' + this.port : ''}`;\n url += `${this.pathname === '/' ? '' : this.pathname}${this.search}${this.hash}`;\n return url;\n }\n\n get url(): string {\n return `${this.pathname}${this.search}${this.hash}`;\n }\n\n private parseChanges(state: unknown, url: string, baseHref: string = '') {\n // When the `history.state` value is stored, it is always copied.\n state = JSON.parse(JSON.stringify(state));\n return {...parseUrl(url, baseHref), state};\n }\n\n replaceState(state: any, title: string, newUrl: string): void {\n const {pathname, search, state: parsedState, hash} = this.parseChanges(state, newUrl);\n\n this.urlChanges[this.urlChangeIndex] = {\n ...this.urlChanges[this.urlChangeIndex],\n pathname,\n search,\n hash,\n state: parsedState,\n };\n }\n\n pushState(state: any, title: string, newUrl: string): void {\n const {pathname, search, state: parsedState, hash} = this.parseChanges(state, newUrl);\n if (this.urlChangeIndex > 0) {\n this.urlChanges.splice(this.urlChangeIndex + 1);\n }\n this.urlChanges.push({\n ...this.urlChanges[this.urlChangeIndex],\n pathname,\n search,\n hash,\n state: parsedState,\n });\n this.urlChangeIndex = this.urlChanges.length - 1;\n }\n\n forward(): void {\n const oldUrl = this.url;\n const oldHash = this.hash;\n if (this.urlChangeIndex < this.urlChanges.length) {\n this.urlChangeIndex++;\n }\n this.emitEvents(oldHash, oldUrl);\n }\n\n back(): void {\n const oldUrl = this.url;\n const oldHash = this.hash;\n if (this.urlChangeIndex > 0) {\n this.urlChangeIndex--;\n }\n this.emitEvents(oldHash, oldUrl);\n }\n\n historyGo(relativePosition: number = 0): void {\n const oldUrl = this.url;\n const oldHash = this.hash;\n const nextPageIndex = this.urlChangeIndex + relativePosition;\n if (nextPageIndex >= 0 && nextPageIndex < this.urlChanges.length) {\n this.urlChangeIndex = nextPageIndex;\n }\n this.emitEvents(oldHash, oldUrl);\n }\n\n getState(): unknown {\n return this.state;\n }\n\n /**\n * Browsers are inconsistent in when they fire events and perform the state updates\n * The most easiest thing to do in our mock is synchronous and that happens to match\n * Firefox and Chrome, at least somewhat closely\n *\n * https://github.com/WICG/navigation-api#watching-for-navigations\n * https://docs.google.com/document/d/1Pdve-DJ1JCGilj9Yqf5HxRJyBKSel5owgOvUJqTauwU/edit#heading=h.3ye4v71wsz94\n * popstate is always sent before hashchange:\n * https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event#when_popstate_is_sent\n */\n private emitEvents(oldHash: string, oldUrl: string) {\n this.popStateSubject.next({\n type: 'popstate',\n state: this.getState(),\n oldUrl,\n newUrl: this.url,\n } as LocationChangeEvent);\n if (oldHash !== this.hash) {\n this.hashUpdate.next({\n type: 'hashchange',\n state: null,\n oldUrl,\n newUrl: this.url,\n } as LocationChangeEvent);\n }\n }\n}\n\n/**\n * Mock implementation of URL state.\n */\n@Injectable()\nexport class FakeNavigationPlatformLocation implements PlatformLocation {\n private _platformNavigation = inject(PlatformNavigation) as FakeNavigation;\n private window = inject(DOCUMENT).defaultView!;\n\n constructor() {\n if (!(this._platformNavigation instanceof FakeNavigation)) {\n throw new Error(\n 'FakePlatformNavigation cannot be used without FakeNavigation. Use ' +\n '`provideFakeNavigation` to have all these services provided together.',\n );\n }\n }\n\n private config = inject(MOCK_PLATFORM_LOCATION_CONFIG, {optional: true});\n getBaseHrefFromDOM(): string {\n return this.config?.appBaseHref ?? '';\n }\n\n onPopState(fn: LocationChangeListener): VoidFunction {\n this.window.addEventListener('popstate', fn);\n return () => this.window.removeEventListener('popstate', fn);\n }\n\n onHashChange(fn: LocationChangeListener): VoidFunction {\n this.window.addEventListener('hashchange', fn as any);\n return () => this.window.removeEventListener('hashchange', fn as any);\n }\n\n get href(): string {\n return this._platformNavigation.currentEntry.url!;\n }\n get protocol(): string {\n return new URL(this._platformNavigation.currentEntry.url!).protocol;\n }\n get hostname(): string {\n return new URL(this._platformNavigation.currentEntry.url!).hostname;\n }\n get port(): string {\n return new URL(this._platformNavigation.currentEntry.url!).port;\n }\n get pathname(): string {\n return new URL(this._platformNavigation.currentEntry.url!).pathname;\n }\n get search(): string {\n return new URL(this._platformNavigation.currentEntry.url!).search;\n }\n get hash(): string {\n return new URL(this._platformNavigation.currentEntry.url!).hash;\n }\n\n pushState(state: any, title: string, url: string): void {\n this._platformNavigation.pushState(state, title, url);\n }\n\n replaceState(state: any, title: string, url: string): void {\n this._platformNavigation.replaceState(state, title, url);\n }\n\n forward(): void {\n this._platformNavigation.forward();\n }\n\n back(): void {\n this._platformNavigation.back();\n }\n\n historyGo(relativePosition: number = 0): void {\n this._platformNavigation.go(relativePosition);\n }\n\n getState(): unknown {\n return this._platformNavigation.currentEntry.getHistoryState();\n }\n}\n","/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.io/license\n */\n\nimport {DOCUMENT, PlatformLocation} from '@angular/common';\nimport {inject, Provider} from '@angular/core';\n\n// @ng_package: ignore-cross-repo-import\nimport {PlatformNavigation} from '../../../src/navigation/platform_navigation';\nimport {\n FakeNavigationPlatformLocation,\n MOCK_PLATFORM_LOCATION_CONFIG,\n} from '../mock_platform_location';\n\nimport {FakeNavigation} from './fake_navigation';\n\n/**\n * Return a provider for the `FakeNavigation` in place of the real Navigation API.\n */\nexport function provideFakePlatformNavigation(): Provider[] {\n return [\n {\n provide: PlatformNavigation,\n useFactory: () => {\n const config = inject(MOCK_PLATFORM_LOCATION_CONFIG, {optional: true});\n return new FakeNavigation(\n inject(DOCUMENT).defaultView!,\n (config?.startUrl as `http${string}`) ?? 'http://_empty_/',\n );\n },\n },\n {provide: PlatformLocation, useClass: FakeNavigationPlatformLocation},\n ];\n}\n","/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.io/license\n */\n\nimport {\n Location,\n LocationStrategy,\n ɵnormalizeQueryParams as normalizeQueryParams,\n} from '@angular/common';\nimport {EventEmitter, Injectable} from '@angular/core';\nimport {SubscriptionLike} from 'rxjs';\n\n/**\n * A spy for {@link Location} that allows tests to fire simulated location events.\n *\n * @publicApi\n */\n@Injectable()\nexport class SpyLocation implements Location {\n urlChanges: string[] = [];\n private _history: LocationState[] = [new LocationState('', '', null)];\n private _historyIndex: number = 0;\n /** @internal */\n _subject: EventEmitter<any> = new EventEmitter();\n /** @internal */\n _basePath: string = '';\n /** @internal */\n _locationStrategy: LocationStrategy = null!;\n /** @internal */\n _urlChangeListeners: ((url: string, state: unknown) => void)[] = [];\n /** @internal */\n _urlChangeSubscription: SubscriptionLike | null = null;\n\n /** @nodoc */\n ngOnDestroy(): void {\n this._urlChangeSubscription?.unsubscribe();\n this._urlChangeListeners = [];\n }\n\n setInitialPath(url: string) {\n this._history[this._historyIndex].path = url;\n }\n\n setBaseHref(url: string) {\n this._basePath = url;\n }\n\n path(): string {\n return this._history[this._historyIndex].path;\n }\n\n getState(): unknown {\n return this._history[this._historyIndex].state;\n }\n\n isCurrentPathEqualTo(path: string, query: string = ''): boolean {\n const givenPath = path.endsWith('/') ? path.substring(0, path.length - 1) : path;\n const currPath = this.path().endsWith('/')\n ? this.path().substring(0, this.path().length - 1)\n : this.path();\n\n return currPath == givenPath + (query.length > 0 ? '?' + query : '');\n }\n\n simulateUrlPop(pathname: string) {\n this._subject.emit({'url': pathname, 'pop': true, 'type': 'popstate'});\n }\n\n simulateHashChange(pathname: string) {\n const path = this.prepareExternalUrl(pathname);\n this.pushHistory(path, '', null);\n\n this.urlChanges.push('hash: ' + pathname);\n // the browser will automatically fire popstate event before each `hashchange` event, so we need\n // to simulate it.\n this._subject.emit({'url': pathname, 'pop': true, 'type': 'popstate'});\n this._subject.emit({'url': pathname, 'pop': true, 'type': 'hashchange'});\n }\n\n prepareExternalUrl(url: string): string {\n if (url.length > 0 && !url.startsWith('/')) {\n url = '/' + url;\n }\n return this._basePath + url;\n }\n\n go(path: string, query: string = '', state: any = null) {\n path = this.prepareExternalUrl(path);\n\n this.pushHistory(path, query, state);\n\n const locationState = this._history[this._historyIndex - 1];\n if (locationState.path == path && locationState.query == query) {\n return;\n }\n\n const url = path + (query.length > 0 ? '?' + query : '');\n this.urlChanges.push(url);\n this._notifyUrlChangeListeners(path + normalizeQueryParams(query), state);\n }\n\n replaceState(path: string, query: string = '', state: any = null) {\n path = this.prepareExternalUrl(path);\n\n const history = this._history[this._historyIndex];\n\n history.state = state;\n\n if (history.path == path && history.query == query) {\n return;\n }\n\n history.path = path;\n history.query = query;\n\n const url = path + (query.length > 0 ? '?' + query : '');\n this.urlChanges.push('replace: ' + url);\n this._notifyUrlChangeListeners(path + normalizeQueryParams(query), state);\n }\n\n forward() {\n if (this._historyIndex < this._history.length - 1) {\n this._historyIndex++;\n this._subject.emit({\n 'url': this.path(),\n 'state': this.getState(),\n 'pop': true,\n 'type': 'popstate',\n });\n }\n }\n\n back() {\n if (this._historyIndex > 0) {\n this._historyIndex--;\n this._subject.emit({\n 'url': this.path(),\n 'state': this.getState(),\n 'pop': true,\n 'type': 'popstate',\n });\n }\n }\n\n historyGo(relativePosition: number = 0): void {\n const nextPageIndex = this._historyIndex + relativePosition;\n if (nextPageIndex >= 0 && nextPageIndex < this._history.length) {\n this._historyIndex = nextPageIndex;\n this._subject.emit({\n 'url': this.path(),\n 'state': this.getState(),\n 'pop': true,\n 'type': 'popstate',\n });\n }\n }\n\n onUrlChange(fn: (url: string, state: unknown) => void): VoidFunction {\n this._urlChangeListeners.push(fn);\n\n this._urlChangeSubscription ??= this.subscribe((v) => {\n this._notifyUrlChangeListeners(v.url, v.state);\n });\n\n return () => {\n const fnIndex = this._urlChangeListeners.indexOf(fn);\n this._urlChangeListeners.splice(fnIndex, 1);\n\n if (this._urlChangeListeners.length === 0) {\n this._urlChangeSubscription?.unsubscribe();\n this._urlChangeSubscription = null;\n }\n };\n }\n\n /** @internal */\n _notifyUrlChangeListeners(url: string = '', state: unknown) {\n this._urlChangeListeners.forEach((fn) => fn(url, state));\n }\n\n subscribe(\n onNext: (value: any) => void,\n o