mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
fix(core): update FakeNavigation to match WHATWG HTML spec
Update FakeNavigation and Navigation API type definitions to match the latest WHATWG HTML specification (https://html.spec.whatwg.org/multipage/nav-history-apis.html): - Add NavigationPrecommitController.addHandler() and support dynamic post-commit handler registration (https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigationprecommitcontroller-addhandler). - Include precommitHandler directly in NavigationInterceptOptions (https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigateevent-intercept). - Add hasUAVisualTransition and sourceElement properties to NavigateEvent (https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-navigateevent-interface). - Fix FakeNavigationHistoryEntry dispose event name from 'disposed' to 'dispose' and implement ondispose (https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigationhistoryentry-ondispose). - Implement Navigation.prototype.updateCurrentEntry (https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigation-updatecurrententry). - Implement Navigation.prototype.reload (https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigation-reload) and share "performing a non-traverse navigation" (https://html.spec.whatwg.org/multipage/nav-history-apis.html#performing-a-non-traverse-navigation) logic with navigate(). - Implement standard event handler IDL properties (onnavigate, onnavigatesuccess, onnavigateerror, oncurrententrychange). - Update internal specification comments and links from legacy PR draft URLs to official WHATWG multipage URLs.
This commit is contained in:
@@ -152,6 +152,7 @@ export declare class NavigateEvent extends Event {
|
||||
readonly formData: FormData | null;
|
||||
readonly downloadRequest: string | null;
|
||||
readonly info?: unknown;
|
||||
readonly sourceElement: Element | null;
|
||||
|
||||
intercept(options?: NavigationInterceptOptions): void;
|
||||
scroll(): void;
|
||||
@@ -168,10 +169,22 @@ export interface NavigateEventInit extends EventInit {
|
||||
formData?: FormData | null;
|
||||
downloadRequest?: string | null;
|
||||
info?: unknown;
|
||||
sourceElement?: Element | null;
|
||||
}
|
||||
|
||||
export type NavigationInterceptHandler = () => PromiseLike<void> | void;
|
||||
export type NavigationPrecommitHandler = (
|
||||
controller: NavigationPrecommitController,
|
||||
) => PromiseLike<void> | void;
|
||||
|
||||
export interface NavigationPrecommitController {
|
||||
redirect: (url: string, options?: NavigationNavigateOptions) => void;
|
||||
addHandler: (handler: NavigationInterceptHandler) => void;
|
||||
}
|
||||
|
||||
export interface NavigationInterceptOptions {
|
||||
handler?: () => Promise<void>;
|
||||
precommitHandler?: NavigationPrecommitHandler;
|
||||
handler?: NavigationInterceptHandler;
|
||||
focusReset?: 'after-transition' | 'manual';
|
||||
scroll?: 'after-transition' | 'manual';
|
||||
}
|
||||
|
||||
@@ -19,8 +19,11 @@ import {
|
||||
NavigationResult,
|
||||
NavigationHistoryEntry,
|
||||
NavigationInterceptOptions,
|
||||
NavigationInterceptHandler,
|
||||
NavigationPrecommitHandler,
|
||||
NavigationDestination,
|
||||
Navigation,
|
||||
NavigationPrecommitController,
|
||||
} from '../src/navigation_types';
|
||||
// 3p-only-end
|
||||
|
||||
@@ -91,15 +94,26 @@ export class FakeNavigation implements Navigation {
|
||||
/** Whether this fake is disposed. */
|
||||
private disposed = false;
|
||||
|
||||
/** Equivalent to `navigation.currentEntry`. */
|
||||
/**
|
||||
* Equivalent to `navigation.currentEntry`.
|
||||
* https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigation-currententry
|
||||
*/
|
||||
get currentEntry(): FakeNavigationHistoryEntry {
|
||||
return this.entriesArr[this.currentEntryIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* Equivalent to `navigation.canGoBack`.
|
||||
* https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigation-cangoback
|
||||
*/
|
||||
get canGoBack(): boolean {
|
||||
return this.currentEntryIndex > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Equivalent to `navigation.canGoForward`.
|
||||
* https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigation-cangoforward
|
||||
*/
|
||||
get canGoForward(): boolean {
|
||||
return this.currentEntryIndex < this.entriesArr.length - 1;
|
||||
}
|
||||
@@ -144,7 +158,7 @@ export class FakeNavigation implements Navigation {
|
||||
): void {
|
||||
if (!this.canSetInitialEntry) {
|
||||
throw new Error(
|
||||
'setInitialEntryForTesting can only be called before any ' + 'navigation has occurred',
|
||||
'setInitialEntryForTesting can only be called before any navigation has occurred',
|
||||
);
|
||||
}
|
||||
const currentInitialEntry = this.entriesArr[0];
|
||||
@@ -171,12 +185,18 @@ export class FakeNavigation implements Navigation {
|
||||
this.synchronousTraversals = synchronousTraversals;
|
||||
}
|
||||
|
||||
/** Equivalent to `navigation.entries()`. */
|
||||
/**
|
||||
* Equivalent to `navigation.entries()`.
|
||||
* https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigation-entries
|
||||
*/
|
||||
entries(): FakeNavigationHistoryEntry[] {
|
||||
return this.entriesArr.slice();
|
||||
}
|
||||
|
||||
/** Equivalent to `navigation.navigate()`. */
|
||||
/**
|
||||
* Equivalent to `navigation.navigate()`.
|
||||
* https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigation-navigate
|
||||
*/
|
||||
navigate(url: string, options?: NavigationNavigateOptions): FakeNavigationResult {
|
||||
const fromUrl = new URL(this.currentEntry.url!);
|
||||
const toUrl = new URL(url, this.currentEntry.url!);
|
||||
@@ -201,9 +221,8 @@ export class FakeNavigation implements Navigation {
|
||||
sameDocument: hashChange,
|
||||
historyState: null,
|
||||
});
|
||||
const result = new InternalNavigationResult(this);
|
||||
|
||||
const intercepted = this.userAgentNavigate(destination, result, {
|
||||
return this.performNonTraverseNavigation(destination, {
|
||||
navigationType,
|
||||
cancelable: true,
|
||||
canIntercept: true,
|
||||
@@ -212,14 +231,6 @@ export class FakeNavigation implements Navigation {
|
||||
hashChange,
|
||||
info: options?.info,
|
||||
});
|
||||
if (!intercepted) {
|
||||
this.updateNavigationEntriesForSameDocumentNavigation(this.navigateEvent!);
|
||||
}
|
||||
|
||||
return {
|
||||
committed: result.committed,
|
||||
finished: result.finished,
|
||||
};
|
||||
}
|
||||
|
||||
/** Equivalent to `history.pushState()`. */
|
||||
@@ -249,9 +260,8 @@ export class FakeNavigation implements Navigation {
|
||||
historyState: data,
|
||||
state: undefined, // No Navigation API state directly from history.pushState
|
||||
});
|
||||
const result = new InternalNavigationResult(this);
|
||||
|
||||
const intercepted = this.userAgentNavigate(destination, result, {
|
||||
this.performNonTraverseNavigation(destination, {
|
||||
navigationType,
|
||||
cancelable: true,
|
||||
canIntercept: true,
|
||||
@@ -259,26 +269,17 @@ export class FakeNavigation implements Navigation {
|
||||
userInitiated: false,
|
||||
hashChange,
|
||||
});
|
||||
if (intercepted) {
|
||||
return;
|
||||
}
|
||||
this.updateNavigationEntriesForSameDocumentNavigation(this.navigateEvent!);
|
||||
}
|
||||
|
||||
/** Equivalent to `navigation.traverseTo()`. */
|
||||
/**
|
||||
* Equivalent to `navigation.traverseTo()`.
|
||||
* https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigation-traverseto
|
||||
*/
|
||||
traverseTo(key: string, options?: NavigationOptions): FakeNavigationResult {
|
||||
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,
|
||||
};
|
||||
return earlyErrorResult(new DOMException('Invalid key', 'InvalidStateError'));
|
||||
}
|
||||
if (entry === this.currentEntry) {
|
||||
return {
|
||||
@@ -295,15 +296,7 @@ export class FakeNavigation implements Navigation {
|
||||
}
|
||||
|
||||
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 destination = this.createDestinationFromEntry(entry);
|
||||
this.propsectiveTraversalDestinations.push(entry.index);
|
||||
const result = new InternalNavigationResult(this);
|
||||
this.traversalQueue.set(entry.key, result);
|
||||
@@ -328,35 +321,25 @@ export class FakeNavigation implements Navigation {
|
||||
};
|
||||
}
|
||||
|
||||
/** Equivalent to `navigation.back()`. */
|
||||
/**
|
||||
* Equivalent to `navigation.back()`.
|
||||
* https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigation-back
|
||||
*/
|
||||
back(options?: NavigationOptions): FakeNavigationResult {
|
||||
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,
|
||||
};
|
||||
return earlyErrorResult(new DOMException('Cannot go back', 'InvalidStateError'));
|
||||
}
|
||||
const entry = this.entriesArr[this.currentEntryIndex - 1];
|
||||
return this.traverseTo(entry.key, options);
|
||||
}
|
||||
|
||||
/** Equivalent to `navigation.forward()`. */
|
||||
/**
|
||||
* Equivalent to `navigation.forward()`.
|
||||
* https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigation-forward
|
||||
*/
|
||||
forward(options?: NavigationOptions): FakeNavigationResult {
|
||||
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,
|
||||
};
|
||||
return earlyErrorResult(new DOMException('Cannot go forward', 'InvalidStateError'));
|
||||
}
|
||||
const entry = this.entriesArr[this.currentEntryIndex + 1];
|
||||
return this.traverseTo(entry.key, options);
|
||||
@@ -385,15 +368,7 @@ export class FakeNavigation implements Navigation {
|
||||
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 destination = this.createDestinationFromEntry(entry);
|
||||
const result = new InternalNavigationResult(this);
|
||||
const intercepted = this.userAgentNavigate(destination, result, {
|
||||
navigationType: 'traverse',
|
||||
@@ -409,6 +384,39 @@ export class FakeNavigation implements Navigation {
|
||||
});
|
||||
}
|
||||
|
||||
/** Creates a FakeNavigationDestination matching a given history entry. */
|
||||
private createDestinationFromEntry(entry: FakeNavigationHistoryEntry): FakeNavigationDestination {
|
||||
return new FakeNavigationDestination({
|
||||
url: entry.url!,
|
||||
state: entry.getState(),
|
||||
historyState: entry.getHistoryState(),
|
||||
key: entry.key,
|
||||
id: entry.id,
|
||||
index: entry.index,
|
||||
sameDocument: entry.sameDocument,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of "performing a non-traverse navigation" from the spec.
|
||||
* https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigation-navigate
|
||||
* https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigation-reload
|
||||
*/
|
||||
private performNonTraverseNavigation(
|
||||
destination: FakeNavigationDestination,
|
||||
options: InternalNavigateOptions,
|
||||
): FakeNavigationResult {
|
||||
const result = new InternalNavigationResult(this);
|
||||
const intercepted = this.userAgentNavigate(destination, result, options);
|
||||
if (!intercepted) {
|
||||
this.updateNavigationEntriesForSameDocumentNavigation(this.navigateEvent!);
|
||||
}
|
||||
return {
|
||||
committed: result.committed,
|
||||
finished: result.finished,
|
||||
};
|
||||
}
|
||||
|
||||
/** Runs a traversal synchronously or asynchronously */
|
||||
private runTraversal(traversal: () => void) {
|
||||
if (this.synchronousTraversals) {
|
||||
@@ -479,6 +487,7 @@ export class FakeNavigation implements Navigation {
|
||||
|
||||
/**
|
||||
* Implementation for all navigations and traversals.
|
||||
* https://html.spec.whatwg.org/multipage/nav-history-apis.html#navigate-event-firing
|
||||
* @returns true if the event was intercepted, otherwise false
|
||||
*/
|
||||
private userAgentNavigate(
|
||||
@@ -507,6 +516,8 @@ export class FakeNavigation implements Navigation {
|
||||
hashChange: options.hashChange,
|
||||
destination,
|
||||
info: options.info,
|
||||
hasUAVisualTransition: options.hasUAVisualTransition,
|
||||
sourceElement: options.sourceElement,
|
||||
sameDocument: destination.sameDocument,
|
||||
result,
|
||||
});
|
||||
@@ -515,8 +526,8 @@ export class FakeNavigation implements Navigation {
|
||||
|
||||
/**
|
||||
* Implementation for a push or replace navigation.
|
||||
* https://whatpr.org/html/10919/browsing-the-web.html#url-and-history-update-steps
|
||||
* https://whatpr.org/html/10919/nav-history-apis.html#update-the-navigation-api-entries-for-a-same-document-navigation
|
||||
* https://html.spec.whatwg.org/multipage/browsing-the-web.html#url-and-history-update-steps
|
||||
* https://html.spec.whatwg.org/multipage/nav-history-apis.html#update-the-navigation-api-entries-for-a-same-document-navigation
|
||||
* @internal
|
||||
*/
|
||||
urlAndHistoryUpdateSteps(navigateEvent: InternalFakeNavigateEvent) {
|
||||
@@ -526,18 +537,18 @@ export class FakeNavigation implements Navigation {
|
||||
/**
|
||||
* Implementation for a traverse navigation.
|
||||
*
|
||||
* https://whatpr.org/html/10919/browsing-the-web.html#apply-the-traverse-history-step
|
||||
* https://html.spec.whatwg.org/multipage/browsing-the-web.html#apply-the-traverse-history-step
|
||||
* ...
|
||||
* > Let updateDocument be an algorithm step which performs update document for history step application given targetEntry's document, targetEntry, changingNavigableContinuation's update-only, scriptHistoryLength, scriptHistoryIndex, navigationType, entriesForNavigationAPI, and previousEntry.
|
||||
* > If targetEntry's document is equal to displayedDocument, then perform updateDocument.
|
||||
* https://whatpr.org/html/10919/browsing-the-web.html#update-document-for-history-step-application
|
||||
* which then goes to https://whatpr.org/html/10919/nav-history-apis.html#update-the-navigation-api-entries-for-a-same-document-navigation
|
||||
* https://html.spec.whatwg.org/multipage/browsing-the-web.html#update-document-for-history-step-application
|
||||
* which then goes to https://html.spec.whatwg.org/multipage/nav-history-apis.html#update-the-navigation-api-entries-for-a-same-document-navigation
|
||||
* @internal
|
||||
*/
|
||||
userAgentTraverse(navigateEvent: InternalFakeNavigateEvent) {
|
||||
const oldUrl = this.currentEntry.url!;
|
||||
this.updateNavigationEntriesForSameDocumentNavigation(navigateEvent);
|
||||
// Happens as part of "updating the document" steps https://whatpr.org/html/10919/browsing-the-web.html#updating-the-document
|
||||
// Happens as part of "updating the document" steps https://html.spec.whatwg.org/multipage/browsing-the-web.html#updating-the-document
|
||||
const popStateEvent = createPopStateEvent({
|
||||
state: navigateEvent.destination.getHistoryState(),
|
||||
hasUAVisualTransition: navigateEvent.hasUAVisualTransition,
|
||||
@@ -550,7 +561,7 @@ export class FakeNavigation implements Navigation {
|
||||
}
|
||||
|
||||
/**
|
||||
* https://whatpr.org/html/10919/nav-history-apis.html#update-the-navigation-api-entries-for-a-same-document-navigation
|
||||
* https://html.spec.whatwg.org/multipage/nav-history-apis.html#update-the-navigation-api-entries-for-a-same-document-navigation
|
||||
* @internal
|
||||
*/
|
||||
updateNavigationEntriesForSameDocumentNavigation({
|
||||
@@ -587,6 +598,8 @@ export class FakeNavigation implements Navigation {
|
||||
historyState: destination.getHistoryState(),
|
||||
});
|
||||
this.entriesArr[this.currentEntryIndex] = newNHE;
|
||||
} else if (navigationType === 'reload') {
|
||||
oldCurrentNHE.setState(destination.getState());
|
||||
}
|
||||
result.committedResolve(this.currentEntry);
|
||||
const currentEntryChangeEvent = createFakeNavigationCurrentEntryChangeEvent({
|
||||
@@ -607,54 +620,79 @@ export class FakeNavigation implements Navigation {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
set onnavigate(
|
||||
// tslint:disable-next-line:no-any
|
||||
_handler: ((this: Navigation, ev: NavigateEvent) => any) | null,
|
||||
) {
|
||||
throw new Error('unimplemented');
|
||||
}
|
||||
|
||||
private _onnavigate: ((this: Navigation, ev: NavigateEvent) => any) | null = null;
|
||||
/**
|
||||
* https://html.spec.whatwg.org/multipage/nav-history-apis.html#handler-navigation-onnavigate
|
||||
*/
|
||||
// tslint:disable-next-line:no-any
|
||||
get onnavigate(): ((this: Navigation, ev: NavigateEvent) => any) | null {
|
||||
throw new Error('unimplemented');
|
||||
return this._onnavigate;
|
||||
}
|
||||
|
||||
set oncurrententrychange(
|
||||
_handler:
|
||||
// tslint:disable-next-line:no-any
|
||||
((this: Navigation, ev: NavigationCurrentEntryChangeEvent) => any) | null,
|
||||
set onnavigate(
|
||||
// tslint:disable-next-line:no-any
|
||||
handler: ((this: Navigation, ev: NavigateEvent) => any) | null,
|
||||
) {
|
||||
throw new Error('unimplemented');
|
||||
this._onnavigate = setEventHandler(this, 'navigate', this._onnavigate, handler);
|
||||
}
|
||||
|
||||
private _oncurrententrychange:
|
||||
((this: Navigation, ev: NavigationCurrentEntryChangeEvent) => any) | null = null;
|
||||
/**
|
||||
* https://html.spec.whatwg.org/multipage/nav-history-apis.html#handler-navigation-oncurrententrychange
|
||||
*/
|
||||
get oncurrententrychange():
|
||||
// tslint:disable-next-line:no-any
|
||||
((this: Navigation, ev: NavigationCurrentEntryChangeEvent) => any) | null {
|
||||
throw new Error('unimplemented');
|
||||
| // tslint:disable-next-line:no-any
|
||||
((this: Navigation, ev: NavigationCurrentEntryChangeEvent) => any)
|
||||
| null {
|
||||
return this._oncurrententrychange;
|
||||
}
|
||||
|
||||
set onnavigatesuccess(
|
||||
// tslint:disable-next-line:no-any
|
||||
_handler: ((this: Navigation, ev: Event) => any) | null,
|
||||
set oncurrententrychange(
|
||||
handler:
|
||||
| // tslint:disable-next-line:no-any
|
||||
((this: Navigation, ev: NavigationCurrentEntryChangeEvent) => any)
|
||||
| null,
|
||||
) {
|
||||
throw new Error('unimplemented');
|
||||
this._oncurrententrychange = setEventHandler(
|
||||
this,
|
||||
'currententrychange',
|
||||
this._oncurrententrychange,
|
||||
handler,
|
||||
);
|
||||
}
|
||||
|
||||
private _onnavigatesuccess: ((this: Navigation, ev: Event) => any) | null = null;
|
||||
/**
|
||||
* https://html.spec.whatwg.org/multipage/nav-history-apis.html#handler-navigation-onnavigatesuccess
|
||||
*/
|
||||
// tslint:disable-next-line:no-any
|
||||
get onnavigatesuccess(): ((this: Navigation, ev: Event) => any) | null {
|
||||
throw new Error('unimplemented');
|
||||
return this._onnavigatesuccess;
|
||||
}
|
||||
|
||||
set onnavigateerror(
|
||||
set onnavigatesuccess(
|
||||
// tslint:disable-next-line:no-any
|
||||
_handler: ((this: Navigation, ev: ErrorEvent) => any) | null,
|
||||
handler: ((this: Navigation, ev: Event) => any) | null,
|
||||
) {
|
||||
throw new Error('unimplemented');
|
||||
this._onnavigatesuccess = setEventHandler(
|
||||
this,
|
||||
'navigatesuccess',
|
||||
this._onnavigatesuccess,
|
||||
handler,
|
||||
);
|
||||
}
|
||||
|
||||
private _onnavigateerror: ((this: Navigation, ev: ErrorEvent) => any) | null = null;
|
||||
/**
|
||||
* https://html.spec.whatwg.org/multipage/nav-history-apis.html#handler-navigation-onnavigateerror
|
||||
*/
|
||||
// tslint:disable-next-line:no-any
|
||||
get onnavigateerror(): ((this: Navigation, ev: ErrorEvent) => any) | null {
|
||||
throw new Error('unimplemented');
|
||||
return this._onnavigateerror;
|
||||
}
|
||||
set onnavigateerror(
|
||||
// tslint:disable-next-line:no-any
|
||||
handler: ((this: Navigation, ev: ErrorEvent) => any) | null,
|
||||
) {
|
||||
this._onnavigateerror = setEventHandler(this, 'navigateerror', this._onnavigateerror, handler);
|
||||
}
|
||||
|
||||
private _transition: NavigationTransition | null = null;
|
||||
@@ -666,12 +704,57 @@ export class FakeNavigation implements Navigation {
|
||||
return this._transition;
|
||||
}
|
||||
|
||||
updateCurrentEntry(_options: NavigationUpdateCurrentEntryOptions): void {
|
||||
throw new Error('unimplemented');
|
||||
/**
|
||||
* Equivalent to `navigation.updateCurrentEntry()`.
|
||||
* https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigation-updatecurrententry
|
||||
*/
|
||||
updateCurrentEntry(options: NavigationUpdateCurrentEntryOptions): void {
|
||||
const current = this.currentEntry;
|
||||
if (!current) {
|
||||
throw new DOMException(
|
||||
'Cannot update current entry when current is null',
|
||||
'InvalidStateError',
|
||||
);
|
||||
}
|
||||
current.setState(cloneState(options.state));
|
||||
const currentEntryChangeEvent = createFakeNavigationCurrentEntryChangeEvent({
|
||||
from: current,
|
||||
navigationType: null,
|
||||
});
|
||||
this.eventTarget.dispatchEvent(currentEntryChangeEvent);
|
||||
}
|
||||
|
||||
reload(_options?: NavigationReloadOptions): NavigationResult {
|
||||
throw new Error('unimplemented');
|
||||
/**
|
||||
* Equivalent to `navigation.reload()`.
|
||||
* https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigation-reload
|
||||
*/
|
||||
reload(options?: NavigationReloadOptions): FakeNavigationResult {
|
||||
const current = this.currentEntry;
|
||||
if (!current) {
|
||||
return earlyErrorResult(
|
||||
new DOMException('Cannot reload when currentEntry is null', 'InvalidStateError'),
|
||||
);
|
||||
}
|
||||
|
||||
const state = options && 'state' in options ? options.state : current.getState();
|
||||
const destination = new FakeNavigationDestination({
|
||||
url: current.url!,
|
||||
state,
|
||||
historyState: current.getHistoryState(),
|
||||
key: '',
|
||||
id: '',
|
||||
index: -1,
|
||||
sameDocument: current.sameDocument,
|
||||
});
|
||||
|
||||
return this.performNonTraverseNavigation(destination, {
|
||||
navigationType: 'reload',
|
||||
cancelable: true,
|
||||
canIntercept: true,
|
||||
userInitiated: false,
|
||||
hashChange: false,
|
||||
info: options?.info,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -686,6 +769,7 @@ interface FakeNavigationResult extends NavigationResult {
|
||||
|
||||
/**
|
||||
* Fake equivalent of `NavigationHistoryEntry`.
|
||||
* https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-navigationhistoryentry-interface
|
||||
*/
|
||||
export class FakeNavigationHistoryEntry implements NavigationHistoryEntry {
|
||||
readonly sameDocument: boolean;
|
||||
@@ -693,11 +777,23 @@ export class FakeNavigationHistoryEntry implements NavigationHistoryEntry {
|
||||
readonly id: string;
|
||||
readonly key: string;
|
||||
readonly index: number;
|
||||
private readonly state: unknown;
|
||||
private state: unknown;
|
||||
private readonly historyState: unknown;
|
||||
|
||||
private _ondispose: ((this: NavigationHistoryEntry, ev: Event) => any) | null = null;
|
||||
/**
|
||||
* https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigationhistoryentry-ondispose
|
||||
*/
|
||||
// tslint:disable-next-line:no-any
|
||||
ondispose: ((this: NavigationHistoryEntry, ev: Event) => any) | null = null;
|
||||
get ondispose(): ((this: NavigationHistoryEntry, ev: Event) => any) | null {
|
||||
return this._ondispose;
|
||||
}
|
||||
set ondispose(
|
||||
// tslint:disable-next-line:no-any
|
||||
handler: ((this: NavigationHistoryEntry, ev: Event) => any) | null,
|
||||
) {
|
||||
this._ondispose = setEventHandler(this, 'dispose', this._ondispose, handler);
|
||||
}
|
||||
|
||||
constructor(
|
||||
private eventTarget: EventTarget,
|
||||
@@ -726,16 +822,20 @@ export class FakeNavigationHistoryEntry implements NavigationHistoryEntry {
|
||||
this.historyState = historyState;
|
||||
}
|
||||
|
||||
/**
|
||||
* https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigationhistoryentry-getstate
|
||||
*/
|
||||
getState(): unknown {
|
||||
// Budget copy.
|
||||
return this.state ? (JSON.parse(JSON.stringify(this.state)) as unknown) : this.state;
|
||||
return cloneState(this.state);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
setState(state: unknown) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
getHistoryState(): unknown {
|
||||
// Budget copy.
|
||||
return this.historyState
|
||||
? (JSON.parse(JSON.stringify(this.historyState)) as unknown)
|
||||
: this.historyState;
|
||||
return cloneState(this.historyState);
|
||||
}
|
||||
|
||||
addEventListener(
|
||||
@@ -758,34 +858,23 @@ export class FakeNavigationHistoryEntry implements NavigationHistoryEntry {
|
||||
return this.eventTarget.dispatchEvent(event);
|
||||
}
|
||||
|
||||
/** internal */
|
||||
/**
|
||||
* internal
|
||||
* https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigationhistoryentry-ondispose
|
||||
*/
|
||||
dispose() {
|
||||
const disposeEvent = new Event('disposed');
|
||||
const disposeEvent = new Event('dispose');
|
||||
this.dispatchEvent(disposeEvent);
|
||||
// release current listeners
|
||||
this.eventTarget = null!;
|
||||
}
|
||||
}
|
||||
|
||||
/** `NavigationInterceptOptions` with experimental commit option. */
|
||||
export interface ExperimentalNavigationInterceptOptions extends NavigationInterceptOptions {
|
||||
precommitHandler?: (controller: NavigationPrecommitController) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface NavigationPrecommitController {
|
||||
redirect: (url: string, options?: NavigationNavigateOptions) => void;
|
||||
}
|
||||
|
||||
export interface ExperimentalNavigateEvent extends NavigateEvent {
|
||||
intercept(options?: ExperimentalNavigationInterceptOptions): void;
|
||||
|
||||
precommitHandler?: () => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fake equivalent of `NavigateEvent`.
|
||||
* https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-navigateevent-interface
|
||||
*/
|
||||
export interface FakeNavigateEvent extends ExperimentalNavigateEvent {
|
||||
export interface FakeNavigateEvent extends NavigateEvent {
|
||||
readonly destination: FakeNavigationDestination;
|
||||
}
|
||||
|
||||
@@ -815,6 +904,7 @@ function dispatchNavigateEvent({
|
||||
navigationType,
|
||||
destination,
|
||||
info,
|
||||
sourceElement = null,
|
||||
sameDocument,
|
||||
result,
|
||||
}: {
|
||||
@@ -826,6 +916,7 @@ function dispatchNavigateEvent({
|
||||
navigationType: NavigationType;
|
||||
destination: FakeNavigationDestination;
|
||||
info: unknown;
|
||||
sourceElement?: Element | null;
|
||||
sameDocument: boolean;
|
||||
result: InternalNavigationResult;
|
||||
}) {
|
||||
@@ -847,6 +938,7 @@ function dispatchNavigateEvent({
|
||||
event.signal = eventAbortController.signal;
|
||||
event.abortController = eventAbortController;
|
||||
event.info = info;
|
||||
event.sourceElement = sourceElement;
|
||||
event.focusResetBehavior = null;
|
||||
event.scrollBehavior = null;
|
||||
event.interceptionState = 'none';
|
||||
@@ -855,13 +947,15 @@ function dispatchNavigateEvent({
|
||||
event.result = result;
|
||||
event.sameDocument = sameDocument;
|
||||
|
||||
let precommitHandlers: Array<(controller: NavigationPrecommitController) => Promise<void>> = [];
|
||||
let precommitHandlers: Array<
|
||||
(controller: NavigationPrecommitController) => PromiseLike<void> | void
|
||||
> = [];
|
||||
let handlers: Array<() => PromiseLike<void> | void> = [];
|
||||
|
||||
// https://whatpr.org/html/10919/nav-history-apis.html#dom-navigateevent-intercept
|
||||
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigateevent-intercept
|
||||
event.intercept = function (
|
||||
this: InternalFakeNavigateEvent,
|
||||
options?: ExperimentalNavigationInterceptOptions,
|
||||
options?: NavigationInterceptOptions,
|
||||
): void {
|
||||
if (!this.canIntercept) {
|
||||
throw new DOMException(`Cannot intercept when canIntercept is 'false'`, 'SecurityError');
|
||||
@@ -891,7 +985,7 @@ function dispatchNavigateEvent({
|
||||
event.scrollBehavior = options?.scroll ?? event.scrollBehavior;
|
||||
};
|
||||
|
||||
// https://whatpr.org/html/10919/nav-history-apis.html#dom-navigateevent-scroll
|
||||
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigateevent-scroll
|
||||
event.scroll = function (this: InternalFakeNavigateEvent): void {
|
||||
if (event.interceptionState !== 'committed') {
|
||||
throw new DOMException(
|
||||
@@ -903,7 +997,7 @@ function dispatchNavigateEvent({
|
||||
processScrollBehavior(event);
|
||||
};
|
||||
|
||||
// https://whatpr.org/html/10919/nav-history-apis.html#dom-navigationprecommitcontroller-redirect
|
||||
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigationprecommitcontroller-redirect
|
||||
function redirect(url: string, options: NavigationNavigateOptions = {}) {
|
||||
if (event.interceptionState === 'none') {
|
||||
throw new Error('cannot redirect when event is not intercepted');
|
||||
@@ -933,6 +1027,17 @@ function dispatchNavigateEvent({
|
||||
}
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigationprecommitcontroller-addhandler
|
||||
function addHandler(handler: () => PromiseLike<void> | void) {
|
||||
if (event.interceptionState !== 'intercepted') {
|
||||
throw new DOMException(
|
||||
`cannot addHandler when event is not in 'intercepted' state`,
|
||||
'InvalidStateError',
|
||||
);
|
||||
}
|
||||
handlers.push(handler);
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#process-navigate-event-handler-failure
|
||||
function processNavigateEventHandlerFailure(reason: any) {
|
||||
if (event.abortController.signal.aborted) {
|
||||
@@ -974,10 +1079,13 @@ function dispatchNavigateEvent({
|
||||
(navigation.transition as InternalNavigationTransition)?.committedResolve();
|
||||
const promisesList: Array<PromiseLike<unknown>> = [];
|
||||
for (const handler of handlers) {
|
||||
const handlerResult = handler();
|
||||
|
||||
if (handlerResult) {
|
||||
promisesList.push(handlerResult);
|
||||
try {
|
||||
const handlerResult = handler();
|
||||
if (handlerResult) {
|
||||
promisesList.push(handlerResult);
|
||||
}
|
||||
} catch (e) {
|
||||
promisesList.push(Promise.reject(e));
|
||||
}
|
||||
}
|
||||
promisesList.push(result.committed);
|
||||
@@ -1063,17 +1171,23 @@ function dispatchNavigateEvent({
|
||||
if (precommitHandlers.length === 0) {
|
||||
commit();
|
||||
} else {
|
||||
const precommitController: NavigationPrecommitController = {redirect};
|
||||
const precommitPromisesList = precommitHandlers.map((handler) => {
|
||||
let p: Promise<void>;
|
||||
const precommitController: NavigationPrecommitController = {redirect, addHandler};
|
||||
const precommitPromisesList: Array<PromiseLike<unknown>> = [];
|
||||
for (const handler of precommitHandlers) {
|
||||
try {
|
||||
p = handler(precommitController);
|
||||
const handlerResult = handler(precommitController);
|
||||
if (handlerResult) {
|
||||
if (typeof (handlerResult as any).catch === 'function') {
|
||||
(handlerResult as any).catch(() => {});
|
||||
}
|
||||
precommitPromisesList.push(handlerResult);
|
||||
}
|
||||
} catch (e) {
|
||||
p = Promise.reject(e);
|
||||
const rejected = Promise.reject(e);
|
||||
rejected.catch(() => {});
|
||||
precommitPromisesList.push(rejected);
|
||||
}
|
||||
p.catch(() => {});
|
||||
return p;
|
||||
});
|
||||
}
|
||||
Promise.all(precommitPromisesList)
|
||||
.then(() => commit())
|
||||
.catch(processNavigateEventHandlerFailure);
|
||||
@@ -1085,7 +1199,7 @@ function dispatchNavigateEvent({
|
||||
return event.interceptionState === 'none';
|
||||
}
|
||||
|
||||
/** https://whatpr.org/html/10919/nav-history-apis.html#navigateevent-finish */
|
||||
/** https://html.spec.whatwg.org/multipage/nav-history-apis.html#navigateevent-finish */
|
||||
function finishNavigationEvent(event: InternalFakeNavigateEvent, didFulfill: boolean) {
|
||||
if (event.interceptionState === 'finished') {
|
||||
throw new Error('Attempting to finish navigation event that was already finished');
|
||||
@@ -1107,7 +1221,7 @@ function finishNavigationEvent(event: InternalFakeNavigateEvent, didFulfill: boo
|
||||
event.interceptionState = 'finished';
|
||||
}
|
||||
|
||||
/** https://whatpr.org/html/10919/nav-history-apis.html#potentially-reset-the-focus */
|
||||
/** https://html.spec.whatwg.org/multipage/nav-history-apis.html#potentially-reset-the-focus */
|
||||
function potentiallyResetFocus(event: InternalFakeNavigateEvent) {
|
||||
if (event.interceptionState !== 'committed' && event.interceptionState !== 'scrolled') {
|
||||
throw new Error('cannot reset focus if navigation event is not committed or scrolled');
|
||||
@@ -1118,6 +1232,7 @@ function potentiallyResetFocus(event: InternalFakeNavigateEvent) {
|
||||
// TODO(atscott): the rest of the steps
|
||||
}
|
||||
|
||||
/** https://html.spec.whatwg.org/multipage/nav-history-apis.html#potentially-reset-the-scroll-position */
|
||||
function potentiallyResetScroll(event: InternalFakeNavigateEvent) {
|
||||
if (event.interceptionState !== 'committed' && event.interceptionState !== 'scrolled') {
|
||||
throw new Error('cannot reset scroll if navigation event is not committed or scrolled');
|
||||
@@ -1128,7 +1243,7 @@ function potentiallyResetScroll(event: InternalFakeNavigateEvent) {
|
||||
processScrollBehavior(event);
|
||||
}
|
||||
|
||||
/* https://whatpr.org/html/10919/nav-history-apis.html#process-scroll-behavior */
|
||||
/** https://html.spec.whatwg.org/multipage/nav-history-apis.html#process-scroll-behavior */
|
||||
function processScrollBehavior(event: InternalFakeNavigateEvent) {
|
||||
if (event.interceptionState !== 'committed') {
|
||||
throw new Error('invalid event interception state when processing scroll behavior');
|
||||
@@ -1137,7 +1252,10 @@ function processScrollBehavior(event: InternalFakeNavigateEvent) {
|
||||
// TODO(atscott): the rest of the steps
|
||||
}
|
||||
|
||||
/** Fake equivalent of `NavigationCurrentEntryChangeEvent`. */
|
||||
/**
|
||||
* Fake equivalent of `NavigationCurrentEntryChangeEvent`.
|
||||
* https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-navigationcurrententrychangeevent-interface
|
||||
*/
|
||||
export interface FakeNavigationCurrentEntryChangeEvent extends NavigationCurrentEntryChangeEvent {
|
||||
readonly from: FakeNavigationHistoryEntry;
|
||||
}
|
||||
@@ -1145,13 +1263,14 @@ export interface FakeNavigationCurrentEntryChangeEvent extends NavigationCurrent
|
||||
/**
|
||||
* Create a fake equivalent of `NavigationCurrentEntryChange`. This does not use
|
||||
* a class because ES5 transpiled JavaScript cannot extend native Event.
|
||||
* https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-navigationcurrententrychangeevent-interface
|
||||
*/
|
||||
function createFakeNavigationCurrentEntryChangeEvent({
|
||||
from,
|
||||
navigationType,
|
||||
}: {
|
||||
from: FakeNavigationHistoryEntry;
|
||||
navigationType: NavigationType;
|
||||
navigationType: NavigationType | null;
|
||||
}) {
|
||||
const event = new Event('currententrychange', {
|
||||
bubbles: false,
|
||||
@@ -1198,6 +1317,7 @@ function createHashChangeEvent(newURL: string, oldURL: string) {
|
||||
|
||||
/**
|
||||
* Fake equivalent of `NavigationDestination`.
|
||||
* https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-navigationdestination-interface
|
||||
*/
|
||||
export class FakeNavigationDestination implements NavigationDestination {
|
||||
url: string;
|
||||
@@ -1254,6 +1374,48 @@ function isHashChange(from: URL, to: URL): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an IDL event listener attribute, removing the old listener and adding the new one.
|
||||
*/
|
||||
function setEventHandler<H extends ((...args: any[]) => any) | null>(
|
||||
target: EventTarget,
|
||||
type: string,
|
||||
current: H,
|
||||
next: H,
|
||||
): H {
|
||||
if (current) {
|
||||
target.removeEventListener(type, current as EventListener);
|
||||
}
|
||||
if (next) {
|
||||
target.addEventListener(type, next as EventListener);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
/** Budget structured clone for state objects. */
|
||||
function cloneState<T>(state: T): T {
|
||||
return state !== undefined && state !== null ? (JSON.parse(JSON.stringify(state)) as T) : state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a FakeNavigationResult where both committed and finished promises
|
||||
* are rejected with the given error and marked as handled.
|
||||
*/
|
||||
function earlyErrorResult(error: DOMException): FakeNavigationResult {
|
||||
const committed = Promise.reject(error);
|
||||
const finished = Promise.reject(error);
|
||||
committed.catch(() => {});
|
||||
finished.catch(() => {});
|
||||
return {
|
||||
committed,
|
||||
finished,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fake equivalent of `NavigationTransition`.
|
||||
* https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-navigationtransition-interface
|
||||
*/
|
||||
class InternalNavigationTransition implements NavigationTransition {
|
||||
readonly finished: Promise<void>;
|
||||
readonly committed: Promise<void>;
|
||||
@@ -1335,4 +1497,6 @@ interface InternalNavigateOptions {
|
||||
userInitiated: boolean;
|
||||
hashChange: boolean;
|
||||
info?: unknown;
|
||||
hasUAVisualTransition?: boolean;
|
||||
sourceElement?: Element | null;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ ts_project(
|
||||
),
|
||||
visibility = ["//:__pkg__"],
|
||||
deps = [
|
||||
"//packages/core/primitives/dom-navigation",
|
||||
"//packages/core/primitives/dom-navigation/testing",
|
||||
"//packages/private/testing",
|
||||
],
|
||||
|
||||
+246
-3
@@ -7,7 +7,10 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
ExperimentalNavigationInterceptOptions,
|
||||
NavigationInterceptOptions,
|
||||
NavigationPrecommitController,
|
||||
} from '../../src/navigation_types';
|
||||
import {
|
||||
FakeNavigateEvent,
|
||||
FakeNavigation,
|
||||
FakeNavigationCurrentEntryChangeEvent,
|
||||
@@ -21,7 +24,7 @@ interface Locals {
|
||||
navigateEvents: FakeNavigateEvent[];
|
||||
navigationCurrentEntryChangeEvents: FakeNavigationCurrentEntryChangeEvent[];
|
||||
popStateEvents: PopStateEvent[];
|
||||
pendingInterceptOptions: ExperimentalNavigationInterceptOptions[];
|
||||
pendingInterceptOptions: NavigationInterceptOptions[];
|
||||
nextNavigateEvent: () => Promise<FakeNavigateEvent>;
|
||||
setExtraNavigateCallback: (callback: (event: FakeNavigateEvent) => void) => void;
|
||||
}
|
||||
@@ -53,7 +56,7 @@ describe('navigation', () => {
|
||||
});
|
||||
const navigationCurrentEntryChangeEvents: FakeNavigationCurrentEntryChangeEvent[] = [];
|
||||
const popStateEvents: PopStateEvent[] = [];
|
||||
const pendingInterceptOptions: ExperimentalNavigationInterceptOptions[] = [];
|
||||
const pendingInterceptOptions: NavigationInterceptOptions[] = [];
|
||||
let extraNavigateCallback: ((event: FakeNavigateEvent) => void) | undefined = undefined;
|
||||
|
||||
navigation.addEventListener('navigate', (event: Event) => {
|
||||
@@ -2186,4 +2189,244 @@ describe('navigation', () => {
|
||||
expect(locals.navigateEvents.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addHandler', () => {
|
||||
it('executes post-commit handlers registered during precommit', async () => {
|
||||
const order: string[] = [];
|
||||
locals.pendingInterceptOptions.push({
|
||||
precommitHandler: (controller) => {
|
||||
order.push('precommit');
|
||||
controller.addHandler(() => {
|
||||
order.push('postcommit-from-precommit');
|
||||
});
|
||||
},
|
||||
handler: () => {
|
||||
order.push('postcommit');
|
||||
},
|
||||
});
|
||||
|
||||
await locals.navigation.navigate('/test').finished;
|
||||
expect(order).toEqual(['precommit', 'postcommit', 'postcommit-from-precommit']);
|
||||
});
|
||||
|
||||
it('waits for async handlers added via addHandler before finished resolves', async () => {
|
||||
let resolveHandler!: () => void;
|
||||
const handlerPromise = new Promise<void>((resolve) => {
|
||||
resolveHandler = resolve;
|
||||
});
|
||||
locals.pendingInterceptOptions.push({
|
||||
precommitHandler: (controller) => {
|
||||
controller.addHandler(() => handlerPromise);
|
||||
},
|
||||
});
|
||||
|
||||
const {finished} = locals.navigation.navigate('/test');
|
||||
await expectAsync(finished).toBePending();
|
||||
resolveHandler();
|
||||
await expectAsync(finished).toBeResolved();
|
||||
});
|
||||
|
||||
it('throws InvalidStateError when addHandler is called outside intercepted state', async () => {
|
||||
let controller!: NavigationPrecommitController;
|
||||
locals.pendingInterceptOptions.push({
|
||||
precommitHandler: (c) => {
|
||||
controller = c;
|
||||
},
|
||||
});
|
||||
|
||||
await locals.navigation.navigate('/test').finished;
|
||||
expect(() => controller.addHandler(() => {})).toThrowMatching(
|
||||
(e: any) => e.name === 'InvalidStateError',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateCurrentEntry', () => {
|
||||
it('updates currentEntry state and dispatches currententrychange with null navigationType', () => {
|
||||
const initialEntry = locals.navigation.currentEntry;
|
||||
expect(initialEntry.getState()).toBeUndefined();
|
||||
|
||||
locals.navigation.updateCurrentEntry({state: {updated: 123}});
|
||||
|
||||
expect(locals.navigation.currentEntry).toBe(initialEntry);
|
||||
expect(locals.navigation.currentEntry.getState()).toEqual({updated: 123});
|
||||
expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
|
||||
expect(locals.navigationCurrentEntryChangeEvents[0].from).toBe(initialEntry);
|
||||
expect(locals.navigationCurrentEntryChangeEvents[0].navigationType).toBeNull();
|
||||
});
|
||||
|
||||
it('clones state so modifications do not affect entry state', () => {
|
||||
const state = {count: 1};
|
||||
locals.navigation.updateCurrentEntry({state});
|
||||
state.count = 2;
|
||||
expect(locals.navigation.currentEntry.getState()).toEqual({count: 1});
|
||||
|
||||
const returnedState = locals.navigation.currentEntry.getState() as {count: number};
|
||||
returnedState.count = 3;
|
||||
expect(locals.navigation.currentEntry.getState()).toEqual({count: 1});
|
||||
});
|
||||
});
|
||||
|
||||
describe('reload', () => {
|
||||
it('reloads the current URL and dispatches navigate event with reload type', async () => {
|
||||
const initialEntry = locals.navigation.currentEntry;
|
||||
const result = locals.navigation.reload({info: 'reloadInfo'});
|
||||
|
||||
expect(locals.navigateEvents.length).toBe(1);
|
||||
expect(locals.navigateEvents[0].navigationType).toBe('reload');
|
||||
expect(locals.navigateEvents[0].info).toBe('reloadInfo');
|
||||
expect(locals.navigateEvents[0].destination.url).toBe(initialEntry.url!);
|
||||
expect(locals.navigateEvents[0].destination.key).toBe('');
|
||||
expect(locals.navigateEvents[0].destination.id).toBe('');
|
||||
expect(locals.navigateEvents[0].destination.index).toBe(-1);
|
||||
|
||||
const reloadedEntry = await result.finished;
|
||||
expect(reloadedEntry).toBe(initialEntry);
|
||||
expect(reloadedEntry.url).toBe(initialEntry.url!);
|
||||
expect(reloadedEntry.key).toBe(initialEntry.key);
|
||||
expect(reloadedEntry.id).toBe(initialEntry.id);
|
||||
});
|
||||
|
||||
it('allows intercepting reload', async () => {
|
||||
let intercepted = false;
|
||||
locals.pendingInterceptOptions.push({
|
||||
handler: () => {
|
||||
intercepted = true;
|
||||
},
|
||||
});
|
||||
|
||||
await locals.navigation.reload().finished;
|
||||
expect(intercepted).toBeTrue();
|
||||
expect(locals.navigateEvents[0].navigationType).toBe('reload');
|
||||
});
|
||||
|
||||
it('reloads with custom state when provided', async () => {
|
||||
const result = locals.navigation.reload({state: {reloaded: true}});
|
||||
const entry = await result.finished;
|
||||
expect(entry.getState()).toEqual({reloaded: true});
|
||||
});
|
||||
|
||||
it('preserves existing state when state option is omitted', async () => {
|
||||
locals.navigation.updateCurrentEntry({state: {preserved: 'yes'}});
|
||||
const result = locals.navigation.reload();
|
||||
const entry = await result.finished;
|
||||
expect(entry.getState()).toEqual({preserved: 'yes'});
|
||||
});
|
||||
|
||||
it('resets state when explicitly passed state: undefined', async () => {
|
||||
locals.navigation.updateCurrentEntry({state: {toReset: true}});
|
||||
const result = locals.navigation.reload({state: undefined});
|
||||
const entry = await result.finished;
|
||||
expect(entry.getState()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('does not dispose the current entry on reload', async () => {
|
||||
const initialEntry = locals.navigation.currentEntry;
|
||||
let disposed = false;
|
||||
initialEntry.ondispose = () => {
|
||||
disposed = true;
|
||||
};
|
||||
|
||||
await locals.navigation.reload().finished;
|
||||
expect(disposed).toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
describe('NavigateEvent properties', () => {
|
||||
it('has default hasUAVisualTransition and sourceElement values', async () => {
|
||||
await locals.navigation.navigate('/page').finished;
|
||||
const event = locals.navigateEvents[0];
|
||||
expect(event.hasUAVisualTransition).toBeFalsy();
|
||||
expect(event.sourceElement).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('NavigationHistoryEntry dispose event', () => {
|
||||
it('fires "dispose" event and triggers ondispose when entry is replaced', async () => {
|
||||
const firstEntry = locals.navigation.currentEntry;
|
||||
let disposeEventFired = false;
|
||||
let ondisposeFired = false;
|
||||
|
||||
firstEntry.addEventListener('dispose', () => {
|
||||
disposeEventFired = true;
|
||||
});
|
||||
firstEntry.ondispose = () => {
|
||||
ondisposeFired = true;
|
||||
};
|
||||
|
||||
await locals.navigation.navigate('/replaced', {history: 'replace'}).finished;
|
||||
|
||||
expect(disposeEventFired).toBeTrue();
|
||||
expect(ondisposeFired).toBeTrue();
|
||||
});
|
||||
|
||||
it('allows removing ondispose handler by setting it to null', async () => {
|
||||
const firstEntry = locals.navigation.currentEntry;
|
||||
let ondisposeFired = false;
|
||||
firstEntry.ondispose = () => {
|
||||
ondisposeFired = true;
|
||||
};
|
||||
firstEntry.ondispose = null;
|
||||
|
||||
await locals.navigation.navigate('/replaced', {history: 'replace'}).finished;
|
||||
expect(ondisposeFired).toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Navigation event handlers (on*)', () => {
|
||||
it('handles onnavigate, onnavigatesuccess, and oncurrententrychange properties', async () => {
|
||||
let onNavigateCalled = false;
|
||||
let onNavigateSuccessCalled = false;
|
||||
let onCurrentEntryChangeCalled = false;
|
||||
|
||||
locals.navigation.onnavigate = () => {
|
||||
onNavigateCalled = true;
|
||||
};
|
||||
locals.navigation.onnavigatesuccess = () => {
|
||||
onNavigateSuccessCalled = true;
|
||||
};
|
||||
locals.navigation.oncurrententrychange = () => {
|
||||
onCurrentEntryChangeCalled = true;
|
||||
};
|
||||
|
||||
await locals.navigation.navigate('/test').finished;
|
||||
|
||||
expect(onNavigateCalled).toBeTrue();
|
||||
expect(onNavigateSuccessCalled).toBeTrue();
|
||||
expect(onCurrentEntryChangeCalled).toBeTrue();
|
||||
});
|
||||
|
||||
it('handles onnavigateerror property when navigation fails', async () => {
|
||||
let errorEvent: ErrorEvent | undefined;
|
||||
const expectedError = new Error('handler failure');
|
||||
|
||||
locals.navigation.onnavigate = (event) => {
|
||||
event.intercept({
|
||||
handler: () => {
|
||||
throw expectedError;
|
||||
},
|
||||
});
|
||||
};
|
||||
locals.navigation.onnavigateerror = (event) => {
|
||||
errorEvent = event;
|
||||
};
|
||||
|
||||
await expectAsync(locals.navigation.navigate('/test').finished).toBeRejectedWith(
|
||||
expectedError,
|
||||
);
|
||||
expect(errorEvent).toBeDefined();
|
||||
expect(errorEvent?.error).toBe(expectedError);
|
||||
});
|
||||
|
||||
it('removes event listener when handler property is set to null', async () => {
|
||||
let onNavigateCalled = false;
|
||||
locals.navigation.onnavigate = () => {
|
||||
onNavigateCalled = true;
|
||||
};
|
||||
locals.navigation.onnavigate = null;
|
||||
|
||||
await locals.navigation.navigate('/test').finished;
|
||||
expect(onNavigateCalled).toBeFalse();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user