From 97a941a9f2a6cace2de915165d4d9094d0fb1662 Mon Sep 17 00:00:00 2001 From: Maksim Sadym Date: Mon, 9 Dec 2024 16:05:15 +0100 Subject: [PATCH] chore: (no-op) move navigation logic to NavigationTracker --- .../modules/context/BrowsingContextImpl.ts | 288 +++++------------- .../modules/context/NavigationTracker.ts | 281 +++++++++++++++++ 2 files changed, 349 insertions(+), 220 deletions(-) create mode 100644 src/bidiMapper/modules/context/NavigationTracker.ts diff --git a/src/bidiMapper/modules/context/BrowsingContextImpl.ts b/src/bidiMapper/modules/context/BrowsingContextImpl.ts index 5f44384ff7..4b3f897557 100644 --- a/src/bidiMapper/modules/context/BrowsingContextImpl.ts +++ b/src/bidiMapper/modules/context/BrowsingContextImpl.ts @@ -33,9 +33,8 @@ import { import {assert} from '../../../utils/assert.js'; import {Deferred} from '../../../utils/Deferred.js'; import {type LoggerFn, LogType} from '../../../utils/log.js'; +import {getTimestamp} from '../../../utils/time.js'; import {inchesFromCm} from '../../../utils/unitConversions.js'; -import {urlMatchesAboutBlank} from '../../../utils/UrlHelpers.js'; -import {uuidv4} from '../../../utils/uuid.js'; import type {CdpTarget} from '../cdp/CdpTarget.js'; import type {Realm} from '../script/Realm.js'; import type {RealmStorage} from '../script/RealmStorage.js'; @@ -43,10 +42,13 @@ import {WindowRealm} from '../script/WindowRealm.js'; import type {EventManager} from '../session/EventManager.js'; import type {BrowsingContextStorage} from './BrowsingContextStorage.js'; +import {NavigationTracker} from './NavigationTracker.js'; export class BrowsingContextImpl { static readonly LOGGER_PREFIX = `${LogType.debug}:browsingContext` as const; + /** Direct children browsing contexts. */ + readonly #children = new Set(); /** The ID of this browsing context. */ readonly #id: BrowsingContext.BrowsingContext; readonly userContext: string; @@ -55,61 +57,29 @@ export class BrowsingContextImpl { * The ID of the parent browsing context. * If null, this is a top-level context. */ + #loaderId?: Protocol.Network.LoaderId; #parentId: BrowsingContext.BrowsingContext | null = null; - - /** Direct children browsing contexts. */ - readonly #children = new Set(); - - readonly #browsingContextStorage: BrowsingContextStorage; + // Keeps track of the previously set viewport. + #previousViewport: {width: number; height: number} = {width: 0, height: 0}; + #originalOpener?: string; #lifecycle = { DOMContentLoaded: new Deferred(), load: new Deferred(), }; - #navigation = { - withinDocument: new Deferred(), - }; - - #url: string; - readonly #eventManager: EventManager; - readonly #realmStorage: RealmStorage; - #loaderId?: Protocol.Network.LoaderId; #cdpTarget: CdpTarget; - // The deferred will be resolved when the default realm is created. #defaultRealmDeferred = new Deferred(); + readonly #browsingContextStorage: BrowsingContextStorage; + readonly #eventManager: EventManager; readonly #logger?: LoggerFn; - // Keeps track of the previously set viewport. - #previousViewport: {width: number; height: number} = {width: 0, height: 0}; - - // The URL of the navigation that is currently in progress. A workaround of the CDP - // lacking URL for the pending navigation events, e.g. `Page.frameStartedLoading`. - // Set on `Page.navigate`, `Page.reload` commands, on `Page.frameRequestedNavigation` or - // on a deprecated `Page.frameScheduledNavigation` event. The latest is required as the - // `Page.frameRequestedNavigation` event is not emitted for same-document navigations. - #pendingNavigationUrl: string | undefined; - // Navigation ID is required, as CDP `loaderId` cannot be mapped 1:1 to all the - // navigations (e.g. same document navigations). Updated after each navigation, - // including same-document ones. - #navigationId: string = uuidv4(); - // When a new navigation is started via `BrowsingContext.navigate` with `wait` set to - // `None`, the command result should have `navigation` value, but mapper does not have - // it yet. This value will be set to `navigationId` after next . - #pendingNavigationId: string | undefined; - // Set if there is a pending navigation initiated by `BrowsingContext.navigate` command. - // The promise is resolved when the navigation is finished or rejected when canceled. - #pendingCommandNavigation: Deferred | undefined; - // Flags if the initial navigation to `about:blank` is in progress. - #initialNavigation = true; - // Flags if the navigation is initiated by `browsingContext.navigate` or - // `browsingContext.reload` command. - #navigationInitiatedByCommand = false; - - #originalOpener?: string; + readonly #navigationTracker: NavigationTracker; + readonly #realmStorage: RealmStorage; + // The deferred will be resolved when the default realm is created. + readonly #unhandledPromptBehavior?: Session.UserPromptHandler; // Set when the user prompt is opened. Required to provide the type in closing event. #lastUserPromptType?: BrowsingContext.UserPromptType; - readonly #unhandledPromptBehavior?: Session.UserPromptHandler; private constructor( id: BrowsingContext.BrowsingContext, @@ -133,9 +103,9 @@ export class BrowsingContextImpl { this.#realmStorage = realmStorage; this.#unhandledPromptBehavior = unhandledPromptBehavior; this.#logger = logger; - this.#url = url; - this.#originalOpener = originalOpener; + + this.#navigationTracker = new NavigationTracker(url, id, eventManager); } static create( @@ -208,14 +178,6 @@ export class BrowsingContextImpl { return context; } - static getTimestamp(): number { - // `timestamp` from the event is MonotonicTime, not real time, so - // the best Mapper can do is to set the timestamp to the epoch time - // of the event arrived. - // https://chromedevtools.github.io/devtools-protocol/tot/Network/#type-MonotonicTime - return new Date().getTime(); - } - /** * @see https://html.spec.whatwg.org/multipage/document-sequences.html#navigable */ @@ -224,13 +186,11 @@ export class BrowsingContextImpl { } get navigationId(): string { - return this.#navigationId; + return this.#navigationTracker.currentNavigationId; } dispose(emitContextDestroyed: boolean) { - this.#pendingCommandNavigation?.reject( - new UnknownErrorException('navigation canceled by context disposal'), - ); + this.#navigationTracker.dispose(); this.#deleteAllChildren(); this.#realmStorage.deleteRealms({ @@ -345,7 +305,7 @@ export class BrowsingContextImpl { } get url(): string { - return this.#url; + return this.#navigationTracker.url; } async lifecycleLoaded() { @@ -412,15 +372,17 @@ export class BrowsingContextImpl { } onTargetInfoChanged(params: Protocol.Target.TargetInfoChangedEvent) { - this.#url = params.targetInfo.url; + this.#navigationTracker.onTargetInfoChanged(params.targetInfo.url); } + #initListeners() { this.#cdpTarget.cdpClient.on('Page.frameNavigated', (params) => { if (this.id !== params.frame.id) { return; } - this.#url = params.frame.url + (params.frame.urlFragment ?? ''); - this.#pendingNavigationUrl = undefined; + this.#navigationTracker.frameNavigated( + params.frame.url + (params.frame.urlFragment ?? ''), + ); // At the point the page is initialized, all the nested iframes from the // previous page are detached and realms are destroyed. @@ -432,41 +394,24 @@ export class BrowsingContextImpl { if (this.id !== params.frameId) { return; } + this.#navigationTracker.navigatedWithinDocument( + params.url, + params.navigationType, + ); if (params.navigationType === 'historyApi') { - this.#url = params.url; this.#eventManager.registerEvent( { type: 'event', method: 'browsingContext.historyUpdated', params: { context: this.id, - url: this.#url, + url: this.#navigationTracker.url, }, }, this.id, ); return; } - this.#pendingNavigationUrl = undefined; - const timestamp = BrowsingContextImpl.getTimestamp(); - this.#url = params.url; - this.#navigation.withinDocument.resolve(); - - if (params.navigationType === 'fragment') { - this.#eventManager.registerEvent( - { - type: 'event', - method: ChromiumBidi.BrowsingContext.EventNames.FragmentNavigated, - params: { - context: this.id, - navigation: this.#navigationId, - timestamp, - url: this.#url, - }, - }, - this.id, - ); - } }); this.#cdpTarget.cdpClient.on('Page.frameStartedLoading', (params) => { @@ -474,33 +419,7 @@ export class BrowsingContextImpl { return; } - if (this.#navigationInitiatedByCommand) { - // In case of the navigation is initiated by `browsingContext.navigate` or - // `browsingContext.reload` commands, the `Page.frameRequestedNavigation` is not - // emitted, which means the `NavigationStarted` is not emitted. - // TODO: consider emit it right after the CDP command `navigate` or `reload` is finished. - - // The URL of the navigation that is currently in progress. Although the URL - // is not yet known in case of user-initiated navigations, it is possible to - // provide the URL in case of BiDi-initiated navigations. - // TODO: provide proper URL in case of user-initiated navigations. - const url = this.#pendingNavigationUrl ?? 'UNKNOWN'; - this.#navigationId = this.#pendingNavigationId ?? uuidv4(); - this.#pendingNavigationId = undefined; - this.#eventManager.registerEvent( - { - type: 'event', - method: ChromiumBidi.BrowsingContext.EventNames.NavigationStarted, - params: { - context: this.id, - navigation: this.#navigationId, - timestamp: BrowsingContextImpl.getTimestamp(), - url, - }, - }, - this.id, - ); - } + this.#navigationTracker.frameStartedLoading(); }); // TODO: don't use deprecated `Page.frameScheduledNavigation` event. @@ -508,7 +427,7 @@ export class BrowsingContextImpl { if (this.id !== params.frameId) { return; } - this.#pendingNavigationUrl = params.url; + this.#navigationTracker.frameScheduledNavigation(params.url); }); this.#cdpTarget.cdpClient.on('Page.frameRequestedNavigation', (params) => { @@ -516,54 +435,7 @@ export class BrowsingContextImpl { return; } - if (this.#pendingCommandNavigation !== undefined) { - // The pending navigation was aborted by the new one. - this.#eventManager.registerEvent( - { - type: 'event', - method: ChromiumBidi.BrowsingContext.EventNames.NavigationAborted, - params: { - context: this.id, - navigation: this.#navigationId, - timestamp: BrowsingContextImpl.getTimestamp(), - url: this.#url, - }, - }, - this.id, - ); - this.#pendingCommandNavigation.reject( - new UnknownErrorException('navigation aborted'), - ); - this.#pendingCommandNavigation = undefined; - this.#navigationInitiatedByCommand = false; - } - if (!urlMatchesAboutBlank(params.url)) { - // If the url does not match about:blank, do not consider it is an initial - // navigation and emit all the required events. - // https://github.com/GoogleChromeLabs/chromium-bidi/issues/2793. - this.#initialNavigation = false; - } - - if (!this.#initialNavigation) { - // Do not emit the event for the initial navigation to `about:blank`. - this.#navigationId = this.#pendingNavigationId ?? uuidv4(); - this.#pendingNavigationId = undefined; - this.#eventManager.registerEvent( - { - type: 'event', - method: ChromiumBidi.BrowsingContext.EventNames.NavigationStarted, - params: { - context: this.id, - navigation: this.#navigationId, - timestamp: BrowsingContextImpl.getTimestamp(), - url: params.url, - }, - }, - this.id, - ); - } - - this.#pendingNavigationUrl = params.url; + this.#navigationTracker.frameRequestedNavigation(params.url); }); this.#cdpTarget.cdpClient.on('Page.lifecycleEvent', (params) => { @@ -593,11 +465,9 @@ export class BrowsingContextImpl { return; } - const timestamp = BrowsingContextImpl.getTimestamp(); - switch (params.name) { case 'DOMContentLoaded': - if (!this.#initialNavigation) { + if (!this.#navigationTracker.initialNavigation) { // Do not emit for the initial navigation. this.#eventManager.registerEvent( { @@ -606,9 +476,9 @@ export class BrowsingContextImpl { ChromiumBidi.BrowsingContext.EventNames.DomContentLoaded, params: { context: this.id, - navigation: this.#navigationId, - timestamp, - url: this.#url, + navigation: this.#navigationTracker.currentNavigationId, + timestamp: getTimestamp(), + url: this.#navigationTracker.url, }, }, this.id, @@ -618,7 +488,7 @@ export class BrowsingContextImpl { break; case 'load': - if (!this.#initialNavigation) { + if (!this.#navigationTracker.initialNavigation) { // Do not emit for the initial navigation. this.#eventManager.registerEvent( { @@ -626,16 +496,16 @@ export class BrowsingContextImpl { method: ChromiumBidi.BrowsingContext.EventNames.Load, params: { context: this.id, - navigation: this.#navigationId, - timestamp, - url: this.#url, + navigation: this.#navigationTracker.currentNavigationId, + timestamp: getTimestamp(), + url: this.#navigationTracker.url, }, }, this.id, ); } // The initial navigation is finished. - this.#initialNavigation = false; + this.#navigationTracker.lifecycleEventLoad(); this.#lifecycle.load.resolve(); break; } @@ -857,14 +727,7 @@ export class BrowsingContextImpl { #documentChanged(loaderId?: Protocol.Network.LoaderId) { if (loaderId === undefined || this.#loaderId === loaderId) { // Same document navigation. Document didn't change. - if (this.#navigation.withinDocument.isFinished) { - this.#navigation.withinDocument = new Deferred(); - } else { - this.#logger?.( - BrowsingContextImpl.LOGGER_PREFIX, - 'Document changed (navigatedWithinDocument)', - ); - } + this.#navigationTracker.navigationFinishedWithinSameDocument(); return; } @@ -919,20 +782,8 @@ export class BrowsingContextImpl { throw new InvalidArgumentException(`Invalid URL: ${url}`); } - this.#pendingCommandNavigation?.reject( - new UnknownErrorException('navigation canceled by concurrent navigation'), - ); - await this.targetUnblockedOrThrow(); - - // Set the pending navigation URL to provide it in `browsingContext.navigationStarted` - // event. - // TODO: detect navigation start not from CDP. Check if - // `Page.frameRequestedNavigation` can be used for this purpose. - this.#pendingNavigationUrl = url; - const navigationId = uuidv4(); - this.#pendingNavigationId = navigationId; - this.#pendingCommandNavigation = new Deferred(); - this.#navigationInitiatedByCommand = true; + const commandNavigation = + this.#navigationTracker.createCommandNavigation(url); // Navigate and wait for the result. If the navigation fails, the error event is // emitted and the promise is rejected. @@ -947,21 +798,7 @@ export class BrowsingContextImpl { if (cdpNavigateResult.errorText) { // If navigation failed, no pending navigation is left. - this.#pendingNavigationUrl = undefined; - this.#eventManager.registerEvent( - { - type: 'event', - method: ChromiumBidi.BrowsingContext.EventNames.NavigationFailed, - params: { - context: this.id, - navigation: navigationId, - timestamp: BrowsingContextImpl.getTimestamp(), - url, - }, - }, - this.id, - ); - + this.#navigationTracker.failCommandNavigation(commandNavigation); throw new UnknownErrorException(cdpNavigateResult.errorText); } @@ -971,11 +808,10 @@ export class BrowsingContextImpl { if (wait === BrowsingContext.ReadinessState.None) { // Do not wait for the result of the navigation promise. - this.#pendingCommandNavigation?.resolve(); - this.#pendingCommandNavigation = undefined; + this.#navigationTracker.finishCommandNavigation(commandNavigation, true); return { - navigation: navigationId, + navigation: commandNavigation.navigationId, url, }; } @@ -987,7 +823,7 @@ export class BrowsingContextImpl { // No `loaderId` means same-document navigation. this.#waitNavigation(wait, cdpNavigateResult.loaderId === undefined), // Throw an error if the navigation is canceled. - this.#pendingCommandNavigation, + this.#navigationTracker.pendingCommandNavigation, ]).catch((e) => { // Aborting navigation should not fail the original navigation command for now. // https://github.com/w3c/webdriver-bidi/issues/799#issue-2605618955 @@ -997,13 +833,11 @@ export class BrowsingContextImpl { }); // `#pendingCommandNavigation` can be already rejected and set to undefined. - this.#pendingCommandNavigation?.resolve(); - this.#navigationInitiatedByCommand = false; - this.#pendingCommandNavigation = undefined; + this.#navigationTracker.finishCommandNavigation(commandNavigation, false); return { - navigation: navigationId, + navigation: commandNavigation.navigationId, // Url can change due to redirect. Get the latest one. - url: this.#url, + url: this.#navigationTracker.url, }; } @@ -1012,7 +846,7 @@ export class BrowsingContextImpl { withinDocument: boolean, ) { if (withinDocument) { - await this.#navigation.withinDocument; + await this.#navigationTracker.navigation.withinDocument; return; } switch (wait) { @@ -1036,7 +870,9 @@ export class BrowsingContextImpl { this.#resetLifecycleIfFinished(); - this.#navigationInitiatedByCommand = true; + const commandNavigation = this.#navigationTracker.createCommandNavigation( + this.#navigationTracker.url, + ); await this.#cdpTarget.cdpClient.sendCommand('Page.reload', { ignoreCache, @@ -1044,17 +880,29 @@ export class BrowsingContextImpl { switch (wait) { case BrowsingContext.ReadinessState.None: + this.#navigationTracker.finishCommandNavigation( + commandNavigation, + true, + ); break; case BrowsingContext.ReadinessState.Interactive: await this.#lifecycle.DOMContentLoaded; + this.#navigationTracker.finishCommandNavigation( + commandNavigation, + false, + ); break; case BrowsingContext.ReadinessState.Complete: await this.#lifecycle.load; + this.#navigationTracker.finishCommandNavigation( + commandNavigation, + false, + ); break; } return { - navigation: this.#navigationId, + navigation: this.#navigationTracker.currentNavigationId, url: this.url, }; } diff --git a/src/bidiMapper/modules/context/NavigationTracker.ts b/src/bidiMapper/modules/context/NavigationTracker.ts new file mode 100644 index 0000000000..d6fb337a0b --- /dev/null +++ b/src/bidiMapper/modules/context/NavigationTracker.ts @@ -0,0 +1,281 @@ +/* + * Copyright 2024 Google LLC. + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import type {Protocol} from 'devtools-protocol'; + +import { + ChromiumBidi, + UnknownErrorException, +} from '../../../protocol/protocol.js'; +import {Deferred} from '../../../utils/Deferred.js'; +import {getTimestamp} from '../../../utils/time.js'; +import {urlMatchesAboutBlank} from '../../../utils/UrlHelpers.js'; +import {uuidv4} from '../../../utils/uuid.js'; +import type {EventManager} from '../session/EventManager.js'; + +class NavigationState { + readonly navigationId = uuidv4(); + url?: string; + + constructor(url?: string) { + this.url = url; + } +} + +export class NavigationTracker { + readonly #eventManager: EventManager; + readonly #browsingContextId: string; + #currentNavigation = new NavigationState(); + // When a new navigation is started via `BrowsingContext.navigate` with `wait` set to + // `None`, the command result should have `navigation` value, but mapper does not have + // it yet. This value will be set to `navigationId` after next . + #pendingNavigation?: NavigationState; + + #url: string; + // The URL of the navigation that is currently in progress. A workaround of the CDP + // lacking URL for the pending navigation events, e.g. `Page.frameStartedLoading`. + // Set on `Page.navigate`, `Page.reload` commands, on `Page.frameRequestedNavigation` or + // on a deprecated `Page.frameScheduledNavigation` event. The latest is required as the + // `Page.frameRequestedNavigation` event is not emitted for same-document navigations. + #pendingNavigationUrl: string | undefined; + + // Flags if the initial navigation to `about:blank` is in progress. + #initialNavigation = true; + // Flags if the navigation is initiated by `browsingContext.navigate` or + // `browsingContext.reload` command. + #navigationInitiatedByCommand = false; + + // Set if there is a pending navigation initiated by `BrowsingContext.navigate` command. + // The promise is resolved when the navigation is finished or rejected when canceled. + #pendingCommandNavigation: Deferred | undefined; + + navigation = { + withinDocument: new Deferred(), + }; + + constructor( + url: string, + browsingContextId: string, + eventManager: EventManager, + ) { + this.#browsingContextId = browsingContextId; + this.#url = url; + this.#eventManager = eventManager; + } + + get currentNavigationId() { + return this.#currentNavigation.navigationId; + } + + get initialNavigation(): boolean { + return this.#initialNavigation; + } + + get pendingCommandNavigation(): Deferred | undefined { + return this.#pendingCommandNavigation; + } + + get url(): string { + return this.#url; + } + + dispose() { + this.#pendingCommandNavigation?.reject( + new UnknownErrorException('navigation canceled by context disposal'), + ); + } + + onTargetInfoChanged(url: string) { + this.#url = url; + } + + frameNavigated(url: string) { + this.#url = url; + this.#pendingNavigationUrl = undefined; + } + + navigatedWithinDocument( + url: string, + navigationType: Protocol.Page.NavigatedWithinDocumentEvent['navigationType'], + ) { + this.#pendingNavigationUrl = undefined; + const timestamp = getTimestamp(); + this.#url = url; + this.navigation.withinDocument.resolve(); + + if (navigationType === 'fragment') { + this.#eventManager.registerEvent( + { + type: 'event', + method: ChromiumBidi.BrowsingContext.EventNames.FragmentNavigated, + params: { + context: this.#browsingContextId, + navigation: this.#currentNavigation.navigationId, + timestamp, + url: this.#url, + }, + }, + this.#browsingContextId, + ); + } + } + + frameStartedLoading() { + if (this.#navigationInitiatedByCommand) { + // In case of the navigation is initiated by `browsingContext.navigate` or + // `browsingContext.reload` commands, the `Page.frameRequestedNavigation` is not + // emitted, which means the `NavigationStarted` is not emitted. + // TODO: consider emit it right after the CDP command `navigate` or `reload` is finished. + + // The URL of the navigation that is currently in progress. Although the URL + // is not yet known in case of user-initiated navigations, it is possible to + // provide the URL in case of BiDi-initiated navigations. + // TODO: provide proper URL in case of user-initiated navigations. + const url = this.#pendingNavigationUrl ?? 'UNKNOWN'; + this.#currentNavigation = + this.#pendingNavigation ?? new NavigationState(); + this.#pendingNavigation = undefined; + this.#eventManager.registerEvent( + { + type: 'event', + method: ChromiumBidi.BrowsingContext.EventNames.NavigationStarted, + params: { + context: this.#browsingContextId, + navigation: this.#currentNavigation.navigationId, + timestamp: getTimestamp(), + url, + }, + }, + this.#browsingContextId, + ); + } + } + + frameScheduledNavigation(url: string) { + this.#pendingNavigationUrl = url; + } + + frameRequestedNavigation(url: string) { + if (this.#pendingCommandNavigation !== undefined) { + // The pending navigation was aborted by the new one. + this.#eventManager.registerEvent( + { + type: 'event', + method: ChromiumBidi.BrowsingContext.EventNames.NavigationAborted, + params: { + context: this.#browsingContextId, + navigation: this.#currentNavigation.navigationId, + timestamp: getTimestamp(), + url: this.#url, + }, + }, + this.#browsingContextId, + ); + this.#pendingCommandNavigation.reject( + new UnknownErrorException('navigation aborted'), + ); + this.#pendingCommandNavigation = undefined; + this.#navigationInitiatedByCommand = false; + } + if (!urlMatchesAboutBlank(url)) { + // If the url does not match about:blank, do not consider it is an initial + // navigation and emit all the required events. + // https://github.com/GoogleChromeLabs/chromium-bidi/issues/2793. + this.#initialNavigation = false; + } + + if (!this.#initialNavigation) { + // Do not emit the event for the initial navigation to `about:blank`. + this.#currentNavigation = + this.#pendingNavigation ?? new NavigationState(); + this.#pendingNavigation = undefined; + this.#eventManager.registerEvent( + { + type: 'event', + method: ChromiumBidi.BrowsingContext.EventNames.NavigationStarted, + params: { + context: this.#browsingContextId, + navigation: this.#currentNavigation.navigationId, + timestamp: getTimestamp(), + url, + }, + }, + this.#browsingContextId, + ); + } + + this.#pendingNavigationUrl = url; + } + + navigationFinishedWithinSameDocument() { + if (this.navigation.withinDocument.isFinished) { + this.navigation.withinDocument = new Deferred(); + } + } + + lifecycleEventLoad() { + this.#initialNavigation = false; + } + + createCommandNavigation(url: string): NavigationState { + this.#pendingCommandNavigation?.reject( + new UnknownErrorException('navigation canceled by concurrent navigation'), + ); + // Set the pending navigation URL to provide it in `browsingContext.navigationStarted` + // event. + // TODO: detect navigation start not from CDP. Check if + // `Page.frameRequestedNavigation` can be used for this purpose. + this.#pendingNavigationUrl = url; + const navigation = new NavigationState(url); + this.#pendingNavigation = navigation; + this.#pendingCommandNavigation = new Deferred(); + this.#navigationInitiatedByCommand = true; + + return navigation; + } + + failCommandNavigation(navigation: NavigationState) { + // If navigation failed, no pending navigation is left. + this.#pendingNavigationUrl = undefined; + this.#eventManager.registerEvent( + { + type: 'event', + method: ChromiumBidi.BrowsingContext.EventNames.NavigationFailed, + params: { + context: this.#browsingContextId, + navigation: this.#currentNavigation.navigationId, + timestamp: getTimestamp(), + url: navigation.url ?? 'UNKNOWN', + }, + }, + this.#browsingContextId, + ); + } + + finishCommandNavigation( + navigation: NavigationState, + finishedByWaitNone: boolean, + ) { + // `#pendingCommandNavigation` can be already rejected and set to undefined. + this.#pendingCommandNavigation?.resolve(); + if (!finishedByWaitNone) { + this.#navigationInitiatedByCommand = false; + } + this.#pendingCommandNavigation = undefined; + } +}