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

View File

@ -6,17 +6,20 @@
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import {BrowserContext} from '../api/BrowserContext.js'; import {BrowserContext, BrowserContextEvent} from '../api/BrowserContext.js';
import type {Page} from '../api/Page.js'; import {PageEvent, type Page} from '../api/Page.js';
import type {Target} from '../api/Target.js'; import type {Target} from '../api/Target.js';
import {UnsupportedOperation} from '../common/Errors.js'; import {UnsupportedOperation} from '../common/Errors.js';
import {debugError} from '../common/util.js'; import {debugError} from '../common/util.js';
import type {Viewport} from '../common/Viewport.js'; import type {Viewport} from '../common/Viewport.js';
import type {BidiBrowser} from './Browser.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 {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 * @internal
@ -29,10 +32,25 @@ export interface BidiBrowserContextOptions {
* @internal * @internal
*/ */
export class BidiBrowserContext extends BrowserContext { export class BidiBrowserContext extends BrowserContext {
#browser: BidiBrowser; static from(
#connection: BidiConnection; browser: BidiBrowser,
#defaultViewport: Viewport | null; userContext: UserContext,
#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( constructor(
browser: BidiBrowser, browser: BidiBrowser,
@ -41,36 +59,78 @@ export class BidiBrowserContext extends BrowserContext {
) { ) {
super(); super();
this.#browser = browser; this.#browser = browser;
this.#userContext = userContext; this.userContext = userContext;
this.#connection = this.#browser.connection;
this.#defaultViewport = options.defaultViewport; this.#defaultViewport = options.defaultViewport;
} }
override targets(): Target[] { #initialize() {
return this.#browser.targets().filter(target => { // Create targets for existing browsing contexts.
return target.browserContext() === this; for (const browsingContext of this.userContext.browsingContexts) {
this.#createPage(browsingContext);
}
this.userContext.on('browsingcontext', ({browsingContext}) => {
this.#createPage(browsingContext);
}); });
} }
get connection(): BidiConnection { #createPage(browsingContext: BrowsingContext): BidiPage {
return this.#connection; 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> { override async newPage(): Promise<Page> {
const {result} = await this.#connection.send('browsingContext.create', { const context = await this.userContext.createBrowsingContext(
type: Bidi.BrowsingContext.CreateType.Tab, Bidi.BrowsingContext.CreateType.Tab
userContext: this.#userContext.id, );
}); const page = this.#pages.get(context)!;
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();
if (!page) { if (!page) {
throw new Error('Page is not found'); throw new Error('Page is not found');
} }
@ -91,7 +151,7 @@ export class BidiBrowserContext extends BrowserContext {
} }
try { try {
await this.#userContext.remove(); await this.userContext.remove();
} catch (error) { } catch (error) {
debugError(error); debugError(error);
} }
@ -102,18 +162,13 @@ export class BidiBrowserContext extends BrowserContext {
} }
override async pages(): Promise<BidiPage[]> { override async pages(): Promise<BidiPage[]> {
const results = await Promise.all( return [...this.userContext.browsingContexts].map(context => {
[...this.targets()].map(t => { return this.#pages.get(context)!;
return t.page();
})
);
return results.filter((p): p is BidiPage => {
return p !== null;
}); });
} }
override isIncognito(): boolean { override isIncognito(): boolean {
return this.#userContext.id !== UserContext.DEFAULT; return this.userContext.id !== UserContext.DEFAULT;
} }
override overridePermissions(): never { override overridePermissions(): never {
@ -125,9 +180,9 @@ export class BidiBrowserContext extends BrowserContext {
} }
override get id(): string | undefined { override get id(): string | undefined {
if (this.#userContext.id === UserContext.DEFAULT) { if (this.userContext.id === UserContext.DEFAULT) {
return undefined; 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. * Copyright 2024 Google Inc.
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import type ProtocolMapping from 'devtools-protocol/types/protocol-mapping.js'; import type ProtocolMapping from 'devtools-protocol/types/protocol-mapping.js';
import {CDPSession} from '../api/CDPSession.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 {TargetCloseError, UnsupportedOperation} from '../common/Errors.js';
import {Deferred} from '../util/Deferred.js'; import {Deferred} from '../util/Deferred.js';
import type {BrowsingContext} from './BrowsingContext.js'; import type {BidiConnection} from './Connection.js';
import type {BidiFrame} from './Frame.js';
/**
* @internal
*/
export const cdpSessions = new Map<string, BidiCdpSession>();
/** /**
* @internal * @internal
*/ */
export class BidiCdpSession extends CDPSession { export class BidiCdpSession extends CDPSession {
#context: BrowsingContext; static sessions = new Map<string, BidiCdpSession>();
#sessionId = Deferred.create<string>();
#detached = false;
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(); super();
this.#context = context; this.frame = frame;
if (!this.#context.supportsCdp()) { if (!this.frame.page().browser().cdpSupported) {
return; return;
} }
const connection = this.frame.page().browser().connection;
this.#connection = connection;
if (sessionId) { if (sessionId) {
this.#sessionId.resolve(sessionId); this.#sessionId.resolve(sessionId);
cdpSessions.set(sessionId, this); BidiCdpSession.sessions.set(sessionId, this);
} else { } else {
context.connection (async () => {
.send('cdp.getSession', { try {
context: context.id, const session = await connection.send('cdp.getSession', {
}) context: frame._id,
.then(session => {
this.#sessionId.resolve(session.result.session!);
cdpSessions.set(session.result.session!, this);
})
.catch(err => {
this.#sessionId.reject(err);
}); });
this.#sessionId.resolve(session.result.session!);
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 { override connection(): CdpConnection | undefined {
@ -59,7 +63,7 @@ export class BidiCdpSession extends CDPSession {
method: T, method: T,
params?: ProtocolMapping.Commands[T]['paramsType'][0] params?: ProtocolMapping.Commands[T]['paramsType'][0]
): Promise<ProtocolMapping.Commands[T]['returnType']> { ): Promise<ProtocolMapping.Commands[T]['returnType']> {
if (!this.#context.supportsCdp()) { if (this.#connection === undefined) {
throw new UnsupportedOperation( throw new UnsupportedOperation(
'CDP support is required for this feature. The current browser does not support CDP.' '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 session = await this.#sessionId.valueOrThrow();
const {result} = await this.#context.connection.send('cdp.sendCommand', { const {result} = await this.#connection.send('cdp.sendCommand', {
method: method, method: method,
params: params, params: params,
session, session,
@ -79,17 +83,21 @@ export class BidiCdpSession extends CDPSession {
} }
override async detach(): Promise<void> { override async detach(): Promise<void> {
cdpSessions.delete(this.id()); if (this.#connection === undefined || this.#detached) {
if (!this.#detached && this.#context.supportsCdp()) { return;
await this.#context.cdpSession.send('Target.detachFromTarget', { }
try {
await this.frame.client.send('Target.detachFromTarget', {
sessionId: this.id(), sessionId: this.id(),
}); });
} } finally {
BidiCdpSession.sessions.delete(this.id());
this.#detached = true; this.#detached = true;
} }
}
override id(): string { override id(): string {
const val = this.#sessionId.value(); const value = this.#sessionId.value();
return val instanceof Error || val === undefined ? '' : val; return typeof value === 'string' ? value : '';
} }
} }

View File

@ -14,11 +14,10 @@ import {EventEmitter} from '../common/EventEmitter.js';
import {debugError} from '../common/util.js'; import {debugError} from '../common/util.js';
import {assert} from '../util/assert.js'; import {assert} from '../util/assert.js';
import type {BrowsingContext} from './BrowsingContext.js'; import {BidiCdpSession} from './CDPSession.js';
import {cdpSessions} from './CDPSession.js';
import type { import type {
BidiEvents,
Commands as BidiCommands, Commands as BidiCommands,
BidiEvents,
Connection, Connection,
} from './core/Connection.js'; } from './core/Connection.js';
@ -52,7 +51,6 @@ export class BidiConnection
#timeout? = 0; #timeout? = 0;
#closed = false; #closed = false;
#callbacks = new CallbackRegistry(); #callbacks = new CallbackRegistry();
#browsingContexts = new Map<string, BrowsingContext>();
#emitters: Array<EventEmitter<any>> = []; #emitters: Array<EventEmitter<any>> = [];
constructor( constructor(
@ -138,12 +136,11 @@ export class BidiConnection
return; return;
case 'event': case 'event':
if (isCdpEvent(object)) { if (isCdpEvent(object)) {
cdpSessions BidiCdpSession.sessions
.get(object.params.session) .get(object.params.session)
?.emit(object.params.event, object.params.params); ?.emit(object.params.event, object.params.params);
return; return;
} }
this.#maybeEmitOnContext(object);
// SAFETY: We know the method and parameter still match here. // SAFETY: We know the method and parameter still match here.
this.emit( this.emit(
object.method, object.method,
@ -164,52 +161,6 @@ export class BidiConnection
debugError(object); 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 * Unbinds the connection, but keeps the transport open. Useful when the transport will
* be reused by other connection e.g. with different protocol. * be reused by other connection e.g. with different protocol.
@ -224,7 +175,6 @@ export class BidiConnection
this.#transport.onmessage = () => {}; this.#transport.onmessage = () => {};
this.#transport.onclose = () => {}; this.#transport.onclose = () => {};
this.#browsingContexts.clear();
this.#callbacks.clear(); this.#callbacks.clear();
} }

View File

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

View File

@ -6,14 +6,13 @@
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; 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 {UnsupportedOperation} from '../common/Errors.js';
import {throwIfDisposed} from '../util/decorators.js'; import {throwIfDisposed} from '../util/decorators.js';
import type {BidiFrame} from './Frame.js'; import type {BidiFrame} from './Frame.js';
import {BidiJSHandle} from './JSHandle.js'; import {BidiJSHandle} from './JSHandle.js';
import type {BidiRealm} from './Realm.js'; import type {BidiFrameRealm} from './Realm.js';
import type {Sandbox} from './Sandbox.js';
/** /**
* @internal * @internal
@ -21,28 +20,28 @@ import type {Sandbox} from './Sandbox.js';
export class BidiElementHandle< export class BidiElementHandle<
ElementType extends Node = Element, ElementType extends Node = Element,
> extends ElementHandle<ElementType> { > extends ElementHandle<ElementType> {
declare handle: BidiJSHandle<ElementType>; static from<ElementType extends Node = Element>(
value: Bidi.Script.RemoteValue,
constructor(sandbox: Sandbox, remoteValue: Bidi.Script.RemoteValue) { realm: BidiFrameRealm
super(new BidiJSHandle(sandbox, remoteValue)); ): BidiElementHandle<ElementType> {
return new BidiElementHandle(value, realm);
} }
override get realm(): Sandbox { declare handle: BidiJSHandle<ElementType>;
return this.handle.realm;
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 { override get frame(): BidiFrame {
return this.realm.environment; return this.realm.environment;
} }
context(): BidiRealm {
return this.handle.context();
}
get isPrimitiveValue(): boolean {
return this.handle.isPrimitiveValue;
}
remoteValue(): Bidi.Script.RemoteValue { remoteValue(): Bidi.Script.RemoteValue {
return this.handle.remoteValue(); return this.handle.remoteValue();
} }
@ -76,7 +75,14 @@ export class BidiElementHandle<
})) as BidiJSHandle; })) as BidiJSHandle;
const value = handle.remoteValue(); const value = handle.remoteValue();
if (value.type === 'window') { 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; 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 {Deferred} from '../util/Deferred.js';
import {interpolateFunction, stringifyFunction} from '../util/Function.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 {BidiDeserializer} from './Deserializer.js';
import type {BidiFrame} from './Frame.js'; import type {BidiFrame} from './Frame.js';
import {BidiSerializer} from './Serializer.js'; import {BidiSerializer} from './Serializer.js';
@ -207,8 +207,8 @@ export class ExposeableFunction<Args extends unknown[], Ret> {
} }
}; };
get #connection(): BidiConnection { get #connection(): Connection {
return this.#frame.context().connection; return this.#frame.page().browser().connection;
} }
get #channelArguments() { get #channelArguments() {

View File

@ -4,17 +4,17 @@
* SPDX-License-Identifier: Apache-2.0 * 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 { import {
combineLatest,
first, first,
firstValueFrom, firstValueFrom,
forkJoin,
from,
map, map,
merge, of,
raceWith, raceWith,
zip, switchMap,
} from '../../third_party/rxjs/rxjs.js'; } from '../../third_party/rxjs/rxjs.js';
import type {CDPSession} from '../api/CDPSession.js'; import type {CDPSession} from '../api/CDPSession.js';
import type {ElementHandle} from '../api/ElementHandle.js'; import type {ElementHandle} from '../api/ElementHandle.js';
@ -25,85 +25,205 @@ import {
type WaitForOptions, type WaitForOptions,
} from '../api/Frame.js'; } from '../api/Frame.js';
import type {WaitForSelectorOptions} from '../api/Page.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 {TimeoutSettings} from '../common/TimeoutSettings.js';
import type {Awaitable, NodeFor} from '../common/types.js'; import type {Awaitable, NodeFor} from '../common/types.js';
import { import {debugError, fromEmitterEvent, timeout} from '../common/util.js';
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 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 {ExposeableFunction} from './ExposedFunction.js';
import {BidiHTTPRequest, requests} from './HTTPRequest.js';
import type {BidiHTTPResponse} from './HTTPResponse.js'; import type {BidiHTTPResponse} from './HTTPResponse.js';
import { import {BidiJSHandle} from './JSHandle.js';
getBiDiLifecycleEvent,
getBiDiReadinessState,
rewriteNavigationError,
} from './lifecycle.js';
import type {BidiPage} from './Page.js'; import type {BidiPage} from './Page.js';
import { import type {BidiRealm} from './Realm.js';
MAIN_SANDBOX, import {BidiFrameRealm} from './Realm.js';
PUPPETEER_SANDBOX, import {rewriteNavigationError} from './util.js';
Sandbox,
type SandboxChart,
} from './Sandbox.js';
/**
* Puppeteer's Frame class could be viewed as a BiDi BrowsingContext implementation
* @internal
*/
export class BidiFrame extends Frame { export class BidiFrame extends Frame {
#page: BidiPage; static from(
#context: BrowsingContext; parent: BidiPage | BidiFrame,
#timeoutSettings: TimeoutSettings; browsingContext: BrowsingContext
#abortDeferred = Deferred.create<never>(); ): BidiFrame {
#disposed = false; const frame = new BidiFrame(parent, browsingContext);
sandboxes: SandboxChart; frame.#initialize();
override _id: string; return frame;
}
constructor( readonly #parent: BidiPage | BidiFrame;
page: BidiPage, readonly browsingContext: BrowsingContext;
context: BrowsingContext, readonly #frames = new WeakMap<BrowsingContext, BidiFrame>();
timeoutSettings: TimeoutSettings, readonly realms: {default: BidiFrameRealm; internal: BidiFrameRealm};
parentId?: string | null
override readonly _id: string;
override readonly client: BidiCdpSession;
private constructor(
parent: BidiPage | BidiFrame,
browsingContext: BrowsingContext
) { ) {
super(); super();
this.#page = page; this.#parent = parent;
this.#context = context; this.browsingContext = browsingContext;
this.#timeoutSettings = timeoutSettings;
this._id = this.#context.id;
this._parentId = parentId ?? undefined;
this.sandboxes = { this._id = browsingContext.id;
[MAIN_SANDBOX]: new Sandbox(undefined, this, context, timeoutSettings), this.client = new BidiCdpSession(this);
[PUPPETEER_SANDBOX]: new Sandbox( this.realms = {
UTILITY_WORLD_NAME, default: BidiFrameRealm.from(this.browsingContext.defaultRealm, this),
this, internal: BidiFrameRealm.from(
context.createRealmForSandbox(), this.browsingContext.createWindowRealm(
timeoutSettings `__puppeteer_internal_${Math.ceil(Math.random() * 10000)}`
),
this
), ),
}; };
} }
override get client(): CDPSession { #initialize(): void {
return this.context().cdpSession; for (const browsingContext of this.browsingContext.children) {
this.#createFrameTarget(browsingContext);
} }
override mainRealm(): Sandbox { this.browsingContext.on('browsingcontext', ({browsingContext}) => {
return this.sandboxes[MAIN_SANDBOX]; 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;
}
}
} }
override isolatedRealm(): Sandbox { error.stack = [...messageLines, ...stackLines].join('\n');
return this.sandboxes[PUPPETEER_SANDBOX]; this.page().emit(PageEvent.PageError, error);
} else {
debugError(
`Unhandled LogEntry with type "${entry.type}", text "${entry.text}" and level "${entry.level}"`
);
}
});
}
#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;
}
get timeoutSettings(): TimeoutSettings {
return this.page()._timeoutSettings;
}
override mainRealm(): BidiRealm {
return this.realms.default;
}
override isolatedRealm(): BidiRealm {
return this.realms.internal;
} }
override page(): BidiPage { override page(): BidiPage {
return this.#page; let parent = this.#parent;
while (parent instanceof BidiFrame) {
parent = parent.#parent;
}
return parent;
} }
override isOOPFrame(): never { override isOOPFrame(): never {
@ -111,15 +231,20 @@ export class BidiFrame extends Frame {
} }
override url(): string { override url(): string {
return this.#context.url; return this.browsingContext.url;
} }
override parentFrame(): BidiFrame | null { override parentFrame(): BidiFrame | null {
return this.#page.frame(this._parentId ?? ''); if (this.#parent instanceof BidiFrame) {
return this.#parent;
}
return null;
} }
override childFrames(): BidiFrame[] { override childFrames(): BidiFrame[] {
return this.#page.childFrames(this.#context.id); return [...this.browsingContext.children].map(child => {
return this.#frames.get(child)!;
});
} }
@throwIfDetached @throwIfDetached
@ -127,40 +252,16 @@ export class BidiFrame extends Frame {
url: string, url: string,
options: GoToOptions = {} options: GoToOptions = {}
): Promise<BidiHTTPResponse | null> { ): Promise<BidiHTTPResponse | null> {
const { const [response] = await Promise.all([
waitUntil = 'load', this.waitForNavigation(options),
timeout: ms = this.#timeoutSettings.navigationTimeout(), this.browsingContext.navigate(url),
} = options; ]).catch(
rewriteNavigationError(
const [readiness, networkIdle] = getBiDiReadinessState(waitUntil);
const result$ = zip(
from(
this.#context.connection.send('browsingContext.navigate', {
context: this.#context.id,
url, url,
wait: readiness, options.timeout ?? this.timeoutSettings.navigationTimeout()
}) )
),
...(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)
); );
return response;
const result = await firstValueFrom(result$);
return this.#page.getNavigationResponse(result.navigation);
} }
@throwIfDetached @throwIfDetached
@ -168,95 +269,58 @@ export class BidiFrame extends Frame {
html: string, html: string,
options: WaitForOptions = {} options: WaitForOptions = {}
): Promise<void> { ): Promise<void> {
const { await Promise.all([
waitUntil = 'load', this.setFrameContent(html),
timeout: ms = this.#timeoutSettings.navigationTimeout(), firstValueFrom(
} = options; combineLatest([
this.#waitForLoad$(options),
const [waitEvent, networkIdle] = getBiDiLifecycleEvent(waitUntil); this.#waitForNetworkIdle$(options),
])
const result$ = zip(
forkJoin([
fromEmitterEvent(this.#context, waitEvent).pipe(first()),
from(this.setFrameContent(html)),
]).pipe(
map(() => {
return null;
})
), ),
...(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 @throwIfDetached
override async waitForNavigation( override async waitForNavigation(
options: WaitForOptions = {} options: WaitForOptions = {}
): Promise<BidiHTTPResponse | null> { ): Promise<BidiHTTPResponse | null> {
const { const {timeout: ms = this.timeoutSettings.navigationTimeout()} = options;
waitUntil = 'load',
timeout: ms = this.#timeoutSettings.navigationTimeout(),
} = options;
const [waitUntilEvent, networkIdle] = getBiDiLifecycleEvent(waitUntil); return await firstValueFrom(
combineLatest([
const navigation$ = merge( fromEmitterEvent(this.browsingContext, 'navigation').pipe(
forkJoin([ switchMap(({navigation}) => {
fromEmitterEvent( return this.#waitForLoad$(options).pipe(
this.#context, raceWith(fromEmitterEvent(navigation, 'fragment')),
Bidi.ChromiumBidi.BrowsingContext.EventNames.NavigationStarted map(() => {
).pipe(first()), return navigation;
fromEmitterEvent(this.#context, waitUntilEvent).pipe(first()),
]),
fromEmitterEvent(
this.#context,
Bidi.ChromiumBidi.BrowsingContext.EventNames.FragmentNavigated
)
).pipe(
map(result => {
if (Array.isArray(result)) {
return {result: result[1]};
}
return {result};
}) })
); );
})
const result$ = zip( ),
navigation$, this.#waitForNetworkIdle$(options),
...(networkIdle !== null ]).pipe(
? [ map(([navigation]) => {
this.#page.waitForNetworkIdle$({ const request = navigation.request;
timeout: ms, if (!request) {
concurrency: networkIdle === 'networkidle2' ? 2 : 0, return null;
idleTime: NETWORK_IDLE_TIME, }
const httpRequest = requests.get(request)!;
const lastRedirect = httpRequest.redirectChain().at(-1);
return (
lastRedirect !== undefined ? lastRedirect : httpRequest
).response();
}), }),
] raceWith(
: []) timeout(ms),
).pipe( fromEmitterEvent(this.browsingContext, 'closed').pipe(
map(([{result}]) => { map(() => {
return result; throw new TargetCloseError('Frame detached.');
}), })
raceWith(timeout(ms), from(this.#abortDeferred.valueOrThrow())) )
)
)
); );
const result = await firstValueFrom(result$);
return this.#page.getNavigationResponse(result.navigation);
} }
override waitForDevicePrompt(): never { override waitForDevicePrompt(): never {
@ -264,18 +328,7 @@ export class BidiFrame extends Frame {
} }
override get detached(): boolean { override get detached(): boolean {
return this.#disposed; return this.browsingContext.closed;
}
[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]();
} }
#exposedFunctions = new Map<string, ExposeableFunction<never[], unknown>>(); #exposedFunctions = new Map<string, ExposeableFunction<never[], unknown>>();
@ -310,4 +363,115 @@ export class BidiFrame extends Frame {
return super.waitForSelector(selector, options); 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 * 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 { import type {
ContinueRequestOverrides, ContinueRequestOverrides,
ResponseForRequest, ResponseForRequest,
} from '../api/HTTPRequest.js'; } from '../api/HTTPRequest.js';
import {HTTPRequest, type ResourceType} 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 {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 * @internal
*/ */
export class BidiHTTPRequest extends HTTPRequest { export class BidiHTTPRequest extends HTTPRequest {
override id: string; static from(
override _response: BidiHTTPResponse | null = null; bidiRequest: Request,
override _redirectChain: BidiHTTPRequest[]; frame: BidiFrame | undefined
_navigationId: string | null; ): BidiHTTPRequest {
const request = new BidiHTTPRequest(bidiRequest, frame);
request.#initialize();
return request;
}
#url: string; #redirect: BidiHTTPRequest | undefined;
#resourceType: ResourceType; #response: BidiHTTPResponse | null = null;
override readonly id: string;
readonly #frame: BidiFrame | undefined;
readonly #request: Request;
#method: string; private constructor(request: Request, frame: BidiFrame | undefined) {
#postData?: string;
#headers: Record<string, string> = {};
#initiator: Bidi.Network.Initiator;
#frame: Frame | null;
constructor(
event: Bidi.Network.BeforeRequestSentParameters,
frame: Frame | null,
redirectChain: BidiHTTPRequest[] = []
) {
super(); super();
requests.set(request, this);
this.#url = event.request.url; this.#request = request;
this.#resourceType = event.initiator.type.toLowerCase() as ResourceType;
this.#method = event.request.method;
this.#postData = undefined;
this.#initiator = event.initiator;
this.#frame = frame; this.#frame = frame;
this.id = request.id;
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;
}
}
} }
override get client(): never { override get client(): CDPSession {
throw new UnsupportedOperation(); 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 { override url(): string {
return this.#url; return this.#request.url;
} }
override resourceType(): ResourceType { override resourceType(): ResourceType {
return this.#resourceType; return this.initiator().type.toLowerCase() as ResourceType;
} }
override method(): string { override method(): string {
return this.#method; return this.#request.method;
} }
override postData(): string | undefined { override postData(): string | undefined {
return this.#postData; throw new UnsupportedOperation();
} }
override hasPostData(): boolean { override hasPostData(): boolean {
return this.#postData !== undefined; throw new UnsupportedOperation();
} }
override async fetchPostData(): Promise<string | undefined> { override async fetchPostData(): Promise<string | undefined> {
return this.#postData; throw new UnsupportedOperation();
} }
override headers(): Record<string, string> { 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 { 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 { override isNavigationRequest(): boolean {
return Boolean(this._navigationId); return this.#request.navigation !== undefined;
} }
override initiator(): Bidi.Network.Initiator { override initiator(): Bidi.Network.Initiator {
return this.#initiator; return this.#request.initiator;
} }
override redirectChain(): BidiHTTPRequest[] { 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( override enqueueInterceptAction(
@ -115,8 +134,8 @@ export class BidiHTTPRequest extends HTTPRequest {
void pendingHandler(); void pendingHandler();
} }
override frame(): Frame | null { override frame(): BidiFrame | null {
return this.#frame; return this.#frame ?? null;
} }
override continueRequestOverrides(): never { override continueRequestOverrides(): never {
@ -157,8 +176,4 @@ export class BidiHTTPRequest extends HTTPRequest {
): never { ): never {
throw new UnsupportedOperation(); 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 Protocol from 'devtools-protocol';
import type {Frame} from '../api/Frame.js'; import type {Frame} from '../api/Frame.js';
import { import {HTTPResponse, type RemoteAddress} from '../api/HTTPResponse.js';
HTTPResponse as HTTPResponse, import {PageEvent} from '../api/Page.js';
type RemoteAddress,
} from '../api/HTTPResponse.js';
import {UnsupportedOperation} from '../common/Errors.js'; import {UnsupportedOperation} from '../common/Errors.js';
import {invokeAtMostOnceForArguments} from '../util/decorators.js';
import type {BidiHTTPRequest} from './HTTPRequest.js'; import type {BidiHTTPRequest} from './HTTPRequest.js';
@ -19,62 +18,62 @@ import type {BidiHTTPRequest} from './HTTPRequest.js';
* @internal * @internal
*/ */
export class BidiHTTPResponse extends HTTPResponse { export class BidiHTTPResponse extends HTTPResponse {
#request: BidiHTTPRequest; static from(
#remoteAddress: RemoteAddress; data: Bidi.Network.ResponseData,
#status: number; request: BidiHTTPRequest
#statusText: string; ): BidiHTTPResponse {
#url: string; const response = new BidiHTTPResponse(data, request);
#fromCache: boolean; response.#initialize();
#headers: Record<string, string> = {}; return response;
#timings: Record<string, string> | null; }
constructor( #data: Bidi.Network.ResponseData;
request: BidiHTTPRequest, #request: BidiHTTPRequest;
{response}: Bidi.Network.ResponseCompletedParameters
private constructor(
data: Bidi.Network.ResponseData,
request: BidiHTTPRequest
) { ) {
super(); super();
this.#data = data;
this.#request = request; this.#request = request;
}
this.#remoteAddress = { #initialize() {
this.#request.frame()?.page().emit(PageEvent.Response, this);
}
@invokeAtMostOnceForArguments
override remoteAddress(): RemoteAddress {
return {
ip: '', ip: '',
port: -1, 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 { override url(): string {
return this.#url; return this.#data.url;
} }
override status(): number { override status(): number {
return this.#status; return this.#data.status;
} }
override statusText(): string { override statusText(): string {
return this.#statusText; return this.#data.statusText;
} }
override headers(): Record<string, string> { 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 { override request(): BidiHTTPRequest {
@ -82,11 +81,12 @@ export class BidiHTTPResponse extends HTTPResponse {
} }
override fromCache(): boolean { override fromCache(): boolean {
return this.#fromCache; return this.#data.fromCache;
} }
override timing(): Protocol.Network.ResourceTiming | null { 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 { override frame(): Frame | null {

View File

@ -12,9 +12,9 @@ import {
Mouse, Mouse,
MouseButton, MouseButton,
Touchscreen, Touchscreen,
type KeyboardTypeOptions,
type KeyDownOptions, type KeyDownOptions,
type KeyPressOptions, type KeyPressOptions,
type KeyboardTypeOptions,
type MouseClickOptions, type MouseClickOptions,
type MouseMoveOptions, type MouseMoveOptions,
type MouseOptions, type MouseOptions,
@ -23,7 +23,6 @@ import {
import {UnsupportedOperation} from '../common/Errors.js'; import {UnsupportedOperation} from '../common/Errors.js';
import type {KeyInput} from '../common/USKeyboardLayout.js'; import type {KeyInput} from '../common/USKeyboardLayout.js';
import type {BrowsingContext} from './BrowsingContext.js';
import type {BidiPage} from './Page.js'; import type {BidiPage} from './Page.js';
const enum InputId { const enum InputId {
@ -288,9 +287,7 @@ export class BidiKeyboard extends Keyboard {
key: KeyInput, key: KeyInput,
_options?: Readonly<KeyDownOptions> _options?: Readonly<KeyDownOptions>
): Promise<void> { ): Promise<void> {
await this.#page.connection.send('input.performActions', { await this.#page.mainFrame().browsingContext.performActions([
context: this.#page.mainFrame()._id,
actions: [
{ {
type: SourceActionsType.Key, type: SourceActionsType.Key,
id: InputId.Keyboard, id: InputId.Keyboard,
@ -301,14 +298,11 @@ export class BidiKeyboard extends Keyboard {
}, },
], ],
}, },
], ]);
});
} }
override async up(key: KeyInput): Promise<void> { override async up(key: KeyInput): Promise<void> {
await this.#page.connection.send('input.performActions', { await this.#page.mainFrame().browsingContext.performActions([
context: this.#page.mainFrame()._id,
actions: [
{ {
type: SourceActionsType.Key, type: SourceActionsType.Key,
id: InputId.Keyboard, id: InputId.Keyboard,
@ -319,8 +313,7 @@ export class BidiKeyboard extends Keyboard {
}, },
], ],
}, },
], ]);
});
} }
override async press( override async press(
@ -344,16 +337,13 @@ export class BidiKeyboard extends Keyboard {
type: ActionType.KeyUp, type: ActionType.KeyUp,
value: getBidiKeyValue(key), value: getBidiKeyValue(key),
}); });
await this.#page.connection.send('input.performActions', { await this.#page.mainFrame().browsingContext.performActions([
context: this.#page.mainFrame()._id,
actions: [
{ {
type: SourceActionsType.Key, type: SourceActionsType.Key,
id: InputId.Keyboard, id: InputId.Keyboard,
actions, actions,
}, },
], ]);
});
} }
override async type( override async type(
@ -396,16 +386,13 @@ export class BidiKeyboard extends Keyboard {
); );
} }
} }
await this.#page.connection.send('input.performActions', { await this.#page.mainFrame().browsingContext.performActions([
context: this.#page.mainFrame()._id,
actions: [
{ {
type: SourceActionsType.Key, type: SourceActionsType.Key,
id: InputId.Keyboard, id: InputId.Keyboard,
actions, actions,
}, },
], ]);
});
} }
override async sendCharacter(char: string): Promise<void> { override async sendCharacter(char: string): Promise<void> {
@ -460,19 +447,17 @@ const getBidiButton = (button: MouseButton) => {
* @internal * @internal
*/ */
export class BidiMouse extends Mouse { export class BidiMouse extends Mouse {
#context: BrowsingContext; #page: BidiPage;
#lastMovePoint: Point = {x: 0, y: 0}; #lastMovePoint: Point = {x: 0, y: 0};
constructor(context: BrowsingContext) { constructor(page: BidiPage) {
super(); super();
this.#context = context; this.#page = page;
} }
override async reset(): Promise<void> { override async reset(): Promise<void> {
this.#lastMovePoint = {x: 0, y: 0}; this.#lastMovePoint = {x: 0, y: 0};
await this.#context.connection.send('input.releaseActions', { await this.#page.mainFrame().browsingContext.releaseActions();
context: this.#context.id,
});
} }
override async move( override async move(
@ -502,22 +487,17 @@ 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 // 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; this.#lastMovePoint = to;
await this.#context.connection.send('input.performActions', { await this.#page.mainFrame().browsingContext.performActions([
context: this.#context.id,
actions: [
{ {
type: SourceActionsType.Pointer, type: SourceActionsType.Pointer,
id: InputId.Mouse, id: InputId.Mouse,
actions, actions,
}, },
], ]);
});
} }
override async down(options: Readonly<MouseOptions> = {}): Promise<void> { override async down(options: Readonly<MouseOptions> = {}): Promise<void> {
await this.#context.connection.send('input.performActions', { await this.#page.mainFrame().browsingContext.performActions([
context: this.#context.id,
actions: [
{ {
type: SourceActionsType.Pointer, type: SourceActionsType.Pointer,
id: InputId.Mouse, id: InputId.Mouse,
@ -528,14 +508,11 @@ export class BidiMouse extends Mouse {
}, },
], ],
}, },
], ]);
});
} }
override async up(options: Readonly<MouseOptions> = {}): Promise<void> { override async up(options: Readonly<MouseOptions> = {}): Promise<void> {
await this.#context.connection.send('input.performActions', { await this.#page.mainFrame().browsingContext.performActions([
context: this.#context.id,
actions: [
{ {
type: SourceActionsType.Pointer, type: SourceActionsType.Pointer,
id: InputId.Mouse, id: InputId.Mouse,
@ -546,8 +523,7 @@ export class BidiMouse extends Mouse {
}, },
], ],
}, },
], ]);
});
} }
override async click( override async click(
@ -582,24 +558,19 @@ export class BidiMouse extends Mouse {
}); });
} }
actions.push(pointerUpAction); actions.push(pointerUpAction);
await this.#context.connection.send('input.performActions', { await this.#page.mainFrame().browsingContext.performActions([
context: this.#context.id,
actions: [
{ {
type: SourceActionsType.Pointer, type: SourceActionsType.Pointer,
id: InputId.Mouse, id: InputId.Mouse,
actions, actions,
}, },
], ]);
});
} }
override async wheel( override async wheel(
options: Readonly<MouseWheelOptions> = {} options: Readonly<MouseWheelOptions> = {}
): Promise<void> { ): Promise<void> {
await this.#context.connection.send('input.performActions', { await this.#page.mainFrame().browsingContext.performActions([
context: this.#context.id,
actions: [
{ {
type: SourceActionsType.Wheel, type: SourceActionsType.Wheel,
id: InputId.Wheel, id: InputId.Wheel,
@ -615,8 +586,7 @@ export class BidiMouse extends Mouse {
}, },
], ],
}, },
], ]);
});
} }
override drag(): never { override drag(): never {
@ -644,11 +614,11 @@ export class BidiMouse extends Mouse {
* @internal * @internal
*/ */
export class BidiTouchscreen extends Touchscreen { export class BidiTouchscreen extends Touchscreen {
#context: BrowsingContext; #page: BidiPage;
constructor(context: BrowsingContext) { constructor(page: BidiPage) {
super(); super();
this.#context = context; this.#page = page;
} }
override async touchStart( override async touchStart(
@ -656,9 +626,7 @@ export class BidiTouchscreen extends Touchscreen {
y: number, y: number,
options: BidiTouchMoveOptions = {} options: BidiTouchMoveOptions = {}
): Promise<void> { ): Promise<void> {
await this.#context.connection.send('input.performActions', { await this.#page.mainFrame().browsingContext.performActions([
context: this.#context.id,
actions: [
{ {
type: SourceActionsType.Pointer, type: SourceActionsType.Pointer,
id: InputId.Finger, id: InputId.Finger,
@ -678,8 +646,7 @@ export class BidiTouchscreen extends Touchscreen {
}, },
], ],
}, },
], ]);
});
} }
override async touchMove( override async touchMove(
@ -687,9 +654,7 @@ export class BidiTouchscreen extends Touchscreen {
y: number, y: number,
options: BidiTouchMoveOptions = {} options: BidiTouchMoveOptions = {}
): Promise<void> { ): Promise<void> {
await this.#context.connection.send('input.performActions', { await this.#page.mainFrame().browsingContext.performActions([
context: this.#context.id,
actions: [
{ {
type: SourceActionsType.Pointer, type: SourceActionsType.Pointer,
id: InputId.Finger, id: InputId.Finger,
@ -705,14 +670,11 @@ export class BidiTouchscreen extends Touchscreen {
}, },
], ],
}, },
], ]);
});
} }
override async touchEnd(): Promise<void> { override async touchEnd(): Promise<void> {
await this.#context.connection.send('input.performActions', { await this.#page.mainFrame().browsingContext.performActions([
context: this.#context.id,
actions: [
{ {
type: SourceActionsType.Pointer, type: SourceActionsType.Pointer,
id: InputId.Finger, id: InputId.Finger,
@ -726,7 +688,6 @@ export class BidiTouchscreen extends Touchscreen {
}, },
], ],
}, },
], ]);
});
} }
} }

View File

@ -12,28 +12,28 @@ import {UnsupportedOperation} from '../common/Errors.js';
import {BidiDeserializer} from './Deserializer.js'; import {BidiDeserializer} from './Deserializer.js';
import type {BidiRealm} from './Realm.js'; import type {BidiRealm} from './Realm.js';
import type {Sandbox} from './Sandbox.js';
/** /**
* @internal * @internal
*/ */
export class BidiJSHandle<T = unknown> extends JSHandle<T> { export class BidiJSHandle<T = unknown> extends JSHandle<T> {
#disposed = false; static from<T>(
readonly #sandbox: Sandbox; value: Bidi.Script.RemoteValue,
realm: BidiRealm
): BidiJSHandle<T> {
return new BidiJSHandle(value, realm);
}
readonly #remoteValue: Bidi.Script.RemoteValue; 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(); super();
this.#sandbox = sandbox; this.#remoteValue = value;
this.#remoteValue = remoteValue; this.realm = realm;
}
context(): BidiRealm {
return this.realm.environment.context();
}
override get realm(): Sandbox {
return this.#sandbox;
} }
override get disposed(): boolean { override get disposed(): boolean {
@ -55,7 +55,7 @@ export class BidiJSHandle<T = unknown> extends JSHandle<T> {
return; return;
} }
this.#disposed = true; this.#disposed = true;
await this.context().destroyHandles([this]); await this.realm.destroyHandles([this]);
} }
get isPrimitiveValue(): boolean { 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 * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import type Protocol from 'devtools-protocol'; import type Protocol from 'devtools-protocol';
import { import {firstValueFrom, from, raceWith} from '../../third_party/rxjs/rxjs.js';
firstValueFrom,
from,
map,
raceWith,
zip,
} from '../../third_party/rxjs/rxjs.js';
import type {CDPSession} from '../api/CDPSession.js'; import type {CDPSession} from '../api/CDPSession.js';
import type {BoundingBox} from '../api/ElementHandle.js'; import type {BoundingBox} from '../api/ElementHandle.js';
import type {WaitForOptions} from '../api/Frame.js'; import type {WaitForOptions} from '../api/Frame.js';
import type {HTTPResponse} from '../api/HTTPResponse.js'; import type {HTTPResponse} from '../api/HTTPResponse.js';
import type {MediaFeature, GeolocationOptions} from '../api/Page.js';
import { import {
Page, Page,
PageEvent, PageEvent,
type GeolocationOptions,
type MediaFeature,
type NewDocumentScriptEvaluation, type NewDocumentScriptEvaluation,
type ScreenshotOptions, type ScreenshotOptions,
} from '../api/Page.js'; } from '../api/Page.js';
import {Accessibility} from '../cdp/Accessibility.js'; import {Accessibility} from '../cdp/Accessibility.js';
import {Coverage} from '../cdp/Coverage.js'; import {Coverage} from '../cdp/Coverage.js';
import {EmulationManager as CdpEmulationManager} from '../cdp/EmulationManager.js'; import {EmulationManager} from '../cdp/EmulationManager.js';
import {FrameTree} from '../cdp/FrameTree.js';
import {Tracing} from '../cdp/Tracing.js'; import {Tracing} from '../cdp/Tracing.js';
import type {ConsoleMessageType} from '../common/ConsoleMessage.js'; import type {Cookie, CookieParam, CookieSameSite} from '../common/Cookie.js';
import { import {UnsupportedOperation} from '../common/Errors.js';
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 {PDFOptions} from '../common/PDFOptions.js'; import type {PDFOptions} from '../common/PDFOptions.js';
import type {Awaitable} from '../common/types.js'; import type {Awaitable} from '../common/types.js';
import { import {evaluationString, parsePDFOptions, timeout} from '../common/util.js';
debugError,
evaluationString,
NETWORK_IDLE_TIME,
parsePDFOptions,
timeout,
validateDialogType,
} from '../common/util.js';
import type {Viewport} from '../common/Viewport.js'; import type {Viewport} from '../common/Viewport.js';
import {assert} from '../util/assert.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 {isErrorLike} from '../util/ErrorLike.js';
import type {BidiBrowser} from './Browser.js'; import type {BidiBrowser} from './Browser.js';
import type {BidiBrowserContext} from './BrowserContext.js'; import type {BidiBrowserContext} from './BrowserContext.js';
import {BrowsingContextEvent, type BrowsingContext} from './BrowsingContext.js'; import type {BidiCdpSession} from './CDPSession.js';
import {BidiCdpSession} from './CDPSession.js'; import type {BrowsingContext} from './core/BrowsingContext.js';
import type {BidiConnection} from './Connection.js';
import {BidiDeserializer} from './Deserializer.js';
import {BidiDialog} from './Dialog.js';
import {BidiElementHandle} from './ElementHandle.js'; import {BidiElementHandle} from './ElementHandle.js';
import {EmulationManager} from './EmulationManager.js';
import {BidiFrame} from './Frame.js'; import {BidiFrame} from './Frame.js';
import type {BidiHTTPRequest} from './HTTPRequest.js';
import type {BidiHTTPResponse} from './HTTPResponse.js'; import type {BidiHTTPResponse} from './HTTPResponse.js';
import {BidiKeyboard, BidiMouse, BidiTouchscreen} from './Input.js'; import {BidiKeyboard, BidiMouse, BidiTouchscreen} from './Input.js';
import type {BidiJSHandle} from './JSHandle.js'; import type {BidiJSHandle} from './JSHandle.js';
import {getBiDiReadinessState, rewriteNavigationError} from './lifecycle.js'; import {rewriteNavigationError} from './util.js';
import {BidiNetworkManager} from './NetworkManager.js';
import {createBidiHandle} from './Realm.js';
import type {BiDiPageTarget} from './Target.js';
/** /**
* @internal * @internal
*/ */
export class BidiPage extends Page { export class BidiPage extends Page {
#accessibility: Accessibility; static from(
#connection: BidiConnection; browserContext: BidiBrowserContext,
#frameTree = new FrameTree<BidiFrame>(); browsingContext: BrowsingContext
#networkManager: BidiNetworkManager; ): BidiPage {
#viewport: Viewport | null = null; const page = new BidiPage(browserContext, browsingContext);
#closedDeferred = Deferred.create<never, TargetCloseError>(); page.#initialize();
#subscribedEvents = new Map<Bidi.Event['method'], Handler<any>>([ return page;
['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;
} }
constructor( readonly #browserContext: BidiBrowserContext;
browsingContext: BrowsingContext, readonly #frame: BidiFrame;
browserContext: BidiBrowserContext #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(); super();
this.#browsingContext = browsingContext;
this.#browserContext = browserContext; this.#browserContext = browserContext;
this.#connection = browsingContext.connection; this.#frame = BidiFrame.from(this, browsingContext);
for (const [event, subscriber] of this.#browsingContextEvents) { this.#cdpEmulationManager = new EmulationManager(this.#frame.client);
this.#browsingContext.on(event, subscriber); 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);
} }
this.#networkManager = new BidiNetworkManager(this.#connection, this); #initialize() {
this.#frame.browsingContext.on('closed', () => {
for (const [event, subscriber] of this.#subscribedEvents) { this.emit(PageEvent.Close, undefined);
this.#connection.on(event, subscriber); this.removeAllListeners();
} });
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);
}
/**
* @internal
*/
get connection(): BidiConnection {
return this.#connection;
} }
override async setUserAgent( override async setUserAgent(
@ -228,46 +120,15 @@ export class BidiPage extends Page {
prototypeHandle.id, prototypeHandle.id,
'Prototype JSHandle must not be referencing primitive value' 'Prototype JSHandle must not be referencing primitive value'
); );
const response = await this.mainFrame().client.send( const response = await this.#frame.client.send('Runtime.queryObjects', {
'Runtime.queryObjects',
{
prototypeObjectId: prototypeHandle.id, prototypeObjectId: prototypeHandle.id,
} });
); return this.#frame.mainRealm().createHandle({
return createBidiHandle(this.mainFrame().mainRealm(), {
type: 'array', type: 'array',
handle: response.objects.objectId, handle: response.objects.objectId,
}) as BidiJSHandle<Prototype[]>; }) 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 { override browser(): BidiBrowser {
return this.browserContext().browser(); return this.browserContext().browser();
} }
@ -277,14 +138,9 @@ export class BidiPage extends Page {
} }
override mainFrame(): BidiFrame { override mainFrame(): BidiFrame {
const mainFrame = this.#frameTree.getMainFrame(); return this.#frame;
assert(mainFrame, 'Requesting main frame too early!');
return mainFrame;
} }
/**
* @internal
*/
async focusedFrame(): Promise<BidiFrame> { async focusedFrame(): Promise<BidiFrame> {
using frame = await this.mainFrame() using frame = await this.mainFrame()
.isolatedRealm() .isolatedRealm()
@ -304,216 +160,38 @@ export class BidiPage extends Page {
} }
override frames(): BidiFrame[] { override frames(): BidiFrame[] {
return Array.from(this.#frameTree.frames()); const frames = [this.#frame];
for (const frame of frames) {
frames.push(...frame.childFrames());
} }
return 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);
}
}
#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);
} }
override isClosed(): boolean { override isClosed(): boolean {
return this.#closedDeferred.finished(); return this.#frame.detached;
} }
override async close(options?: {runBeforeUnload?: boolean}): Promise<void> { override async close(options?: {runBeforeUnload?: boolean}): Promise<void> {
if (this.#closedDeferred.finished()) { try {
await this.#frame.browsingContext.close(options?.runBeforeUnload);
} catch {
return; 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( override async reload(
options: WaitForOptions = {} options: WaitForOptions = {}
): Promise<BidiHTTPResponse | null> { ): Promise<BidiHTTPResponse | null> {
const { const [response] = await Promise.all([
waitUntil = 'load', this.#frame.waitForNavigation(options),
timeout: ms = this._timeoutSettings.navigationTimeout(), this.#frame.browsingContext.reload(),
} = options; ]).catch(
rewriteNavigationError(
const [readiness, networkIdle] = getBiDiReadinessState(waitUntil); this.url(),
options.timeout ?? this._timeoutSettings.navigationTimeout()
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)
); );
return response;
const result = await firstValueFrom(result$);
return this.getNavigationResponse(result.navigation);
} }
override setDefaultNavigationTimeout(timeout: number): void { override setDefaultNavigationTimeout(timeout: number): void {
@ -572,8 +250,19 @@ export class BidiPage extends Page {
} }
override async setViewport(viewport: Viewport): Promise<void> { override async setViewport(viewport: Viewport): Promise<void> {
if (!this.#browsingContext.supportsCdp()) { if (!this.browser().cdpSupported) {
await this.#emulationManager.emulateViewport(viewport); 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; this.#viewport = viewport;
return; return;
} }
@ -603,10 +292,9 @@ export class BidiPage extends Page {
preferCSSPageSize, preferCSSPageSize,
} = parsePDFOptions(options, 'cm'); } = parsePDFOptions(options, 'cm');
const pageRanges = ranges ? ranges.split(', ') : []; const pageRanges = ranges ? ranges.split(', ') : [];
const {result} = await firstValueFrom( const data = await firstValueFrom(
from( from(
this.#connection.send('browsingContext.print', { this.#frame.browsingContext.print({
context: this.mainFrame()._id,
background, background,
margin, margin,
orientation: landscape ? 'landscape' : 'portrait', orientation: landscape ? 'landscape' : 'portrait',
@ -621,7 +309,7 @@ export class BidiPage extends Page {
).pipe(raceWith(timeout(ms))) ).pipe(raceWith(timeout(ms)))
); );
const buffer = Buffer.from(result.data, 'base64'); const buffer = Buffer.from(data, 'base64');
await this._maybeWriteBufferToFile(path, buffer); await this._maybeWriteBufferToFile(path, buffer);
@ -687,10 +375,7 @@ export class BidiPage extends Page {
} }
} }
const { const data = await this.#frame.browsingContext.captureScreenshot({
result: {data},
} = await this.#connection.send('browsingContext.captureScreenshot', {
context: this.mainFrame()._id,
origin: captureBeyondViewport ? 'document' : 'viewport', origin: captureBeyondViewport ? 'document' : 'viewport',
format: { format: {
type: `image/${type}`, type: `image/${type}`,
@ -702,19 +387,11 @@ export class BidiPage extends Page {
} }
override async createCDPSession(): Promise<CDPSession> { override async createCDPSession(): Promise<CDPSession> {
const {sessionId} = await this.mainFrame() return await this.#frame.createCDPSession();
.context()
.cdpSession.send('Target.attachToTarget', {
targetId: this.mainFrame()._id,
flatten: true,
});
return new BidiCdpSession(this.mainFrame().context(), sessionId);
} }
override async bringToFront(): Promise<void> { override async bringToFront(): Promise<void> {
await this.#connection.send('browsingContext.activate', { await this.#frame.browsingContext.activate();
context: this.mainFrame()._id,
});
} }
override async evaluateOnNewDocument< override async evaluateOnNewDocument<
@ -725,20 +402,16 @@ export class BidiPage extends Page {
...args: Params ...args: Params
): Promise<NewDocumentScriptEvaluation> { ): Promise<NewDocumentScriptEvaluation> {
const expression = evaluationExpression(pageFunction, ...args); const expression = evaluationExpression(pageFunction, ...args);
const {result} = await this.#connection.send('script.addPreloadScript', { const script =
functionDeclaration: expression, await this.#frame.browsingContext.addPreloadScript(expression);
contexts: [this.mainFrame()._id],
});
return {identifier: result.script}; return {identifier: script};
} }
override async removeScriptToEvaluateOnNewDocument( override async removeScriptToEvaluateOnNewDocument(
id: string id: string
): Promise<void> { ): Promise<void> {
await this.#connection.send('script.removePreloadScript', { await this.#frame.browsingContext.removePreloadScript(id);
script: id,
});
} }
override async exposeFunction<Args extends unknown[], Ret>( override async exposeFunction<Args extends unknown[], Ret>(
@ -769,13 +442,8 @@ export class BidiPage extends Page {
return new URL(url); return new URL(url);
}); });
const bidiCookies = await this.#connection.send('storage.getCookies', { const cookies = await this.#frame.browsingContext.getCookies();
partition: { return cookies
type: 'context',
context: this.mainFrame()._id,
},
});
return bidiCookies.result.cookies
.map(cookie => { .map(cookie => {
return bidiToPuppeteerCookie(cookie); return bidiToPuppeteerCookie(cookie);
}) })
@ -790,7 +458,7 @@ export class BidiPage extends Page {
throw new UnsupportedOperation(); throw new UnsupportedOperation();
} }
override target(): BiDiPageTarget { override target(): never {
throw new UnsupportedOperation(); throw new UnsupportedOperation();
} }
@ -876,22 +544,14 @@ export class BidiPage extends Page {
// TODO: delete cookie before setting them. // TODO: delete cookie before setting them.
// await this.deleteCookie(bidiCookie); // await this.deleteCookie(bidiCookie);
const partition: Bidi.Storage.PartitionDescriptor = if (cookie.partitionKey !== undefined) {
cookie.partitionKey !== undefined await this.browserContext().userContext.setCookie(
? { bidiCookie,
type: 'storageKey', cookie.partitionKey
sourceOrigin: cookie.partitionKey, );
userContext: this.#browserContext.id, } else {
await this.#frame.browsingContext.setCookie(bidiCookie);
} }
: {
type: 'context',
context: this.mainFrame()._id,
};
await this.#connection.send('storage.setCookie', {
cookie: bidiCookie,
partition,
});
} }
} }
@ -925,7 +585,7 @@ export class BidiPage extends Page {
override async goForward( override async goForward(
options: WaitForOptions = {} options: WaitForOptions = {}
): Promise<HTTPResponse | null> { ): Promise<HTTPResponse | null> {
return await this.#go(+1, options); return await this.#go(1, options);
} }
async #go( async #go(
@ -933,22 +593,19 @@ export class BidiPage extends Page {
options: WaitForOptions options: WaitForOptions
): Promise<HTTPResponse | null> { ): Promise<HTTPResponse | null> {
try { try {
const result = await Promise.all([ const [response] = await Promise.all([
this.waitForNavigation(options), this.waitForNavigation(options),
this.#connection.send('browsingContext.traverseHistory', { this.#frame.browsingContext.traverseHistory(delta),
delta,
context: this.mainFrame()._id,
}),
]); ]);
return result[0]; return response;
} catch (err) { } catch (error) {
// TODO: waitForNavigation should be cancelled if an error happens. // TODO: waitForNavigation should be cancelled if an error happens.
if (isErrorLike(err)) { if (isErrorLike(error)) {
if (err.message.includes('no such history entry')) { if (error.message.includes('no such history entry')) {
return null; 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[]) { function evaluationExpression(fun: Function | string, ...args: unknown[]) {
return `() => {${evaluationString(fun, ...args)}}`; return `() => {${evaluationString(fun, ...args)}}`;
} }

View File

@ -5,8 +5,11 @@
*/ */
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; 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 {scriptInjector} from '../common/ScriptInjector.js';
import type {TimeoutSettings} from '../common/TimeoutSettings.js';
import type {EvaluateFunc, HandleFor} from '../common/types.js'; import type {EvaluateFunc, HandleFor} from '../common/types.js';
import { import {
debugError, debugError,
@ -17,69 +20,33 @@ import {
SOURCE_URL_REGEX, SOURCE_URL_REGEX,
} from '../common/util.js'; } from '../common/util.js';
import type PuppeteerUtil from '../injected/injected.js'; import type PuppeteerUtil from '../injected/injected.js';
import {disposeSymbol} from '../util/disposable.js';
import {stringifyFunction} from '../util/Function.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 {BidiDeserializer} from './Deserializer.js';
import {BidiElementHandle} from './ElementHandle.js'; import {BidiElementHandle} from './ElementHandle.js';
import type {BidiFrame} from './Frame.js';
import {BidiJSHandle} from './JSHandle.js'; import {BidiJSHandle} from './JSHandle.js';
import type {Sandbox} from './Sandbox.js'; import {BidiSerializer} from './Serializer.js';
import {createEvaluationError} from './util.js'; import {createEvaluationError} from './util.js';
/** export abstract class BidiRealm extends Realm {
* @internal realm: BidiRealmCore;
*/
export class BidiRealm extends EventEmitter<Record<EventType, any>> {
readonly connection: BidiConnection;
#id!: string; constructor(realm: BidiRealmCore, timeoutSettings: TimeoutSettings) {
#sandbox!: Sandbox; super(timeoutSettings);
this.realm = realm;
constructor(connection: BidiConnection) {
super();
this.connection = connection;
} }
get target(): Bidi.Script.Target { protected initialize(): void {
return { this.realm.on('destroyed', ({reason}) => {
context: this.#sandbox.environment._id, this.taskManager.terminateAll(new Error(reason));
sandbox: this.#sandbox.name, });
}; this.realm.on('updated', () => {
}
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.
this.internalPuppeteerUtil = undefined; this.internalPuppeteerUtil = undefined;
this.#sandbox.environment.clearDocumentHandle(); void this.taskManager.rerunAll();
} });
};
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
);
} }
protected internalPuppeteerUtil?: Promise<BidiJSHandle<PuppeteerUtil>>; protected internalPuppeteerUtil?: Promise<BidiJSHandle<PuppeteerUtil>>;
@ -100,7 +67,7 @@ export class BidiRealm extends EventEmitter<Record<EventType, any>> {
return this.internalPuppeteerUtil as Promise<BidiJSHandle<PuppeteerUtil>>; return this.internalPuppeteerUtil as Promise<BidiJSHandle<PuppeteerUtil>>;
} }
async evaluateHandle< override async evaluateHandle<
Params extends unknown[], Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>, 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); return await this.#evaluate(false, pageFunction, ...args);
} }
async evaluate< override async evaluate<
Params extends unknown[], Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>, Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
>( >(
@ -149,8 +116,6 @@ export class BidiRealm extends EventEmitter<Record<EventType, any>> {
PuppeteerURL.INTERNAL_URL PuppeteerURL.INTERNAL_URL
); );
const sandbox = this.#sandbox;
let responsePromise; let responsePromise;
const resultOwnership = returnByValue const resultOwnership = returnByValue
? Bidi.Script.ResultOwnership.None ? Bidi.Script.ResultOwnership.None
@ -166,11 +131,8 @@ export class BidiRealm extends EventEmitter<Record<EventType, any>> {
? pageFunction ? pageFunction
: `${pageFunction}\n${sourceUrlComment}\n`; : `${pageFunction}\n${sourceUrlComment}\n`;
responsePromise = this.connection.send('script.evaluate', { responsePromise = this.realm.evaluate(expression, true, {
expression,
target: this.target,
resultOwnership, resultOwnership,
awaitPromise: true,
userActivation: true, userActivation: true,
serializationOptions, serializationOptions,
}); });
@ -179,24 +141,25 @@ export class BidiRealm extends EventEmitter<Record<EventType, any>> {
functionDeclaration = SOURCE_URL_REGEX.test(functionDeclaration) functionDeclaration = SOURCE_URL_REGEX.test(functionDeclaration)
? functionDeclaration ? functionDeclaration
: `${functionDeclaration}\n${sourceUrlComment}\n`; : `${functionDeclaration}\n${sourceUrlComment}\n`;
responsePromise = this.connection.send('script.callFunction', { responsePromise = this.realm.callFunction(
functionDeclaration, functionDeclaration,
/* awaitPromise= */ true,
{
arguments: args.length arguments: args.length
? await Promise.all( ? await Promise.all(
args.map(arg => { args.map(arg => {
return sandbox.serialize(arg); return this.serialize(arg);
}) })
) )
: [], : [],
target: this.target,
resultOwnership, resultOwnership,
awaitPromise: true,
userActivation: true, userActivation: true,
serializationOptions, serializationOptions,
}); }
);
} }
const {result} = await responsePromise; const result = await responsePromise;
if ('type' in result && result.type === 'exception') { if ('type' in result && result.type === 'exception') {
throw createEvaluationError(result.exceptionDetails); throw createEvaluationError(result.exceptionDetails);
@ -204,7 +167,49 @@ export class BidiRealm extends EventEmitter<Record<EventType, any>> {
return returnByValue return returnByValue
? BidiDeserializer.deserialize(result.result) ? 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> { 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 => { .filter((id): id is string => {
return id !== undefined; return id !== undefined;
}); });
if (handleIds.length === 0) { if (handleIds.length === 0) {
return; return;
} }
await this.connection await this.realm.disown(handleIds).catch(error => {
.send('script.disown', {
target: this.target,
handles: handleIds,
})
.catch(error => {
// Exceptions might happen in case of a page been navigated or closed. // 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. // Swallow these since they are harmless and we don't leak anything in this case.
debugError(error); debugError(error);
}); });
} }
[disposeSymbol](): void { override async adoptHandle<T extends JSHandle<Node>>(handle: T): Promise<T> {
this.connection.off( return (await this.evaluateHandle(node => {
Bidi.ChromiumBidi.Script.EventNames.RealmCreated, return node;
this.handleRealmCreated }, handle)) as unknown as T;
); }
this.connection.off(
Bidi.ChromiumBidi.Script.EventNames.RealmDestroyed, override async transferHandle<T extends JSHandle<Node>>(
this.handleRealmDestroyed handle: T
); ): Promise<T> {
if (handle.realm === this) {
return handle;
}
const transferredHandle = this.adoptHandle(handle);
await handle.dispose();
return await transferredHandle;
} }
} }
/** export class BidiFrameRealm extends BidiRealm {
* @internal static from(realm: WindowRealm, frame: BidiFrame): BidiFrameRealm {
*/ const frameRealm = new BidiFrameRealm(realm, frame);
export function createBidiHandle( frameRealm.#initialize();
sandbox: Sandbox, return frameRealm;
result: Bidi.Script.RemoteValue }
): BidiJSHandle<unknown> | BidiElementHandle<Node> { declare readonly realm: WindowRealm;
if (result.type === 'node' || result.type === 'window') {
return new BidiElementHandle(sandbox, result); 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 * 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 {Target, TargetType} from '../api/Target.js';
import {UnsupportedOperation} from '../common/Errors.js'; import {UnsupportedOperation} from '../common/Errors.js';
import type {CDPSession} from '../puppeteer-core.js';
import type {BidiBrowser} from './Browser.js'; import type {BidiBrowser} from './Browser.js';
import type {BidiBrowserContext} from './BrowserContext.js'; import type {BidiBrowserContext} from './BrowserContext.js';
import type {BrowsingContext} from './BrowsingContext.js'; import type {BidiFrame} from './Frame.js';
import {BidiCdpSession} from './CDPSession.js';
import {BidiPage} from './Page.js'; import {BidiPage} from './Page.js';
/** /**
* @internal * @internal
*/ */
export abstract class BidiTarget extends Target { export class BidiBrowserTarget 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 {
#browser: BidiBrowser; #browser: BidiBrowser;
constructor(browser: BidiBrowser) { constructor(browser: BidiBrowser) {
@ -62,91 +24,109 @@ export class BiDiBrowserTarget extends Target {
this.#browser = browser; this.#browser = browser;
} }
override asPage(): Promise<BidiPage> {
throw new UnsupportedOperation();
}
override url(): string { override url(): string {
return ''; 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> { override createCDPSession(): Promise<CDPSession> {
throw new UnsupportedOperation(); 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 { 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 * @internal
*/ */
export class BiDiPageTarget extends BiDiBrowsingContextTarget { export class BidiPageTarget extends Target {
#page: BidiPage; #page: BidiPage;
constructor( constructor(page: BidiPage) {
browserContext: BidiBrowserContext, super();
browsingContext: BrowsingContext this.#page = page;
) {
super(browserContext, browsingContext);
this.#page = new BidiPage(browsingContext, browserContext);
} }
override async page(): Promise<BidiPage> { override async page(): Promise<BidiPage> {
return this.#page; return this.#page;
} }
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();
}
}
override _setBrowserContext(browserContext: BidiBrowserContext): void { /**
super._setBrowserContext(browserContext); * @internal
this.#page._setBrowserContext(browserContext); */
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 './BidiOverCdp.js';
export * from './Browser.js'; export * from './Browser.js';
export * from './BrowserContext.js'; export * from './BrowserContext.js';
export * from './BrowsingContext.js';
export * from './Connection.js'; export * from './Connection.js';
export * from './ElementHandle.js'; export * from './ElementHandle.js';
export * from './Frame.js'; export * from './Frame.js';
@ -15,8 +14,5 @@ export * from './HTTPRequest.js';
export * from './HTTPResponse.js'; export * from './HTTPResponse.js';
export * from './Input.js'; export * from './Input.js';
export * from './JSHandle.js'; export * from './JSHandle.js';
export * from './NetworkManager.js';
export * from './Page.js'; export * from './Page.js';
export * from './Realm.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 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 {PuppeteerURL} from '../common/util.js';
import {BidiDeserializer} from './Deserializer.js'; import {BidiDeserializer} from './Deserializer.js';
@ -56,3 +57,20 @@ export function createEvaluationError(
error.stack = [details.text, ...stackLines].join('\n'); error.stack = [details.text, ...stackLines].join('\n');
return error; 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 { export {
bufferCount, bufferCount,
catchError, catchError,
combineLatest,
concat, concat,
concatMap, concatMap,
debounceTime,
defaultIfEmpty, defaultIfEmpty,
defer, defer,
delay, delay,
@ -22,7 +24,6 @@ export {
ignoreElements, ignoreElements,
lastValueFrom, lastValueFrom,
map, map,
ReplaySubject,
merge, merge,
mergeMap, mergeMap,
mergeScan, mergeScan,
@ -33,6 +34,7 @@ export {
pipe, pipe,
race, race,
raceWith, raceWith,
ReplaySubject,
retry, retry,
startWith, startWith,
switchMap, switchMap,

View File

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

View File

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

View File

@ -408,9 +408,10 @@ describe('Evaluation specs', function () {
return (error = error_); return (error = error_);
}); });
expect(error).toBeTruthy(); expect(error).toBeTruthy();
expect(error.message).toContain( expect(error.message).atLeastOneToContain([
'JSHandles can be evaluated only in the context they were created' '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 () => { it('should simulate a user gesture', async () => {
const {page} = await getTestState(); const {page} = await getTestState();

View File

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

View File

@ -5,10 +5,9 @@
*/ */
import expect from 'expect'; 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 type {CDPSession} from 'puppeteer-core/internal/api/CDPSession.js';
import {CDPSessionEvent} 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 {getTestState, launch} from './mocha-utils.js';
import {attachFrame, detachFrame, navigateFrame} from './utils.js'; import {attachFrame, detachFrame, navigateFrame} from './utils.js';
@ -266,24 +265,24 @@ describe('OOPIF', function () {
await frame.waitForSelector('#clicked'); await frame.waitForSelector('#clicked');
}); });
it('should report oopif frames', async () => { it('should report oopif frames', async () => {
const {server, page, context} = state; const {server, page} = state;
const frame = page.waitForFrame(frame => { const frame = page.waitForFrame(frame => {
return frame.url().endsWith('/oopif.html'); return frame.url().endsWith('/oopif.html');
}); });
await page.goto(server.PREFIX + '/dynamic-oopif.html'); await page.goto(server.PREFIX + '/dynamic-oopif.html');
await frame; await frame;
expect(oopifs(context)).toHaveLength(1); expect(await iframes(page)).toHaveLength(1);
expect(page.frames()).toHaveLength(2); expect(page.frames()).toHaveLength(2);
}); });
it('should wait for inner OOPIFs', async () => { 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`); await page.goto(`http://mainframe:${server.PORT}/main-frame.html`);
const frame2 = await page.waitForFrame(frame => { const frame2 = await page.waitForFrame(frame => {
return frame.url().endsWith('inner-frame2.html'); return frame.url().endsWith('inner-frame2.html');
}); });
expect(oopifs(context)).toHaveLength(2); expect(await iframes(page)).toHaveLength(2);
expect( expect(
page.frames().filter(frame => { page.frames().filter(frame => {
return frame.isOOPFrame(); return frame.isOOPFrame();
@ -297,7 +296,7 @@ describe('OOPIF', function () {
}); });
it('should load oopif iframes with subresources and request interception', async () => { 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 => { const framePromise = page.waitForFrame(frame => {
return frame.url().endsWith('/oopif.html'); return frame.url().endsWith('/oopif.html');
@ -312,7 +311,7 @@ describe('OOPIF', function () {
await page.goto(server.PREFIX + '/dynamic-oopif.html'); await page.goto(server.PREFIX + '/dynamic-oopif.html');
const frame = await framePromise; const frame = await framePromise;
const request = await requestPromise; const request = await requestPromise;
expect(oopifs(context)).toHaveLength(1); expect(await iframes(page)).toHaveLength(1);
expect(request.frame()).toBe(frame); 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 () => { 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 => { const frame = page.waitForFrame(frame => {
return frame.url().endsWith('/oopif.html'); return frame.url().endsWith('/oopif.html');
}); });
await page.goto(server.PREFIX + '/dynamic-oopif.html'); await page.goto(server.PREFIX + '/dynamic-oopif.html');
await frame; await frame;
expect(oopifs(context)).toHaveLength(1); expect(await iframes(page)).toHaveLength(1);
expect(page.frames()).toHaveLength(2); expect(page.frames()).toHaveLength(2);
const browserURL = 'http://127.0.0.1:21222'; const browserURL = 'http://127.0.0.1:21222';
@ -520,8 +519,13 @@ describe('OOPIF', function () {
}); });
}); });
function oopifs(context: BrowserContext) { async function iframes(page: Page) {
return context.targets().filter(target => { const iframes = await Promise.all(
return (target as CdpTarget)._getTargetInfo().type === 'iframe'; 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++) { for (let i = 0; i < 2; i++) {
const message = results[i].message; 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'); expect(message).not.toContain('Timeout');
} }
}); });

View File

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