Skip to content

Commit

Permalink
chore: refactor navigation (#2881)
Browse files Browse the repository at this point in the history
Addressing [Align navigation with the
spec](#2856).

Navigation tracker relies on the following events:
* `Page.frameNavigated`
* TargetEvents.FrameStartedNavigating (`Network.requestWillBeSent`)
* `Page.navigatedWithinDocument`
* `Page.frameRequestedNavigation`
* `Page.javascriptDialogOpening:beforeunload`

---------

Co-authored-by: browser-automation-bot <[email protected]>
  • Loading branch information
sadym-chromium and browser-automation-bot authored Dec 16, 2024
1 parent 003dca4 commit 960531f
Show file tree
Hide file tree
Showing 18 changed files with 2,000 additions and 620 deletions.
176 changes: 100 additions & 76 deletions src/bidiMapper/modules/context/BrowsingContextImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,11 @@ import {WindowRealm} from '../script/WindowRealm.js';
import type {EventManager} from '../session/EventManager.js';

import type {BrowsingContextStorage} from './BrowsingContextStorage.js';
import {NavigationTracker} from './NavigationTracker.js';
import {
NavigationEventName,
NavigationResult,
NavigationTracker,
} from './NavigationTracker.js';

export class BrowsingContextImpl {
static readonly LOGGER_PREFIX = `${LogType.debug}:browsingContext` as const;
Expand Down Expand Up @@ -106,7 +110,12 @@ export class BrowsingContextImpl {
this.#logger = logger;
this.#originalOpener = originalOpener;

this.#navigationTracker = new NavigationTracker(url, id, eventManager);
this.#navigationTracker = new NavigationTracker(
url,
id,
eventManager,
logger,
);
}

static create(
Expand Down Expand Up @@ -377,18 +386,32 @@ export class BrowsingContextImpl {
}

#initListeners() {
this.#cdpTarget.cdpClient.on('Network.loadingFailed', (params) => {
// Detect navigation errors like `net::ERR_BLOCKED_BY_RESPONSE`.
// Network related to navigation has request id equals to navigation's loader id.
this.#navigationTracker.networkLoadingFailed(
params.requestId,
params.errorText,
);
});

this.#cdpTarget.cdpClient.on('Page.frameNavigated', (params) => {
if (this.id !== params.frame.id) {
return;
}
this.#navigationTracker.frameNavigated(
params.frame.url + (params.frame.urlFragment ?? ''),
params.frame.loaderId,
// `unreachableUrl` indicates if the navigation failed.
params.frame.unreachableUrl,
);

// At the point the page is initialized, all the nested iframes from the
// previous page are detached and realms are destroyed.
// Delete children from context.
this.#deleteAllChildren();

this.#documentChanged(params.frame.loaderId);
});

this.#cdpTarget.on(TargetEvents.FrameStartedNavigating, (params) => {
Expand All @@ -397,6 +420,21 @@ export class BrowsingContextImpl {
`Received ${TargetEvents.FrameStartedNavigating} event`,
params,
);

// The frame ID can be either a browsing context id, or not set in case of the frame
// is the top-level in the current CDP target.
const possibleFrameIds = [
this.id,
...(this.cdpTarget.id === this.id ? [undefined] : []),
];
if (!possibleFrameIds.includes(params.frameId)) {
return;
}

this.#navigationTracker.frameStartedNavigating(
params.url,
params.loaderId,
);
});

this.#cdpTarget.cdpClient.on('Page.navigatedWithinDocument', (params) => {
Expand All @@ -423,22 +461,6 @@ export class BrowsingContextImpl {
}
});

this.#cdpTarget.cdpClient.on('Page.frameStartedLoading', (params) => {
if (this.id !== params.frameId) {
return;
}

this.#navigationTracker.frameStartedLoading();
});

// TODO: don't use deprecated `Page.frameScheduledNavigation` event.
this.#cdpTarget.cdpClient.on('Page.frameScheduledNavigation', (params) => {
if (this.id !== params.frameId) {
return;
}
this.#navigationTracker.frameScheduledNavigation(params.url);
});

this.#cdpTarget.cdpClient.on('Page.frameRequestedNavigation', (params) => {
if (this.id !== params.frameId) {
return;
Expand Down Expand Up @@ -476,7 +498,7 @@ export class BrowsingContextImpl {

switch (params.name) {
case 'DOMContentLoaded':
if (!this.#navigationTracker.initialNavigation) {
if (!this.#navigationTracker.isInitialNavigation) {
// Do not emit for the initial navigation.
this.#eventManager.registerEvent(
{
Expand All @@ -497,7 +519,7 @@ export class BrowsingContextImpl {
break;

case 'load':
if (!this.#navigationTracker.initialNavigation) {
if (!this.#navigationTracker.isInitialNavigation) {
// Do not emit for the initial navigation.
this.#eventManager.registerEvent(
{
Expand All @@ -514,7 +536,7 @@ export class BrowsingContextImpl {
);
}
// The initial navigation is finished.
this.#navigationTracker.lifecycleEventLoad();
this.#navigationTracker.loadPageEvent(params.loaderId);
this.#lifecycle.load.resolve();
break;
}
Expand Down Expand Up @@ -646,6 +668,9 @@ export class BrowsingContextImpl {

this.#cdpTarget.cdpClient.on('Page.javascriptDialogOpening', (params) => {
const promptType = BrowsingContextImpl.#getPromptType(params.type);
if (params.type === 'beforeunload') {
this.#navigationTracker.beforeunload();
}
// Set the last prompt type to provide it in closing event.
this.#lastUserPromptType = promptType;
const promptHandler = this.#getPromptHandler(promptType);
Expand Down Expand Up @@ -735,8 +760,6 @@ export class BrowsingContextImpl {

#documentChanged(loaderId?: Protocol.Network.LoaderId) {
if (loaderId === undefined || this.#loaderId === loaderId) {
// Same document navigation. Document didn't change.
this.#navigationTracker.navigationFinishedWithinSameDocument();
return;
}

Expand Down Expand Up @@ -792,7 +815,7 @@ export class BrowsingContextImpl {
}

const commandNavigation =
this.#navigationTracker.createCommandNavigation(url);
this.#navigationTracker.createPendingNavigation(url);

// Navigate and wait for the result. If the navigation fails, the error event is
// emitted and the promise is rejected.
Expand All @@ -807,64 +830,67 @@ export class BrowsingContextImpl {

if (cdpNavigateResult.errorText) {
// If navigation failed, no pending navigation is left.
this.#navigationTracker.failCommandNavigation(commandNavigation);
this.#navigationTracker.failNavigation(
commandNavigation,
cdpNavigateResult.errorText,
);
throw new UnknownErrorException(cdpNavigateResult.errorText);
}

this.#navigationTracker.navigationCommandFinished(
commandNavigation,
cdpNavigateResult.loaderId,
);

this.#documentChanged(cdpNavigateResult.loaderId);
return cdpNavigateResult;
})();

if (wait === BrowsingContext.ReadinessState.None) {
// Do not wait for the result of the navigation promise.
this.#navigationTracker.finishCommandNavigation(commandNavigation, true);

return {
navigation: commandNavigation.navigationId,
url,
};
}

const cdpNavigateResult = await cdpNavigatePromise;

// Wait for either the navigation is finished or canceled by another navigation.
await Promise.race([
const result = await Promise.race([
// No `loaderId` means same-document navigation.
this.#waitNavigation(wait, cdpNavigateResult.loaderId === undefined),
this.#waitNavigation(wait, cdpNavigatePromise),
// Throw an error if the navigation is canceled.
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
if (e.message !== 'navigation aborted') {
throw e;
commandNavigation.finished,
]);

if (result instanceof NavigationResult) {
if (
// TODO: check after decision on the spec is done:
// https://github.com/w3c/webdriver-bidi/issues/799.
result.eventName === NavigationEventName.NavigationAborted ||
result.eventName === NavigationEventName.NavigationFailed
) {
throw new UnknownErrorException(result.message ?? 'unknown exception');
}
});
}

// `#pendingCommandNavigation` can be already rejected and set to undefined.
this.#navigationTracker.finishCommandNavigation(commandNavigation, false);
return {
navigation: commandNavigation.navigationId,
// Url can change due to redirect. Get the latest one.
url: this.#navigationTracker.url,
// Url can change due to redirects. Get the one from commandNavigation.
url: commandNavigation.url,
};
}

async #waitNavigation(
wait: BrowsingContext.ReadinessState,
withinDocument: boolean,
cdpCommandPromise: Promise<void>,
) {
if (withinDocument) {
await this.#navigationTracker.navigation.withinDocument;
return;
}
switch (wait) {
case BrowsingContext.ReadinessState.None:
return;
case BrowsingContext.ReadinessState.Interactive:
await cdpCommandPromise;
await this.#lifecycle.DOMContentLoaded;
return;
case BrowsingContext.ReadinessState.Complete:
await cdpCommandPromise;
await this.#lifecycle.load;
return;
}
Expand All @@ -879,40 +905,38 @@ export class BrowsingContextImpl {

this.#resetLifecycleIfFinished();

const commandNavigation = this.#navigationTracker.createCommandNavigation(
const commandNavigation = this.#navigationTracker.createPendingNavigation(
this.#navigationTracker.url,
);

await this.#cdpTarget.cdpClient.sendCommand('Page.reload', {
ignoreCache,
});
const cdpReloadPromise = this.#cdpTarget.cdpClient.sendCommand(
'Page.reload',
{
ignoreCache,
},
);

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;
// Wait for either the navigation is finished or canceled by another navigation.
const result = await Promise.race([
// No `loaderId` means same-document navigation.
this.#waitNavigation(wait, cdpReloadPromise),
// Throw an error if the navigation is canceled.
commandNavigation.finished,
]);

if (result instanceof NavigationResult) {
if (
result.eventName === NavigationEventName.NavigationAborted ||
result.eventName === NavigationEventName.NavigationFailed
) {
throw new UnknownErrorException(result.message ?? 'unknown exception');
}
}

return {
navigation: this.#navigationTracker.currentNavigationId,
url: this.url,
navigation: commandNavigation.navigationId,
// Url can change due to redirects. Get the one from commandNavigation.
url: commandNavigation.url,
};
}

Expand Down
Loading

0 comments on commit 960531f

Please sign in to comment.