Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: refactor navigation #2881

Merged
merged 22 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading