refactor: adopt the rest of bidi/core (#11836)

This commit is contained in:
jrandolf 2024-02-12 17:28:21 +01:00 committed by GitHub
parent d3f00bf032
commit e9f9f4c356
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 1352 additions and 2143 deletions

View File

@ -19,22 +19,15 @@ import {BrowserContextEvent} from '../api/BrowserContext.js';
import type {Page} from '../api/Page.js';
import type {Target} from '../api/Target.js';
import {UnsupportedOperation} from '../common/Errors.js';
import type {Handler} from '../common/EventEmitter.js';
import {debugError} from '../common/util.js';
import type {Viewport} from '../common/Viewport.js';
import {BidiBrowserContext} from './BrowserContext.js';
import {BrowsingContext, BrowsingContextEvent} from './BrowsingContext.js';
import type {BidiConnection} from './Connection.js';
import type {Browser as BrowserCore} from './core/Browser.js';
import {Session} from './core/Session.js';
import {UserContext} from './core/UserContext.js';
import {
BiDiBrowserTarget,
BiDiBrowsingContextTarget,
BiDiPageTarget,
type BidiTarget,
} from './Target.js';
import type {UserContext} from './core/UserContext.js';
import {BidiBrowserTarget} from './Target.js';
/**
* @internal
@ -89,7 +82,6 @@ export class BidiBrowser extends Browser {
const browser = new BidiBrowser(session.browser, opts);
browser.#initialize();
await browser.#getTree();
return browser;
}
@ -97,20 +89,8 @@ export class BidiBrowser extends Browser {
#closeCallback?: BrowserCloseCallback;
#browserCore: BrowserCore;
#defaultViewport: Viewport | null;
#targets = new Map<string, BidiTarget>();
#browserContexts = new WeakMap<UserContext, BidiBrowserContext>();
#browserTarget: BiDiBrowserTarget;
#connectionEventHandlers = new Map<
Bidi.BrowsingContextEvent['method'],
Handler<any>
>([
['browsingContext.contextCreated', this.#onContextCreated.bind(this)],
['browsingContext.contextDestroyed', this.#onContextDestroyed.bind(this)],
['browsingContext.domContentLoaded', this.#onContextDomLoaded.bind(this)],
['browsingContext.fragmentNavigated', this.#onContextNavigation.bind(this)],
['browsingContext.navigationStarted', this.#onContextNavigation.bind(this)],
]);
#target = new BidiBrowserTarget(this);
private constructor(browserCore: BrowserCore, opts: BidiBrowserOptions) {
super();
@ -118,13 +98,14 @@ export class BidiBrowser extends Browser {
this.#closeCallback = opts.closeCallback;
this.#browserCore = browserCore;
this.#defaultViewport = opts.defaultViewport;
this.#browserTarget = new BiDiBrowserTarget(this);
for (const context of this.#browserCore.userContexts) {
this.#createBrowserContext(context);
}
}
#initialize() {
// Initializing existing contexts.
for (const userContext of this.#browserCore.userContexts) {
this.#createBrowserContext(userContext);
}
this.#browserCore.once('disconnected', () => {
this.emit(BrowserEvent.Disconnected, undefined);
});
@ -132,10 +113,6 @@ export class BidiBrowser extends Browser {
this.#browserCore.dispose('Browser process exited.', true);
this.connection.dispose();
});
for (const [eventName, handler] of this.#connectionEventHandlers) {
this.connection.on(eventName, handler);
}
}
get #browserName() {
@ -145,85 +122,31 @@ export class BidiBrowser extends Browser {
return this.#browserCore.session.capabilities.browserVersion;
}
get cdpSupported(): boolean {
return !this.#browserName.toLocaleLowerCase().includes('firefox');
}
override userAgent(): never {
throw new UnsupportedOperation();
}
#createBrowserContext(userContext: UserContext) {
const browserContext = new BidiBrowserContext(this, userContext, {
const browserContext = BidiBrowserContext.from(this, userContext, {
defaultViewport: this.#defaultViewport,
});
this.#browserContexts.set(userContext, browserContext);
return browserContext;
}
#onContextDomLoaded(event: Bidi.BrowsingContext.Info) {
const target = this.#targets.get(event.context);
if (target) {
browserContext.on(BrowserContextEvent.TargetCreated, target => {
this.emit(BrowserEvent.TargetCreated, target);
});
browserContext.on(BrowserContextEvent.TargetChanged, target => {
this.emit(BrowserEvent.TargetChanged, target);
target.browserContext().emit(BrowserContextEvent.TargetChanged, target);
}
}
#onContextNavigation(event: Bidi.BrowsingContext.NavigationInfo) {
const target = this.#targets.get(event.context);
if (target) {
this.emit(BrowserEvent.TargetChanged, target);
target.browserContext().emit(BrowserContextEvent.TargetChanged, target);
}
}
#onContextCreated(event: Bidi.BrowsingContext.ContextCreated['params']) {
const context = new BrowsingContext(
this.connection,
event,
this.#browserName
);
this.connection.registerBrowsingContexts(context);
const browserContext =
event.userContext === UserContext.DEFAULT
? this.defaultBrowserContext()
: this.browserContexts().find(browserContext => {
return browserContext.id === event.userContext;
});
if (!browserContext) {
throw new Error('Missing browser contexts');
}
const target = !context.parent
? new BiDiPageTarget(browserContext, context)
: new BiDiBrowsingContextTarget(browserContext, context);
this.#targets.set(event.context, target);
this.emit(BrowserEvent.TargetCreated, target);
target.browserContext().emit(BrowserContextEvent.TargetCreated, target);
if (context.parent) {
const topLevel = this.connection.getTopLevelContext(context.parent);
topLevel.emit(BrowsingContextEvent.Created, context);
}
}
async #getTree(): Promise<void> {
const {result} = await this.connection.send('browsingContext.getTree', {});
for (const context of result.contexts) {
this.#onContextCreated(context);
}
}
async #onContextDestroyed(
event: Bidi.BrowsingContext.ContextDestroyed['params']
) {
const context = this.connection.getBrowsingContext(event.context);
const topLevelContext = this.connection.getTopLevelContext(event.context);
topLevelContext.emit(BrowsingContextEvent.Destroyed, context);
const target = this.#targets.get(event.context);
const page = await target?.page();
await page?.close().catch(debugError);
this.#targets.delete(event.context);
if (target) {
});
browserContext.on(BrowserContextEvent.TargetDestroyed, target => {
this.emit(BrowserEvent.TargetDestroyed, target);
target.browserContext().emit(BrowserContextEvent.TargetDestroyed, target);
}
});
return browserContext;
}
get connection(): BidiConnection {
@ -236,9 +159,6 @@ export class BidiBrowser extends Browser {
}
override async close(): Promise<void> {
for (const [eventName, handler] of this.#connectionEventHandlers) {
this.connection.off(eventName, handler);
}
if (this.connection.closed) {
return;
}
@ -255,7 +175,7 @@ export class BidiBrowser extends Browser {
}
override get connected(): boolean {
return !this.#browserCore.disposed;
return !this.#browserCore.disconnected;
}
override process(): ChildProcess | null {
@ -288,19 +208,16 @@ export class BidiBrowser extends Browser {
}
override targets(): Target[] {
return [this.#browserTarget, ...Array.from(this.#targets.values())];
return [
this.#target,
...this.browserContexts().flatMap(context => {
return context.targets();
}),
];
}
_getTargetById(id: string): BidiTarget {
const target = this.#targets.get(id);
if (!target) {
throw new Error('Target not found');
}
return target;
}
override target(): Target {
return this.#browserTarget;
override target(): BidiBrowserTarget {
return this.#target;
}
override async disconnect(): Promise<void> {

View File

@ -6,17 +6,20 @@
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import {BrowserContext} from '../api/BrowserContext.js';
import type {Page} from '../api/Page.js';
import {BrowserContext, BrowserContextEvent} from '../api/BrowserContext.js';
import {PageEvent, type Page} from '../api/Page.js';
import type {Target} from '../api/Target.js';
import {UnsupportedOperation} from '../common/Errors.js';
import {debugError} from '../common/util.js';
import type {Viewport} from '../common/Viewport.js';
import type {BidiBrowser} from './Browser.js';
import type {BidiConnection} from './Connection.js';
import type {BrowsingContext} from './core/BrowsingContext.js';
import {UserContext} from './core/UserContext.js';
import type {BidiPage} from './Page.js';
import type {BidiFrame} from './Frame.js';
import {BidiPage} from './Page.js';
import {BidiPageTarget} from './Target.js';
import {BidiFrameTarget} from './Target.js';
/**
* @internal
@ -29,10 +32,25 @@ export interface BidiBrowserContextOptions {
* @internal
*/
export class BidiBrowserContext extends BrowserContext {
#browser: BidiBrowser;
#connection: BidiConnection;
#defaultViewport: Viewport | null;
#userContext: UserContext;
static from(
browser: BidiBrowser,
userContext: UserContext,
options: BidiBrowserContextOptions
): BidiBrowserContext {
const context = new BidiBrowserContext(browser, userContext, options);
context.#initialize();
return context;
}
readonly #browser: BidiBrowser;
readonly #defaultViewport: Viewport | null;
// This is public because of cookies.
readonly userContext: UserContext;
readonly #pages = new WeakMap<BrowsingContext, BidiPage>();
readonly #targets = new Map<
BidiPage,
[BidiPageTarget, Map<BidiFrame, BidiFrameTarget>]
>();
constructor(
browser: BidiBrowser,
@ -41,36 +59,78 @@ export class BidiBrowserContext extends BrowserContext {
) {
super();
this.#browser = browser;
this.#userContext = userContext;
this.#connection = this.#browser.connection;
this.userContext = userContext;
this.#defaultViewport = options.defaultViewport;
}
override targets(): Target[] {
return this.#browser.targets().filter(target => {
return target.browserContext() === this;
#initialize() {
// Create targets for existing browsing contexts.
for (const browsingContext of this.userContext.browsingContexts) {
this.#createPage(browsingContext);
}
this.userContext.on('browsingcontext', ({browsingContext}) => {
this.#createPage(browsingContext);
});
}
get connection(): BidiConnection {
return this.#connection;
#createPage(browsingContext: BrowsingContext): BidiPage {
const page = BidiPage.from(this, browsingContext);
this.#pages.set(browsingContext, page);
page.on(PageEvent.Close, () => {
this.#pages.delete(browsingContext);
});
// -- Target stuff starts here --
const pageTarget = new BidiPageTarget(page);
const frameTargets = new Map();
this.#targets.set(page, [pageTarget, frameTargets]);
page.on(PageEvent.FrameAttached, frame => {
const bidiFrame = frame as BidiFrame;
const target = new BidiFrameTarget(bidiFrame);
frameTargets.set(bidiFrame, target);
this.emit(BrowserContextEvent.TargetCreated, target);
});
page.on(PageEvent.FrameNavigated, frame => {
const bidiFrame = frame as BidiFrame;
const target = frameTargets.get(bidiFrame);
// If there is no target, then this is the page's frame.
if (target === undefined) {
this.emit(BrowserContextEvent.TargetChanged, pageTarget);
} else {
this.emit(BrowserContextEvent.TargetChanged, target);
}
});
page.on(PageEvent.FrameDetached, frame => {
const bidiFrame = frame as BidiFrame;
const target = frameTargets.get(bidiFrame);
if (target === undefined) {
return;
}
frameTargets.delete(bidiFrame);
this.emit(BrowserContextEvent.TargetDestroyed, target);
});
page.on(PageEvent.Close, () => {
this.#targets.delete(page);
this.emit(BrowserContextEvent.TargetDestroyed, pageTarget);
});
this.emit(BrowserContextEvent.TargetCreated, pageTarget);
// -- Target stuff ends here --
return page;
}
override targets(): Target[] {
return [...this.#targets.values()].flatMap(([target, frames]) => {
return [target, ...frames.values()];
});
}
override async newPage(): Promise<Page> {
const {result} = await this.#connection.send('browsingContext.create', {
type: Bidi.BrowsingContext.CreateType.Tab,
userContext: this.#userContext.id,
});
const target = this.#browser._getTargetById(result.context);
// TODO: once BiDi has some concept matching BrowserContext, the newly
// created contexts should get automatically assigned to the right
// BrowserContext. For now, we assume that only explicitly created pages go
// to the current BrowserContext. Otherwise, the contexts get assigned to
// the default BrowserContext by the Browser.
target._setBrowserContext(this);
const page = await target.page();
const context = await this.userContext.createBrowsingContext(
Bidi.BrowsingContext.CreateType.Tab
);
const page = this.#pages.get(context)!;
if (!page) {
throw new Error('Page is not found');
}
@ -91,7 +151,7 @@ export class BidiBrowserContext extends BrowserContext {
}
try {
await this.#userContext.remove();
await this.userContext.remove();
} catch (error) {
debugError(error);
}
@ -102,18 +162,13 @@ export class BidiBrowserContext extends BrowserContext {
}
override async pages(): Promise<BidiPage[]> {
const results = await Promise.all(
[...this.targets()].map(t => {
return t.page();
})
);
return results.filter((p): p is BidiPage => {
return p !== null;
return [...this.userContext.browsingContexts].map(context => {
return this.#pages.get(context)!;
});
}
override isIncognito(): boolean {
return this.#userContext.id !== UserContext.DEFAULT;
return this.userContext.id !== UserContext.DEFAULT;
}
override overridePermissions(): never {
@ -125,9 +180,9 @@ export class BidiBrowserContext extends BrowserContext {
}
override get id(): string | undefined {
if (this.#userContext.id === UserContext.DEFAULT) {
if (this.userContext.id === UserContext.DEFAULT) {
return undefined;
}
return this.#userContext.id;
return this.userContext.id;
}
}

View File

@ -1,110 +0,0 @@
/**
* @license
* Copyright 2024 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import type ProtocolMapping from 'devtools-protocol/types/protocol-mapping.js';
import type {CDPSession} from '../api/CDPSession.js';
import type {EventType} from '../common/EventEmitter.js';
import {debugError} from '../common/util.js';
import {BidiCdpSession} from './CDPSession.js';
import type {BidiConnection} from './Connection.js';
import {BidiRealm} from './Realm.js';
/**
* Internal events that the BrowsingContext class emits.
*
* @internal
*/
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace BrowsingContextEvent {
/**
* Emitted on the top-level context, when a descendant context is created.
*/
export const Created = Symbol('BrowsingContext.created');
/**
* Emitted on the top-level context, when a descendant context or the
* top-level context itself is destroyed.
*/
export const Destroyed = Symbol('BrowsingContext.destroyed');
}
/**
* @internal
*/
export interface BrowsingContextEvents extends Record<EventType, unknown> {
[BrowsingContextEvent.Created]: BrowsingContext;
[BrowsingContextEvent.Destroyed]: BrowsingContext;
}
/**
* @internal
*/
export class BrowsingContext extends BidiRealm {
#id: string;
#url: string;
#cdpSession: CDPSession;
#parent?: string | null;
#browserName = '';
constructor(
connection: BidiConnection,
info: Bidi.BrowsingContext.Info,
browserName: string
) {
super(connection);
this.#id = info.context;
this.#url = info.url;
this.#parent = info.parent;
this.#browserName = browserName;
this.#cdpSession = new BidiCdpSession(this, undefined);
this.on('browsingContext.domContentLoaded', this.#updateUrl.bind(this));
this.on('browsingContext.fragmentNavigated', this.#updateUrl.bind(this));
this.on('browsingContext.load', this.#updateUrl.bind(this));
}
supportsCdp(): boolean {
return !this.#browserName.toLowerCase().includes('firefox');
}
#updateUrl(info: Bidi.BrowsingContext.NavigationInfo) {
this.#url = info.url;
}
createRealmForSandbox(): BidiRealm {
return new BidiRealm(this.connection);
}
get url(): string {
return this.#url;
}
get id(): string {
return this.#id;
}
get parent(): string | undefined | null {
return this.#parent;
}
get cdpSession(): CDPSession {
return this.#cdpSession;
}
async sendCdpCommand<T extends keyof ProtocolMapping.Commands>(
method: T,
...paramArgs: ProtocolMapping.Commands[T]['paramsType']
): Promise<ProtocolMapping.Commands[T]['returnType']> {
return await this.#cdpSession.send(method, ...paramArgs);
}
dispose(): void {
this.removeAllListeners();
this.connection.unregisterBrowsingContexts(this.#id);
void this.#cdpSession.detach().catch(debugError);
}
}

View File

@ -3,7 +3,6 @@
* Copyright 2024 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type ProtocolMapping from 'devtools-protocol/types/protocol-mapping.js';
import {CDPSession} from '../api/CDPSession.js';
@ -11,44 +10,49 @@ import type {Connection as CdpConnection} from '../cdp/Connection.js';
import {TargetCloseError, UnsupportedOperation} from '../common/Errors.js';
import {Deferred} from '../util/Deferred.js';
import type {BrowsingContext} from './BrowsingContext.js';
/**
* @internal
*/
export const cdpSessions = new Map<string, BidiCdpSession>();
import type {BidiConnection} from './Connection.js';
import type {BidiFrame} from './Frame.js';
/**
* @internal
*/
export class BidiCdpSession extends CDPSession {
#context: BrowsingContext;
#sessionId = Deferred.create<string>();
#detached = false;
static sessions = new Map<string, BidiCdpSession>();
constructor(context: BrowsingContext, sessionId?: string) {
#detached = false;
readonly #connection: BidiConnection | undefined = undefined;
readonly #sessionId = Deferred.create<string>();
readonly frame: BidiFrame;
constructor(frame: BidiFrame, sessionId?: string) {
super();
this.#context = context;
if (!this.#context.supportsCdp()) {
this.frame = frame;
if (!this.frame.page().browser().cdpSupported) {
return;
}
const connection = this.frame.page().browser().connection;
this.#connection = connection;
if (sessionId) {
this.#sessionId.resolve(sessionId);
cdpSessions.set(sessionId, this);
BidiCdpSession.sessions.set(sessionId, this);
} else {
context.connection
.send('cdp.getSession', {
context: context.id,
})
.then(session => {
(async () => {
try {
const session = await connection.send('cdp.getSession', {
context: frame._id,
});
this.#sessionId.resolve(session.result.session!);
cdpSessions.set(session.result.session!, this);
})
.catch(err => {
this.#sessionId.reject(err);
});
BidiCdpSession.sessions.set(session.result.session!, this);
} catch (error) {
this.#sessionId.reject(error as Error);
}
})();
}
// SAFETY: We never throw #sessionId.
BidiCdpSession.sessions.set(this.#sessionId.value() as string, this);
}
override connection(): CdpConnection | undefined {
@ -59,7 +63,7 @@ export class BidiCdpSession extends CDPSession {
method: T,
params?: ProtocolMapping.Commands[T]['paramsType'][0]
): Promise<ProtocolMapping.Commands[T]['returnType']> {
if (!this.#context.supportsCdp()) {
if (this.#connection === undefined) {
throw new UnsupportedOperation(
'CDP support is required for this feature. The current browser does not support CDP.'
);
@ -70,7 +74,7 @@ export class BidiCdpSession extends CDPSession {
);
}
const session = await this.#sessionId.valueOrThrow();
const {result} = await this.#context.connection.send('cdp.sendCommand', {
const {result} = await this.#connection.send('cdp.sendCommand', {
method: method,
params: params,
session,
@ -79,17 +83,21 @@ export class BidiCdpSession extends CDPSession {
}
override async detach(): Promise<void> {
cdpSessions.delete(this.id());
if (!this.#detached && this.#context.supportsCdp()) {
await this.#context.cdpSession.send('Target.detachFromTarget', {
if (this.#connection === undefined || this.#detached) {
return;
}
try {
await this.frame.client.send('Target.detachFromTarget', {
sessionId: this.id(),
});
} finally {
BidiCdpSession.sessions.delete(this.id());
this.#detached = true;
}
this.#detached = true;
}
override id(): string {
const val = this.#sessionId.value();
return val instanceof Error || val === undefined ? '' : val;
const value = this.#sessionId.value();
return typeof value === 'string' ? value : '';
}
}

View File

@ -14,11 +14,10 @@ import {EventEmitter} from '../common/EventEmitter.js';
import {debugError} from '../common/util.js';
import {assert} from '../util/assert.js';
import type {BrowsingContext} from './BrowsingContext.js';
import {cdpSessions} from './CDPSession.js';
import {BidiCdpSession} from './CDPSession.js';
import type {
BidiEvents,
Commands as BidiCommands,
BidiEvents,
Connection,
} from './core/Connection.js';
@ -52,7 +51,6 @@ export class BidiConnection
#timeout? = 0;
#closed = false;
#callbacks = new CallbackRegistry();
#browsingContexts = new Map<string, BrowsingContext>();
#emitters: Array<EventEmitter<any>> = [];
constructor(
@ -138,12 +136,11 @@ export class BidiConnection
return;
case 'event':
if (isCdpEvent(object)) {
cdpSessions
BidiCdpSession.sessions
.get(object.params.session)
?.emit(object.params.event, object.params.params);
return;
}
this.#maybeEmitOnContext(object);
// SAFETY: We know the method and parameter still match here.
this.emit(
object.method,
@ -164,52 +161,6 @@ export class BidiConnection
debugError(object);
}
#maybeEmitOnContext(event: Bidi.ChromiumBidi.Event) {
let context: BrowsingContext | undefined;
// Context specific events
if ('context' in event.params && event.params.context !== null) {
context = this.#browsingContexts.get(event.params.context);
// `log.entryAdded` specific context
} else if (
'source' in event.params &&
event.params.source.context !== undefined
) {
context = this.#browsingContexts.get(event.params.source.context);
}
context?.emit(event.method, event.params);
}
registerBrowsingContexts(context: BrowsingContext): void {
this.#browsingContexts.set(context.id, context);
}
getBrowsingContext(contextId: string): BrowsingContext {
const currentContext = this.#browsingContexts.get(contextId);
if (!currentContext) {
throw new Error(`BrowsingContext ${contextId} does not exist.`);
}
return currentContext;
}
getTopLevelContext(contextId: string): BrowsingContext {
let currentContext = this.#browsingContexts.get(contextId);
if (!currentContext) {
throw new Error(`BrowsingContext ${contextId} does not exist.`);
}
while (currentContext.parent) {
contextId = currentContext.parent;
currentContext = this.#browsingContexts.get(contextId);
if (!currentContext) {
throw new Error(`BrowsingContext ${contextId} does not exist.`);
}
}
return currentContext;
}
unregisterBrowsingContexts(id: string): void {
this.#browsingContexts.delete(id);
}
/**
* Unbinds the connection, but keeps the transport open. Useful when the transport will
* be reused by other connection e.g. with different protocol.
@ -224,7 +175,6 @@ export class BidiConnection
this.#transport.onmessage = () => {};
this.#transport.onclose = () => {};
this.#browsingContexts.clear();
this.#callbacks.clear();
}

View File

@ -4,40 +4,26 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import {Dialog} from '../api/Dialog.js';
import type {BrowsingContext} from './BrowsingContext.js';
import type {UserPrompt} from './core/UserPrompt.js';
/**
* @internal
*/
export class BidiDialog extends Dialog {
#context: BrowsingContext;
/**
* @internal
*/
constructor(
context: BrowsingContext,
type: Bidi.BrowsingContext.UserPromptOpenedParameters['type'],
message: string,
defaultValue?: string
) {
super(type, message, defaultValue);
this.#context = context;
static from(prompt: UserPrompt): BidiDialog {
return new BidiDialog(prompt);
}
#prompt: UserPrompt;
private constructor(prompt: UserPrompt) {
super(prompt.info.type, prompt.info.message, prompt.info.defaultValue);
this.#prompt = prompt;
}
/**
* @internal
*/
override async handle(options: {
accept: boolean;
text?: string;
}): Promise<void> {
await this.#context.connection.send('browsingContext.handleUserPrompt', {
context: this.#context.id,
await this.#prompt.handle({
accept: options.accept,
userText: options.text,
});

View File

@ -6,14 +6,13 @@
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import {type AutofillData, ElementHandle} from '../api/ElementHandle.js';
import {ElementHandle, type AutofillData} from '../api/ElementHandle.js';
import {UnsupportedOperation} from '../common/Errors.js';
import {throwIfDisposed} from '../util/decorators.js';
import type {BidiFrame} from './Frame.js';
import {BidiJSHandle} from './JSHandle.js';
import type {BidiRealm} from './Realm.js';
import type {Sandbox} from './Sandbox.js';
import type {BidiFrameRealm} from './Realm.js';
/**
* @internal
@ -21,28 +20,28 @@ import type {Sandbox} from './Sandbox.js';
export class BidiElementHandle<
ElementType extends Node = Element,
> extends ElementHandle<ElementType> {
declare handle: BidiJSHandle<ElementType>;
constructor(sandbox: Sandbox, remoteValue: Bidi.Script.RemoteValue) {
super(new BidiJSHandle(sandbox, remoteValue));
static from<ElementType extends Node = Element>(
value: Bidi.Script.RemoteValue,
realm: BidiFrameRealm
): BidiElementHandle<ElementType> {
return new BidiElementHandle(value, realm);
}
override get realm(): Sandbox {
return this.handle.realm;
declare handle: BidiJSHandle<ElementType>;
constructor(value: Bidi.Script.RemoteValue, realm: BidiFrameRealm) {
super(BidiJSHandle.from(value, realm));
}
override get realm(): BidiFrameRealm {
// SAFETY: See the super call in the constructor.
return this.handle.realm as BidiFrameRealm;
}
override get frame(): BidiFrame {
return this.realm.environment;
}
context(): BidiRealm {
return this.handle.context();
}
get isPrimitiveValue(): boolean {
return this.handle.isPrimitiveValue;
}
remoteValue(): Bidi.Script.RemoteValue {
return this.handle.remoteValue();
}
@ -76,7 +75,14 @@ export class BidiElementHandle<
})) as BidiJSHandle;
const value = handle.remoteValue();
if (value.type === 'window') {
return this.frame.page().frame(value.value.context);
return (
this.frame
.page()
.frames()
.find(frame => {
return frame._id === value.value.context;
}) ?? null
);
}
return null;
}

View File

@ -1,35 +0,0 @@
/**
* @license
* Copyright 2023 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {Viewport} from '../common/Viewport.js';
import type {BrowsingContext} from './BrowsingContext.js';
/**
* @internal
*/
export class EmulationManager {
#browsingContext: BrowsingContext;
constructor(browsingContext: BrowsingContext) {
this.#browsingContext = browsingContext;
}
async emulateViewport(viewport: Viewport): Promise<void> {
await this.#browsingContext.connection.send('browsingContext.setViewport', {
context: this.#browsingContext.id,
viewport:
viewport.width && viewport.height
? {
width: viewport.width,
height: viewport.height,
}
: null,
devicePixelRatio: viewport.deviceScaleFactor
? viewport.deviceScaleFactor
: null,
});
}
}

View File

@ -12,7 +12,7 @@ import {assert} from '../util/assert.js';
import {Deferred} from '../util/Deferred.js';
import {interpolateFunction, stringifyFunction} from '../util/Function.js';
import type {BidiConnection} from './Connection.js';
import type {Connection} from './core/Connection.js';
import {BidiDeserializer} from './Deserializer.js';
import type {BidiFrame} from './Frame.js';
import {BidiSerializer} from './Serializer.js';
@ -207,8 +207,8 @@ export class ExposeableFunction<Args extends unknown[], Ret> {
}
};
get #connection(): BidiConnection {
return this.#frame.context().connection;
get #connection(): Connection {
return this.#frame.page().browser().connection;
}
get #channelArguments() {

View File

@ -4,17 +4,17 @@
* SPDX-License-Identifier: Apache-2.0
*/
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import type {Observable} from '../../third_party/rxjs/rxjs.js';
import {
combineLatest,
first,
firstValueFrom,
forkJoin,
from,
map,
merge,
of,
raceWith,
zip,
switchMap,
} from '../../third_party/rxjs/rxjs.js';
import type {CDPSession} from '../api/CDPSession.js';
import type {ElementHandle} from '../api/ElementHandle.js';
@ -25,85 +25,205 @@ import {
type WaitForOptions,
} from '../api/Frame.js';
import type {WaitForSelectorOptions} from '../api/Page.js';
import {UnsupportedOperation} from '../common/Errors.js';
import {PageEvent} from '../api/Page.js';
import {
ConsoleMessage,
type ConsoleMessageLocation,
} from '../common/ConsoleMessage.js';
import {TargetCloseError, UnsupportedOperation} from '../common/Errors.js';
import type {TimeoutSettings} from '../common/TimeoutSettings.js';
import type {Awaitable, NodeFor} from '../common/types.js';
import {
fromEmitterEvent,
NETWORK_IDLE_TIME,
timeout,
UTILITY_WORLD_NAME,
} from '../common/util.js';
import {Deferred} from '../util/Deferred.js';
import {disposeSymbol} from '../util/disposable.js';
import {debugError, fromEmitterEvent, timeout} from '../common/util.js';
import type {BrowsingContext} from './BrowsingContext.js';
import {BidiCdpSession} from './CDPSession.js';
import type {BrowsingContext} from './core/BrowsingContext.js';
import {BidiDeserializer} from './Deserializer.js';
import {BidiDialog} from './Dialog.js';
import {ExposeableFunction} from './ExposedFunction.js';
import {BidiHTTPRequest, requests} from './HTTPRequest.js';
import type {BidiHTTPResponse} from './HTTPResponse.js';
import {
getBiDiLifecycleEvent,
getBiDiReadinessState,
rewriteNavigationError,
} from './lifecycle.js';
import {BidiJSHandle} from './JSHandle.js';
import type {BidiPage} from './Page.js';
import {
MAIN_SANDBOX,
PUPPETEER_SANDBOX,
Sandbox,
type SandboxChart,
} from './Sandbox.js';
import type {BidiRealm} from './Realm.js';
import {BidiFrameRealm} from './Realm.js';
import {rewriteNavigationError} from './util.js';
/**
* Puppeteer's Frame class could be viewed as a BiDi BrowsingContext implementation
* @internal
*/
export class BidiFrame extends Frame {
#page: BidiPage;
#context: BrowsingContext;
#timeoutSettings: TimeoutSettings;
#abortDeferred = Deferred.create<never>();
#disposed = false;
sandboxes: SandboxChart;
override _id: string;
static from(
parent: BidiPage | BidiFrame,
browsingContext: BrowsingContext
): BidiFrame {
const frame = new BidiFrame(parent, browsingContext);
frame.#initialize();
return frame;
}
constructor(
page: BidiPage,
context: BrowsingContext,
timeoutSettings: TimeoutSettings,
parentId?: string | null
readonly #parent: BidiPage | BidiFrame;
readonly browsingContext: BrowsingContext;
readonly #frames = new WeakMap<BrowsingContext, BidiFrame>();
readonly realms: {default: BidiFrameRealm; internal: BidiFrameRealm};
override readonly _id: string;
override readonly client: BidiCdpSession;
private constructor(
parent: BidiPage | BidiFrame,
browsingContext: BrowsingContext
) {
super();
this.#page = page;
this.#context = context;
this.#timeoutSettings = timeoutSettings;
this._id = this.#context.id;
this._parentId = parentId ?? undefined;
this.#parent = parent;
this.browsingContext = browsingContext;
this.sandboxes = {
[MAIN_SANDBOX]: new Sandbox(undefined, this, context, timeoutSettings),
[PUPPETEER_SANDBOX]: new Sandbox(
UTILITY_WORLD_NAME,
this,
context.createRealmForSandbox(),
timeoutSettings
this._id = browsingContext.id;
this.client = new BidiCdpSession(this);
this.realms = {
default: BidiFrameRealm.from(this.browsingContext.defaultRealm, this),
internal: BidiFrameRealm.from(
this.browsingContext.createWindowRealm(
`__puppeteer_internal_${Math.ceil(Math.random() * 10000)}`
),
this
),
};
}
override get client(): CDPSession {
return this.context().cdpSession;
#initialize(): void {
for (const browsingContext of this.browsingContext.children) {
this.#createFrameTarget(browsingContext);
}
this.browsingContext.on('browsingcontext', ({browsingContext}) => {
this.#createFrameTarget(browsingContext);
});
this.browsingContext.on('closed', () => {
for (const session of BidiCdpSession.sessions.values()) {
if (session.frame === this) {
void session.detach().catch(debugError);
}
}
this.page().emit(PageEvent.FrameDetached, this);
this.removeAllListeners();
});
this.browsingContext.on('request', ({request}) => {
const httpRequest = BidiHTTPRequest.from(request, this);
request.once('success', () => {
// SAFETY: BidiHTTPRequest will create this before here.
this.page().emit(PageEvent.RequestFinished, httpRequest);
});
request.once('error', () => {
this.page().emit(PageEvent.RequestFailed, httpRequest);
});
});
this.browsingContext.on('navigation', ({navigation}) => {
navigation.once('fragment', () => {
this.page().emit(PageEvent.FrameNavigated, this);
});
});
this.browsingContext.on('load', () => {
this.page().emit(PageEvent.Load, undefined);
});
this.browsingContext.on('DOMContentLoaded', () => {
this._hasStartedLoading = true;
this.page().emit(PageEvent.DOMContentLoaded, undefined);
this.page().emit(PageEvent.FrameNavigated, this);
});
this.browsingContext.on('userprompt', ({userPrompt}) => {
this.page().emit(PageEvent.Dialog, BidiDialog.from(userPrompt));
});
this.browsingContext.on('log', ({entry}) => {
if (this._id !== entry.source.context) {
return;
}
if (isConsoleLogEntry(entry)) {
const args = entry.args.map(arg => {
return this.mainRealm().createHandle(arg);
});
const text = args
.reduce((value, arg) => {
const parsedValue =
arg instanceof BidiJSHandle && arg.isPrimitiveValue
? BidiDeserializer.deserialize(arg.remoteValue())
: arg.toString();
return `${value} ${parsedValue}`;
}, '')
.slice(1);
this.page().emit(
PageEvent.Console,
new ConsoleMessage(
entry.method as any,
text,
args,
getStackTraceLocations(entry.stackTrace)
)
);
} else if (isJavaScriptLogEntry(entry)) {
const error = new Error(entry.text ?? '');
const messageHeight = error.message.split('\n').length;
const messageLines = error.stack!.split('\n').splice(0, messageHeight);
const stackLines = [];
if (entry.stackTrace) {
for (const frame of entry.stackTrace.callFrames) {
// Note we need to add `1` because the values are 0-indexed.
stackLines.push(
` at ${frame.functionName || '<anonymous>'} (${frame.url}:${
frame.lineNumber + 1
}:${frame.columnNumber + 1})`
);
if (stackLines.length >= Error.stackTraceLimit) {
break;
}
}
}
error.stack = [...messageLines, ...stackLines].join('\n');
this.page().emit(PageEvent.PageError, error);
} else {
debugError(
`Unhandled LogEntry with type "${entry.type}", text "${entry.text}" and level "${entry.level}"`
);
}
});
}
override mainRealm(): Sandbox {
return this.sandboxes[MAIN_SANDBOX];
#createFrameTarget(browsingContext: BrowsingContext) {
const frame = BidiFrame.from(this, browsingContext);
this.#frames.set(browsingContext, frame);
this.page().emit(PageEvent.FrameAttached, frame);
browsingContext.on('closed', () => {
this.#frames.delete(browsingContext);
});
return frame;
}
override isolatedRealm(): Sandbox {
return this.sandboxes[PUPPETEER_SANDBOX];
get timeoutSettings(): TimeoutSettings {
return this.page()._timeoutSettings;
}
override mainRealm(): BidiRealm {
return this.realms.default;
}
override isolatedRealm(): BidiRealm {
return this.realms.internal;
}
override page(): BidiPage {
return this.#page;
let parent = this.#parent;
while (parent instanceof BidiFrame) {
parent = parent.#parent;
}
return parent;
}
override isOOPFrame(): never {
@ -111,15 +231,20 @@ export class BidiFrame extends Frame {
}
override url(): string {
return this.#context.url;
return this.browsingContext.url;
}
override parentFrame(): BidiFrame | null {
return this.#page.frame(this._parentId ?? '');
if (this.#parent instanceof BidiFrame) {
return this.#parent;
}
return null;
}
override childFrames(): BidiFrame[] {
return this.#page.childFrames(this.#context.id);
return [...this.browsingContext.children].map(child => {
return this.#frames.get(child)!;
});
}
@throwIfDetached
@ -127,40 +252,16 @@ export class BidiFrame extends Frame {
url: string,
options: GoToOptions = {}
): Promise<BidiHTTPResponse | null> {
const {
waitUntil = 'load',
timeout: ms = this.#timeoutSettings.navigationTimeout(),
} = options;
const [readiness, networkIdle] = getBiDiReadinessState(waitUntil);
const result$ = zip(
from(
this.#context.connection.send('browsingContext.navigate', {
context: this.#context.id,
url,
wait: readiness,
})
),
...(networkIdle !== null
? [
this.#page.waitForNetworkIdle$({
timeout: ms,
concurrency: networkIdle === 'networkidle2' ? 2 : 0,
idleTime: NETWORK_IDLE_TIME,
}),
]
: [])
).pipe(
map(([{result}]) => {
return result;
}),
raceWith(timeout(ms), from(this.#abortDeferred.valueOrThrow())),
rewriteNavigationError(url, ms)
const [response] = await Promise.all([
this.waitForNavigation(options),
this.browsingContext.navigate(url),
]).catch(
rewriteNavigationError(
url,
options.timeout ?? this.timeoutSettings.navigationTimeout()
)
);
const result = await firstValueFrom(result$);
return this.#page.getNavigationResponse(result.navigation);
return response;
}
@throwIfDetached
@ -168,95 +269,58 @@ export class BidiFrame extends Frame {
html: string,
options: WaitForOptions = {}
): Promise<void> {
const {
waitUntil = 'load',
timeout: ms = this.#timeoutSettings.navigationTimeout(),
} = options;
const [waitEvent, networkIdle] = getBiDiLifecycleEvent(waitUntil);
const result$ = zip(
forkJoin([
fromEmitterEvent(this.#context, waitEvent).pipe(first()),
from(this.setFrameContent(html)),
]).pipe(
map(() => {
return null;
})
await Promise.all([
this.setFrameContent(html),
firstValueFrom(
combineLatest([
this.#waitForLoad$(options),
this.#waitForNetworkIdle$(options),
])
),
...(networkIdle !== null
? [
this.#page.waitForNetworkIdle$({
timeout: ms,
concurrency: networkIdle === 'networkidle2' ? 2 : 0,
idleTime: NETWORK_IDLE_TIME,
}),
]
: [])
).pipe(
raceWith(timeout(ms), from(this.#abortDeferred.valueOrThrow())),
rewriteNavigationError('setContent', ms)
);
await firstValueFrom(result$);
}
context(): BrowsingContext {
return this.#context;
]);
}
@throwIfDetached
override async waitForNavigation(
options: WaitForOptions = {}
): Promise<BidiHTTPResponse | null> {
const {
waitUntil = 'load',
timeout: ms = this.#timeoutSettings.navigationTimeout(),
} = options;
const {timeout: ms = this.timeoutSettings.navigationTimeout()} = options;
const [waitUntilEvent, networkIdle] = getBiDiLifecycleEvent(waitUntil);
const navigation$ = merge(
forkJoin([
fromEmitterEvent(
this.#context,
Bidi.ChromiumBidi.BrowsingContext.EventNames.NavigationStarted
).pipe(first()),
fromEmitterEvent(this.#context, waitUntilEvent).pipe(first()),
]),
fromEmitterEvent(
this.#context,
Bidi.ChromiumBidi.BrowsingContext.EventNames.FragmentNavigated
return await firstValueFrom(
combineLatest([
fromEmitterEvent(this.browsingContext, 'navigation').pipe(
switchMap(({navigation}) => {
return this.#waitForLoad$(options).pipe(
raceWith(fromEmitterEvent(navigation, 'fragment')),
map(() => {
return navigation;
})
);
})
),
this.#waitForNetworkIdle$(options),
]).pipe(
map(([navigation]) => {
const request = navigation.request;
if (!request) {
return null;
}
const httpRequest = requests.get(request)!;
const lastRedirect = httpRequest.redirectChain().at(-1);
return (
lastRedirect !== undefined ? lastRedirect : httpRequest
).response();
}),
raceWith(
timeout(ms),
fromEmitterEvent(this.browsingContext, 'closed').pipe(
map(() => {
throw new TargetCloseError('Frame detached.');
})
)
)
)
).pipe(
map(result => {
if (Array.isArray(result)) {
return {result: result[1]};
}
return {result};
})
);
const result$ = zip(
navigation$,
...(networkIdle !== null
? [
this.#page.waitForNetworkIdle$({
timeout: ms,
concurrency: networkIdle === 'networkidle2' ? 2 : 0,
idleTime: NETWORK_IDLE_TIME,
}),
]
: [])
).pipe(
map(([{result}]) => {
return result;
}),
raceWith(timeout(ms), from(this.#abortDeferred.valueOrThrow()))
);
const result = await firstValueFrom(result$);
return this.#page.getNavigationResponse(result.navigation);
}
override waitForDevicePrompt(): never {
@ -264,18 +328,7 @@ export class BidiFrame extends Frame {
}
override get detached(): boolean {
return this.#disposed;
}
[disposeSymbol](): void {
if (this.#disposed) {
return;
}
this.#disposed = true;
this.#abortDeferred.reject(new Error('Frame detached'));
this.#context.dispose();
this.sandboxes[MAIN_SANDBOX][disposeSymbol]();
this.sandboxes[PUPPETEER_SANDBOX][disposeSymbol]();
return this.browsingContext.closed;
}
#exposedFunctions = new Map<string, ExposeableFunction<never[], unknown>>();
@ -310,4 +363,115 @@ export class BidiFrame extends Frame {
return super.waitForSelector(selector, options);
}
async createCDPSession(): Promise<CDPSession> {
const {sessionId} = await this.client.send('Target.attachToTarget', {
targetId: this._id,
flatten: true,
});
return new BidiCdpSession(this, sessionId);
}
@throwIfDetached
#waitForLoad$(options: WaitForOptions = {}): Observable<void> {
let {waitUntil = 'load'} = options;
const {timeout: ms = this.timeoutSettings.navigationTimeout()} = options;
if (!Array.isArray(waitUntil)) {
waitUntil = [waitUntil];
}
const events = new Set<'load' | 'DOMContentLoaded'>();
for (const lifecycleEvent of waitUntil) {
switch (lifecycleEvent) {
case 'load': {
events.add('load');
break;
}
case 'domcontentloaded': {
events.add('DOMContentLoaded');
break;
}
}
}
if (events.size === 0) {
return of(undefined);
}
return combineLatest(
[...events].map(event => {
return fromEmitterEvent(this.browsingContext, event);
})
).pipe(
map(() => {}),
first(),
raceWith(
timeout(ms),
fromEmitterEvent(this.browsingContext, 'closed').pipe(
map(() => {
throw new Error('Frame detached.');
})
)
)
);
}
@throwIfDetached
#waitForNetworkIdle$(options: WaitForOptions = {}): Observable<void> {
let {waitUntil = 'load'} = options;
if (!Array.isArray(waitUntil)) {
waitUntil = [waitUntil];
}
let concurrency = Infinity;
for (const event of waitUntil) {
switch (event) {
case 'networkidle0': {
concurrency = Math.min(0, concurrency);
break;
}
case 'networkidle2': {
concurrency = Math.min(2, concurrency);
break;
}
}
}
if (concurrency === Infinity) {
return of(undefined);
}
return this.page().waitForNetworkIdle$({
idleTime: 500,
timeout: options.timeout ?? this.timeoutSettings.timeout(),
concurrency,
});
}
}
function isConsoleLogEntry(
event: Bidi.Log.Entry
): event is Bidi.Log.ConsoleLogEntry {
return event.type === 'console';
}
function isJavaScriptLogEntry(
event: Bidi.Log.Entry
): event is Bidi.Log.JavascriptLogEntry {
return event.type === 'javascript';
}
function getStackTraceLocations(
stackTrace?: Bidi.Script.StackTrace
): ConsoleMessageLocation[] {
const stackTraceLocations: ConsoleMessageLocation[] = [];
if (stackTrace) {
for (const callFrame of stackTrace.callFrames) {
stackTraceLocations.push({
url: callFrame.url,
lineNumber: callFrame.lineNumber,
columnNumber: callFrame.columnNumber,
});
}
}
return stackTraceLocations;
}

View File

@ -5,107 +5,126 @@
*/
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import type {Frame} from '../api/Frame.js';
import type {CDPSession} from '../api/CDPSession.js';
import type {
ContinueRequestOverrides,
ResponseForRequest,
} from '../api/HTTPRequest.js';
import {HTTPRequest, type ResourceType} from '../api/HTTPRequest.js';
import {PageEvent} from '../api/Page.js';
import {UnsupportedOperation} from '../common/Errors.js';
import type {BidiHTTPResponse} from './HTTPResponse.js';
import type {Request} from './core/Request.js';
import type {BidiFrame} from './Frame.js';
import {BidiHTTPResponse} from './HTTPResponse.js';
export const requests = new WeakMap<Request, BidiHTTPRequest>();
/**
* @internal
*/
export class BidiHTTPRequest extends HTTPRequest {
override id: string;
override _response: BidiHTTPResponse | null = null;
override _redirectChain: BidiHTTPRequest[];
_navigationId: string | null;
#url: string;
#resourceType: ResourceType;
#method: string;
#postData?: string;
#headers: Record<string, string> = {};
#initiator: Bidi.Network.Initiator;
#frame: Frame | null;
constructor(
event: Bidi.Network.BeforeRequestSentParameters,
frame: Frame | null,
redirectChain: BidiHTTPRequest[] = []
) {
super();
this.#url = event.request.url;
this.#resourceType = event.initiator.type.toLowerCase() as ResourceType;
this.#method = event.request.method;
this.#postData = undefined;
this.#initiator = event.initiator;
this.#frame = frame;
this.id = event.request.request;
this._redirectChain = redirectChain;
this._navigationId = event.navigation;
for (const header of event.request.headers) {
// TODO: How to handle Binary Headers
// https://w3c.github.io/webdriver-bidi/#type-network-Header
if (header.value.type === 'string') {
this.#headers[header.name.toLowerCase()] = header.value.value;
}
}
static from(
bidiRequest: Request,
frame: BidiFrame | undefined
): BidiHTTPRequest {
const request = new BidiHTTPRequest(bidiRequest, frame);
request.#initialize();
return request;
}
override get client(): never {
#redirect: BidiHTTPRequest | undefined;
#response: BidiHTTPResponse | null = null;
override readonly id: string;
readonly #frame: BidiFrame | undefined;
readonly #request: Request;
private constructor(request: Request, frame: BidiFrame | undefined) {
super();
requests.set(request, this);
this.#request = request;
this.#frame = frame;
this.id = request.id;
}
override get client(): CDPSession {
throw new UnsupportedOperation();
}
#initialize() {
this.#request.on('redirect', request => {
this.#redirect = BidiHTTPRequest.from(request, this.#frame);
});
this.#request.once('success', data => {
this.#response = BidiHTTPResponse.from(data, this);
});
this.#frame?.page().emit(PageEvent.Request, this);
}
override url(): string {
return this.#url;
return this.#request.url;
}
override resourceType(): ResourceType {
return this.#resourceType;
return this.initiator().type.toLowerCase() as ResourceType;
}
override method(): string {
return this.#method;
return this.#request.method;
}
override postData(): string | undefined {
return this.#postData;
throw new UnsupportedOperation();
}
override hasPostData(): boolean {
return this.#postData !== undefined;
throw new UnsupportedOperation();
}
override async fetchPostData(): Promise<string | undefined> {
return this.#postData;
throw new UnsupportedOperation();
}
override headers(): Record<string, string> {
return this.#headers;
const headers: Record<string, string> = {};
for (const header of this.#request.headers) {
headers[header.name.toLowerCase()] = header.value.value;
}
return headers;
}
override response(): BidiHTTPResponse | null {
return this._response;
return this.#response;
}
override failure(): {errorText: string} | null {
if (this.#request.error === undefined) {
return null;
}
return {errorText: this.#request.error};
}
override isNavigationRequest(): boolean {
return Boolean(this._navigationId);
return this.#request.navigation !== undefined;
}
override initiator(): Bidi.Network.Initiator {
return this.#initiator;
return this.#request.initiator;
}
override redirectChain(): BidiHTTPRequest[] {
return this._redirectChain.slice();
if (this.#redirect === undefined) {
return [];
}
const redirects = [this.#redirect];
for (const redirect of redirects) {
if (redirect.#redirect !== undefined) {
redirects.push(redirect.#redirect);
}
}
return redirects;
}
override enqueueInterceptAction(
@ -115,8 +134,8 @@ export class BidiHTTPRequest extends HTTPRequest {
void pendingHandler();
}
override frame(): Frame | null {
return this.#frame;
override frame(): BidiFrame | null {
return this.#frame ?? null;
}
override continueRequestOverrides(): never {
@ -157,8 +176,4 @@ export class BidiHTTPRequest extends HTTPRequest {
): never {
throw new UnsupportedOperation();
}
override failure(): never {
throw new UnsupportedOperation();
}
}

View File

@ -7,11 +7,10 @@ import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import type Protocol from 'devtools-protocol';
import type {Frame} from '../api/Frame.js';
import {
HTTPResponse as HTTPResponse,
type RemoteAddress,
} from '../api/HTTPResponse.js';
import {HTTPResponse, type RemoteAddress} from '../api/HTTPResponse.js';
import {PageEvent} from '../api/Page.js';
import {UnsupportedOperation} from '../common/Errors.js';
import {invokeAtMostOnceForArguments} from '../util/decorators.js';
import type {BidiHTTPRequest} from './HTTPRequest.js';
@ -19,62 +18,62 @@ import type {BidiHTTPRequest} from './HTTPRequest.js';
* @internal
*/
export class BidiHTTPResponse extends HTTPResponse {
#request: BidiHTTPRequest;
#remoteAddress: RemoteAddress;
#status: number;
#statusText: string;
#url: string;
#fromCache: boolean;
#headers: Record<string, string> = {};
#timings: Record<string, string> | null;
static from(
data: Bidi.Network.ResponseData,
request: BidiHTTPRequest
): BidiHTTPResponse {
const response = new BidiHTTPResponse(data, request);
response.#initialize();
return response;
}
constructor(
request: BidiHTTPRequest,
{response}: Bidi.Network.ResponseCompletedParameters
#data: Bidi.Network.ResponseData;
#request: BidiHTTPRequest;
private constructor(
data: Bidi.Network.ResponseData,
request: BidiHTTPRequest
) {
super();
this.#data = data;
this.#request = request;
}
this.#remoteAddress = {
#initialize() {
this.#request.frame()?.page().emit(PageEvent.Response, this);
}
@invokeAtMostOnceForArguments
override remoteAddress(): RemoteAddress {
return {
ip: '',
port: -1,
};
this.#url = response.url;
this.#fromCache = response.fromCache;
this.#status = response.status;
this.#statusText = response.statusText;
// TODO: File and issue with BiDi spec
this.#timings = null;
// TODO: Removed once the Firefox implementation is compliant with https://w3c.github.io/webdriver-bidi/#get-the-response-data.
for (const header of response.headers || []) {
// TODO: How to handle Binary Headers
// https://w3c.github.io/webdriver-bidi/#type-network-Header
if (header.value.type === 'string') {
this.#headers[header.name.toLowerCase()] = header.value.value;
}
}
}
override remoteAddress(): RemoteAddress {
return this.#remoteAddress;
}
override url(): string {
return this.#url;
return this.#data.url;
}
override status(): number {
return this.#status;
return this.#data.status;
}
override statusText(): string {
return this.#statusText;
return this.#data.statusText;
}
override headers(): Record<string, string> {
return this.#headers;
const headers: Record<string, string> = {};
// TODO: Remove once the Firefox implementation is compliant with https://w3c.github.io/webdriver-bidi/#get-the-response-data.
for (const header of this.#data.headers || []) {
// TODO: How to handle Binary Headers
// https://w3c.github.io/webdriver-bidi/#type-network-Header
if (header.value.type === 'string') {
headers[header.name.toLowerCase()] = header.value.value;
}
}
return headers;
}
override request(): BidiHTTPRequest {
@ -82,11 +81,12 @@ export class BidiHTTPResponse extends HTTPResponse {
}
override fromCache(): boolean {
return this.#fromCache;
return this.#data.fromCache;
}
override timing(): Protocol.Network.ResourceTiming | null {
return this.#timings as any;
// TODO: File and issue with BiDi spec
throw new UnsupportedOperation();
}
override frame(): Frame | null {

View File

@ -12,9 +12,9 @@ import {
Mouse,
MouseButton,
Touchscreen,
type KeyboardTypeOptions,
type KeyDownOptions,
type KeyPressOptions,
type KeyboardTypeOptions,
type MouseClickOptions,
type MouseMoveOptions,
type MouseOptions,
@ -23,7 +23,6 @@ import {
import {UnsupportedOperation} from '../common/Errors.js';
import type {KeyInput} from '../common/USKeyboardLayout.js';
import type {BrowsingContext} from './BrowsingContext.js';
import type {BidiPage} from './Page.js';
const enum InputId {
@ -288,39 +287,33 @@ export class BidiKeyboard extends Keyboard {
key: KeyInput,
_options?: Readonly<KeyDownOptions>
): Promise<void> {
await this.#page.connection.send('input.performActions', {
context: this.#page.mainFrame()._id,
actions: [
{
type: SourceActionsType.Key,
id: InputId.Keyboard,
actions: [
{
type: ActionType.KeyDown,
value: getBidiKeyValue(key),
},
],
},
],
});
await this.#page.mainFrame().browsingContext.performActions([
{
type: SourceActionsType.Key,
id: InputId.Keyboard,
actions: [
{
type: ActionType.KeyDown,
value: getBidiKeyValue(key),
},
],
},
]);
}
override async up(key: KeyInput): Promise<void> {
await this.#page.connection.send('input.performActions', {
context: this.#page.mainFrame()._id,
actions: [
{
type: SourceActionsType.Key,
id: InputId.Keyboard,
actions: [
{
type: ActionType.KeyUp,
value: getBidiKeyValue(key),
},
],
},
],
});
await this.#page.mainFrame().browsingContext.performActions([
{
type: SourceActionsType.Key,
id: InputId.Keyboard,
actions: [
{
type: ActionType.KeyUp,
value: getBidiKeyValue(key),
},
],
},
]);
}
override async press(
@ -344,16 +337,13 @@ export class BidiKeyboard extends Keyboard {
type: ActionType.KeyUp,
value: getBidiKeyValue(key),
});
await this.#page.connection.send('input.performActions', {
context: this.#page.mainFrame()._id,
actions: [
{
type: SourceActionsType.Key,
id: InputId.Keyboard,
actions,
},
],
});
await this.#page.mainFrame().browsingContext.performActions([
{
type: SourceActionsType.Key,
id: InputId.Keyboard,
actions,
},
]);
}
override async type(
@ -396,16 +386,13 @@ export class BidiKeyboard extends Keyboard {
);
}
}
await this.#page.connection.send('input.performActions', {
context: this.#page.mainFrame()._id,
actions: [
{
type: SourceActionsType.Key,
id: InputId.Keyboard,
actions,
},
],
});
await this.#page.mainFrame().browsingContext.performActions([
{
type: SourceActionsType.Key,
id: InputId.Keyboard,
actions,
},
]);
}
override async sendCharacter(char: string): Promise<void> {
@ -460,19 +447,17 @@ const getBidiButton = (button: MouseButton) => {
* @internal
*/
export class BidiMouse extends Mouse {
#context: BrowsingContext;
#page: BidiPage;
#lastMovePoint: Point = {x: 0, y: 0};
constructor(context: BrowsingContext) {
constructor(page: BidiPage) {
super();
this.#context = context;
this.#page = page;
}
override async reset(): Promise<void> {
this.#lastMovePoint = {x: 0, y: 0};
await this.#context.connection.send('input.releaseActions', {
context: this.#context.id,
});
await this.#page.mainFrame().browsingContext.releaseActions();
}
override async move(
@ -502,52 +487,43 @@ export class BidiMouse extends Mouse {
});
// https://w3c.github.io/webdriver-bidi/#command-input-performActions:~:text=input.PointerMoveAction%20%3D%20%7B%0A%20%20type%3A%20%22pointerMove%22%2C%0A%20%20x%3A%20js%2Dint%2C
this.#lastMovePoint = to;
await this.#context.connection.send('input.performActions', {
context: this.#context.id,
actions: [
{
type: SourceActionsType.Pointer,
id: InputId.Mouse,
actions,
},
],
});
await this.#page.mainFrame().browsingContext.performActions([
{
type: SourceActionsType.Pointer,
id: InputId.Mouse,
actions,
},
]);
}
override async down(options: Readonly<MouseOptions> = {}): Promise<void> {
await this.#context.connection.send('input.performActions', {
context: this.#context.id,
actions: [
{
type: SourceActionsType.Pointer,
id: InputId.Mouse,
actions: [
{
type: ActionType.PointerDown,
button: getBidiButton(options.button ?? MouseButton.Left),
},
],
},
],
});
await this.#page.mainFrame().browsingContext.performActions([
{
type: SourceActionsType.Pointer,
id: InputId.Mouse,
actions: [
{
type: ActionType.PointerDown,
button: getBidiButton(options.button ?? MouseButton.Left),
},
],
},
]);
}
override async up(options: Readonly<MouseOptions> = {}): Promise<void> {
await this.#context.connection.send('input.performActions', {
context: this.#context.id,
actions: [
{
type: SourceActionsType.Pointer,
id: InputId.Mouse,
actions: [
{
type: ActionType.PointerUp,
button: getBidiButton(options.button ?? MouseButton.Left),
},
],
},
],
});
await this.#page.mainFrame().browsingContext.performActions([
{
type: SourceActionsType.Pointer,
id: InputId.Mouse,
actions: [
{
type: ActionType.PointerUp,
button: getBidiButton(options.button ?? MouseButton.Left),
},
],
},
]);
}
override async click(
@ -582,41 +558,35 @@ export class BidiMouse extends Mouse {
});
}
actions.push(pointerUpAction);
await this.#context.connection.send('input.performActions', {
context: this.#context.id,
actions: [
{
type: SourceActionsType.Pointer,
id: InputId.Mouse,
actions,
},
],
});
await this.#page.mainFrame().browsingContext.performActions([
{
type: SourceActionsType.Pointer,
id: InputId.Mouse,
actions,
},
]);
}
override async wheel(
options: Readonly<MouseWheelOptions> = {}
): Promise<void> {
await this.#context.connection.send('input.performActions', {
context: this.#context.id,
actions: [
{
type: SourceActionsType.Wheel,
id: InputId.Wheel,
actions: [
{
type: ActionType.Scroll,
...(this.#lastMovePoint ?? {
x: 0,
y: 0,
}),
deltaX: options.deltaX ?? 0,
deltaY: options.deltaY ?? 0,
},
],
},
],
});
await this.#page.mainFrame().browsingContext.performActions([
{
type: SourceActionsType.Wheel,
id: InputId.Wheel,
actions: [
{
type: ActionType.Scroll,
...(this.#lastMovePoint ?? {
x: 0,
y: 0,
}),
deltaX: options.deltaX ?? 0,
deltaY: options.deltaY ?? 0,
},
],
},
]);
}
override drag(): never {
@ -644,11 +614,11 @@ export class BidiMouse extends Mouse {
* @internal
*/
export class BidiTouchscreen extends Touchscreen {
#context: BrowsingContext;
#page: BidiPage;
constructor(context: BrowsingContext) {
constructor(page: BidiPage) {
super();
this.#context = context;
this.#page = page;
}
override async touchStart(
@ -656,30 +626,27 @@ export class BidiTouchscreen extends Touchscreen {
y: number,
options: BidiTouchMoveOptions = {}
): Promise<void> {
await this.#context.connection.send('input.performActions', {
context: this.#context.id,
actions: [
{
type: SourceActionsType.Pointer,
id: InputId.Finger,
parameters: {
pointerType: Bidi.Input.PointerType.Touch,
},
actions: [
{
type: ActionType.PointerMove,
x: Math.round(x),
y: Math.round(y),
origin: options.origin,
},
{
type: ActionType.PointerDown,
button: 0,
},
],
await this.#page.mainFrame().browsingContext.performActions([
{
type: SourceActionsType.Pointer,
id: InputId.Finger,
parameters: {
pointerType: Bidi.Input.PointerType.Touch,
},
],
});
actions: [
{
type: ActionType.PointerMove,
x: Math.round(x),
y: Math.round(y),
origin: options.origin,
},
{
type: ActionType.PointerDown,
button: 0,
},
],
},
]);
}
override async touchMove(
@ -687,46 +654,40 @@ export class BidiTouchscreen extends Touchscreen {
y: number,
options: BidiTouchMoveOptions = {}
): Promise<void> {
await this.#context.connection.send('input.performActions', {
context: this.#context.id,
actions: [
{
type: SourceActionsType.Pointer,
id: InputId.Finger,
parameters: {
pointerType: Bidi.Input.PointerType.Touch,
},
actions: [
{
type: ActionType.PointerMove,
x: Math.round(x),
y: Math.round(y),
origin: options.origin,
},
],
await this.#page.mainFrame().browsingContext.performActions([
{
type: SourceActionsType.Pointer,
id: InputId.Finger,
parameters: {
pointerType: Bidi.Input.PointerType.Touch,
},
],
});
actions: [
{
type: ActionType.PointerMove,
x: Math.round(x),
y: Math.round(y),
origin: options.origin,
},
],
},
]);
}
override async touchEnd(): Promise<void> {
await this.#context.connection.send('input.performActions', {
context: this.#context.id,
actions: [
{
type: SourceActionsType.Pointer,
id: InputId.Finger,
parameters: {
pointerType: Bidi.Input.PointerType.Touch,
},
actions: [
{
type: ActionType.PointerUp,
button: 0,
},
],
await this.#page.mainFrame().browsingContext.performActions([
{
type: SourceActionsType.Pointer,
id: InputId.Finger,
parameters: {
pointerType: Bidi.Input.PointerType.Touch,
},
],
});
actions: [
{
type: ActionType.PointerUp,
button: 0,
},
],
},
]);
}
}

View File

@ -12,28 +12,28 @@ import {UnsupportedOperation} from '../common/Errors.js';
import {BidiDeserializer} from './Deserializer.js';
import type {BidiRealm} from './Realm.js';
import type {Sandbox} from './Sandbox.js';
/**
* @internal
*/
export class BidiJSHandle<T = unknown> extends JSHandle<T> {
#disposed = false;
readonly #sandbox: Sandbox;
static from<T>(
value: Bidi.Script.RemoteValue,
realm: BidiRealm
): BidiJSHandle<T> {
return new BidiJSHandle(value, realm);
}
readonly #remoteValue: Bidi.Script.RemoteValue;
constructor(sandbox: Sandbox, remoteValue: Bidi.Script.RemoteValue) {
override readonly realm: BidiRealm;
#disposed = false;
constructor(value: Bidi.Script.RemoteValue, realm: BidiRealm) {
super();
this.#sandbox = sandbox;
this.#remoteValue = remoteValue;
}
context(): BidiRealm {
return this.realm.environment.context();
}
override get realm(): Sandbox {
return this.#sandbox;
this.#remoteValue = value;
this.realm = realm;
}
override get disposed(): boolean {
@ -55,7 +55,7 @@ export class BidiJSHandle<T = unknown> extends JSHandle<T> {
return;
}
this.#disposed = true;
await this.context().destroyHandles([this]);
await this.realm.destroyHandles([this]);
}
get isPrimitiveValue(): boolean {

View File

@ -1,155 +0,0 @@
/**
* @license
* Copyright 2023 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import {EventEmitter, EventSubscription} from '../common/EventEmitter.js';
import {
NetworkManagerEvent,
type NetworkManagerEvents,
} from '../common/NetworkManagerEvents.js';
import {DisposableStack} from '../util/disposable.js';
import type {BidiConnection} from './Connection.js';
import type {BidiFrame} from './Frame.js';
import {BidiHTTPRequest} from './HTTPRequest.js';
import {BidiHTTPResponse} from './HTTPResponse.js';
import type {BidiPage} from './Page.js';
/**
* @internal
*/
export class BidiNetworkManager extends EventEmitter<NetworkManagerEvents> {
#connection: BidiConnection;
#page: BidiPage;
#subscriptions = new DisposableStack();
#requestMap = new Map<string, BidiHTTPRequest>();
#navigationMap = new Map<string, BidiHTTPResponse>();
constructor(connection: BidiConnection, page: BidiPage) {
super();
this.#connection = connection;
this.#page = page;
// TODO: Subscribe to the Frame individually
this.#subscriptions.use(
new EventSubscription(
this.#connection,
'network.beforeRequestSent',
this.#onBeforeRequestSent.bind(this)
)
);
this.#subscriptions.use(
new EventSubscription(
this.#connection,
'network.responseStarted',
this.#onResponseStarted.bind(this)
)
);
this.#subscriptions.use(
new EventSubscription(
this.#connection,
'network.responseCompleted',
this.#onResponseCompleted.bind(this)
)
);
this.#subscriptions.use(
new EventSubscription(
this.#connection,
'network.fetchError',
this.#onFetchError.bind(this)
)
);
}
#onBeforeRequestSent(event: Bidi.Network.BeforeRequestSentParameters): void {
const frame = this.#page.frame(event.context ?? '');
if (!frame) {
return;
}
const request = this.#requestMap.get(event.request.request);
let upsertRequest: BidiHTTPRequest;
if (request) {
request._redirectChain.push(request);
upsertRequest = new BidiHTTPRequest(event, frame, request._redirectChain);
} else {
upsertRequest = new BidiHTTPRequest(event, frame, []);
}
this.#requestMap.set(event.request.request, upsertRequest);
this.emit(NetworkManagerEvent.Request, upsertRequest);
}
#onResponseStarted(_event: Bidi.Network.ResponseStartedParameters) {}
#onResponseCompleted(event: Bidi.Network.ResponseCompletedParameters): void {
const request = this.#requestMap.get(event.request.request);
if (!request) {
return;
}
const response = new BidiHTTPResponse(request, event);
request._response = response;
if (event.navigation) {
this.#navigationMap.set(event.navigation, response);
}
if (response.fromCache()) {
this.emit(NetworkManagerEvent.RequestServedFromCache, request);
}
this.emit(NetworkManagerEvent.Response, response);
this.emit(NetworkManagerEvent.RequestFinished, request);
}
#onFetchError(event: Bidi.Network.FetchErrorParameters) {
const request = this.#requestMap.get(event.request.request);
if (!request) {
return;
}
request._failureText = event.errorText;
this.emit(NetworkManagerEvent.RequestFailed, request);
this.#requestMap.delete(event.request.request);
}
getNavigationResponse(navigationId?: string | null): BidiHTTPResponse | null {
if (!navigationId) {
return null;
}
const response = this.#navigationMap.get(navigationId);
return response ?? null;
}
inFlightRequestsCount(): number {
let inFlightRequestCounter = 0;
for (const request of this.#requestMap.values()) {
if (!request.response() || request._failureText) {
inFlightRequestCounter++;
}
}
return inFlightRequestCounter;
}
clearMapAfterFrameDispose(frame: BidiFrame): void {
for (const [id, request] of this.#requestMap.entries()) {
if (request.frame() === frame) {
this.#requestMap.delete(id);
}
}
for (const [id, response] of this.#navigationMap.entries()) {
if (response.frame() === frame) {
this.#navigationMap.delete(id);
}
}
}
dispose(): void {
this.removeAllListeners();
this.#requestMap.clear();
this.#navigationMap.clear();
this.#subscriptions.dispose();
}
}

View File

@ -7,201 +7,93 @@
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import type Protocol from 'devtools-protocol';
import {
firstValueFrom,
from,
map,
raceWith,
zip,
} from '../../third_party/rxjs/rxjs.js';
import {firstValueFrom, from, raceWith} from '../../third_party/rxjs/rxjs.js';
import type {CDPSession} from '../api/CDPSession.js';
import type {BoundingBox} from '../api/ElementHandle.js';
import type {WaitForOptions} from '../api/Frame.js';
import type {HTTPResponse} from '../api/HTTPResponse.js';
import type {MediaFeature, GeolocationOptions} from '../api/Page.js';
import {
Page,
PageEvent,
type GeolocationOptions,
type MediaFeature,
type NewDocumentScriptEvaluation,
type ScreenshotOptions,
} from '../api/Page.js';
import {Accessibility} from '../cdp/Accessibility.js';
import {Coverage} from '../cdp/Coverage.js';
import {EmulationManager as CdpEmulationManager} from '../cdp/EmulationManager.js';
import {FrameTree} from '../cdp/FrameTree.js';
import {EmulationManager} from '../cdp/EmulationManager.js';
import {Tracing} from '../cdp/Tracing.js';
import type {ConsoleMessageType} from '../common/ConsoleMessage.js';
import {
ConsoleMessage,
type ConsoleMessageLocation,
} from '../common/ConsoleMessage.js';
import type {Cookie, CookieSameSite, CookieParam} from '../common/Cookie.js';
import {TargetCloseError, UnsupportedOperation} from '../common/Errors.js';
import type {Handler} from '../common/EventEmitter.js';
import {NetworkManagerEvent} from '../common/NetworkManagerEvents.js';
import type {Cookie, CookieParam, CookieSameSite} from '../common/Cookie.js';
import {UnsupportedOperation} from '../common/Errors.js';
import type {PDFOptions} from '../common/PDFOptions.js';
import type {Awaitable} from '../common/types.js';
import {
debugError,
evaluationString,
NETWORK_IDLE_TIME,
parsePDFOptions,
timeout,
validateDialogType,
} from '../common/util.js';
import {evaluationString, parsePDFOptions, timeout} from '../common/util.js';
import type {Viewport} from '../common/Viewport.js';
import {assert} from '../util/assert.js';
import {Deferred} from '../util/Deferred.js';
import {disposeSymbol} from '../util/disposable.js';
import {isErrorLike} from '../util/ErrorLike.js';
import type {BidiBrowser} from './Browser.js';
import type {BidiBrowserContext} from './BrowserContext.js';
import {BrowsingContextEvent, type BrowsingContext} from './BrowsingContext.js';
import {BidiCdpSession} from './CDPSession.js';
import type {BidiConnection} from './Connection.js';
import {BidiDeserializer} from './Deserializer.js';
import {BidiDialog} from './Dialog.js';
import type {BidiCdpSession} from './CDPSession.js';
import type {BrowsingContext} from './core/BrowsingContext.js';
import {BidiElementHandle} from './ElementHandle.js';
import {EmulationManager} from './EmulationManager.js';
import {BidiFrame} from './Frame.js';
import type {BidiHTTPRequest} from './HTTPRequest.js';
import type {BidiHTTPResponse} from './HTTPResponse.js';
import {BidiKeyboard, BidiMouse, BidiTouchscreen} from './Input.js';
import type {BidiJSHandle} from './JSHandle.js';
import {getBiDiReadinessState, rewriteNavigationError} from './lifecycle.js';
import {BidiNetworkManager} from './NetworkManager.js';
import {createBidiHandle} from './Realm.js';
import type {BiDiPageTarget} from './Target.js';
import {rewriteNavigationError} from './util.js';
/**
* @internal
*/
export class BidiPage extends Page {
#accessibility: Accessibility;
#connection: BidiConnection;
#frameTree = new FrameTree<BidiFrame>();
#networkManager: BidiNetworkManager;
#viewport: Viewport | null = null;
#closedDeferred = Deferred.create<never, TargetCloseError>();
#subscribedEvents = new Map<Bidi.Event['method'], Handler<any>>([
['log.entryAdded', this.#onLogEntryAdded.bind(this)],
['browsingContext.load', this.#onFrameLoaded.bind(this)],
[
'browsingContext.fragmentNavigated',
this.#onFrameFragmentNavigated.bind(this),
],
[
'browsingContext.domContentLoaded',
this.#onFrameDOMContentLoaded.bind(this),
],
['browsingContext.userPromptOpened', this.#onDialog.bind(this)],
]);
readonly #networkManagerEvents = [
[
NetworkManagerEvent.Request,
(request: BidiHTTPRequest) => {
this.emit(PageEvent.Request, request);
},
],
[
NetworkManagerEvent.RequestServedFromCache,
(request: BidiHTTPRequest) => {
this.emit(PageEvent.RequestServedFromCache, request);
},
],
[
NetworkManagerEvent.RequestFailed,
(request: BidiHTTPRequest) => {
this.emit(PageEvent.RequestFailed, request);
},
],
[
NetworkManagerEvent.RequestFinished,
(request: BidiHTTPRequest) => {
this.emit(PageEvent.RequestFinished, request);
},
],
[
NetworkManagerEvent.Response,
(response: BidiHTTPResponse) => {
this.emit(PageEvent.Response, response);
},
],
] as const;
readonly #browsingContextEvents = new Map<symbol, Handler<any>>([
[BrowsingContextEvent.Created, this.#onContextCreated.bind(this)],
[BrowsingContextEvent.Destroyed, this.#onContextDestroyed.bind(this)],
]);
#tracing: Tracing;
#coverage: Coverage;
#cdpEmulationManager: CdpEmulationManager;
#emulationManager: EmulationManager;
#mouse: BidiMouse;
#touchscreen: BidiTouchscreen;
#keyboard: BidiKeyboard;
#browsingContext: BrowsingContext;
#browserContext: BidiBrowserContext;
_client(): CDPSession {
return this.mainFrame().context().cdpSession;
static from(
browserContext: BidiBrowserContext,
browsingContext: BrowsingContext
): BidiPage {
const page = new BidiPage(browserContext, browsingContext);
page.#initialize();
return page;
}
constructor(
browsingContext: BrowsingContext,
browserContext: BidiBrowserContext
readonly #browserContext: BidiBrowserContext;
readonly #frame: BidiFrame;
#viewport: Viewport | null = null;
readonly keyboard: BidiKeyboard;
readonly mouse: BidiMouse;
readonly touchscreen: BidiTouchscreen;
readonly accessibility: Accessibility;
readonly tracing: Tracing;
readonly coverage: Coverage;
readonly #cdpEmulationManager: EmulationManager;
_client(): BidiCdpSession {
return this.#frame.client;
}
private constructor(
browserContext: BidiBrowserContext,
browsingContext: BrowsingContext
) {
super();
this.#browsingContext = browsingContext;
this.#browserContext = browserContext;
this.#connection = browsingContext.connection;
this.#frame = BidiFrame.from(this, browsingContext);
for (const [event, subscriber] of this.#browsingContextEvents) {
this.#browsingContext.on(event, subscriber);
}
this.#networkManager = new BidiNetworkManager(this.#connection, this);
for (const [event, subscriber] of this.#subscribedEvents) {
this.#connection.on(event, subscriber);
}
for (const [event, subscriber] of this.#networkManagerEvents) {
// TODO: remove any
this.#networkManager.on(event, subscriber as any);
}
const frame = new BidiFrame(
this,
this.#browsingContext,
this._timeoutSettings,
this.#browsingContext.parent
);
this.#frameTree.addFrame(frame);
this.emit(PageEvent.FrameAttached, frame);
// TODO: https://github.com/w3c/webdriver-bidi/issues/443
this.#accessibility = new Accessibility(
this.mainFrame().context().cdpSession
);
this.#tracing = new Tracing(this.mainFrame().context().cdpSession);
this.#coverage = new Coverage(this.mainFrame().context().cdpSession);
this.#cdpEmulationManager = new CdpEmulationManager(
this.mainFrame().context().cdpSession
);
this.#emulationManager = new EmulationManager(browsingContext);
this.#mouse = new BidiMouse(this.mainFrame().context());
this.#touchscreen = new BidiTouchscreen(this.mainFrame().context());
this.#keyboard = new BidiKeyboard(this);
this.#cdpEmulationManager = new EmulationManager(this.#frame.client);
this.accessibility = new Accessibility(this.#frame.client);
this.tracing = new Tracing(this.#frame.client);
this.coverage = new Coverage(this.#frame.client);
this.keyboard = new BidiKeyboard(this);
this.mouse = new BidiMouse(this);
this.touchscreen = new BidiTouchscreen(this);
}
/**
* @internal
*/
get connection(): BidiConnection {
return this.#connection;
#initialize() {
this.#frame.browsingContext.on('closed', () => {
this.emit(PageEvent.Close, undefined);
this.removeAllListeners();
});
}
override async setUserAgent(
@ -228,46 +120,15 @@ export class BidiPage extends Page {
prototypeHandle.id,
'Prototype JSHandle must not be referencing primitive value'
);
const response = await this.mainFrame().client.send(
'Runtime.queryObjects',
{
prototypeObjectId: prototypeHandle.id,
}
);
return createBidiHandle(this.mainFrame().mainRealm(), {
const response = await this.#frame.client.send('Runtime.queryObjects', {
prototypeObjectId: prototypeHandle.id,
});
return this.#frame.mainRealm().createHandle({
type: 'array',
handle: response.objects.objectId,
}) as BidiJSHandle<Prototype[]>;
}
_setBrowserContext(browserContext: BidiBrowserContext): void {
this.#browserContext = browserContext;
}
override get accessibility(): Accessibility {
return this.#accessibility;
}
override get tracing(): Tracing {
return this.#tracing;
}
override get coverage(): Coverage {
return this.#coverage;
}
override get mouse(): BidiMouse {
return this.#mouse;
}
override get touchscreen(): BidiTouchscreen {
return this.#touchscreen;
}
override get keyboard(): BidiKeyboard {
return this.#keyboard;
}
override browser(): BidiBrowser {
return this.browserContext().browser();
}
@ -277,14 +138,9 @@ export class BidiPage extends Page {
}
override mainFrame(): BidiFrame {
const mainFrame = this.#frameTree.getMainFrame();
assert(mainFrame, 'Requesting main frame too early!');
return mainFrame;
return this.#frame;
}
/**
* @internal
*/
async focusedFrame(): Promise<BidiFrame> {
using frame = await this.mainFrame()
.isolatedRealm()
@ -304,216 +160,38 @@ export class BidiPage extends Page {
}
override frames(): BidiFrame[] {
return Array.from(this.#frameTree.frames());
}
frame(frameId?: string): BidiFrame | null {
return this.#frameTree.getById(frameId ?? '') || null;
}
childFrames(frameId: string): BidiFrame[] {
return this.#frameTree.childFrames(frameId);
}
#onFrameLoaded(info: Bidi.BrowsingContext.NavigationInfo): void {
const frame = this.frame(info.context);
if (frame && this.mainFrame() === frame) {
this.emit(PageEvent.Load, undefined);
const frames = [this.#frame];
for (const frame of frames) {
frames.push(...frame.childFrames());
}
}
#onFrameFragmentNavigated(info: Bidi.BrowsingContext.NavigationInfo): void {
const frame = this.frame(info.context);
if (frame) {
this.emit(PageEvent.FrameNavigated, frame);
}
}
#onFrameDOMContentLoaded(info: Bidi.BrowsingContext.NavigationInfo): void {
const frame = this.frame(info.context);
if (frame) {
frame._hasStartedLoading = true;
if (this.mainFrame() === frame) {
this.emit(PageEvent.DOMContentLoaded, undefined);
}
this.emit(PageEvent.FrameNavigated, frame);
}
}
#onContextCreated(context: BrowsingContext): void {
if (
!this.frame(context.id) &&
(this.frame(context.parent ?? '') || !this.#frameTree.getMainFrame())
) {
const frame = new BidiFrame(
this,
context,
this._timeoutSettings,
context.parent
);
this.#frameTree.addFrame(frame);
if (frame !== this.mainFrame()) {
this.emit(PageEvent.FrameAttached, frame);
}
}
}
#onContextDestroyed(context: BrowsingContext): void {
const frame = this.frame(context.id);
if (frame) {
if (frame === this.mainFrame()) {
this.emit(PageEvent.Close, undefined);
}
this.#removeFramesRecursively(frame);
}
}
#removeFramesRecursively(frame: BidiFrame): void {
for (const child of frame.childFrames()) {
this.#removeFramesRecursively(child);
}
frame[disposeSymbol]();
this.#networkManager.clearMapAfterFrameDispose(frame);
this.#frameTree.removeFrame(frame);
this.emit(PageEvent.FrameDetached, frame);
}
#onLogEntryAdded(event: Bidi.Log.Entry): void {
const frame = this.frame(event.source.context);
if (!frame) {
return;
}
if (isConsoleLogEntry(event)) {
const args = event.args.map(arg => {
return createBidiHandle(frame.mainRealm(), arg);
});
const text = args
.reduce((value, arg) => {
const parsedValue = arg.isPrimitiveValue
? BidiDeserializer.deserialize(arg.remoteValue())
: arg.toString();
return `${value} ${parsedValue}`;
}, '')
.slice(1);
this.emit(
PageEvent.Console,
new ConsoleMessage(
event.method as ConsoleMessageType,
text,
args,
getStackTraceLocations(event.stackTrace)
)
);
} else if (isJavaScriptLogEntry(event)) {
const error = new Error(event.text ?? '');
const messageHeight = error.message.split('\n').length;
const messageLines = error.stack!.split('\n').splice(0, messageHeight);
const stackLines = [];
if (event.stackTrace) {
for (const frame of event.stackTrace.callFrames) {
// Note we need to add `1` because the values are 0-indexed.
stackLines.push(
` at ${frame.functionName || '<anonymous>'} (${frame.url}:${
frame.lineNumber + 1
}:${frame.columnNumber + 1})`
);
if (stackLines.length >= Error.stackTraceLimit) {
break;
}
}
}
error.stack = [...messageLines, ...stackLines].join('\n');
this.emit(PageEvent.PageError, error);
} else {
debugError(
`Unhandled LogEntry with type "${event.type}", text "${event.text}" and level "${event.level}"`
);
}
}
#onDialog(event: Bidi.BrowsingContext.UserPromptOpenedParameters): void {
const frame = this.frame(event.context);
if (!frame) {
return;
}
const type = validateDialogType(event.type);
const dialog = new BidiDialog(
frame.context(),
type,
event.message,
event.defaultValue
);
this.emit(PageEvent.Dialog, dialog);
}
getNavigationResponse(id?: string | null): BidiHTTPResponse | null {
return this.#networkManager.getNavigationResponse(id);
return frames;
}
override isClosed(): boolean {
return this.#closedDeferred.finished();
return this.#frame.detached;
}
override async close(options?: {runBeforeUnload?: boolean}): Promise<void> {
if (this.#closedDeferred.finished()) {
try {
await this.#frame.browsingContext.close(options?.runBeforeUnload);
} catch {
return;
}
this.#closedDeferred.reject(new TargetCloseError('Page closed!'));
this.#networkManager.dispose();
await this.#connection.send('browsingContext.close', {
context: this.mainFrame()._id,
promptUnload: options?.runBeforeUnload ?? false,
});
this.emit(PageEvent.Close, undefined);
this.removeAllListeners();
}
override async reload(
options: WaitForOptions = {}
): Promise<BidiHTTPResponse | null> {
const {
waitUntil = 'load',
timeout: ms = this._timeoutSettings.navigationTimeout(),
} = options;
const [readiness, networkIdle] = getBiDiReadinessState(waitUntil);
const result$ = zip(
from(
this.#connection.send('browsingContext.reload', {
context: this.mainFrame()._id,
wait: readiness,
})
),
...(networkIdle !== null
? [
this.waitForNetworkIdle$({
timeout: ms,
concurrency: networkIdle === 'networkidle2' ? 2 : 0,
idleTime: NETWORK_IDLE_TIME,
}),
]
: [])
).pipe(
map(([{result}]) => {
return result;
}),
raceWith(timeout(ms), from(this.#closedDeferred.valueOrThrow())),
rewriteNavigationError(this.url(), ms)
const [response] = await Promise.all([
this.#frame.waitForNavigation(options),
this.#frame.browsingContext.reload(),
]).catch(
rewriteNavigationError(
this.url(),
options.timeout ?? this._timeoutSettings.navigationTimeout()
)
);
const result = await firstValueFrom(result$);
return this.getNavigationResponse(result.navigation);
return response;
}
override setDefaultNavigationTimeout(timeout: number): void {
@ -572,8 +250,19 @@ export class BidiPage extends Page {
}
override async setViewport(viewport: Viewport): Promise<void> {
if (!this.#browsingContext.supportsCdp()) {
await this.#emulationManager.emulateViewport(viewport);
if (!this.browser().cdpSupported) {
await this.#frame.browsingContext.setViewport({
viewport:
viewport.width && viewport.height
? {
width: viewport.width,
height: viewport.height,
}
: null,
devicePixelRatio: viewport.deviceScaleFactor
? viewport.deviceScaleFactor
: null,
});
this.#viewport = viewport;
return;
}
@ -603,10 +292,9 @@ export class BidiPage extends Page {
preferCSSPageSize,
} = parsePDFOptions(options, 'cm');
const pageRanges = ranges ? ranges.split(', ') : [];
const {result} = await firstValueFrom(
const data = await firstValueFrom(
from(
this.#connection.send('browsingContext.print', {
context: this.mainFrame()._id,
this.#frame.browsingContext.print({
background,
margin,
orientation: landscape ? 'landscape' : 'portrait',
@ -621,7 +309,7 @@ export class BidiPage extends Page {
).pipe(raceWith(timeout(ms)))
);
const buffer = Buffer.from(result.data, 'base64');
const buffer = Buffer.from(data, 'base64');
await this._maybeWriteBufferToFile(path, buffer);
@ -687,10 +375,7 @@ export class BidiPage extends Page {
}
}
const {
result: {data},
} = await this.#connection.send('browsingContext.captureScreenshot', {
context: this.mainFrame()._id,
const data = await this.#frame.browsingContext.captureScreenshot({
origin: captureBeyondViewport ? 'document' : 'viewport',
format: {
type: `image/${type}`,
@ -702,19 +387,11 @@ export class BidiPage extends Page {
}
override async createCDPSession(): Promise<CDPSession> {
const {sessionId} = await this.mainFrame()
.context()
.cdpSession.send('Target.attachToTarget', {
targetId: this.mainFrame()._id,
flatten: true,
});
return new BidiCdpSession(this.mainFrame().context(), sessionId);
return await this.#frame.createCDPSession();
}
override async bringToFront(): Promise<void> {
await this.#connection.send('browsingContext.activate', {
context: this.mainFrame()._id,
});
await this.#frame.browsingContext.activate();
}
override async evaluateOnNewDocument<
@ -725,20 +402,16 @@ export class BidiPage extends Page {
...args: Params
): Promise<NewDocumentScriptEvaluation> {
const expression = evaluationExpression(pageFunction, ...args);
const {result} = await this.#connection.send('script.addPreloadScript', {
functionDeclaration: expression,
contexts: [this.mainFrame()._id],
});
const script =
await this.#frame.browsingContext.addPreloadScript(expression);
return {identifier: result.script};
return {identifier: script};
}
override async removeScriptToEvaluateOnNewDocument(
id: string
): Promise<void> {
await this.#connection.send('script.removePreloadScript', {
script: id,
});
await this.#frame.browsingContext.removePreloadScript(id);
}
override async exposeFunction<Args extends unknown[], Ret>(
@ -769,13 +442,8 @@ export class BidiPage extends Page {
return new URL(url);
});
const bidiCookies = await this.#connection.send('storage.getCookies', {
partition: {
type: 'context',
context: this.mainFrame()._id,
},
});
return bidiCookies.result.cookies
const cookies = await this.#frame.browsingContext.getCookies();
return cookies
.map(cookie => {
return bidiToPuppeteerCookie(cookie);
})
@ -790,7 +458,7 @@ export class BidiPage extends Page {
throw new UnsupportedOperation();
}
override target(): BiDiPageTarget {
override target(): never {
throw new UnsupportedOperation();
}
@ -876,22 +544,14 @@ export class BidiPage extends Page {
// TODO: delete cookie before setting them.
// await this.deleteCookie(bidiCookie);
const partition: Bidi.Storage.PartitionDescriptor =
cookie.partitionKey !== undefined
? {
type: 'storageKey',
sourceOrigin: cookie.partitionKey,
userContext: this.#browserContext.id,
}
: {
type: 'context',
context: this.mainFrame()._id,
};
await this.#connection.send('storage.setCookie', {
cookie: bidiCookie,
partition,
});
if (cookie.partitionKey !== undefined) {
await this.browserContext().userContext.setCookie(
bidiCookie,
cookie.partitionKey
);
} else {
await this.#frame.browsingContext.setCookie(bidiCookie);
}
}
}
@ -925,7 +585,7 @@ export class BidiPage extends Page {
override async goForward(
options: WaitForOptions = {}
): Promise<HTTPResponse | null> {
return await this.#go(+1, options);
return await this.#go(1, options);
}
async #go(
@ -933,22 +593,19 @@ export class BidiPage extends Page {
options: WaitForOptions
): Promise<HTTPResponse | null> {
try {
const result = await Promise.all([
const [response] = await Promise.all([
this.waitForNavigation(options),
this.#connection.send('browsingContext.traverseHistory', {
delta,
context: this.mainFrame()._id,
}),
this.#frame.browsingContext.traverseHistory(delta),
]);
return result[0];
} catch (err) {
return response;
} catch (error) {
// TODO: waitForNavigation should be cancelled if an error happens.
if (isErrorLike(err)) {
if (err.message.includes('no such history entry')) {
if (isErrorLike(error)) {
if (error.message.includes('no such history entry')) {
return null;
}
}
throw err;
throw error;
}
}
@ -957,34 +614,6 @@ export class BidiPage extends Page {
}
}
function isConsoleLogEntry(
event: Bidi.Log.Entry
): event is Bidi.Log.ConsoleLogEntry {
return event.type === 'console';
}
function isJavaScriptLogEntry(
event: Bidi.Log.Entry
): event is Bidi.Log.JavascriptLogEntry {
return event.type === 'javascript';
}
function getStackTraceLocations(
stackTrace?: Bidi.Script.StackTrace
): ConsoleMessageLocation[] {
const stackTraceLocations: ConsoleMessageLocation[] = [];
if (stackTrace) {
for (const callFrame of stackTrace.callFrames) {
stackTraceLocations.push({
url: callFrame.url,
lineNumber: callFrame.lineNumber,
columnNumber: callFrame.columnNumber,
});
}
}
return stackTraceLocations;
}
function evaluationExpression(fun: Function | string, ...args: unknown[]) {
return `() => {${evaluationString(fun, ...args)}}`;
}

View File

@ -5,8 +5,11 @@
*/
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import {EventEmitter, type EventType} from '../common/EventEmitter.js';
import type {JSHandle} from '../api/JSHandle.js';
import {Realm} from '../api/Realm.js';
import {LazyArg} from '../common/LazyArg.js';
import {scriptInjector} from '../common/ScriptInjector.js';
import type {TimeoutSettings} from '../common/TimeoutSettings.js';
import type {EvaluateFunc, HandleFor} from '../common/types.js';
import {
debugError,
@ -17,69 +20,33 @@ import {
SOURCE_URL_REGEX,
} from '../common/util.js';
import type PuppeteerUtil from '../injected/injected.js';
import {disposeSymbol} from '../util/disposable.js';
import {stringifyFunction} from '../util/Function.js';
import type {BidiConnection} from './Connection.js';
import type {Realm as BidiRealmCore} from './core/Realm.js';
import type {WindowRealm} from './core/Realm.js';
import {BidiDeserializer} from './Deserializer.js';
import {BidiElementHandle} from './ElementHandle.js';
import type {BidiFrame} from './Frame.js';
import {BidiJSHandle} from './JSHandle.js';
import type {Sandbox} from './Sandbox.js';
import {BidiSerializer} from './Serializer.js';
import {createEvaluationError} from './util.js';
/**
* @internal
*/
export class BidiRealm extends EventEmitter<Record<EventType, any>> {
readonly connection: BidiConnection;
export abstract class BidiRealm extends Realm {
realm: BidiRealmCore;
#id!: string;
#sandbox!: Sandbox;
constructor(connection: BidiConnection) {
super();
this.connection = connection;
constructor(realm: BidiRealmCore, timeoutSettings: TimeoutSettings) {
super(timeoutSettings);
this.realm = realm;
}
get target(): Bidi.Script.Target {
return {
context: this.#sandbox.environment._id,
sandbox: this.#sandbox.name,
};
}
handleRealmDestroyed = async (
params: Bidi.Script.RealmDestroyed['params']
): Promise<void> => {
if (params.realm === this.#id) {
// Note: The Realm is destroyed, so in theory the handle should be as
// well.
protected initialize(): void {
this.realm.on('destroyed', ({reason}) => {
this.taskManager.terminateAll(new Error(reason));
});
this.realm.on('updated', () => {
this.internalPuppeteerUtil = undefined;
this.#sandbox.environment.clearDocumentHandle();
}
};
handleRealmCreated = (params: Bidi.Script.RealmCreated['params']): void => {
if (
params.type === 'window' &&
params.context === this.#sandbox.environment._id &&
params.sandbox === this.#sandbox.name
) {
this.#id = params.realm;
void this.#sandbox.taskManager.rerunAll();
}
};
setSandbox(sandbox: Sandbox): void {
this.#sandbox = sandbox;
this.connection.on(
Bidi.ChromiumBidi.Script.EventNames.RealmCreated,
this.handleRealmCreated
);
this.connection.on(
Bidi.ChromiumBidi.Script.EventNames.RealmDestroyed,
this.handleRealmDestroyed
);
void this.taskManager.rerunAll();
});
}
protected internalPuppeteerUtil?: Promise<BidiJSHandle<PuppeteerUtil>>;
@ -100,7 +67,7 @@ export class BidiRealm extends EventEmitter<Record<EventType, any>> {
return this.internalPuppeteerUtil as Promise<BidiJSHandle<PuppeteerUtil>>;
}
async evaluateHandle<
override async evaluateHandle<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
>(
@ -110,7 +77,7 @@ export class BidiRealm extends EventEmitter<Record<EventType, any>> {
return await this.#evaluate(false, pageFunction, ...args);
}
async evaluate<
override async evaluate<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
>(
@ -149,8 +116,6 @@ export class BidiRealm extends EventEmitter<Record<EventType, any>> {
PuppeteerURL.INTERNAL_URL
);
const sandbox = this.#sandbox;
let responsePromise;
const resultOwnership = returnByValue
? Bidi.Script.ResultOwnership.None
@ -166,11 +131,8 @@ export class BidiRealm extends EventEmitter<Record<EventType, any>> {
? pageFunction
: `${pageFunction}\n${sourceUrlComment}\n`;
responsePromise = this.connection.send('script.evaluate', {
expression,
target: this.target,
responsePromise = this.realm.evaluate(expression, true, {
resultOwnership,
awaitPromise: true,
userActivation: true,
serializationOptions,
});
@ -179,24 +141,25 @@ export class BidiRealm extends EventEmitter<Record<EventType, any>> {
functionDeclaration = SOURCE_URL_REGEX.test(functionDeclaration)
? functionDeclaration
: `${functionDeclaration}\n${sourceUrlComment}\n`;
responsePromise = this.connection.send('script.callFunction', {
responsePromise = this.realm.callFunction(
functionDeclaration,
arguments: args.length
? await Promise.all(
args.map(arg => {
return sandbox.serialize(arg);
})
)
: [],
target: this.target,
resultOwnership,
awaitPromise: true,
userActivation: true,
serializationOptions,
});
/* awaitPromise= */ true,
{
arguments: args.length
? await Promise.all(
args.map(arg => {
return this.serialize(arg);
})
)
: [],
resultOwnership,
userActivation: true,
serializationOptions,
}
);
}
const {result} = await responsePromise;
const result = await responsePromise;
if ('type' in result && result.type === 'exception') {
throw createEvaluationError(result.exceptionDetails);
@ -204,7 +167,49 @@ export class BidiRealm extends EventEmitter<Record<EventType, any>> {
return returnByValue
? BidiDeserializer.deserialize(result.result)
: createBidiHandle(sandbox, result.result);
: this.createHandle(result.result);
}
createHandle(
result: Bidi.Script.RemoteValue
): BidiJSHandle<unknown> | BidiElementHandle<Node> {
if (
(result.type === 'node' || result.type === 'window') &&
this instanceof BidiFrameRealm
) {
return BidiElementHandle.from(result, this);
}
return BidiJSHandle.from(result, this);
}
async serialize(arg: unknown): Promise<Bidi.Script.LocalValue> {
if (arg instanceof LazyArg) {
arg = await arg.get(this);
}
if (arg instanceof BidiJSHandle || arg instanceof BidiElementHandle) {
if (arg.realm !== this) {
if (
!(arg.realm instanceof BidiFrameRealm) ||
!(this instanceof BidiFrameRealm)
) {
throw new Error(
"Trying to evaluate JSHandle from different global types. Usually this means you're using a handle from a worker in a page or vice versa."
);
}
if (arg.realm.environment !== this.environment) {
throw new Error(
"Trying to evaluate JSHandle from different frames. Usually this means you're using a handle from a page on a different page."
);
}
}
if (arg.disposed) {
throw new Error('JSHandle is disposed!');
}
return arg.remoteValue() as Bidi.Script.RemoteReference;
}
return BidiSerializer.serialize(arg);
}
async destroyHandles(handles: Array<BidiJSHandle<unknown>>): Promise<void> {
@ -215,43 +220,80 @@ export class BidiRealm extends EventEmitter<Record<EventType, any>> {
.filter((id): id is string => {
return id !== undefined;
});
if (handleIds.length === 0) {
return;
}
await this.connection
.send('script.disown', {
target: this.target,
handles: handleIds,
})
.catch(error => {
// Exceptions might happen in case of a page been navigated or closed.
// Swallow these since they are harmless and we don't leak anything in this case.
debugError(error);
});
await this.realm.disown(handleIds).catch(error => {
// Exceptions might happen in case of a page been navigated or closed.
// Swallow these since they are harmless and we don't leak anything in this case.
debugError(error);
});
}
[disposeSymbol](): void {
this.connection.off(
Bidi.ChromiumBidi.Script.EventNames.RealmCreated,
this.handleRealmCreated
);
this.connection.off(
Bidi.ChromiumBidi.Script.EventNames.RealmDestroyed,
this.handleRealmDestroyed
);
override async adoptHandle<T extends JSHandle<Node>>(handle: T): Promise<T> {
return (await this.evaluateHandle(node => {
return node;
}, handle)) as unknown as T;
}
override async transferHandle<T extends JSHandle<Node>>(
handle: T
): Promise<T> {
if (handle.realm === this) {
return handle;
}
const transferredHandle = this.adoptHandle(handle);
await handle.dispose();
return await transferredHandle;
}
}
/**
* @internal
*/
export function createBidiHandle(
sandbox: Sandbox,
result: Bidi.Script.RemoteValue
): BidiJSHandle<unknown> | BidiElementHandle<Node> {
if (result.type === 'node' || result.type === 'window') {
return new BidiElementHandle(sandbox, result);
export class BidiFrameRealm extends BidiRealm {
static from(realm: WindowRealm, frame: BidiFrame): BidiFrameRealm {
const frameRealm = new BidiFrameRealm(realm, frame);
frameRealm.#initialize();
return frameRealm;
}
declare readonly realm: WindowRealm;
readonly #frame: BidiFrame;
private constructor(realm: WindowRealm, frame: BidiFrame) {
super(realm, frame.timeoutSettings);
this.#frame = frame;
}
#initialize() {
// This should run first.
this.realm.on('updated', () => {
this.environment.clearDocumentHandle();
});
super.initialize();
}
get sandbox(): string | undefined {
return this.realm.sandbox;
}
override get environment(): BidiFrame {
return this.#frame;
}
override async adoptBackendNode(
backendNodeId?: number | undefined
): Promise<JSHandle<Node>> {
const {object} = await this.#frame.client.send('DOM.resolveNode', {
backendNodeId,
});
return BidiElementHandle.from(
{
handle: object.objectId,
type: 'node',
},
this
);
}
return new BidiJSHandle(sandbox, result);
}

View File

@ -1,154 +0,0 @@
/**
* @license
* Copyright 2023 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import type {JSHandle} from '../api/JSHandle.js';
import {Realm} from '../api/Realm.js';
import {LazyArg} from '../common/LazyArg.js';
import type {TimeoutSettings} from '../common/TimeoutSettings.js';
import type {EvaluateFunc, HandleFor} from '../common/types.js';
import {withSourcePuppeteerURLIfNone} from '../common/util.js';
import type {BrowsingContext} from './BrowsingContext.js';
import {BidiElementHandle} from './ElementHandle.js';
import type {BidiFrame} from './Frame.js';
import {BidiJSHandle} from './JSHandle.js';
import type {BidiRealm as BidiRealm} from './Realm.js';
import {BidiSerializer} from './Serializer.js';
/**
* A unique key for {@link SandboxChart} to denote the default world.
* Realms are automatically created in the default sandbox.
*
* @internal
*/
export const MAIN_SANDBOX = Symbol('mainSandbox');
/**
* A unique key for {@link SandboxChart} to denote the puppeteer sandbox.
* This world contains all puppeteer-internal bindings/code.
*
* @internal
*/
export const PUPPETEER_SANDBOX = Symbol('puppeteerSandbox');
/**
* @internal
*/
export interface SandboxChart {
[key: string]: Sandbox;
[MAIN_SANDBOX]: Sandbox;
[PUPPETEER_SANDBOX]: Sandbox;
}
/**
* @internal
*/
export class Sandbox extends Realm {
readonly name: string | undefined;
readonly realm: BidiRealm;
#frame: BidiFrame;
constructor(
name: string | undefined,
frame: BidiFrame,
// TODO: We should split the Realm and BrowsingContext
realm: BidiRealm | BrowsingContext,
timeoutSettings: TimeoutSettings
) {
super(timeoutSettings);
this.name = name;
this.realm = realm;
this.#frame = frame;
this.realm.setSandbox(this);
}
override get environment(): BidiFrame {
return this.#frame;
}
async evaluateHandle<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
>(
pageFunction: Func | string,
...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
pageFunction = withSourcePuppeteerURLIfNone(
this.evaluateHandle.name,
pageFunction
);
return await this.realm.evaluateHandle(pageFunction, ...args);
}
async evaluate<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
>(
pageFunction: Func | string,
...args: Params
): Promise<Awaited<ReturnType<Func>>> {
pageFunction = withSourcePuppeteerURLIfNone(
this.evaluate.name,
pageFunction
);
return await this.realm.evaluate(pageFunction, ...args);
}
async adoptHandle<T extends JSHandle<Node>>(handle: T): Promise<T> {
return (await this.evaluateHandle(node => {
return node;
}, handle)) as unknown as T;
}
async transferHandle<T extends JSHandle<Node>>(handle: T): Promise<T> {
if (handle.realm === this) {
return handle;
}
const transferredHandle = await this.evaluateHandle(node => {
return node;
}, handle);
await handle.dispose();
return transferredHandle as unknown as T;
}
override async adoptBackendNode(
backendNodeId?: number
): Promise<JSHandle<Node>> {
const {object} = await this.environment.client.send('DOM.resolveNode', {
backendNodeId: backendNodeId,
});
return new BidiElementHandle(this, {
handle: object.objectId,
type: 'node',
});
}
async serialize(arg: unknown): Promise<Bidi.Script.LocalValue> {
if (arg instanceof LazyArg) {
arg = await arg.get(this.realm);
}
// eslint-disable-next-line rulesdir/use-using -- We want this to continue living.
const objectHandle =
arg && (arg instanceof BidiJSHandle || arg instanceof BidiElementHandle)
? arg
: null;
if (objectHandle) {
if (
objectHandle.realm.environment.context() !== this.environment.context()
) {
throw new Error(
'JSHandles can be evaluated only in the context they were created!'
);
}
if (objectHandle.disposed) {
throw new Error('JSHandle is disposed!');
}
return objectHandle.remoteValue() as Bidi.Script.RemoteReference;
}
return BidiSerializer.serialize(arg);
}
}

View File

@ -4,57 +4,19 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type {CDPSession} from '../api/CDPSession.js';
import type {Page} from '../api/Page.js';
import {Target, TargetType} from '../api/Target.js';
import {UnsupportedOperation} from '../common/Errors.js';
import type {CDPSession} from '../puppeteer-core.js';
import type {BidiBrowser} from './Browser.js';
import type {BidiBrowserContext} from './BrowserContext.js';
import type {BrowsingContext} from './BrowsingContext.js';
import {BidiCdpSession} from './CDPSession.js';
import type {BidiFrame} from './Frame.js';
import {BidiPage} from './Page.js';
/**
* @internal
*/
export abstract class BidiTarget extends Target {
protected _browserContext: BidiBrowserContext;
constructor(browserContext: BidiBrowserContext) {
super();
this._browserContext = browserContext;
}
_setBrowserContext(browserContext: BidiBrowserContext): void {
this._browserContext = browserContext;
}
override asPage(): Promise<Page> {
throw new UnsupportedOperation();
}
override browser(): BidiBrowser {
return this._browserContext.browser();
}
override browserContext(): BidiBrowserContext {
return this._browserContext;
}
override opener(): never {
throw new UnsupportedOperation();
}
override createCDPSession(): Promise<CDPSession> {
throw new UnsupportedOperation();
}
}
/**
* @internal
*/
export class BiDiBrowserTarget extends Target {
export class BidiBrowserTarget extends Target {
#browser: BidiBrowser;
constructor(browser: BidiBrowser) {
@ -62,91 +24,109 @@ export class BiDiBrowserTarget extends Target {
this.#browser = browser;
}
override asPage(): Promise<BidiPage> {
throw new UnsupportedOperation();
}
override url(): string {
return '';
}
override type(): TargetType {
return TargetType.BROWSER;
}
override asPage(): Promise<Page> {
throw new UnsupportedOperation();
}
override browser(): BidiBrowser {
return this.#browser;
}
override browserContext(): BidiBrowserContext {
return this.#browser.defaultBrowserContext();
}
override opener(): never {
throw new UnsupportedOperation();
}
override createCDPSession(): Promise<CDPSession> {
throw new UnsupportedOperation();
}
}
/**
* @internal
*/
export class BiDiBrowsingContextTarget extends BidiTarget {
protected _browsingContext: BrowsingContext;
constructor(
browserContext: BidiBrowserContext,
browsingContext: BrowsingContext
) {
super(browserContext);
this._browsingContext = browsingContext;
}
override url(): string {
return this._browsingContext.url;
}
override async createCDPSession(): Promise<CDPSession> {
const {sessionId} = await this._browsingContext.cdpSession.send(
'Target.attachToTarget',
{
targetId: this._browsingContext.id,
flatten: true,
}
);
return new BidiCdpSession(this._browsingContext, sessionId);
}
override type(): TargetType {
return TargetType.PAGE;
return TargetType.BROWSER;
}
override browser(): BidiBrowser {
return this.#browser;
}
override browserContext(): BidiBrowserContext {
return this.#browser.defaultBrowserContext();
}
override opener(): Target | undefined {
throw new UnsupportedOperation();
}
}
/**
* @internal
*/
export class BiDiPageTarget extends BiDiBrowsingContextTarget {
export class BidiPageTarget extends Target {
#page: BidiPage;
constructor(
browserContext: BidiBrowserContext,
browsingContext: BrowsingContext
) {
super(browserContext, browsingContext);
this.#page = new BidiPage(browsingContext, browserContext);
constructor(page: BidiPage) {
super();
this.#page = page;
}
override async page(): Promise<BidiPage> {
return this.#page;
}
override _setBrowserContext(browserContext: BidiBrowserContext): void {
super._setBrowserContext(browserContext);
this.#page._setBrowserContext(browserContext);
override async asPage(): Promise<BidiPage> {
return BidiPage.from(
this.browserContext(),
this.#page.mainFrame().browsingContext
);
}
override url(): string {
return this.#page.url();
}
override createCDPSession(): Promise<CDPSession> {
return this.#page.createCDPSession();
}
override type(): TargetType {
return TargetType.PAGE;
}
override browser(): BidiBrowser {
return this.browserContext().browser();
}
override browserContext(): BidiBrowserContext {
return this.#page.browserContext();
}
override opener(): Target | undefined {
throw new UnsupportedOperation();
}
}
/**
* @internal
*/
export class BidiFrameTarget extends Target {
#frame: BidiFrame;
#page: BidiPage | undefined;
constructor(frame: BidiFrame) {
super();
this.#frame = frame;
}
override async page(): Promise<BidiPage> {
if (this.#page === undefined) {
this.#page = BidiPage.from(
this.browserContext(),
this.#frame.browsingContext
);
}
return this.#page;
}
override async asPage(): Promise<BidiPage> {
return BidiPage.from(this.browserContext(), this.#frame.browsingContext);
}
override url(): string {
return this.#frame.url();
}
override createCDPSession(): Promise<CDPSession> {
return this.#frame.createCDPSession();
}
override type(): TargetType {
return TargetType.PAGE;
}
override browser(): BidiBrowser {
return this.browserContext().browser();
}
override browserContext(): BidiBrowserContext {
return this.#frame.page().browserContext();
}
override opener(): Target | undefined {
throw new UnsupportedOperation();
}
}

View File

@ -7,7 +7,6 @@
export * from './BidiOverCdp.js';
export * from './Browser.js';
export * from './BrowserContext.js';
export * from './BrowsingContext.js';
export * from './Connection.js';
export * from './ElementHandle.js';
export * from './Frame.js';
@ -15,8 +14,5 @@ export * from './HTTPRequest.js';
export * from './HTTPResponse.js';
export * from './Input.js';
export * from './JSHandle.js';
export * from './NetworkManager.js';
export * from './Page.js';
export * from './Realm.js';
export * from './Sandbox.js';
export * from './Target.js';

View File

@ -1,119 +0,0 @@
/**
* @license
* Copyright 2023 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import type {
ObservableInput,
ObservedValueOf,
OperatorFunction,
} from '../../third_party/rxjs/rxjs.js';
import {catchError} from '../../third_party/rxjs/rxjs.js';
import type {PuppeteerLifeCycleEvent} from '../cdp/LifecycleWatcher.js';
import {ProtocolError, TimeoutError} from '../common/Errors.js';
/**
* @internal
*/
export type BiDiNetworkIdle = Extract<
PuppeteerLifeCycleEvent,
'networkidle0' | 'networkidle2'
> | null;
/**
* @internal
*/
export function getBiDiLifeCycles(
event: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[]
): [
Extract<PuppeteerLifeCycleEvent, 'load' | 'domcontentloaded'>,
BiDiNetworkIdle,
] {
if (Array.isArray(event)) {
const pageLifeCycle = event.some(lifeCycle => {
return lifeCycle !== 'domcontentloaded';
})
? 'load'
: 'domcontentloaded';
const networkLifeCycle = event.reduce((acc, lifeCycle) => {
if (lifeCycle === 'networkidle0') {
return lifeCycle;
} else if (acc !== 'networkidle0' && lifeCycle === 'networkidle2') {
return lifeCycle;
}
return acc;
}, null as BiDiNetworkIdle);
return [pageLifeCycle, networkLifeCycle];
}
if (event === 'networkidle0' || event === 'networkidle2') {
return ['load', event];
}
return [event, null];
}
/**
* @internal
*/
export const lifeCycleToReadinessState = new Map<
PuppeteerLifeCycleEvent,
Bidi.BrowsingContext.ReadinessState
>([
['load', Bidi.BrowsingContext.ReadinessState.Complete],
['domcontentloaded', Bidi.BrowsingContext.ReadinessState.Interactive],
]);
export function getBiDiReadinessState(
event: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[]
): [Bidi.BrowsingContext.ReadinessState, BiDiNetworkIdle] {
const lifeCycles = getBiDiLifeCycles(event);
const readiness = lifeCycleToReadinessState.get(lifeCycles[0])!;
return [readiness, lifeCycles[1]];
}
/**
* @internal
*/
export const lifeCycleToSubscribedEvent = new Map<
PuppeteerLifeCycleEvent,
'browsingContext.load' | 'browsingContext.domContentLoaded'
>([
['load', 'browsingContext.load'],
['domcontentloaded', 'browsingContext.domContentLoaded'],
]);
/**
* @internal
*/
export function getBiDiLifecycleEvent(
event: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[]
): [
'browsingContext.load' | 'browsingContext.domContentLoaded',
BiDiNetworkIdle,
] {
const lifeCycles = getBiDiLifeCycles(event);
const bidiEvent = lifeCycleToSubscribedEvent.get(lifeCycles[0])!;
return [bidiEvent, lifeCycles[1]];
}
/**
* @internal
*/
export function rewriteNavigationError<T, R extends ObservableInput<T>>(
message: string,
ms: number
): OperatorFunction<T, T | ObservedValueOf<R>> {
return catchError<T, R>(error => {
if (error instanceof ProtocolError) {
error.message += ` at ${message}`;
} else if (error instanceof TimeoutError) {
error.message = `Navigation timeout of ${ms} ms exceeded`;
}
throw error;
});
}

View File

@ -6,6 +6,7 @@
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import {ProtocolError, TimeoutError} from '../common/Errors.js';
import {PuppeteerURL} from '../common/util.js';
import {BidiDeserializer} from './Deserializer.js';
@ -56,3 +57,20 @@ export function createEvaluationError(
error.stack = [details.text, ...stackLines].join('\n');
return error;
}
/**
* @internal
*/
export function rewriteNavigationError(
message: string,
ms: number
): (error: unknown) => never {
return error => {
if (error instanceof ProtocolError) {
error.message += ` at ${message}`;
} else if (error instanceof TimeoutError) {
error.message = `Navigation timeout of ${ms} ms exceeded`;
}
throw error;
};
}

View File

@ -6,8 +6,10 @@
export {
bufferCount,
catchError,
combineLatest,
concat,
concatMap,
debounceTime,
defaultIfEmpty,
defer,
delay,
@ -22,7 +24,6 @@ export {
ignoreElements,
lastValueFrom,
map,
ReplaySubject,
merge,
mergeMap,
mergeScan,
@ -33,6 +34,7 @@ export {
pipe,
race,
raceWith,
ReplaySubject,
retry,
startWith,
switchMap,

View File

@ -537,7 +537,7 @@
"testIdPattern": "[coverage.spec] Coverage specs JSCoverage should ignore pptr internal scripts if reportAnonymousScripts is true",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["FAIL", "PASS"]
"expectations": ["FAIL"]
},
{
"testIdPattern": "[devtools.spec] DevTools should expose DevTools as a page",
@ -579,7 +579,7 @@
"testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.click should return Point data",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["FAIL", "PASS"]
"expectations": ["PASS"]
},
{
"testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.clickablePoint should not work if the click box is not visible due to the iframe",
@ -670,7 +670,7 @@
"testIdPattern": "[frame.spec] Frame specs Frame Management should support lazy frames",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome"],
"expectations": ["FAIL", "PASS"]
"expectations": ["PASS"]
},
{
"testIdPattern": "[headful.spec] *",
@ -712,7 +712,7 @@
"testIdPattern": "[jshandle.spec] JSHandle Page.evaluateHandle should use the same JS wrappers",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["FAIL", "PASS"]
"expectations": ["PASS"]
},
{
"testIdPattern": "[keyboard.spec] Keyboard should send a character with sendCharacter in iframe",
@ -820,7 +820,8 @@
"testIdPattern": "[navigation.spec] navigation Page.goto should fail when navigating to bad SSL",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["firefox"],
"expectations": ["FAIL"]
"expectations": ["SKIP"],
"comment": "https://github.com/w3c/webdriver-bidi/issues/657"
},
{
"testIdPattern": "[navigation.spec] navigation Page.goto should send referer",
@ -846,12 +847,6 @@
"parameters": ["firefox", "webDriverBiDi"],
"expectations": ["SKIP"]
},
{
"testIdPattern": "[network.spec] network raw network headers Cross-origin set-cookie",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["FAIL", "PASS"]
},
{
"testIdPattern": "[network.spec] network Request.initiator should return the initiator",
"platforms": ["darwin", "linux", "win32"],
@ -870,6 +865,13 @@
"parameters": ["webDriverBiDi"],
"expectations": ["PASS"]
},
{
"testIdPattern": "[network.spec] network Request.postData should be |undefined| when there is no post data",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["FAIL"],
"comment": "Unsupported"
},
{
"testIdPattern": "[network.spec] network Request.postData should work",
"platforms": ["darwin", "linux", "win32"],
@ -1218,7 +1220,7 @@
"testIdPattern": "[target.spec] Target Browser.waitForTarget should timeout waiting for a non-existent target",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["FAIL", "PASS"]
"expectations": ["PASS"]
},
{
"testIdPattern": "[target.spec] Target should be able to use async waitForTarget",
@ -1609,7 +1611,7 @@
"testIdPattern": "[click.spec] Page.click should click on checkbox input and toggle",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["firefox", "webDriverBiDi"],
"expectations": ["FAIL", "PASS"]
"expectations": ["PASS"]
},
{
"testIdPattern": "[click.spec] Page.click should click on checkbox label and toggle",
@ -1783,13 +1785,13 @@
"testIdPattern": "[coverage.spec] Coverage specs CSSCoverage should work with complicated usecases",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "webDriverBiDi"],
"expectations": ["FAIL", "PASS"]
"expectations": ["PASS"]
},
{
"testIdPattern": "[coverage.spec] Coverage specs JSCoverage should not ignore eval() scripts if reportAnonymousScripts is true",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "webDriverBiDi"],
"expectations": ["FAIL", "PASS"]
"expectations": ["PASS"]
},
{
"testIdPattern": "[debugInfo.spec] DebugInfo Browser.debugInfo should work",
@ -2125,7 +2127,7 @@
"testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluate should await promise",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["firefox", "webDriverBiDi"],
"expectations": ["FAIL", "PASS"]
"expectations": ["PASS"]
},
{
"testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluate should simulate a user gesture",
@ -2198,7 +2200,8 @@
"testIdPattern": "[frame.spec] Frame specs Frame Management should detach child frames on navigation",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["firefox", "webDriverBiDi"],
"expectations": ["PASS"]
"expectations": ["FAIL"],
"comment": "https://github.com/w3c/webdriver-bidi/issues/659"
},
{
"testIdPattern": "[frame.spec] Frame specs Frame Management should report different frame instance when frame re-attaches",
@ -2236,6 +2239,13 @@
"parameters": ["cdp", "firefox"],
"expectations": ["SKIP"]
},
{
"testIdPattern": "[frame.spec] Frame specs Frame Management should send \"framenavigated\" when navigating on anchor URLs",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["firefox", "webDriverBiDi"],
"expectations": ["SKIP"],
"comment": "https://github.com/w3c/webdriver-bidi/issues/657"
},
{
"testIdPattern": "[frame.spec] Frame specs Frame Management should send events when frames are manipulated dynamically",
"platforms": ["darwin", "linux", "win32"],
@ -2252,7 +2262,8 @@
"testIdPattern": "[frame.spec] Frame specs Frame Management should support framesets",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["firefox", "webDriverBiDi"],
"expectations": ["PASS"]
"expectations": ["FAIL"],
"comment": "https://github.com/w3c/webdriver-bidi/issues/659"
},
{
"testIdPattern": "[frame.spec] Frame specs Frame Management should support lazy frames",
@ -3015,7 +3026,7 @@
"testIdPattern": "[navigation.spec] navigation Frame.goto should navigate subframes",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["firefox", "webDriverBiDi"],
"expectations": ["FAIL", "PASS"]
"expectations": ["PASS"]
},
{
"testIdPattern": "[navigation.spec] navigation Frame.goto should reject when frame detaches",
@ -3059,11 +3070,33 @@
"parameters": ["cdp", "firefox"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[navigation.spec] navigation Page.goBack should work with HistoryAPI",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["firefox", "webDriverBiDi"],
"expectations": ["SKIP"],
"comment": "browsingContext.navigationStarted event not emitted for fragment navigation"
},
{
"testIdPattern": "[navigation.spec] navigation Page.goto should fail when main resources failed to load",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["firefox", "webDriverBiDi"],
"expectations": ["FAIL", "PASS"]
"expectations": ["SKIP"],
"comment": "https://github.com/w3c/webdriver-bidi/issues/657"
},
{
"testIdPattern": "[navigation.spec] navigation Page.goto should fail when navigating and show the url at the error message",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["firefox", "webDriverBiDi"],
"expectations": ["SKIP"],
"comment": "https://github.com/w3c/webdriver-bidi/issues/657"
},
{
"testIdPattern": "[navigation.spec] navigation Page.goto should fail when navigating to bad SSL after redirects",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["firefox", "webDriverBiDi"],
"expectations": ["SKIP"],
"comment": "https://github.com/w3c/webdriver-bidi/issues/657"
},
{
"testIdPattern": "[navigation.spec] navigation Page.goto should fail when server returns 204",
@ -3071,6 +3104,13 @@
"parameters": ["cdp", "firefox"],
"expectations": ["SKIP"]
},
{
"testIdPattern": "[navigation.spec] navigation Page.goto should fail when server returns 204",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["firefox", "webDriverBiDi"],
"expectations": ["SKIP"],
"comment": "https://github.com/w3c/webdriver-bidi/issues/657"
},
{
"testIdPattern": "[navigation.spec] navigation Page.goto should navigate to dataURL and fire dataURL requests",
"platforms": ["darwin", "linux", "win32"],
@ -3087,7 +3127,7 @@
"testIdPattern": "[navigation.spec] navigation Page.goto should navigate to dataURL and fire dataURL requests",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "webDriverBiDi"],
"expectations": ["FAIL", "PASS"]
"expectations": ["PASS"]
},
{
"testIdPattern": "[navigation.spec] navigation Page.goto should navigate to empty page with networkidle0",
@ -3131,11 +3171,17 @@
"parameters": ["chrome", "webDriverBiDi"],
"expectations": ["PASS"]
},
{
"testIdPattern": "[navigation.spec] navigation Page.goto should return last response in redirect chain",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["firefox", "webDriverBiDi"],
"expectations": ["PASS"]
},
{
"testIdPattern": "[navigation.spec] navigation Page.goto should return response when page changes its URL after load",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["firefox", "webDriverBiDi"],
"expectations": ["FAIL", "PASS"]
"expectations": ["PASS"]
},
{
"testIdPattern": "[navigation.spec] navigation Page.goto should send referer",
@ -3161,18 +3207,19 @@
"parameters": ["cdp", "firefox"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[navigation.spec] navigation Page.goto should work when page calls history API in beforeunload",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["firefox", "webDriverBiDi"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[navigation.spec] navigation Page.goto should work with anchor navigation",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "chrome-headless-shell"],
"expectations": ["SKIP"]
},
{
"testIdPattern": "[navigation.spec] navigation Page.goto should work with anchor navigation",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["firefox", "webDriverBiDi"],
"expectations": ["SKIP"],
"comment": "browsingContext.navigationStarted event not emitted for fragment navigation"
},
{
"testIdPattern": "[navigation.spec] navigation Page.goto should work with subframes return 204",
"platforms": ["darwin", "linux", "win32"],
@ -3209,6 +3256,13 @@
"parameters": ["cdp", "firefox"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[navigation.spec] navigation Page.waitForNavigation should work with DOM history.back()/history.forward()",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["firefox", "webDriverBiDi"],
"expectations": ["SKIP"],
"comment": "browsingContext.navigationStarted event not emitted for fragment navigation"
},
{
"testIdPattern": "[navigation.spec] navigation Page.waitForNavigation should work with history.pushState()",
"platforms": ["darwin", "linux", "win32"],
@ -3217,9 +3271,10 @@
},
{
"testIdPattern": "[navigation.spec] navigation Page.waitForNavigation should work with history.pushState()",
"platforms": ["linux"],
"platforms": ["darwin", "linux", "win32"],
"parameters": ["firefox", "webDriverBiDi"],
"expectations": ["SKIP"]
"expectations": ["SKIP"],
"comment": "browsingContext.navigationStarted event not emitted for fragment navigation"
},
{
"testIdPattern": "[navigation.spec] navigation Page.waitForNavigation should work with history.replaceState()",
@ -3268,7 +3323,7 @@
"testIdPattern": "[network.spec] network Network Events Page.Events.RequestServedFromCache",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "webDriverBiDi"],
"expectations": ["FAIL", "PASS"]
"expectations": ["FAIL"]
},
{
"testIdPattern": "[network.spec] network Network Events Page.Events.Response",
@ -3342,6 +3397,12 @@
"parameters": ["cdp", "firefox"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[network.spec] network raw network headers Cross-origin set-cookie",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "webDriverBiDi"],
"expectations": ["FAIL", "PASS"]
},
{
"testIdPattern": "[network.spec] network raw network headers Same-origin set-cookie navigation",
"platforms": ["darwin", "linux", "win32"],
@ -3388,7 +3449,7 @@
"testIdPattern": "[network.spec] network Request.frame should work for subframe navigation request",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["firefox", "webDriverBiDi"],
"expectations": ["FAIL", "PASS"]
"expectations": ["PASS"]
},
{
"testIdPattern": "[network.spec] network Request.initiator should return the initiator",
@ -3455,7 +3516,7 @@
"testIdPattern": "[network.spec] network Response.fromCache should work",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["firefox", "webDriverBiDi"],
"expectations": ["FAIL", "PASS"]
"expectations": ["FAIL"]
},
{
"testIdPattern": "[network.spec] network Response.fromCache should work",
@ -3529,12 +3590,6 @@
"parameters": ["firefox", "webDriverBiDi"],
"expectations": ["PASS"]
},
{
"testIdPattern": "[oopif.spec] OOPIF should detect existing OOPIFs when Puppeteer connects to an existing page",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "webDriverBiDi"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[oopif.spec] OOPIF should detect existing OOPIFs when Puppeteer connects to an existing page",
"platforms": ["darwin", "linux", "win32"],
@ -3597,28 +3652,18 @@
"expectations": ["FAIL"]
},
{
"testIdPattern": "[oopif.spec] OOPIF should report oopif frames",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "webDriverBiDi"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[oopif.spec] OOPIF should report oopif frames",
"testIdPattern": "[oopif.spec] OOPIF should support frames within OOP iframes",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["firefox", "webDriverBiDi"],
"expectations": ["FAIL"]
"expectations": ["SKIP"],
"comment": "Fetch error"
},
{
"testIdPattern": "[oopif.spec] OOPIF should support lazy OOP frames",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["firefox", "webDriverBiDi"],
"expectations": ["FAIL", "PASS"]
},
{
"testIdPattern": "[oopif.spec] OOPIF should support lazy OOP frames",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "webDriverBiDi"],
"expectations": ["PASS"]
"expectations": ["FAIL"],
"comment": "https://bugzilla.mozilla.org/show_bug.cgi?id=187816"
},
{
"testIdPattern": "[oopif.spec] OOPIF should support lazy OOP frames",
@ -3678,7 +3723,8 @@
"testIdPattern": "[oopif.spec] OOPIF should wait for inner OOPIFs",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["firefox", "webDriverBiDi"],
"expectations": ["FAIL"]
"expectations": ["SKIP"],
"comment": "Fetch error"
},
{
"testIdPattern": "[page.spec] Page Page.addScriptTag should throw when added with content to the CSP page",
@ -4080,7 +4126,7 @@
"testIdPattern": "[proxy.spec] request proxy should respect proxy bypass list",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["firefox", "webDriverBiDi"],
"expectations": ["FAIL", "PASS"]
"expectations": ["PASS"]
},
{
"testIdPattern": "[proxy.spec] request proxy should respect proxy bypass list",
@ -4224,7 +4270,7 @@
"testIdPattern": "[target.spec] Target Browser.waitForTarget should wait for a target",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "webDriverBiDi"],
"expectations": ["FAIL", "PASS"]
"expectations": ["PASS"]
},
{
"testIdPattern": "[target.spec] Target Browser.waitForTarget should wait for a target",
@ -4232,12 +4278,6 @@
"parameters": ["cdp", "firefox"],
"expectations": ["SKIP"]
},
{
"testIdPattern": "[target.spec] Target Browser.waitForTarget should wait for a target",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["firefox", "webDriverBiDi"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[target.spec] Target should be able to use async waitForTarget",
"platforms": ["darwin", "linux", "win32"],
@ -4332,7 +4372,7 @@
"testIdPattern": "[target.spec] Target should not crash while redirecting if original request was missed",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "webDriverBiDi"],
"expectations": ["FAIL", "PASS"]
"expectations": ["PASS"]
},
{
"testIdPattern": "[target.spec] Target should not report uninitialized pages",

View File

@ -370,9 +370,10 @@ describe('AriaQueryHandler', () => {
await detachFrame(page, 'frame1');
await waitPromise;
expect(waitError).toBeTruthy();
expect(waitError.message).toContain(
'waitForFunction failed: frame got detached.'
);
expect(waitError.message).atLeastOneToContain([
'waitForFunction failed: frame got detached.',
'Browsing context already closed.',
]);
});
it('should survive cross-process navigation', async () => {

View File

@ -408,9 +408,10 @@ describe('Evaluation specs', function () {
return (error = error_);
});
expect(error).toBeTruthy();
expect(error.message).toContain(
'JSHandles can be evaluated only in the context they were created'
);
expect(error.message).atLeastOneToContain([
'JSHandles can be evaluated only in the context they were created',
"Trying to evaluate JSHandle from different frames. Usually this means you're using a handle from a page on a different page.",
]);
});
it('should simulate a user gesture', async () => {
const {page} = await getTestState();

View File

@ -102,6 +102,7 @@ describe('Launcher specs', function () {
expect(message).atLeastOneToContain([
'Target closed',
'Page closed!',
'Browser already closed',
]);
expect(message).not.toContain('Timeout');
}

View File

@ -5,10 +5,9 @@
*/
import expect from 'expect';
import type {BrowserContext} from 'puppeteer-core/internal/api/BrowserContext.js';
import type {CDPSession} from 'puppeteer-core/internal/api/CDPSession.js';
import {CDPSessionEvent} from 'puppeteer-core/internal/api/CDPSession.js';
import type {CdpTarget} from 'puppeteer-core/internal/cdp/Target.js';
import type {Page} from 'puppeteer-core/internal/api/Page.js';
import {getTestState, launch} from './mocha-utils.js';
import {attachFrame, detachFrame, navigateFrame} from './utils.js';
@ -266,24 +265,24 @@ describe('OOPIF', function () {
await frame.waitForSelector('#clicked');
});
it('should report oopif frames', async () => {
const {server, page, context} = state;
const {server, page} = state;
const frame = page.waitForFrame(frame => {
return frame.url().endsWith('/oopif.html');
});
await page.goto(server.PREFIX + '/dynamic-oopif.html');
await frame;
expect(oopifs(context)).toHaveLength(1);
expect(await iframes(page)).toHaveLength(1);
expect(page.frames()).toHaveLength(2);
});
it('should wait for inner OOPIFs', async () => {
const {server, page, context} = state;
const {server, page} = state;
await page.goto(`http://mainframe:${server.PORT}/main-frame.html`);
const frame2 = await page.waitForFrame(frame => {
return frame.url().endsWith('inner-frame2.html');
});
expect(oopifs(context)).toHaveLength(2);
expect(await iframes(page)).toHaveLength(2);
expect(
page.frames().filter(frame => {
return frame.isOOPFrame();
@ -297,7 +296,7 @@ describe('OOPIF', function () {
});
it('should load oopif iframes with subresources and request interception', async () => {
const {server, page, context} = state;
const {server, page} = state;
const framePromise = page.waitForFrame(frame => {
return frame.url().endsWith('/oopif.html');
@ -312,7 +311,7 @@ describe('OOPIF', function () {
await page.goto(server.PREFIX + '/dynamic-oopif.html');
const frame = await framePromise;
const request = await requestPromise;
expect(oopifs(context)).toHaveLength(1);
expect(await iframes(page)).toHaveLength(1);
expect(request.frame()).toBe(frame);
});
@ -394,14 +393,14 @@ describe('OOPIF', function () {
});
it('should detect existing OOPIFs when Puppeteer connects to an existing page', async () => {
const {server, puppeteer, page, context} = state;
const {server, puppeteer, page} = state;
const frame = page.waitForFrame(frame => {
return frame.url().endsWith('/oopif.html');
});
await page.goto(server.PREFIX + '/dynamic-oopif.html');
await frame;
expect(oopifs(context)).toHaveLength(1);
expect(await iframes(page)).toHaveLength(1);
expect(page.frames()).toHaveLength(2);
const browserURL = 'http://127.0.0.1:21222';
@ -520,8 +519,13 @@ describe('OOPIF', function () {
});
});
function oopifs(context: BrowserContext) {
return context.targets().filter(target => {
return (target as CdpTarget)._getTargetInfo().type === 'iframe';
async function iframes(page: Page) {
const iframes = await Promise.all(
page.frames().map(async frame => {
return await frame.frameElement();
})
);
return iframes.filter(frame => {
return frame !== null;
});
}

View File

@ -102,7 +102,11 @@ describe('Page', function () {
]);
for (let i = 0; i < 2; i++) {
const message = results[i].message;
expect(message).atLeastOneToContain(['Target closed', 'Page closed!']);
expect(message).atLeastOneToContain([
'Target closed',
'Page closed!',
'Frame detached',
]);
expect(message).not.toContain('Timeout');
}
});

View File

@ -446,9 +446,10 @@ describe('waittask specs', function () {
await detachFrame(page, 'frame1');
await waitPromise;
expect(waitError).toBeTruthy();
expect(waitError?.message).toContain(
'waitForFunction failed: frame got detached.'
);
expect(waitError?.message).atLeastOneToContain([
'waitForFunction failed: frame got detached.',
'Browsing context already closed.',
]);
});
it('should survive cross-process navigation', async () => {
const {page, server} = await getTestState();
@ -754,9 +755,10 @@ describe('waittask specs', function () {
await detachFrame(page, 'frame1');
await waitPromise;
expect(waitError).toBeTruthy();
expect(waitError?.message).toContain(
'waitForFunction failed: frame got detached.'
);
expect(waitError?.message).atLeastOneToContain([
'waitForFunction failed: frame got detached.',
'Browsing context already closed.',
]);
});
it('hidden should wait for display: none', async () => {
const {page} = await getTestState();