From e9f9f4c356716fccaf5998901c0cdf79d44258e4 Mon Sep 17 00:00:00 2001 From: jrandolf <101637635+jrandolf@users.noreply.github.com> Date: Mon, 12 Feb 2024 17:28:21 +0100 Subject: [PATCH] refactor: adopt the rest of `bidi/core` (#11836) --- packages/puppeteer-core/src/bidi/Browser.ts | 145 +---- .../puppeteer-core/src/bidi/BrowserContext.ts | 135 ++-- .../src/bidi/BrowsingContext.ts | 110 ---- .../puppeteer-core/src/bidi/CDPSession.ts | 74 ++- .../puppeteer-core/src/bidi/Connection.ts | 56 +- packages/puppeteer-core/src/bidi/Dialog.ts | 34 +- .../puppeteer-core/src/bidi/ElementHandle.ts | 42 +- .../src/bidi/EmulationManager.ts | 35 -- .../src/bidi/ExposedFunction.ts | 6 +- packages/puppeteer-core/src/bidi/Frame.ts | 540 ++++++++++------ .../puppeteer-core/src/bidi/HTTPRequest.ts | 133 ++-- .../puppeteer-core/src/bidi/HTTPResponse.ts | 84 +-- packages/puppeteer-core/src/bidi/Input.ts | 341 +++++----- packages/puppeteer-core/src/bidi/JSHandle.ts | 30 +- .../puppeteer-core/src/bidi/NetworkManager.ts | 155 ----- packages/puppeteer-core/src/bidi/Page.ts | 595 ++++-------------- packages/puppeteer-core/src/bidi/Realm.ts | 254 ++++---- packages/puppeteer-core/src/bidi/Sandbox.ts | 154 ----- packages/puppeteer-core/src/bidi/Target.ts | 194 +++--- packages/puppeteer-core/src/bidi/bidi.ts | 4 - packages/puppeteer-core/src/bidi/lifecycle.ts | 119 ---- packages/puppeteer-core/src/bidi/util.ts | 18 + .../puppeteer-core/third_party/rxjs/rxjs.ts | 4 +- test/TestExpectations.json | 168 +++-- test/src/ariaqueryhandler.spec.ts | 7 +- test/src/evaluation.spec.ts | 7 +- test/src/launcher.spec.ts | 1 + test/src/oopif.spec.ts | 30 +- test/src/page.spec.ts | 6 +- test/src/waittask.spec.ts | 14 +- 30 files changed, 1352 insertions(+), 2143 deletions(-) delete mode 100644 packages/puppeteer-core/src/bidi/BrowsingContext.ts delete mode 100644 packages/puppeteer-core/src/bidi/EmulationManager.ts delete mode 100644 packages/puppeteer-core/src/bidi/NetworkManager.ts delete mode 100644 packages/puppeteer-core/src/bidi/Sandbox.ts delete mode 100644 packages/puppeteer-core/src/bidi/lifecycle.ts diff --git a/packages/puppeteer-core/src/bidi/Browser.ts b/packages/puppeteer-core/src/bidi/Browser.ts index 1f303e525b0..aaef567def4 100644 --- a/packages/puppeteer-core/src/bidi/Browser.ts +++ b/packages/puppeteer-core/src/bidi/Browser.ts @@ -19,22 +19,15 @@ import {BrowserContextEvent} from '../api/BrowserContext.js'; import type {Page} from '../api/Page.js'; import type {Target} from '../api/Target.js'; import {UnsupportedOperation} from '../common/Errors.js'; -import type {Handler} from '../common/EventEmitter.js'; import {debugError} from '../common/util.js'; import type {Viewport} from '../common/Viewport.js'; import {BidiBrowserContext} from './BrowserContext.js'; -import {BrowsingContext, BrowsingContextEvent} from './BrowsingContext.js'; import type {BidiConnection} from './Connection.js'; import type {Browser as BrowserCore} from './core/Browser.js'; import {Session} from './core/Session.js'; -import {UserContext} from './core/UserContext.js'; -import { - BiDiBrowserTarget, - BiDiBrowsingContextTarget, - BiDiPageTarget, - type BidiTarget, -} from './Target.js'; +import type {UserContext} from './core/UserContext.js'; +import {BidiBrowserTarget} from './Target.js'; /** * @internal @@ -89,7 +82,6 @@ export class BidiBrowser extends Browser { const browser = new BidiBrowser(session.browser, opts); browser.#initialize(); - await browser.#getTree(); return browser; } @@ -97,20 +89,8 @@ export class BidiBrowser extends Browser { #closeCallback?: BrowserCloseCallback; #browserCore: BrowserCore; #defaultViewport: Viewport | null; - #targets = new Map(); #browserContexts = new WeakMap(); - #browserTarget: BiDiBrowserTarget; - - #connectionEventHandlers = new Map< - Bidi.BrowsingContextEvent['method'], - Handler - >([ - ['browsingContext.contextCreated', this.#onContextCreated.bind(this)], - ['browsingContext.contextDestroyed', this.#onContextDestroyed.bind(this)], - ['browsingContext.domContentLoaded', this.#onContextDomLoaded.bind(this)], - ['browsingContext.fragmentNavigated', this.#onContextNavigation.bind(this)], - ['browsingContext.navigationStarted', this.#onContextNavigation.bind(this)], - ]); + #target = new BidiBrowserTarget(this); private constructor(browserCore: BrowserCore, opts: BidiBrowserOptions) { super(); @@ -118,13 +98,14 @@ export class BidiBrowser extends Browser { this.#closeCallback = opts.closeCallback; this.#browserCore = browserCore; this.#defaultViewport = opts.defaultViewport; - this.#browserTarget = new BiDiBrowserTarget(this); - for (const context of this.#browserCore.userContexts) { - this.#createBrowserContext(context); - } } #initialize() { + // Initializing existing contexts. + for (const userContext of this.#browserCore.userContexts) { + this.#createBrowserContext(userContext); + } + this.#browserCore.once('disconnected', () => { this.emit(BrowserEvent.Disconnected, undefined); }); @@ -132,10 +113,6 @@ export class BidiBrowser extends Browser { this.#browserCore.dispose('Browser process exited.', true); this.connection.dispose(); }); - - for (const [eventName, handler] of this.#connectionEventHandlers) { - this.connection.on(eventName, handler); - } } get #browserName() { @@ -145,85 +122,31 @@ export class BidiBrowser extends Browser { return this.#browserCore.session.capabilities.browserVersion; } + get cdpSupported(): boolean { + return !this.#browserName.toLocaleLowerCase().includes('firefox'); + } + override userAgent(): never { throw new UnsupportedOperation(); } #createBrowserContext(userContext: UserContext) { - const browserContext = new BidiBrowserContext(this, userContext, { + const browserContext = BidiBrowserContext.from(this, userContext, { defaultViewport: this.#defaultViewport, }); this.#browserContexts.set(userContext, browserContext); - return browserContext; - } - #onContextDomLoaded(event: Bidi.BrowsingContext.Info) { - const target = this.#targets.get(event.context); - if (target) { + browserContext.on(BrowserContextEvent.TargetCreated, target => { + this.emit(BrowserEvent.TargetCreated, target); + }); + browserContext.on(BrowserContextEvent.TargetChanged, target => { this.emit(BrowserEvent.TargetChanged, target); - target.browserContext().emit(BrowserContextEvent.TargetChanged, target); - } - } - - #onContextNavigation(event: Bidi.BrowsingContext.NavigationInfo) { - const target = this.#targets.get(event.context); - if (target) { - this.emit(BrowserEvent.TargetChanged, target); - target.browserContext().emit(BrowserContextEvent.TargetChanged, target); - } - } - - #onContextCreated(event: Bidi.BrowsingContext.ContextCreated['params']) { - const context = new BrowsingContext( - this.connection, - event, - this.#browserName - ); - this.connection.registerBrowsingContexts(context); - const browserContext = - event.userContext === UserContext.DEFAULT - ? this.defaultBrowserContext() - : this.browserContexts().find(browserContext => { - return browserContext.id === event.userContext; - }); - if (!browserContext) { - throw new Error('Missing browser contexts'); - } - const target = !context.parent - ? new BiDiPageTarget(browserContext, context) - : new BiDiBrowsingContextTarget(browserContext, context); - this.#targets.set(event.context, target); - - this.emit(BrowserEvent.TargetCreated, target); - target.browserContext().emit(BrowserContextEvent.TargetCreated, target); - - if (context.parent) { - const topLevel = this.connection.getTopLevelContext(context.parent); - topLevel.emit(BrowsingContextEvent.Created, context); - } - } - - async #getTree(): Promise { - const {result} = await this.connection.send('browsingContext.getTree', {}); - for (const context of result.contexts) { - this.#onContextCreated(context); - } - } - - async #onContextDestroyed( - event: Bidi.BrowsingContext.ContextDestroyed['params'] - ) { - const context = this.connection.getBrowsingContext(event.context); - const topLevelContext = this.connection.getTopLevelContext(event.context); - topLevelContext.emit(BrowsingContextEvent.Destroyed, context); - const target = this.#targets.get(event.context); - const page = await target?.page(); - await page?.close().catch(debugError); - this.#targets.delete(event.context); - if (target) { + }); + browserContext.on(BrowserContextEvent.TargetDestroyed, target => { this.emit(BrowserEvent.TargetDestroyed, target); - target.browserContext().emit(BrowserContextEvent.TargetDestroyed, target); - } + }); + + return browserContext; } get connection(): BidiConnection { @@ -236,9 +159,6 @@ export class BidiBrowser extends Browser { } override async close(): Promise { - for (const [eventName, handler] of this.#connectionEventHandlers) { - this.connection.off(eventName, handler); - } if (this.connection.closed) { return; } @@ -255,7 +175,7 @@ export class BidiBrowser extends Browser { } override get connected(): boolean { - return !this.#browserCore.disposed; + return !this.#browserCore.disconnected; } override process(): ChildProcess | null { @@ -288,19 +208,16 @@ export class BidiBrowser extends Browser { } override targets(): Target[] { - return [this.#browserTarget, ...Array.from(this.#targets.values())]; + return [ + this.#target, + ...this.browserContexts().flatMap(context => { + return context.targets(); + }), + ]; } - _getTargetById(id: string): BidiTarget { - const target = this.#targets.get(id); - if (!target) { - throw new Error('Target not found'); - } - return target; - } - - override target(): Target { - return this.#browserTarget; + override target(): BidiBrowserTarget { + return this.#target; } override async disconnect(): Promise { diff --git a/packages/puppeteer-core/src/bidi/BrowserContext.ts b/packages/puppeteer-core/src/bidi/BrowserContext.ts index e96f45fb939..769a2d5baee 100644 --- a/packages/puppeteer-core/src/bidi/BrowserContext.ts +++ b/packages/puppeteer-core/src/bidi/BrowserContext.ts @@ -6,17 +6,20 @@ import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; -import {BrowserContext} from '../api/BrowserContext.js'; -import type {Page} from '../api/Page.js'; +import {BrowserContext, BrowserContextEvent} from '../api/BrowserContext.js'; +import {PageEvent, type Page} from '../api/Page.js'; import type {Target} from '../api/Target.js'; import {UnsupportedOperation} from '../common/Errors.js'; import {debugError} from '../common/util.js'; import type {Viewport} from '../common/Viewport.js'; import type {BidiBrowser} from './Browser.js'; -import type {BidiConnection} from './Connection.js'; +import type {BrowsingContext} from './core/BrowsingContext.js'; import {UserContext} from './core/UserContext.js'; -import type {BidiPage} from './Page.js'; +import type {BidiFrame} from './Frame.js'; +import {BidiPage} from './Page.js'; +import {BidiPageTarget} from './Target.js'; +import {BidiFrameTarget} from './Target.js'; /** * @internal @@ -29,10 +32,25 @@ export interface BidiBrowserContextOptions { * @internal */ export class BidiBrowserContext extends BrowserContext { - #browser: BidiBrowser; - #connection: BidiConnection; - #defaultViewport: Viewport | null; - #userContext: UserContext; + static from( + browser: BidiBrowser, + userContext: UserContext, + options: BidiBrowserContextOptions + ): BidiBrowserContext { + const context = new BidiBrowserContext(browser, userContext, options); + context.#initialize(); + return context; + } + + readonly #browser: BidiBrowser; + readonly #defaultViewport: Viewport | null; + // This is public because of cookies. + readonly userContext: UserContext; + readonly #pages = new WeakMap(); + readonly #targets = new Map< + BidiPage, + [BidiPageTarget, Map] + >(); constructor( browser: BidiBrowser, @@ -41,36 +59,78 @@ export class BidiBrowserContext extends BrowserContext { ) { super(); this.#browser = browser; - this.#userContext = userContext; - this.#connection = this.#browser.connection; + this.userContext = userContext; this.#defaultViewport = options.defaultViewport; } - override targets(): Target[] { - return this.#browser.targets().filter(target => { - return target.browserContext() === this; + #initialize() { + // Create targets for existing browsing contexts. + for (const browsingContext of this.userContext.browsingContexts) { + this.#createPage(browsingContext); + } + + this.userContext.on('browsingcontext', ({browsingContext}) => { + this.#createPage(browsingContext); }); } - get connection(): BidiConnection { - return this.#connection; + #createPage(browsingContext: BrowsingContext): BidiPage { + const page = BidiPage.from(this, browsingContext); + this.#pages.set(browsingContext, page); + page.on(PageEvent.Close, () => { + this.#pages.delete(browsingContext); + }); + + // -- Target stuff starts here -- + const pageTarget = new BidiPageTarget(page); + const frameTargets = new Map(); + this.#targets.set(page, [pageTarget, frameTargets]); + page.on(PageEvent.FrameAttached, frame => { + const bidiFrame = frame as BidiFrame; + const target = new BidiFrameTarget(bidiFrame); + frameTargets.set(bidiFrame, target); + this.emit(BrowserContextEvent.TargetCreated, target); + }); + page.on(PageEvent.FrameNavigated, frame => { + const bidiFrame = frame as BidiFrame; + const target = frameTargets.get(bidiFrame); + // If there is no target, then this is the page's frame. + if (target === undefined) { + this.emit(BrowserContextEvent.TargetChanged, pageTarget); + } else { + this.emit(BrowserContextEvent.TargetChanged, target); + } + }); + page.on(PageEvent.FrameDetached, frame => { + const bidiFrame = frame as BidiFrame; + const target = frameTargets.get(bidiFrame); + if (target === undefined) { + return; + } + frameTargets.delete(bidiFrame); + this.emit(BrowserContextEvent.TargetDestroyed, target); + }); + page.on(PageEvent.Close, () => { + this.#targets.delete(page); + this.emit(BrowserContextEvent.TargetDestroyed, pageTarget); + }); + this.emit(BrowserContextEvent.TargetCreated, pageTarget); + // -- Target stuff ends here -- + + return page; + } + + override targets(): Target[] { + return [...this.#targets.values()].flatMap(([target, frames]) => { + return [target, ...frames.values()]; + }); } override async newPage(): Promise { - const {result} = await this.#connection.send('browsingContext.create', { - type: Bidi.BrowsingContext.CreateType.Tab, - userContext: this.#userContext.id, - }); - const target = this.#browser._getTargetById(result.context); - - // TODO: once BiDi has some concept matching BrowserContext, the newly - // created contexts should get automatically assigned to the right - // BrowserContext. For now, we assume that only explicitly created pages go - // to the current BrowserContext. Otherwise, the contexts get assigned to - // the default BrowserContext by the Browser. - target._setBrowserContext(this); - - const page = await target.page(); + const context = await this.userContext.createBrowsingContext( + Bidi.BrowsingContext.CreateType.Tab + ); + const page = this.#pages.get(context)!; if (!page) { throw new Error('Page is not found'); } @@ -91,7 +151,7 @@ export class BidiBrowserContext extends BrowserContext { } try { - await this.#userContext.remove(); + await this.userContext.remove(); } catch (error) { debugError(error); } @@ -102,18 +162,13 @@ export class BidiBrowserContext extends BrowserContext { } override async pages(): Promise { - const results = await Promise.all( - [...this.targets()].map(t => { - return t.page(); - }) - ); - return results.filter((p): p is BidiPage => { - return p !== null; + return [...this.userContext.browsingContexts].map(context => { + return this.#pages.get(context)!; }); } override isIncognito(): boolean { - return this.#userContext.id !== UserContext.DEFAULT; + return this.userContext.id !== UserContext.DEFAULT; } override overridePermissions(): never { @@ -125,9 +180,9 @@ export class BidiBrowserContext extends BrowserContext { } override get id(): string | undefined { - if (this.#userContext.id === UserContext.DEFAULT) { + if (this.userContext.id === UserContext.DEFAULT) { return undefined; } - return this.#userContext.id; + return this.userContext.id; } } diff --git a/packages/puppeteer-core/src/bidi/BrowsingContext.ts b/packages/puppeteer-core/src/bidi/BrowsingContext.ts deleted file mode 100644 index da2a8491297..00000000000 --- a/packages/puppeteer-core/src/bidi/BrowsingContext.ts +++ /dev/null @@ -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 { - [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( - method: T, - ...paramArgs: ProtocolMapping.Commands[T]['paramsType'] - ): Promise { - return await this.#cdpSession.send(method, ...paramArgs); - } - - dispose(): void { - this.removeAllListeners(); - this.connection.unregisterBrowsingContexts(this.#id); - void this.#cdpSession.detach().catch(debugError); - } -} diff --git a/packages/puppeteer-core/src/bidi/CDPSession.ts b/packages/puppeteer-core/src/bidi/CDPSession.ts index 5b842998cb7..1e0c5034988 100644 --- a/packages/puppeteer-core/src/bidi/CDPSession.ts +++ b/packages/puppeteer-core/src/bidi/CDPSession.ts @@ -3,7 +3,6 @@ * Copyright 2024 Google Inc. * SPDX-License-Identifier: Apache-2.0 */ - import type ProtocolMapping from 'devtools-protocol/types/protocol-mapping.js'; import {CDPSession} from '../api/CDPSession.js'; @@ -11,44 +10,49 @@ import type {Connection as CdpConnection} from '../cdp/Connection.js'; import {TargetCloseError, UnsupportedOperation} from '../common/Errors.js'; import {Deferred} from '../util/Deferred.js'; -import type {BrowsingContext} from './BrowsingContext.js'; - -/** - * @internal - */ - -export const cdpSessions = new Map(); +import type {BidiConnection} from './Connection.js'; +import type {BidiFrame} from './Frame.js'; /** * @internal */ export class BidiCdpSession extends CDPSession { - #context: BrowsingContext; - #sessionId = Deferred.create(); - #detached = false; + static sessions = new Map(); - constructor(context: BrowsingContext, sessionId?: string) { + #detached = false; + readonly #connection: BidiConnection | undefined = undefined; + readonly #sessionId = Deferred.create(); + readonly frame: BidiFrame; + + constructor(frame: BidiFrame, sessionId?: string) { super(); - this.#context = context; - if (!this.#context.supportsCdp()) { + this.frame = frame; + if (!this.frame.page().browser().cdpSupported) { return; } + + const connection = this.frame.page().browser().connection; + this.#connection = connection; + if (sessionId) { this.#sessionId.resolve(sessionId); - cdpSessions.set(sessionId, this); + BidiCdpSession.sessions.set(sessionId, this); } else { - context.connection - .send('cdp.getSession', { - context: context.id, - }) - .then(session => { + (async () => { + try { + const session = await connection.send('cdp.getSession', { + context: frame._id, + }); this.#sessionId.resolve(session.result.session!); - cdpSessions.set(session.result.session!, this); - }) - .catch(err => { - this.#sessionId.reject(err); - }); + BidiCdpSession.sessions.set(session.result.session!, this); + } catch (error) { + this.#sessionId.reject(error as Error); + } + })(); } + + // SAFETY: We never throw #sessionId. + BidiCdpSession.sessions.set(this.#sessionId.value() as string, this); } override connection(): CdpConnection | undefined { @@ -59,7 +63,7 @@ export class BidiCdpSession extends CDPSession { method: T, params?: ProtocolMapping.Commands[T]['paramsType'][0] ): Promise { - if (!this.#context.supportsCdp()) { + if (this.#connection === undefined) { throw new UnsupportedOperation( 'CDP support is required for this feature. The current browser does not support CDP.' ); @@ -70,7 +74,7 @@ export class BidiCdpSession extends CDPSession { ); } const session = await this.#sessionId.valueOrThrow(); - const {result} = await this.#context.connection.send('cdp.sendCommand', { + const {result} = await this.#connection.send('cdp.sendCommand', { method: method, params: params, session, @@ -79,17 +83,21 @@ export class BidiCdpSession extends CDPSession { } override async detach(): Promise { - cdpSessions.delete(this.id()); - if (!this.#detached && this.#context.supportsCdp()) { - await this.#context.cdpSession.send('Target.detachFromTarget', { + if (this.#connection === undefined || this.#detached) { + return; + } + try { + await this.frame.client.send('Target.detachFromTarget', { sessionId: this.id(), }); + } finally { + BidiCdpSession.sessions.delete(this.id()); + this.#detached = true; } - this.#detached = true; } override id(): string { - const val = this.#sessionId.value(); - return val instanceof Error || val === undefined ? '' : val; + const value = this.#sessionId.value(); + return typeof value === 'string' ? value : ''; } } diff --git a/packages/puppeteer-core/src/bidi/Connection.ts b/packages/puppeteer-core/src/bidi/Connection.ts index df70f8dd7a9..381ec65abae 100644 --- a/packages/puppeteer-core/src/bidi/Connection.ts +++ b/packages/puppeteer-core/src/bidi/Connection.ts @@ -14,11 +14,10 @@ import {EventEmitter} from '../common/EventEmitter.js'; import {debugError} from '../common/util.js'; import {assert} from '../util/assert.js'; -import type {BrowsingContext} from './BrowsingContext.js'; -import {cdpSessions} from './CDPSession.js'; +import {BidiCdpSession} from './CDPSession.js'; import type { - BidiEvents, Commands as BidiCommands, + BidiEvents, Connection, } from './core/Connection.js'; @@ -52,7 +51,6 @@ export class BidiConnection #timeout? = 0; #closed = false; #callbacks = new CallbackRegistry(); - #browsingContexts = new Map(); #emitters: Array> = []; constructor( @@ -138,12 +136,11 @@ export class BidiConnection return; case 'event': if (isCdpEvent(object)) { - cdpSessions + BidiCdpSession.sessions .get(object.params.session) ?.emit(object.params.event, object.params.params); return; } - this.#maybeEmitOnContext(object); // SAFETY: We know the method and parameter still match here. this.emit( object.method, @@ -164,52 +161,6 @@ export class BidiConnection debugError(object); } - #maybeEmitOnContext(event: Bidi.ChromiumBidi.Event) { - let context: BrowsingContext | undefined; - // Context specific events - if ('context' in event.params && event.params.context !== null) { - context = this.#browsingContexts.get(event.params.context); - // `log.entryAdded` specific context - } else if ( - 'source' in event.params && - event.params.source.context !== undefined - ) { - context = this.#browsingContexts.get(event.params.source.context); - } - context?.emit(event.method, event.params); - } - - registerBrowsingContexts(context: BrowsingContext): void { - this.#browsingContexts.set(context.id, context); - } - - getBrowsingContext(contextId: string): BrowsingContext { - const currentContext = this.#browsingContexts.get(contextId); - if (!currentContext) { - throw new Error(`BrowsingContext ${contextId} does not exist.`); - } - return currentContext; - } - - getTopLevelContext(contextId: string): BrowsingContext { - let currentContext = this.#browsingContexts.get(contextId); - if (!currentContext) { - throw new Error(`BrowsingContext ${contextId} does not exist.`); - } - while (currentContext.parent) { - contextId = currentContext.parent; - currentContext = this.#browsingContexts.get(contextId); - if (!currentContext) { - throw new Error(`BrowsingContext ${contextId} does not exist.`); - } - } - return currentContext; - } - - unregisterBrowsingContexts(id: string): void { - this.#browsingContexts.delete(id); - } - /** * Unbinds the connection, but keeps the transport open. Useful when the transport will * be reused by other connection e.g. with different protocol. @@ -224,7 +175,6 @@ export class BidiConnection this.#transport.onmessage = () => {}; this.#transport.onclose = () => {}; - this.#browsingContexts.clear(); this.#callbacks.clear(); } diff --git a/packages/puppeteer-core/src/bidi/Dialog.ts b/packages/puppeteer-core/src/bidi/Dialog.ts index ce222234614..1774a29f6b2 100644 --- a/packages/puppeteer-core/src/bidi/Dialog.ts +++ b/packages/puppeteer-core/src/bidi/Dialog.ts @@ -4,40 +4,26 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; - import {Dialog} from '../api/Dialog.js'; -import type {BrowsingContext} from './BrowsingContext.js'; +import type {UserPrompt} from './core/UserPrompt.js'; -/** - * @internal - */ export class BidiDialog extends Dialog { - #context: BrowsingContext; - - /** - * @internal - */ - constructor( - context: BrowsingContext, - type: Bidi.BrowsingContext.UserPromptOpenedParameters['type'], - message: string, - defaultValue?: string - ) { - super(type, message, defaultValue); - this.#context = context; + static from(prompt: UserPrompt): BidiDialog { + return new BidiDialog(prompt); + } + + #prompt: UserPrompt; + private constructor(prompt: UserPrompt) { + super(prompt.info.type, prompt.info.message, prompt.info.defaultValue); + this.#prompt = prompt; } - /** - * @internal - */ override async handle(options: { accept: boolean; text?: string; }): Promise { - await this.#context.connection.send('browsingContext.handleUserPrompt', { - context: this.#context.id, + await this.#prompt.handle({ accept: options.accept, userText: options.text, }); diff --git a/packages/puppeteer-core/src/bidi/ElementHandle.ts b/packages/puppeteer-core/src/bidi/ElementHandle.ts index fd886e8c262..62b879e2117 100644 --- a/packages/puppeteer-core/src/bidi/ElementHandle.ts +++ b/packages/puppeteer-core/src/bidi/ElementHandle.ts @@ -6,14 +6,13 @@ import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; -import {type AutofillData, ElementHandle} from '../api/ElementHandle.js'; +import {ElementHandle, type AutofillData} from '../api/ElementHandle.js'; import {UnsupportedOperation} from '../common/Errors.js'; import {throwIfDisposed} from '../util/decorators.js'; import type {BidiFrame} from './Frame.js'; import {BidiJSHandle} from './JSHandle.js'; -import type {BidiRealm} from './Realm.js'; -import type {Sandbox} from './Sandbox.js'; +import type {BidiFrameRealm} from './Realm.js'; /** * @internal @@ -21,28 +20,28 @@ import type {Sandbox} from './Sandbox.js'; export class BidiElementHandle< ElementType extends Node = Element, > extends ElementHandle { - declare handle: BidiJSHandle; - - constructor(sandbox: Sandbox, remoteValue: Bidi.Script.RemoteValue) { - super(new BidiJSHandle(sandbox, remoteValue)); + static from( + value: Bidi.Script.RemoteValue, + realm: BidiFrameRealm + ): BidiElementHandle { + return new BidiElementHandle(value, realm); } - override get realm(): Sandbox { - return this.handle.realm; + declare handle: BidiJSHandle; + + constructor(value: Bidi.Script.RemoteValue, realm: BidiFrameRealm) { + super(BidiJSHandle.from(value, realm)); + } + + override get realm(): BidiFrameRealm { + // SAFETY: See the super call in the constructor. + return this.handle.realm as BidiFrameRealm; } override get frame(): BidiFrame { return this.realm.environment; } - context(): BidiRealm { - return this.handle.context(); - } - - get isPrimitiveValue(): boolean { - return this.handle.isPrimitiveValue; - } - remoteValue(): Bidi.Script.RemoteValue { return this.handle.remoteValue(); } @@ -76,7 +75,14 @@ export class BidiElementHandle< })) as BidiJSHandle; const value = handle.remoteValue(); if (value.type === 'window') { - return this.frame.page().frame(value.value.context); + return ( + this.frame + .page() + .frames() + .find(frame => { + return frame._id === value.value.context; + }) ?? null + ); } return null; } diff --git a/packages/puppeteer-core/src/bidi/EmulationManager.ts b/packages/puppeteer-core/src/bidi/EmulationManager.ts deleted file mode 100644 index de956957856..00000000000 --- a/packages/puppeteer-core/src/bidi/EmulationManager.ts +++ /dev/null @@ -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 { - 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, - }); - } -} diff --git a/packages/puppeteer-core/src/bidi/ExposedFunction.ts b/packages/puppeteer-core/src/bidi/ExposedFunction.ts index 02e47e43049..383f34f38da 100644 --- a/packages/puppeteer-core/src/bidi/ExposedFunction.ts +++ b/packages/puppeteer-core/src/bidi/ExposedFunction.ts @@ -12,7 +12,7 @@ import {assert} from '../util/assert.js'; import {Deferred} from '../util/Deferred.js'; import {interpolateFunction, stringifyFunction} from '../util/Function.js'; -import type {BidiConnection} from './Connection.js'; +import type {Connection} from './core/Connection.js'; import {BidiDeserializer} from './Deserializer.js'; import type {BidiFrame} from './Frame.js'; import {BidiSerializer} from './Serializer.js'; @@ -207,8 +207,8 @@ export class ExposeableFunction { } }; - get #connection(): BidiConnection { - return this.#frame.context().connection; + get #connection(): Connection { + return this.#frame.page().browser().connection; } get #channelArguments() { diff --git a/packages/puppeteer-core/src/bidi/Frame.ts b/packages/puppeteer-core/src/bidi/Frame.ts index 1638c2cbdff..d03625ce273 100644 --- a/packages/puppeteer-core/src/bidi/Frame.ts +++ b/packages/puppeteer-core/src/bidi/Frame.ts @@ -4,17 +4,17 @@ * SPDX-License-Identifier: Apache-2.0 */ -import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; +import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; +import type {Observable} from '../../third_party/rxjs/rxjs.js'; import { + combineLatest, first, firstValueFrom, - forkJoin, - from, map, - merge, + of, raceWith, - zip, + switchMap, } from '../../third_party/rxjs/rxjs.js'; import type {CDPSession} from '../api/CDPSession.js'; import type {ElementHandle} from '../api/ElementHandle.js'; @@ -25,85 +25,205 @@ import { type WaitForOptions, } from '../api/Frame.js'; import type {WaitForSelectorOptions} from '../api/Page.js'; -import {UnsupportedOperation} from '../common/Errors.js'; +import {PageEvent} from '../api/Page.js'; +import { + ConsoleMessage, + type ConsoleMessageLocation, +} from '../common/ConsoleMessage.js'; +import {TargetCloseError, UnsupportedOperation} from '../common/Errors.js'; import type {TimeoutSettings} from '../common/TimeoutSettings.js'; import type {Awaitable, NodeFor} from '../common/types.js'; -import { - fromEmitterEvent, - NETWORK_IDLE_TIME, - timeout, - UTILITY_WORLD_NAME, -} from '../common/util.js'; -import {Deferred} from '../util/Deferred.js'; -import {disposeSymbol} from '../util/disposable.js'; +import {debugError, fromEmitterEvent, timeout} from '../common/util.js'; -import type {BrowsingContext} from './BrowsingContext.js'; +import {BidiCdpSession} from './CDPSession.js'; +import type {BrowsingContext} from './core/BrowsingContext.js'; +import {BidiDeserializer} from './Deserializer.js'; +import {BidiDialog} from './Dialog.js'; import {ExposeableFunction} from './ExposedFunction.js'; +import {BidiHTTPRequest, requests} from './HTTPRequest.js'; import type {BidiHTTPResponse} from './HTTPResponse.js'; -import { - getBiDiLifecycleEvent, - getBiDiReadinessState, - rewriteNavigationError, -} from './lifecycle.js'; +import {BidiJSHandle} from './JSHandle.js'; import type {BidiPage} from './Page.js'; -import { - MAIN_SANDBOX, - PUPPETEER_SANDBOX, - Sandbox, - type SandboxChart, -} from './Sandbox.js'; +import type {BidiRealm} from './Realm.js'; +import {BidiFrameRealm} from './Realm.js'; +import {rewriteNavigationError} from './util.js'; -/** - * Puppeteer's Frame class could be viewed as a BiDi BrowsingContext implementation - * @internal - */ export class BidiFrame extends Frame { - #page: BidiPage; - #context: BrowsingContext; - #timeoutSettings: TimeoutSettings; - #abortDeferred = Deferred.create(); - #disposed = false; - sandboxes: SandboxChart; - override _id: string; + static from( + parent: BidiPage | BidiFrame, + browsingContext: BrowsingContext + ): BidiFrame { + const frame = new BidiFrame(parent, browsingContext); + frame.#initialize(); + return frame; + } - constructor( - page: BidiPage, - context: BrowsingContext, - timeoutSettings: TimeoutSettings, - parentId?: string | null + readonly #parent: BidiPage | BidiFrame; + readonly browsingContext: BrowsingContext; + readonly #frames = new WeakMap(); + readonly realms: {default: BidiFrameRealm; internal: BidiFrameRealm}; + + override readonly _id: string; + override readonly client: BidiCdpSession; + + private constructor( + parent: BidiPage | BidiFrame, + browsingContext: BrowsingContext ) { super(); - this.#page = page; - this.#context = context; - this.#timeoutSettings = timeoutSettings; - this._id = this.#context.id; - this._parentId = parentId ?? undefined; + this.#parent = parent; + this.browsingContext = browsingContext; - this.sandboxes = { - [MAIN_SANDBOX]: new Sandbox(undefined, this, context, timeoutSettings), - [PUPPETEER_SANDBOX]: new Sandbox( - UTILITY_WORLD_NAME, - this, - context.createRealmForSandbox(), - timeoutSettings + this._id = browsingContext.id; + this.client = new BidiCdpSession(this); + this.realms = { + default: BidiFrameRealm.from(this.browsingContext.defaultRealm, this), + internal: BidiFrameRealm.from( + this.browsingContext.createWindowRealm( + `__puppeteer_internal_${Math.ceil(Math.random() * 10000)}` + ), + this ), }; } - override get client(): CDPSession { - return this.context().cdpSession; + #initialize(): void { + for (const browsingContext of this.browsingContext.children) { + this.#createFrameTarget(browsingContext); + } + + this.browsingContext.on('browsingcontext', ({browsingContext}) => { + this.#createFrameTarget(browsingContext); + }); + this.browsingContext.on('closed', () => { + for (const session of BidiCdpSession.sessions.values()) { + if (session.frame === this) { + void session.detach().catch(debugError); + } + } + this.page().emit(PageEvent.FrameDetached, this); + this.removeAllListeners(); + }); + + this.browsingContext.on('request', ({request}) => { + const httpRequest = BidiHTTPRequest.from(request, this); + request.once('success', () => { + // SAFETY: BidiHTTPRequest will create this before here. + this.page().emit(PageEvent.RequestFinished, httpRequest); + }); + + request.once('error', () => { + this.page().emit(PageEvent.RequestFailed, httpRequest); + }); + }); + + this.browsingContext.on('navigation', ({navigation}) => { + navigation.once('fragment', () => { + this.page().emit(PageEvent.FrameNavigated, this); + }); + }); + this.browsingContext.on('load', () => { + this.page().emit(PageEvent.Load, undefined); + }); + this.browsingContext.on('DOMContentLoaded', () => { + this._hasStartedLoading = true; + this.page().emit(PageEvent.DOMContentLoaded, undefined); + this.page().emit(PageEvent.FrameNavigated, this); + }); + + this.browsingContext.on('userprompt', ({userPrompt}) => { + this.page().emit(PageEvent.Dialog, BidiDialog.from(userPrompt)); + }); + + this.browsingContext.on('log', ({entry}) => { + if (this._id !== entry.source.context) { + return; + } + if (isConsoleLogEntry(entry)) { + const args = entry.args.map(arg => { + return this.mainRealm().createHandle(arg); + }); + + const text = args + .reduce((value, arg) => { + const parsedValue = + arg instanceof BidiJSHandle && arg.isPrimitiveValue + ? BidiDeserializer.deserialize(arg.remoteValue()) + : arg.toString(); + return `${value} ${parsedValue}`; + }, '') + .slice(1); + + this.page().emit( + PageEvent.Console, + new ConsoleMessage( + entry.method as any, + text, + args, + getStackTraceLocations(entry.stackTrace) + ) + ); + } else if (isJavaScriptLogEntry(entry)) { + const error = new Error(entry.text ?? ''); + + const messageHeight = error.message.split('\n').length; + const messageLines = error.stack!.split('\n').splice(0, messageHeight); + + const stackLines = []; + if (entry.stackTrace) { + for (const frame of entry.stackTrace.callFrames) { + // Note we need to add `1` because the values are 0-indexed. + stackLines.push( + ` at ${frame.functionName || ''} (${frame.url}:${ + frame.lineNumber + 1 + }:${frame.columnNumber + 1})` + ); + if (stackLines.length >= Error.stackTraceLimit) { + break; + } + } + } + + error.stack = [...messageLines, ...stackLines].join('\n'); + this.page().emit(PageEvent.PageError, error); + } else { + debugError( + `Unhandled LogEntry with type "${entry.type}", text "${entry.text}" and level "${entry.level}"` + ); + } + }); } - override mainRealm(): Sandbox { - return this.sandboxes[MAIN_SANDBOX]; + #createFrameTarget(browsingContext: BrowsingContext) { + const frame = BidiFrame.from(this, browsingContext); + this.#frames.set(browsingContext, frame); + this.page().emit(PageEvent.FrameAttached, frame); + + browsingContext.on('closed', () => { + this.#frames.delete(browsingContext); + }); + + return frame; } - override isolatedRealm(): Sandbox { - return this.sandboxes[PUPPETEER_SANDBOX]; + get timeoutSettings(): TimeoutSettings { + return this.page()._timeoutSettings; + } + + override mainRealm(): BidiRealm { + return this.realms.default; + } + + override isolatedRealm(): BidiRealm { + return this.realms.internal; } override page(): BidiPage { - return this.#page; + let parent = this.#parent; + while (parent instanceof BidiFrame) { + parent = parent.#parent; + } + return parent; } override isOOPFrame(): never { @@ -111,15 +231,20 @@ export class BidiFrame extends Frame { } override url(): string { - return this.#context.url; + return this.browsingContext.url; } override parentFrame(): BidiFrame | null { - return this.#page.frame(this._parentId ?? ''); + if (this.#parent instanceof BidiFrame) { + return this.#parent; + } + return null; } override childFrames(): BidiFrame[] { - return this.#page.childFrames(this.#context.id); + return [...this.browsingContext.children].map(child => { + return this.#frames.get(child)!; + }); } @throwIfDetached @@ -127,40 +252,16 @@ export class BidiFrame extends Frame { url: string, options: GoToOptions = {} ): Promise { - const { - waitUntil = 'load', - timeout: ms = this.#timeoutSettings.navigationTimeout(), - } = options; - - const [readiness, networkIdle] = getBiDiReadinessState(waitUntil); - - const result$ = zip( - from( - this.#context.connection.send('browsingContext.navigate', { - context: this.#context.id, - url, - wait: readiness, - }) - ), - ...(networkIdle !== null - ? [ - this.#page.waitForNetworkIdle$({ - timeout: ms, - concurrency: networkIdle === 'networkidle2' ? 2 : 0, - idleTime: NETWORK_IDLE_TIME, - }), - ] - : []) - ).pipe( - map(([{result}]) => { - return result; - }), - raceWith(timeout(ms), from(this.#abortDeferred.valueOrThrow())), - rewriteNavigationError(url, ms) + const [response] = await Promise.all([ + this.waitForNavigation(options), + this.browsingContext.navigate(url), + ]).catch( + rewriteNavigationError( + url, + options.timeout ?? this.timeoutSettings.navigationTimeout() + ) ); - - const result = await firstValueFrom(result$); - return this.#page.getNavigationResponse(result.navigation); + return response; } @throwIfDetached @@ -168,95 +269,58 @@ export class BidiFrame extends Frame { html: string, options: WaitForOptions = {} ): Promise { - const { - waitUntil = 'load', - timeout: ms = this.#timeoutSettings.navigationTimeout(), - } = options; - - const [waitEvent, networkIdle] = getBiDiLifecycleEvent(waitUntil); - - const result$ = zip( - forkJoin([ - fromEmitterEvent(this.#context, waitEvent).pipe(first()), - from(this.setFrameContent(html)), - ]).pipe( - map(() => { - return null; - }) + await Promise.all([ + this.setFrameContent(html), + firstValueFrom( + combineLatest([ + this.#waitForLoad$(options), + this.#waitForNetworkIdle$(options), + ]) ), - ...(networkIdle !== null - ? [ - this.#page.waitForNetworkIdle$({ - timeout: ms, - concurrency: networkIdle === 'networkidle2' ? 2 : 0, - idleTime: NETWORK_IDLE_TIME, - }), - ] - : []) - ).pipe( - raceWith(timeout(ms), from(this.#abortDeferred.valueOrThrow())), - rewriteNavigationError('setContent', ms) - ); - - await firstValueFrom(result$); - } - - context(): BrowsingContext { - return this.#context; + ]); } @throwIfDetached override async waitForNavigation( options: WaitForOptions = {} ): Promise { - const { - waitUntil = 'load', - timeout: ms = this.#timeoutSettings.navigationTimeout(), - } = options; + const {timeout: ms = this.timeoutSettings.navigationTimeout()} = options; - const [waitUntilEvent, networkIdle] = getBiDiLifecycleEvent(waitUntil); - - const navigation$ = merge( - forkJoin([ - fromEmitterEvent( - this.#context, - Bidi.ChromiumBidi.BrowsingContext.EventNames.NavigationStarted - ).pipe(first()), - fromEmitterEvent(this.#context, waitUntilEvent).pipe(first()), - ]), - fromEmitterEvent( - this.#context, - Bidi.ChromiumBidi.BrowsingContext.EventNames.FragmentNavigated + return await firstValueFrom( + combineLatest([ + fromEmitterEvent(this.browsingContext, 'navigation').pipe( + switchMap(({navigation}) => { + return this.#waitForLoad$(options).pipe( + raceWith(fromEmitterEvent(navigation, 'fragment')), + map(() => { + return navigation; + }) + ); + }) + ), + this.#waitForNetworkIdle$(options), + ]).pipe( + map(([navigation]) => { + const request = navigation.request; + if (!request) { + return null; + } + const httpRequest = requests.get(request)!; + const lastRedirect = httpRequest.redirectChain().at(-1); + return ( + lastRedirect !== undefined ? lastRedirect : httpRequest + ).response(); + }), + raceWith( + timeout(ms), + fromEmitterEvent(this.browsingContext, 'closed').pipe( + map(() => { + throw new TargetCloseError('Frame detached.'); + }) + ) + ) ) - ).pipe( - map(result => { - if (Array.isArray(result)) { - return {result: result[1]}; - } - return {result}; - }) ); - - const result$ = zip( - navigation$, - ...(networkIdle !== null - ? [ - this.#page.waitForNetworkIdle$({ - timeout: ms, - concurrency: networkIdle === 'networkidle2' ? 2 : 0, - idleTime: NETWORK_IDLE_TIME, - }), - ] - : []) - ).pipe( - map(([{result}]) => { - return result; - }), - raceWith(timeout(ms), from(this.#abortDeferred.valueOrThrow())) - ); - - const result = await firstValueFrom(result$); - return this.#page.getNavigationResponse(result.navigation); } override waitForDevicePrompt(): never { @@ -264,18 +328,7 @@ export class BidiFrame extends Frame { } override get detached(): boolean { - return this.#disposed; - } - - [disposeSymbol](): void { - if (this.#disposed) { - return; - } - this.#disposed = true; - this.#abortDeferred.reject(new Error('Frame detached')); - this.#context.dispose(); - this.sandboxes[MAIN_SANDBOX][disposeSymbol](); - this.sandboxes[PUPPETEER_SANDBOX][disposeSymbol](); + return this.browsingContext.closed; } #exposedFunctions = new Map>(); @@ -310,4 +363,115 @@ export class BidiFrame extends Frame { return super.waitForSelector(selector, options); } + + async createCDPSession(): Promise { + const {sessionId} = await this.client.send('Target.attachToTarget', { + targetId: this._id, + flatten: true, + }); + return new BidiCdpSession(this, sessionId); + } + + @throwIfDetached + #waitForLoad$(options: WaitForOptions = {}): Observable { + 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 { + 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; } diff --git a/packages/puppeteer-core/src/bidi/HTTPRequest.ts b/packages/puppeteer-core/src/bidi/HTTPRequest.ts index da0ba091656..5eb41f1325f 100644 --- a/packages/puppeteer-core/src/bidi/HTTPRequest.ts +++ b/packages/puppeteer-core/src/bidi/HTTPRequest.ts @@ -5,107 +5,126 @@ */ import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; -import type {Frame} from '../api/Frame.js'; +import type {CDPSession} from '../api/CDPSession.js'; import type { ContinueRequestOverrides, ResponseForRequest, } from '../api/HTTPRequest.js'; import {HTTPRequest, type ResourceType} from '../api/HTTPRequest.js'; +import {PageEvent} from '../api/Page.js'; import {UnsupportedOperation} from '../common/Errors.js'; -import type {BidiHTTPResponse} from './HTTPResponse.js'; +import type {Request} from './core/Request.js'; +import type {BidiFrame} from './Frame.js'; +import {BidiHTTPResponse} from './HTTPResponse.js'; + +export const requests = new WeakMap(); /** * @internal */ export class BidiHTTPRequest extends HTTPRequest { - override id: string; - override _response: BidiHTTPResponse | null = null; - override _redirectChain: BidiHTTPRequest[]; - _navigationId: string | null; - - #url: string; - #resourceType: ResourceType; - - #method: string; - #postData?: string; - #headers: Record = {}; - #initiator: Bidi.Network.Initiator; - #frame: Frame | null; - - constructor( - event: Bidi.Network.BeforeRequestSentParameters, - frame: Frame | null, - redirectChain: BidiHTTPRequest[] = [] - ) { - super(); - - this.#url = event.request.url; - this.#resourceType = event.initiator.type.toLowerCase() as ResourceType; - this.#method = event.request.method; - this.#postData = undefined; - this.#initiator = event.initiator; - this.#frame = frame; - - this.id = event.request.request; - this._redirectChain = redirectChain; - this._navigationId = event.navigation; - - for (const header of event.request.headers) { - // TODO: How to handle Binary Headers - // https://w3c.github.io/webdriver-bidi/#type-network-Header - if (header.value.type === 'string') { - this.#headers[header.name.toLowerCase()] = header.value.value; - } - } + static from( + bidiRequest: Request, + frame: BidiFrame | undefined + ): BidiHTTPRequest { + const request = new BidiHTTPRequest(bidiRequest, frame); + request.#initialize(); + return request; } - override get client(): never { + #redirect: BidiHTTPRequest | undefined; + #response: BidiHTTPResponse | null = null; + override readonly id: string; + readonly #frame: BidiFrame | undefined; + readonly #request: Request; + + private constructor(request: Request, frame: BidiFrame | undefined) { + super(); + requests.set(request, this); + + this.#request = request; + this.#frame = frame; + this.id = request.id; + } + + override get client(): CDPSession { throw new UnsupportedOperation(); } + #initialize() { + this.#request.on('redirect', request => { + this.#redirect = BidiHTTPRequest.from(request, this.#frame); + }); + this.#request.once('success', data => { + this.#response = BidiHTTPResponse.from(data, this); + }); + + this.#frame?.page().emit(PageEvent.Request, this); + } + override url(): string { - return this.#url; + return this.#request.url; } override resourceType(): ResourceType { - return this.#resourceType; + return this.initiator().type.toLowerCase() as ResourceType; } override method(): string { - return this.#method; + return this.#request.method; } override postData(): string | undefined { - return this.#postData; + throw new UnsupportedOperation(); } override hasPostData(): boolean { - return this.#postData !== undefined; + throw new UnsupportedOperation(); } override async fetchPostData(): Promise { - return this.#postData; + throw new UnsupportedOperation(); } override headers(): Record { - return this.#headers; + const headers: Record = {}; + for (const header of this.#request.headers) { + headers[header.name.toLowerCase()] = header.value.value; + } + return headers; } override response(): BidiHTTPResponse | null { - return this._response; + return this.#response; + } + + override failure(): {errorText: string} | null { + if (this.#request.error === undefined) { + return null; + } + return {errorText: this.#request.error}; } override isNavigationRequest(): boolean { - return Boolean(this._navigationId); + return this.#request.navigation !== undefined; } override initiator(): Bidi.Network.Initiator { - return this.#initiator; + return this.#request.initiator; } override redirectChain(): BidiHTTPRequest[] { - return this._redirectChain.slice(); + if (this.#redirect === undefined) { + return []; + } + const redirects = [this.#redirect]; + for (const redirect of redirects) { + if (redirect.#redirect !== undefined) { + redirects.push(redirect.#redirect); + } + } + return redirects; } override enqueueInterceptAction( @@ -115,8 +134,8 @@ export class BidiHTTPRequest extends HTTPRequest { void pendingHandler(); } - override frame(): Frame | null { - return this.#frame; + override frame(): BidiFrame | null { + return this.#frame ?? null; } override continueRequestOverrides(): never { @@ -157,8 +176,4 @@ export class BidiHTTPRequest extends HTTPRequest { ): never { throw new UnsupportedOperation(); } - - override failure(): never { - throw new UnsupportedOperation(); - } } diff --git a/packages/puppeteer-core/src/bidi/HTTPResponse.ts b/packages/puppeteer-core/src/bidi/HTTPResponse.ts index ce28820a65f..7ef17435391 100644 --- a/packages/puppeteer-core/src/bidi/HTTPResponse.ts +++ b/packages/puppeteer-core/src/bidi/HTTPResponse.ts @@ -7,11 +7,10 @@ import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; import type Protocol from 'devtools-protocol'; import type {Frame} from '../api/Frame.js'; -import { - HTTPResponse as HTTPResponse, - type RemoteAddress, -} from '../api/HTTPResponse.js'; +import {HTTPResponse, type RemoteAddress} from '../api/HTTPResponse.js'; +import {PageEvent} from '../api/Page.js'; import {UnsupportedOperation} from '../common/Errors.js'; +import {invokeAtMostOnceForArguments} from '../util/decorators.js'; import type {BidiHTTPRequest} from './HTTPRequest.js'; @@ -19,62 +18,62 @@ import type {BidiHTTPRequest} from './HTTPRequest.js'; * @internal */ export class BidiHTTPResponse extends HTTPResponse { - #request: BidiHTTPRequest; - #remoteAddress: RemoteAddress; - #status: number; - #statusText: string; - #url: string; - #fromCache: boolean; - #headers: Record = {}; - #timings: Record | null; + static from( + data: Bidi.Network.ResponseData, + request: BidiHTTPRequest + ): BidiHTTPResponse { + const response = new BidiHTTPResponse(data, request); + response.#initialize(); + return response; + } - constructor( - request: BidiHTTPRequest, - {response}: Bidi.Network.ResponseCompletedParameters + #data: Bidi.Network.ResponseData; + #request: BidiHTTPRequest; + + private constructor( + data: Bidi.Network.ResponseData, + request: BidiHTTPRequest ) { super(); + this.#data = data; this.#request = request; + } - this.#remoteAddress = { + #initialize() { + this.#request.frame()?.page().emit(PageEvent.Response, this); + } + + @invokeAtMostOnceForArguments + override remoteAddress(): RemoteAddress { + return { ip: '', port: -1, }; - - this.#url = response.url; - this.#fromCache = response.fromCache; - this.#status = response.status; - this.#statusText = response.statusText; - // TODO: File and issue with BiDi spec - this.#timings = null; - - // TODO: Removed once the Firefox implementation is compliant with https://w3c.github.io/webdriver-bidi/#get-the-response-data. - for (const header of response.headers || []) { - // TODO: How to handle Binary Headers - // https://w3c.github.io/webdriver-bidi/#type-network-Header - if (header.value.type === 'string') { - this.#headers[header.name.toLowerCase()] = header.value.value; - } - } - } - - override remoteAddress(): RemoteAddress { - return this.#remoteAddress; } override url(): string { - return this.#url; + return this.#data.url; } override status(): number { - return this.#status; + return this.#data.status; } override statusText(): string { - return this.#statusText; + return this.#data.statusText; } override headers(): Record { - return this.#headers; + const headers: Record = {}; + // TODO: Remove once the Firefox implementation is compliant with https://w3c.github.io/webdriver-bidi/#get-the-response-data. + for (const header of this.#data.headers || []) { + // TODO: How to handle Binary Headers + // https://w3c.github.io/webdriver-bidi/#type-network-Header + if (header.value.type === 'string') { + headers[header.name.toLowerCase()] = header.value.value; + } + } + return headers; } override request(): BidiHTTPRequest { @@ -82,11 +81,12 @@ export class BidiHTTPResponse extends HTTPResponse { } override fromCache(): boolean { - return this.#fromCache; + return this.#data.fromCache; } override timing(): Protocol.Network.ResourceTiming | null { - return this.#timings as any; + // TODO: File and issue with BiDi spec + throw new UnsupportedOperation(); } override frame(): Frame | null { diff --git a/packages/puppeteer-core/src/bidi/Input.ts b/packages/puppeteer-core/src/bidi/Input.ts index 5406556d642..dc70850c127 100644 --- a/packages/puppeteer-core/src/bidi/Input.ts +++ b/packages/puppeteer-core/src/bidi/Input.ts @@ -12,9 +12,9 @@ import { Mouse, MouseButton, Touchscreen, + type KeyboardTypeOptions, type KeyDownOptions, type KeyPressOptions, - type KeyboardTypeOptions, type MouseClickOptions, type MouseMoveOptions, type MouseOptions, @@ -23,7 +23,6 @@ import { import {UnsupportedOperation} from '../common/Errors.js'; import type {KeyInput} from '../common/USKeyboardLayout.js'; -import type {BrowsingContext} from './BrowsingContext.js'; import type {BidiPage} from './Page.js'; const enum InputId { @@ -288,39 +287,33 @@ export class BidiKeyboard extends Keyboard { key: KeyInput, _options?: Readonly ): Promise { - await this.#page.connection.send('input.performActions', { - context: this.#page.mainFrame()._id, - actions: [ - { - type: SourceActionsType.Key, - id: InputId.Keyboard, - actions: [ - { - type: ActionType.KeyDown, - value: getBidiKeyValue(key), - }, - ], - }, - ], - }); + await this.#page.mainFrame().browsingContext.performActions([ + { + type: SourceActionsType.Key, + id: InputId.Keyboard, + actions: [ + { + type: ActionType.KeyDown, + value: getBidiKeyValue(key), + }, + ], + }, + ]); } override async up(key: KeyInput): Promise { - await this.#page.connection.send('input.performActions', { - context: this.#page.mainFrame()._id, - actions: [ - { - type: SourceActionsType.Key, - id: InputId.Keyboard, - actions: [ - { - type: ActionType.KeyUp, - value: getBidiKeyValue(key), - }, - ], - }, - ], - }); + await this.#page.mainFrame().browsingContext.performActions([ + { + type: SourceActionsType.Key, + id: InputId.Keyboard, + actions: [ + { + type: ActionType.KeyUp, + value: getBidiKeyValue(key), + }, + ], + }, + ]); } override async press( @@ -344,16 +337,13 @@ export class BidiKeyboard extends Keyboard { type: ActionType.KeyUp, value: getBidiKeyValue(key), }); - await this.#page.connection.send('input.performActions', { - context: this.#page.mainFrame()._id, - actions: [ - { - type: SourceActionsType.Key, - id: InputId.Keyboard, - actions, - }, - ], - }); + await this.#page.mainFrame().browsingContext.performActions([ + { + type: SourceActionsType.Key, + id: InputId.Keyboard, + actions, + }, + ]); } override async type( @@ -396,16 +386,13 @@ export class BidiKeyboard extends Keyboard { ); } } - await this.#page.connection.send('input.performActions', { - context: this.#page.mainFrame()._id, - actions: [ - { - type: SourceActionsType.Key, - id: InputId.Keyboard, - actions, - }, - ], - }); + await this.#page.mainFrame().browsingContext.performActions([ + { + type: SourceActionsType.Key, + id: InputId.Keyboard, + actions, + }, + ]); } override async sendCharacter(char: string): Promise { @@ -460,19 +447,17 @@ const getBidiButton = (button: MouseButton) => { * @internal */ export class BidiMouse extends Mouse { - #context: BrowsingContext; + #page: BidiPage; #lastMovePoint: Point = {x: 0, y: 0}; - constructor(context: BrowsingContext) { + constructor(page: BidiPage) { super(); - this.#context = context; + this.#page = page; } override async reset(): Promise { this.#lastMovePoint = {x: 0, y: 0}; - await this.#context.connection.send('input.releaseActions', { - context: this.#context.id, - }); + await this.#page.mainFrame().browsingContext.releaseActions(); } override async move( @@ -502,52 +487,43 @@ export class BidiMouse extends Mouse { }); // https://w3c.github.io/webdriver-bidi/#command-input-performActions:~:text=input.PointerMoveAction%20%3D%20%7B%0A%20%20type%3A%20%22pointerMove%22%2C%0A%20%20x%3A%20js%2Dint%2C this.#lastMovePoint = to; - await this.#context.connection.send('input.performActions', { - context: this.#context.id, - actions: [ - { - type: SourceActionsType.Pointer, - id: InputId.Mouse, - actions, - }, - ], - }); + await this.#page.mainFrame().browsingContext.performActions([ + { + type: SourceActionsType.Pointer, + id: InputId.Mouse, + actions, + }, + ]); } override async down(options: Readonly = {}): Promise { - await this.#context.connection.send('input.performActions', { - context: this.#context.id, - actions: [ - { - type: SourceActionsType.Pointer, - id: InputId.Mouse, - actions: [ - { - type: ActionType.PointerDown, - button: getBidiButton(options.button ?? MouseButton.Left), - }, - ], - }, - ], - }); + await this.#page.mainFrame().browsingContext.performActions([ + { + type: SourceActionsType.Pointer, + id: InputId.Mouse, + actions: [ + { + type: ActionType.PointerDown, + button: getBidiButton(options.button ?? MouseButton.Left), + }, + ], + }, + ]); } override async up(options: Readonly = {}): Promise { - await this.#context.connection.send('input.performActions', { - context: this.#context.id, - actions: [ - { - type: SourceActionsType.Pointer, - id: InputId.Mouse, - actions: [ - { - type: ActionType.PointerUp, - button: getBidiButton(options.button ?? MouseButton.Left), - }, - ], - }, - ], - }); + await this.#page.mainFrame().browsingContext.performActions([ + { + type: SourceActionsType.Pointer, + id: InputId.Mouse, + actions: [ + { + type: ActionType.PointerUp, + button: getBidiButton(options.button ?? MouseButton.Left), + }, + ], + }, + ]); } override async click( @@ -582,41 +558,35 @@ export class BidiMouse extends Mouse { }); } actions.push(pointerUpAction); - await this.#context.connection.send('input.performActions', { - context: this.#context.id, - actions: [ - { - type: SourceActionsType.Pointer, - id: InputId.Mouse, - actions, - }, - ], - }); + await this.#page.mainFrame().browsingContext.performActions([ + { + type: SourceActionsType.Pointer, + id: InputId.Mouse, + actions, + }, + ]); } override async wheel( options: Readonly = {} ): Promise { - await this.#context.connection.send('input.performActions', { - context: this.#context.id, - actions: [ - { - type: SourceActionsType.Wheel, - id: InputId.Wheel, - actions: [ - { - type: ActionType.Scroll, - ...(this.#lastMovePoint ?? { - x: 0, - y: 0, - }), - deltaX: options.deltaX ?? 0, - deltaY: options.deltaY ?? 0, - }, - ], - }, - ], - }); + await this.#page.mainFrame().browsingContext.performActions([ + { + type: SourceActionsType.Wheel, + id: InputId.Wheel, + actions: [ + { + type: ActionType.Scroll, + ...(this.#lastMovePoint ?? { + x: 0, + y: 0, + }), + deltaX: options.deltaX ?? 0, + deltaY: options.deltaY ?? 0, + }, + ], + }, + ]); } override drag(): never { @@ -644,11 +614,11 @@ export class BidiMouse extends Mouse { * @internal */ export class BidiTouchscreen extends Touchscreen { - #context: BrowsingContext; + #page: BidiPage; - constructor(context: BrowsingContext) { + constructor(page: BidiPage) { super(); - this.#context = context; + this.#page = page; } override async touchStart( @@ -656,30 +626,27 @@ export class BidiTouchscreen extends Touchscreen { y: number, options: BidiTouchMoveOptions = {} ): Promise { - await this.#context.connection.send('input.performActions', { - context: this.#context.id, - actions: [ - { - type: SourceActionsType.Pointer, - id: InputId.Finger, - parameters: { - pointerType: Bidi.Input.PointerType.Touch, - }, - actions: [ - { - type: ActionType.PointerMove, - x: Math.round(x), - y: Math.round(y), - origin: options.origin, - }, - { - type: ActionType.PointerDown, - button: 0, - }, - ], + await this.#page.mainFrame().browsingContext.performActions([ + { + type: SourceActionsType.Pointer, + id: InputId.Finger, + parameters: { + pointerType: Bidi.Input.PointerType.Touch, }, - ], - }); + actions: [ + { + type: ActionType.PointerMove, + x: Math.round(x), + y: Math.round(y), + origin: options.origin, + }, + { + type: ActionType.PointerDown, + button: 0, + }, + ], + }, + ]); } override async touchMove( @@ -687,46 +654,40 @@ export class BidiTouchscreen extends Touchscreen { y: number, options: BidiTouchMoveOptions = {} ): Promise { - await this.#context.connection.send('input.performActions', { - context: this.#context.id, - actions: [ - { - type: SourceActionsType.Pointer, - id: InputId.Finger, - parameters: { - pointerType: Bidi.Input.PointerType.Touch, - }, - actions: [ - { - type: ActionType.PointerMove, - x: Math.round(x), - y: Math.round(y), - origin: options.origin, - }, - ], + await this.#page.mainFrame().browsingContext.performActions([ + { + type: SourceActionsType.Pointer, + id: InputId.Finger, + parameters: { + pointerType: Bidi.Input.PointerType.Touch, }, - ], - }); + actions: [ + { + type: ActionType.PointerMove, + x: Math.round(x), + y: Math.round(y), + origin: options.origin, + }, + ], + }, + ]); } override async touchEnd(): Promise { - await this.#context.connection.send('input.performActions', { - context: this.#context.id, - actions: [ - { - type: SourceActionsType.Pointer, - id: InputId.Finger, - parameters: { - pointerType: Bidi.Input.PointerType.Touch, - }, - actions: [ - { - type: ActionType.PointerUp, - button: 0, - }, - ], + await this.#page.mainFrame().browsingContext.performActions([ + { + type: SourceActionsType.Pointer, + id: InputId.Finger, + parameters: { + pointerType: Bidi.Input.PointerType.Touch, }, - ], - }); + actions: [ + { + type: ActionType.PointerUp, + button: 0, + }, + ], + }, + ]); } } diff --git a/packages/puppeteer-core/src/bidi/JSHandle.ts b/packages/puppeteer-core/src/bidi/JSHandle.ts index 5f703b68c86..10f564f78ae 100644 --- a/packages/puppeteer-core/src/bidi/JSHandle.ts +++ b/packages/puppeteer-core/src/bidi/JSHandle.ts @@ -12,28 +12,28 @@ import {UnsupportedOperation} from '../common/Errors.js'; import {BidiDeserializer} from './Deserializer.js'; import type {BidiRealm} from './Realm.js'; -import type {Sandbox} from './Sandbox.js'; /** * @internal */ export class BidiJSHandle extends JSHandle { - #disposed = false; - readonly #sandbox: Sandbox; + static from( + value: Bidi.Script.RemoteValue, + realm: BidiRealm + ): BidiJSHandle { + return new BidiJSHandle(value, realm); + } + readonly #remoteValue: Bidi.Script.RemoteValue; - constructor(sandbox: Sandbox, remoteValue: Bidi.Script.RemoteValue) { + override readonly realm: BidiRealm; + + #disposed = false; + + constructor(value: Bidi.Script.RemoteValue, realm: BidiRealm) { super(); - this.#sandbox = sandbox; - this.#remoteValue = remoteValue; - } - - context(): BidiRealm { - return this.realm.environment.context(); - } - - override get realm(): Sandbox { - return this.#sandbox; + this.#remoteValue = value; + this.realm = realm; } override get disposed(): boolean { @@ -55,7 +55,7 @@ export class BidiJSHandle extends JSHandle { return; } this.#disposed = true; - await this.context().destroyHandles([this]); + await this.realm.destroyHandles([this]); } get isPrimitiveValue(): boolean { diff --git a/packages/puppeteer-core/src/bidi/NetworkManager.ts b/packages/puppeteer-core/src/bidi/NetworkManager.ts deleted file mode 100644 index 2caaf0ad502..00000000000 --- a/packages/puppeteer-core/src/bidi/NetworkManager.ts +++ /dev/null @@ -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 { - #connection: BidiConnection; - #page: BidiPage; - #subscriptions = new DisposableStack(); - - #requestMap = new Map(); - #navigationMap = new Map(); - - 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(); - } -} diff --git a/packages/puppeteer-core/src/bidi/Page.ts b/packages/puppeteer-core/src/bidi/Page.ts index a3f04038aac..3234e1aec2f 100644 --- a/packages/puppeteer-core/src/bidi/Page.ts +++ b/packages/puppeteer-core/src/bidi/Page.ts @@ -7,201 +7,93 @@ import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; import type Protocol from 'devtools-protocol'; -import { - firstValueFrom, - from, - map, - raceWith, - zip, -} from '../../third_party/rxjs/rxjs.js'; +import {firstValueFrom, from, raceWith} from '../../third_party/rxjs/rxjs.js'; import type {CDPSession} from '../api/CDPSession.js'; import type {BoundingBox} from '../api/ElementHandle.js'; import type {WaitForOptions} from '../api/Frame.js'; import type {HTTPResponse} from '../api/HTTPResponse.js'; +import type {MediaFeature, GeolocationOptions} from '../api/Page.js'; import { Page, PageEvent, - type GeolocationOptions, - type MediaFeature, type NewDocumentScriptEvaluation, type ScreenshotOptions, } from '../api/Page.js'; import {Accessibility} from '../cdp/Accessibility.js'; import {Coverage} from '../cdp/Coverage.js'; -import {EmulationManager as CdpEmulationManager} from '../cdp/EmulationManager.js'; -import {FrameTree} from '../cdp/FrameTree.js'; +import {EmulationManager} from '../cdp/EmulationManager.js'; import {Tracing} from '../cdp/Tracing.js'; -import type {ConsoleMessageType} from '../common/ConsoleMessage.js'; -import { - ConsoleMessage, - type ConsoleMessageLocation, -} from '../common/ConsoleMessage.js'; -import type {Cookie, CookieSameSite, CookieParam} from '../common/Cookie.js'; -import {TargetCloseError, UnsupportedOperation} from '../common/Errors.js'; -import type {Handler} from '../common/EventEmitter.js'; -import {NetworkManagerEvent} from '../common/NetworkManagerEvents.js'; +import type {Cookie, CookieParam, CookieSameSite} from '../common/Cookie.js'; +import {UnsupportedOperation} from '../common/Errors.js'; import type {PDFOptions} from '../common/PDFOptions.js'; import type {Awaitable} from '../common/types.js'; -import { - debugError, - evaluationString, - NETWORK_IDLE_TIME, - parsePDFOptions, - timeout, - validateDialogType, -} from '../common/util.js'; +import {evaluationString, parsePDFOptions, timeout} from '../common/util.js'; import type {Viewport} from '../common/Viewport.js'; import {assert} from '../util/assert.js'; -import {Deferred} from '../util/Deferred.js'; -import {disposeSymbol} from '../util/disposable.js'; import {isErrorLike} from '../util/ErrorLike.js'; import type {BidiBrowser} from './Browser.js'; import type {BidiBrowserContext} from './BrowserContext.js'; -import {BrowsingContextEvent, type BrowsingContext} from './BrowsingContext.js'; -import {BidiCdpSession} from './CDPSession.js'; -import type {BidiConnection} from './Connection.js'; -import {BidiDeserializer} from './Deserializer.js'; -import {BidiDialog} from './Dialog.js'; +import type {BidiCdpSession} from './CDPSession.js'; +import type {BrowsingContext} from './core/BrowsingContext.js'; import {BidiElementHandle} from './ElementHandle.js'; -import {EmulationManager} from './EmulationManager.js'; import {BidiFrame} from './Frame.js'; -import type {BidiHTTPRequest} from './HTTPRequest.js'; import type {BidiHTTPResponse} from './HTTPResponse.js'; import {BidiKeyboard, BidiMouse, BidiTouchscreen} from './Input.js'; import type {BidiJSHandle} from './JSHandle.js'; -import {getBiDiReadinessState, rewriteNavigationError} from './lifecycle.js'; -import {BidiNetworkManager} from './NetworkManager.js'; -import {createBidiHandle} from './Realm.js'; -import type {BiDiPageTarget} from './Target.js'; +import {rewriteNavigationError} from './util.js'; /** * @internal */ export class BidiPage extends Page { - #accessibility: Accessibility; - #connection: BidiConnection; - #frameTree = new FrameTree(); - #networkManager: BidiNetworkManager; - #viewport: Viewport | null = null; - #closedDeferred = Deferred.create(); - #subscribedEvents = new Map>([ - ['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>([ - [BrowsingContextEvent.Created, this.#onContextCreated.bind(this)], - [BrowsingContextEvent.Destroyed, this.#onContextDestroyed.bind(this)], - ]); - #tracing: Tracing; - #coverage: Coverage; - #cdpEmulationManager: CdpEmulationManager; - #emulationManager: EmulationManager; - #mouse: BidiMouse; - #touchscreen: BidiTouchscreen; - #keyboard: BidiKeyboard; - #browsingContext: BrowsingContext; - #browserContext: BidiBrowserContext; - - _client(): CDPSession { - return this.mainFrame().context().cdpSession; + static from( + browserContext: BidiBrowserContext, + browsingContext: BrowsingContext + ): BidiPage { + const page = new BidiPage(browserContext, browsingContext); + page.#initialize(); + return page; } - constructor( - browsingContext: BrowsingContext, - browserContext: BidiBrowserContext + readonly #browserContext: BidiBrowserContext; + readonly #frame: BidiFrame; + #viewport: Viewport | null = null; + + readonly keyboard: BidiKeyboard; + readonly mouse: BidiMouse; + readonly touchscreen: BidiTouchscreen; + readonly accessibility: Accessibility; + readonly tracing: Tracing; + readonly coverage: Coverage; + readonly #cdpEmulationManager: EmulationManager; + + _client(): BidiCdpSession { + return this.#frame.client; + } + + private constructor( + browserContext: BidiBrowserContext, + browsingContext: BrowsingContext ) { super(); - this.#browsingContext = browsingContext; this.#browserContext = browserContext; - this.#connection = browsingContext.connection; + this.#frame = BidiFrame.from(this, browsingContext); - for (const [event, subscriber] of this.#browsingContextEvents) { - this.#browsingContext.on(event, subscriber); - } - - this.#networkManager = new BidiNetworkManager(this.#connection, this); - - for (const [event, subscriber] of this.#subscribedEvents) { - this.#connection.on(event, subscriber); - } - - for (const [event, subscriber] of this.#networkManagerEvents) { - // TODO: remove any - this.#networkManager.on(event, subscriber as any); - } - - const frame = new BidiFrame( - this, - this.#browsingContext, - this._timeoutSettings, - this.#browsingContext.parent - ); - this.#frameTree.addFrame(frame); - this.emit(PageEvent.FrameAttached, frame); - - // TODO: https://github.com/w3c/webdriver-bidi/issues/443 - this.#accessibility = new Accessibility( - this.mainFrame().context().cdpSession - ); - this.#tracing = new Tracing(this.mainFrame().context().cdpSession); - this.#coverage = new Coverage(this.mainFrame().context().cdpSession); - this.#cdpEmulationManager = new CdpEmulationManager( - this.mainFrame().context().cdpSession - ); - this.#emulationManager = new EmulationManager(browsingContext); - this.#mouse = new BidiMouse(this.mainFrame().context()); - this.#touchscreen = new BidiTouchscreen(this.mainFrame().context()); - this.#keyboard = new BidiKeyboard(this); + this.#cdpEmulationManager = new EmulationManager(this.#frame.client); + this.accessibility = new Accessibility(this.#frame.client); + this.tracing = new Tracing(this.#frame.client); + this.coverage = new Coverage(this.#frame.client); + this.keyboard = new BidiKeyboard(this); + this.mouse = new BidiMouse(this); + this.touchscreen = new BidiTouchscreen(this); } - /** - * @internal - */ - get connection(): BidiConnection { - return this.#connection; + #initialize() { + this.#frame.browsingContext.on('closed', () => { + this.emit(PageEvent.Close, undefined); + this.removeAllListeners(); + }); } override async setUserAgent( @@ -228,46 +120,15 @@ export class BidiPage extends Page { prototypeHandle.id, 'Prototype JSHandle must not be referencing primitive value' ); - const response = await this.mainFrame().client.send( - 'Runtime.queryObjects', - { - prototypeObjectId: prototypeHandle.id, - } - ); - return createBidiHandle(this.mainFrame().mainRealm(), { + const response = await this.#frame.client.send('Runtime.queryObjects', { + prototypeObjectId: prototypeHandle.id, + }); + return this.#frame.mainRealm().createHandle({ type: 'array', handle: response.objects.objectId, }) as BidiJSHandle; } - _setBrowserContext(browserContext: BidiBrowserContext): void { - this.#browserContext = browserContext; - } - - override get accessibility(): Accessibility { - return this.#accessibility; - } - - override get tracing(): Tracing { - return this.#tracing; - } - - override get coverage(): Coverage { - return this.#coverage; - } - - override get mouse(): BidiMouse { - return this.#mouse; - } - - override get touchscreen(): BidiTouchscreen { - return this.#touchscreen; - } - - override get keyboard(): BidiKeyboard { - return this.#keyboard; - } - override browser(): BidiBrowser { return this.browserContext().browser(); } @@ -277,14 +138,9 @@ export class BidiPage extends Page { } override mainFrame(): BidiFrame { - const mainFrame = this.#frameTree.getMainFrame(); - assert(mainFrame, 'Requesting main frame too early!'); - return mainFrame; + return this.#frame; } - /** - * @internal - */ async focusedFrame(): Promise { using frame = await this.mainFrame() .isolatedRealm() @@ -304,216 +160,38 @@ export class BidiPage extends Page { } override frames(): BidiFrame[] { - return Array.from(this.#frameTree.frames()); - } - - frame(frameId?: string): BidiFrame | null { - return this.#frameTree.getById(frameId ?? '') || null; - } - - childFrames(frameId: string): BidiFrame[] { - return this.#frameTree.childFrames(frameId); - } - - #onFrameLoaded(info: Bidi.BrowsingContext.NavigationInfo): void { - const frame = this.frame(info.context); - if (frame && this.mainFrame() === frame) { - this.emit(PageEvent.Load, undefined); + const frames = [this.#frame]; + for (const frame of frames) { + frames.push(...frame.childFrames()); } - } - - #onFrameFragmentNavigated(info: Bidi.BrowsingContext.NavigationInfo): void { - const frame = this.frame(info.context); - if (frame) { - this.emit(PageEvent.FrameNavigated, frame); - } - } - - #onFrameDOMContentLoaded(info: Bidi.BrowsingContext.NavigationInfo): void { - const frame = this.frame(info.context); - if (frame) { - frame._hasStartedLoading = true; - if (this.mainFrame() === frame) { - this.emit(PageEvent.DOMContentLoaded, undefined); - } - this.emit(PageEvent.FrameNavigated, frame); - } - } - - #onContextCreated(context: BrowsingContext): void { - if ( - !this.frame(context.id) && - (this.frame(context.parent ?? '') || !this.#frameTree.getMainFrame()) - ) { - const frame = new BidiFrame( - this, - context, - this._timeoutSettings, - context.parent - ); - this.#frameTree.addFrame(frame); - if (frame !== this.mainFrame()) { - this.emit(PageEvent.FrameAttached, frame); - } - } - } - - #onContextDestroyed(context: BrowsingContext): void { - const frame = this.frame(context.id); - - if (frame) { - if (frame === this.mainFrame()) { - this.emit(PageEvent.Close, undefined); - } - this.#removeFramesRecursively(frame); - } - } - - #removeFramesRecursively(frame: BidiFrame): void { - for (const child of frame.childFrames()) { - this.#removeFramesRecursively(child); - } - frame[disposeSymbol](); - this.#networkManager.clearMapAfterFrameDispose(frame); - this.#frameTree.removeFrame(frame); - this.emit(PageEvent.FrameDetached, frame); - } - - #onLogEntryAdded(event: Bidi.Log.Entry): void { - const frame = this.frame(event.source.context); - if (!frame) { - return; - } - if (isConsoleLogEntry(event)) { - const args = event.args.map(arg => { - return createBidiHandle(frame.mainRealm(), arg); - }); - - const text = args - .reduce((value, arg) => { - const parsedValue = arg.isPrimitiveValue - ? BidiDeserializer.deserialize(arg.remoteValue()) - : arg.toString(); - return `${value} ${parsedValue}`; - }, '') - .slice(1); - - this.emit( - PageEvent.Console, - new ConsoleMessage( - event.method as ConsoleMessageType, - text, - args, - getStackTraceLocations(event.stackTrace) - ) - ); - } else if (isJavaScriptLogEntry(event)) { - const error = new Error(event.text ?? ''); - - const messageHeight = error.message.split('\n').length; - const messageLines = error.stack!.split('\n').splice(0, messageHeight); - - const stackLines = []; - if (event.stackTrace) { - for (const frame of event.stackTrace.callFrames) { - // Note we need to add `1` because the values are 0-indexed. - stackLines.push( - ` at ${frame.functionName || ''} (${frame.url}:${ - frame.lineNumber + 1 - }:${frame.columnNumber + 1})` - ); - if (stackLines.length >= Error.stackTraceLimit) { - break; - } - } - } - - error.stack = [...messageLines, ...stackLines].join('\n'); - this.emit(PageEvent.PageError, error); - } else { - debugError( - `Unhandled LogEntry with type "${event.type}", text "${event.text}" and level "${event.level}"` - ); - } - } - - #onDialog(event: Bidi.BrowsingContext.UserPromptOpenedParameters): void { - const frame = this.frame(event.context); - if (!frame) { - return; - } - const type = validateDialogType(event.type); - - const dialog = new BidiDialog( - frame.context(), - type, - event.message, - event.defaultValue - ); - this.emit(PageEvent.Dialog, dialog); - } - - getNavigationResponse(id?: string | null): BidiHTTPResponse | null { - return this.#networkManager.getNavigationResponse(id); + return frames; } override isClosed(): boolean { - return this.#closedDeferred.finished(); + return this.#frame.detached; } override async close(options?: {runBeforeUnload?: boolean}): Promise { - if (this.#closedDeferred.finished()) { + try { + await this.#frame.browsingContext.close(options?.runBeforeUnload); + } catch { return; } - - this.#closedDeferred.reject(new TargetCloseError('Page closed!')); - this.#networkManager.dispose(); - - await this.#connection.send('browsingContext.close', { - context: this.mainFrame()._id, - promptUnload: options?.runBeforeUnload ?? false, - }); - - this.emit(PageEvent.Close, undefined); - this.removeAllListeners(); } override async reload( options: WaitForOptions = {} ): Promise { - const { - waitUntil = 'load', - timeout: ms = this._timeoutSettings.navigationTimeout(), - } = options; - - const [readiness, networkIdle] = getBiDiReadinessState(waitUntil); - - const result$ = zip( - from( - this.#connection.send('browsingContext.reload', { - context: this.mainFrame()._id, - wait: readiness, - }) - ), - ...(networkIdle !== null - ? [ - this.waitForNetworkIdle$({ - timeout: ms, - concurrency: networkIdle === 'networkidle2' ? 2 : 0, - idleTime: NETWORK_IDLE_TIME, - }), - ] - : []) - ).pipe( - map(([{result}]) => { - return result; - }), - raceWith(timeout(ms), from(this.#closedDeferred.valueOrThrow())), - rewriteNavigationError(this.url(), ms) + const [response] = await Promise.all([ + this.#frame.waitForNavigation(options), + this.#frame.browsingContext.reload(), + ]).catch( + rewriteNavigationError( + this.url(), + options.timeout ?? this._timeoutSettings.navigationTimeout() + ) ); - - const result = await firstValueFrom(result$); - return this.getNavigationResponse(result.navigation); + return response; } override setDefaultNavigationTimeout(timeout: number): void { @@ -572,8 +250,19 @@ export class BidiPage extends Page { } override async setViewport(viewport: Viewport): Promise { - if (!this.#browsingContext.supportsCdp()) { - await this.#emulationManager.emulateViewport(viewport); + if (!this.browser().cdpSupported) { + await this.#frame.browsingContext.setViewport({ + viewport: + viewport.width && viewport.height + ? { + width: viewport.width, + height: viewport.height, + } + : null, + devicePixelRatio: viewport.deviceScaleFactor + ? viewport.deviceScaleFactor + : null, + }); this.#viewport = viewport; return; } @@ -603,10 +292,9 @@ export class BidiPage extends Page { preferCSSPageSize, } = parsePDFOptions(options, 'cm'); const pageRanges = ranges ? ranges.split(', ') : []; - const {result} = await firstValueFrom( + const data = await firstValueFrom( from( - this.#connection.send('browsingContext.print', { - context: this.mainFrame()._id, + this.#frame.browsingContext.print({ background, margin, orientation: landscape ? 'landscape' : 'portrait', @@ -621,7 +309,7 @@ export class BidiPage extends Page { ).pipe(raceWith(timeout(ms))) ); - const buffer = Buffer.from(result.data, 'base64'); + const buffer = Buffer.from(data, 'base64'); await this._maybeWriteBufferToFile(path, buffer); @@ -687,10 +375,7 @@ export class BidiPage extends Page { } } - const { - result: {data}, - } = await this.#connection.send('browsingContext.captureScreenshot', { - context: this.mainFrame()._id, + const data = await this.#frame.browsingContext.captureScreenshot({ origin: captureBeyondViewport ? 'document' : 'viewport', format: { type: `image/${type}`, @@ -702,19 +387,11 @@ export class BidiPage extends Page { } override async createCDPSession(): Promise { - const {sessionId} = await this.mainFrame() - .context() - .cdpSession.send('Target.attachToTarget', { - targetId: this.mainFrame()._id, - flatten: true, - }); - return new BidiCdpSession(this.mainFrame().context(), sessionId); + return await this.#frame.createCDPSession(); } override async bringToFront(): Promise { - await this.#connection.send('browsingContext.activate', { - context: this.mainFrame()._id, - }); + await this.#frame.browsingContext.activate(); } override async evaluateOnNewDocument< @@ -725,20 +402,16 @@ export class BidiPage extends Page { ...args: Params ): Promise { const expression = evaluationExpression(pageFunction, ...args); - const {result} = await this.#connection.send('script.addPreloadScript', { - functionDeclaration: expression, - contexts: [this.mainFrame()._id], - }); + const script = + await this.#frame.browsingContext.addPreloadScript(expression); - return {identifier: result.script}; + return {identifier: script}; } override async removeScriptToEvaluateOnNewDocument( id: string ): Promise { - await this.#connection.send('script.removePreloadScript', { - script: id, - }); + await this.#frame.browsingContext.removePreloadScript(id); } override async exposeFunction( @@ -769,13 +442,8 @@ export class BidiPage extends Page { return new URL(url); }); - const bidiCookies = await this.#connection.send('storage.getCookies', { - partition: { - type: 'context', - context: this.mainFrame()._id, - }, - }); - return bidiCookies.result.cookies + const cookies = await this.#frame.browsingContext.getCookies(); + return cookies .map(cookie => { return bidiToPuppeteerCookie(cookie); }) @@ -790,7 +458,7 @@ export class BidiPage extends Page { throw new UnsupportedOperation(); } - override target(): BiDiPageTarget { + override target(): never { throw new UnsupportedOperation(); } @@ -876,22 +544,14 @@ export class BidiPage extends Page { // TODO: delete cookie before setting them. // await this.deleteCookie(bidiCookie); - const partition: Bidi.Storage.PartitionDescriptor = - cookie.partitionKey !== undefined - ? { - type: 'storageKey', - sourceOrigin: cookie.partitionKey, - userContext: this.#browserContext.id, - } - : { - type: 'context', - context: this.mainFrame()._id, - }; - - await this.#connection.send('storage.setCookie', { - cookie: bidiCookie, - partition, - }); + if (cookie.partitionKey !== undefined) { + await this.browserContext().userContext.setCookie( + bidiCookie, + cookie.partitionKey + ); + } else { + await this.#frame.browsingContext.setCookie(bidiCookie); + } } } @@ -925,7 +585,7 @@ export class BidiPage extends Page { override async goForward( options: WaitForOptions = {} ): Promise { - return await this.#go(+1, options); + return await this.#go(1, options); } async #go( @@ -933,22 +593,19 @@ export class BidiPage extends Page { options: WaitForOptions ): Promise { try { - const result = await Promise.all([ + const [response] = await Promise.all([ this.waitForNavigation(options), - this.#connection.send('browsingContext.traverseHistory', { - delta, - context: this.mainFrame()._id, - }), + this.#frame.browsingContext.traverseHistory(delta), ]); - return result[0]; - } catch (err) { + return response; + } catch (error) { // TODO: waitForNavigation should be cancelled if an error happens. - if (isErrorLike(err)) { - if (err.message.includes('no such history entry')) { + if (isErrorLike(error)) { + if (error.message.includes('no such history entry')) { return null; } } - throw err; + throw error; } } @@ -957,34 +614,6 @@ export class BidiPage extends Page { } } -function isConsoleLogEntry( - event: Bidi.Log.Entry -): event is Bidi.Log.ConsoleLogEntry { - return event.type === 'console'; -} - -function isJavaScriptLogEntry( - event: Bidi.Log.Entry -): event is Bidi.Log.JavascriptLogEntry { - return event.type === 'javascript'; -} - -function getStackTraceLocations( - stackTrace?: Bidi.Script.StackTrace -): ConsoleMessageLocation[] { - const stackTraceLocations: ConsoleMessageLocation[] = []; - if (stackTrace) { - for (const callFrame of stackTrace.callFrames) { - stackTraceLocations.push({ - url: callFrame.url, - lineNumber: callFrame.lineNumber, - columnNumber: callFrame.columnNumber, - }); - } - } - return stackTraceLocations; -} - function evaluationExpression(fun: Function | string, ...args: unknown[]) { return `() => {${evaluationString(fun, ...args)}}`; } diff --git a/packages/puppeteer-core/src/bidi/Realm.ts b/packages/puppeteer-core/src/bidi/Realm.ts index 037e8f43baa..f7f0a1dc8ed 100644 --- a/packages/puppeteer-core/src/bidi/Realm.ts +++ b/packages/puppeteer-core/src/bidi/Realm.ts @@ -5,8 +5,11 @@ */ import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; -import {EventEmitter, type EventType} from '../common/EventEmitter.js'; +import type {JSHandle} from '../api/JSHandle.js'; +import {Realm} from '../api/Realm.js'; +import {LazyArg} from '../common/LazyArg.js'; import {scriptInjector} from '../common/ScriptInjector.js'; +import type {TimeoutSettings} from '../common/TimeoutSettings.js'; import type {EvaluateFunc, HandleFor} from '../common/types.js'; import { debugError, @@ -17,69 +20,33 @@ import { SOURCE_URL_REGEX, } from '../common/util.js'; import type PuppeteerUtil from '../injected/injected.js'; -import {disposeSymbol} from '../util/disposable.js'; import {stringifyFunction} from '../util/Function.js'; -import type {BidiConnection} from './Connection.js'; +import type {Realm as BidiRealmCore} from './core/Realm.js'; +import type {WindowRealm} from './core/Realm.js'; import {BidiDeserializer} from './Deserializer.js'; import {BidiElementHandle} from './ElementHandle.js'; +import type {BidiFrame} from './Frame.js'; import {BidiJSHandle} from './JSHandle.js'; -import type {Sandbox} from './Sandbox.js'; +import {BidiSerializer} from './Serializer.js'; import {createEvaluationError} from './util.js'; -/** - * @internal - */ -export class BidiRealm extends EventEmitter> { - readonly connection: BidiConnection; +export abstract class BidiRealm extends Realm { + realm: BidiRealmCore; - #id!: string; - #sandbox!: Sandbox; - - constructor(connection: BidiConnection) { - super(); - this.connection = connection; + constructor(realm: BidiRealmCore, timeoutSettings: TimeoutSettings) { + super(timeoutSettings); + this.realm = realm; } - get target(): Bidi.Script.Target { - return { - context: this.#sandbox.environment._id, - sandbox: this.#sandbox.name, - }; - } - - handleRealmDestroyed = async ( - params: Bidi.Script.RealmDestroyed['params'] - ): Promise => { - if (params.realm === this.#id) { - // Note: The Realm is destroyed, so in theory the handle should be as - // well. + protected initialize(): void { + this.realm.on('destroyed', ({reason}) => { + this.taskManager.terminateAll(new Error(reason)); + }); + this.realm.on('updated', () => { this.internalPuppeteerUtil = undefined; - this.#sandbox.environment.clearDocumentHandle(); - } - }; - - handleRealmCreated = (params: Bidi.Script.RealmCreated['params']): void => { - if ( - params.type === 'window' && - params.context === this.#sandbox.environment._id && - params.sandbox === this.#sandbox.name - ) { - this.#id = params.realm; - void this.#sandbox.taskManager.rerunAll(); - } - }; - - setSandbox(sandbox: Sandbox): void { - this.#sandbox = sandbox; - this.connection.on( - Bidi.ChromiumBidi.Script.EventNames.RealmCreated, - this.handleRealmCreated - ); - this.connection.on( - Bidi.ChromiumBidi.Script.EventNames.RealmDestroyed, - this.handleRealmDestroyed - ); + void this.taskManager.rerunAll(); + }); } protected internalPuppeteerUtil?: Promise>; @@ -100,7 +67,7 @@ export class BidiRealm extends EventEmitter> { return this.internalPuppeteerUtil as Promise>; } - async evaluateHandle< + override async evaluateHandle< Params extends unknown[], Func extends EvaluateFunc = EvaluateFunc, >( @@ -110,7 +77,7 @@ export class BidiRealm extends EventEmitter> { return await this.#evaluate(false, pageFunction, ...args); } - async evaluate< + override async evaluate< Params extends unknown[], Func extends EvaluateFunc = EvaluateFunc, >( @@ -149,8 +116,6 @@ export class BidiRealm extends EventEmitter> { PuppeteerURL.INTERNAL_URL ); - const sandbox = this.#sandbox; - let responsePromise; const resultOwnership = returnByValue ? Bidi.Script.ResultOwnership.None @@ -166,11 +131,8 @@ export class BidiRealm extends EventEmitter> { ? pageFunction : `${pageFunction}\n${sourceUrlComment}\n`; - responsePromise = this.connection.send('script.evaluate', { - expression, - target: this.target, + responsePromise = this.realm.evaluate(expression, true, { resultOwnership, - awaitPromise: true, userActivation: true, serializationOptions, }); @@ -179,24 +141,25 @@ export class BidiRealm extends EventEmitter> { functionDeclaration = SOURCE_URL_REGEX.test(functionDeclaration) ? functionDeclaration : `${functionDeclaration}\n${sourceUrlComment}\n`; - responsePromise = this.connection.send('script.callFunction', { + responsePromise = this.realm.callFunction( functionDeclaration, - arguments: args.length - ? await Promise.all( - args.map(arg => { - return sandbox.serialize(arg); - }) - ) - : [], - target: this.target, - resultOwnership, - awaitPromise: true, - userActivation: true, - serializationOptions, - }); + /* awaitPromise= */ true, + { + arguments: args.length + ? await Promise.all( + args.map(arg => { + return this.serialize(arg); + }) + ) + : [], + resultOwnership, + userActivation: true, + serializationOptions, + } + ); } - const {result} = await responsePromise; + const result = await responsePromise; if ('type' in result && result.type === 'exception') { throw createEvaluationError(result.exceptionDetails); @@ -204,7 +167,49 @@ export class BidiRealm extends EventEmitter> { return returnByValue ? BidiDeserializer.deserialize(result.result) - : createBidiHandle(sandbox, result.result); + : this.createHandle(result.result); + } + + createHandle( + result: Bidi.Script.RemoteValue + ): BidiJSHandle | BidiElementHandle { + 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 { + 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>): Promise { @@ -215,43 +220,80 @@ export class BidiRealm extends EventEmitter> { .filter((id): id is string => { return id !== undefined; }); + if (handleIds.length === 0) { return; } - await this.connection - .send('script.disown', { - target: this.target, - handles: handleIds, - }) - .catch(error => { - // Exceptions might happen in case of a page been navigated or closed. - // Swallow these since they are harmless and we don't leak anything in this case. - debugError(error); - }); + await this.realm.disown(handleIds).catch(error => { + // Exceptions might happen in case of a page been navigated or closed. + // Swallow these since they are harmless and we don't leak anything in this case. + debugError(error); + }); } - [disposeSymbol](): void { - this.connection.off( - Bidi.ChromiumBidi.Script.EventNames.RealmCreated, - this.handleRealmCreated - ); - this.connection.off( - Bidi.ChromiumBidi.Script.EventNames.RealmDestroyed, - this.handleRealmDestroyed - ); + override async adoptHandle>(handle: T): Promise { + return (await this.evaluateHandle(node => { + return node; + }, handle)) as unknown as T; + } + + override async transferHandle>( + handle: T + ): Promise { + if (handle.realm === this) { + return handle; + } + const transferredHandle = this.adoptHandle(handle); + await handle.dispose(); + return await transferredHandle; } } -/** - * @internal - */ -export function createBidiHandle( - sandbox: Sandbox, - result: Bidi.Script.RemoteValue -): BidiJSHandle | BidiElementHandle { - if (result.type === 'node' || result.type === 'window') { - return new BidiElementHandle(sandbox, result); +export class BidiFrameRealm extends BidiRealm { + static from(realm: WindowRealm, frame: BidiFrame): BidiFrameRealm { + const frameRealm = new BidiFrameRealm(realm, frame); + frameRealm.#initialize(); + return frameRealm; + } + declare readonly realm: WindowRealm; + + readonly #frame: BidiFrame; + + private constructor(realm: WindowRealm, frame: BidiFrame) { + super(realm, frame.timeoutSettings); + this.#frame = frame; + } + + #initialize() { + // This should run first. + this.realm.on('updated', () => { + this.environment.clearDocumentHandle(); + }); + + super.initialize(); + } + + get sandbox(): string | undefined { + return this.realm.sandbox; + } + + override get environment(): BidiFrame { + return this.#frame; + } + + override async adoptBackendNode( + backendNodeId?: number | undefined + ): Promise> { + const {object} = await this.#frame.client.send('DOM.resolveNode', { + backendNodeId, + }); + return BidiElementHandle.from( + { + handle: object.objectId, + type: 'node', + }, + this + ); } - return new BidiJSHandle(sandbox, result); } diff --git a/packages/puppeteer-core/src/bidi/Sandbox.ts b/packages/puppeteer-core/src/bidi/Sandbox.ts deleted file mode 100644 index 69d81dc78de..00000000000 --- a/packages/puppeteer-core/src/bidi/Sandbox.ts +++ /dev/null @@ -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 = EvaluateFunc, - >( - pageFunction: Func | string, - ...args: Params - ): Promise>>> { - pageFunction = withSourcePuppeteerURLIfNone( - this.evaluateHandle.name, - pageFunction - ); - return await this.realm.evaluateHandle(pageFunction, ...args); - } - - async evaluate< - Params extends unknown[], - Func extends EvaluateFunc = EvaluateFunc, - >( - pageFunction: Func | string, - ...args: Params - ): Promise>> { - pageFunction = withSourcePuppeteerURLIfNone( - this.evaluate.name, - pageFunction - ); - return await this.realm.evaluate(pageFunction, ...args); - } - - async adoptHandle>(handle: T): Promise { - return (await this.evaluateHandle(node => { - return node; - }, handle)) as unknown as T; - } - - async transferHandle>(handle: T): Promise { - 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> { - 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 { - 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); - } -} diff --git a/packages/puppeteer-core/src/bidi/Target.ts b/packages/puppeteer-core/src/bidi/Target.ts index 5974877ff3b..5a7660a5327 100644 --- a/packages/puppeteer-core/src/bidi/Target.ts +++ b/packages/puppeteer-core/src/bidi/Target.ts @@ -4,57 +4,19 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type {CDPSession} from '../api/CDPSession.js'; -import type {Page} from '../api/Page.js'; import {Target, TargetType} from '../api/Target.js'; import {UnsupportedOperation} from '../common/Errors.js'; +import type {CDPSession} from '../puppeteer-core.js'; import type {BidiBrowser} from './Browser.js'; import type {BidiBrowserContext} from './BrowserContext.js'; -import type {BrowsingContext} from './BrowsingContext.js'; -import {BidiCdpSession} from './CDPSession.js'; +import type {BidiFrame} from './Frame.js'; import {BidiPage} from './Page.js'; /** * @internal */ -export abstract class BidiTarget extends Target { - protected _browserContext: BidiBrowserContext; - - constructor(browserContext: BidiBrowserContext) { - super(); - this._browserContext = browserContext; - } - - _setBrowserContext(browserContext: BidiBrowserContext): void { - this._browserContext = browserContext; - } - - override asPage(): Promise { - 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 { - throw new UnsupportedOperation(); - } -} - -/** - * @internal - */ -export class BiDiBrowserTarget extends Target { +export class BidiBrowserTarget extends Target { #browser: BidiBrowser; constructor(browser: BidiBrowser) { @@ -62,91 +24,109 @@ export class BiDiBrowserTarget extends Target { this.#browser = browser; } + override asPage(): Promise { + throw new UnsupportedOperation(); + } override url(): string { return ''; } - - override type(): TargetType { - return TargetType.BROWSER; - } - - override asPage(): Promise { - 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 { 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 { - const {sessionId} = await this._browsingContext.cdpSession.send( - 'Target.attachToTarget', - { - targetId: this._browsingContext.id, - flatten: true, - } - ); - return new BidiCdpSession(this._browsingContext, sessionId); - } - override type(): TargetType { - return TargetType.PAGE; + return TargetType.BROWSER; + } + override browser(): BidiBrowser { + return this.#browser; + } + override browserContext(): BidiBrowserContext { + return this.#browser.defaultBrowserContext(); + } + override opener(): Target | undefined { + throw new UnsupportedOperation(); } } /** * @internal */ -export class BiDiPageTarget extends BiDiBrowsingContextTarget { +export class BidiPageTarget extends Target { #page: BidiPage; - constructor( - browserContext: BidiBrowserContext, - browsingContext: BrowsingContext - ) { - super(browserContext, browsingContext); - - this.#page = new BidiPage(browsingContext, browserContext); + constructor(page: BidiPage) { + super(); + this.#page = page; } override async page(): Promise { return this.#page; } - - override _setBrowserContext(browserContext: BidiBrowserContext): void { - super._setBrowserContext(browserContext); - this.#page._setBrowserContext(browserContext); + override async asPage(): Promise { + return BidiPage.from( + this.browserContext(), + this.#page.mainFrame().browsingContext + ); + } + override url(): string { + return this.#page.url(); + } + override createCDPSession(): Promise { + return this.#page.createCDPSession(); + } + override type(): TargetType { + return TargetType.PAGE; + } + override browser(): BidiBrowser { + return this.browserContext().browser(); + } + override browserContext(): BidiBrowserContext { + return this.#page.browserContext(); + } + override opener(): Target | undefined { + throw new UnsupportedOperation(); + } +} + +/** + * @internal + */ +export class BidiFrameTarget extends Target { + #frame: BidiFrame; + #page: BidiPage | undefined; + + constructor(frame: BidiFrame) { + super(); + this.#frame = frame; + } + + override async page(): Promise { + if (this.#page === undefined) { + this.#page = BidiPage.from( + this.browserContext(), + this.#frame.browsingContext + ); + } + return this.#page; + } + override async asPage(): Promise { + return BidiPage.from(this.browserContext(), this.#frame.browsingContext); + } + override url(): string { + return this.#frame.url(); + } + override createCDPSession(): Promise { + 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(); } } diff --git a/packages/puppeteer-core/src/bidi/bidi.ts b/packages/puppeteer-core/src/bidi/bidi.ts index 373d6d999c6..4279ba96fdf 100644 --- a/packages/puppeteer-core/src/bidi/bidi.ts +++ b/packages/puppeteer-core/src/bidi/bidi.ts @@ -7,7 +7,6 @@ export * from './BidiOverCdp.js'; export * from './Browser.js'; export * from './BrowserContext.js'; -export * from './BrowsingContext.js'; export * from './Connection.js'; export * from './ElementHandle.js'; export * from './Frame.js'; @@ -15,8 +14,5 @@ export * from './HTTPRequest.js'; export * from './HTTPResponse.js'; export * from './Input.js'; export * from './JSHandle.js'; -export * from './NetworkManager.js'; export * from './Page.js'; export * from './Realm.js'; -export * from './Sandbox.js'; -export * from './Target.js'; diff --git a/packages/puppeteer-core/src/bidi/lifecycle.ts b/packages/puppeteer-core/src/bidi/lifecycle.ts deleted file mode 100644 index 73b86cba9c2..00000000000 --- a/packages/puppeteer-core/src/bidi/lifecycle.ts +++ /dev/null @@ -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, - 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>( - message: string, - ms: number -): OperatorFunction> { - return catchError(error => { - if (error instanceof ProtocolError) { - error.message += ` at ${message}`; - } else if (error instanceof TimeoutError) { - error.message = `Navigation timeout of ${ms} ms exceeded`; - } - throw error; - }); -} diff --git a/packages/puppeteer-core/src/bidi/util.ts b/packages/puppeteer-core/src/bidi/util.ts index 7cd45cd1e23..e1d64c2f4cf 100644 --- a/packages/puppeteer-core/src/bidi/util.ts +++ b/packages/puppeteer-core/src/bidi/util.ts @@ -6,6 +6,7 @@ import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; +import {ProtocolError, TimeoutError} from '../common/Errors.js'; import {PuppeteerURL} from '../common/util.js'; import {BidiDeserializer} from './Deserializer.js'; @@ -56,3 +57,20 @@ export function createEvaluationError( error.stack = [details.text, ...stackLines].join('\n'); return error; } + +/** + * @internal + */ +export function rewriteNavigationError( + message: string, + ms: number +): (error: unknown) => never { + return error => { + if (error instanceof ProtocolError) { + error.message += ` at ${message}`; + } else if (error instanceof TimeoutError) { + error.message = `Navigation timeout of ${ms} ms exceeded`; + } + throw error; + }; +} diff --git a/packages/puppeteer-core/third_party/rxjs/rxjs.ts b/packages/puppeteer-core/third_party/rxjs/rxjs.ts index 2460f047bd2..89c9b077650 100644 --- a/packages/puppeteer-core/third_party/rxjs/rxjs.ts +++ b/packages/puppeteer-core/third_party/rxjs/rxjs.ts @@ -6,8 +6,10 @@ export { bufferCount, catchError, + combineLatest, concat, concatMap, + debounceTime, defaultIfEmpty, defer, delay, @@ -22,7 +24,6 @@ export { ignoreElements, lastValueFrom, map, - ReplaySubject, merge, mergeMap, mergeScan, @@ -33,6 +34,7 @@ export { pipe, race, raceWith, + ReplaySubject, retry, startWith, switchMap, diff --git a/test/TestExpectations.json b/test/TestExpectations.json index f49d337d55d..aea3ea8117d 100644 --- a/test/TestExpectations.json +++ b/test/TestExpectations.json @@ -537,7 +537,7 @@ "testIdPattern": "[coverage.spec] Coverage specs JSCoverage should ignore pptr internal scripts if reportAnonymousScripts is true", "platforms": ["darwin", "linux", "win32"], "parameters": ["webDriverBiDi"], - "expectations": ["FAIL", "PASS"] + "expectations": ["FAIL"] }, { "testIdPattern": "[devtools.spec] DevTools should expose DevTools as a page", @@ -579,7 +579,7 @@ "testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.click should return Point data", "platforms": ["darwin", "linux", "win32"], "parameters": ["webDriverBiDi"], - "expectations": ["FAIL", "PASS"] + "expectations": ["PASS"] }, { "testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.clickablePoint should not work if the click box is not visible due to the iframe", @@ -670,7 +670,7 @@ "testIdPattern": "[frame.spec] Frame specs Frame Management should support lazy frames", "platforms": ["darwin", "linux", "win32"], "parameters": ["chrome"], - "expectations": ["FAIL", "PASS"] + "expectations": ["PASS"] }, { "testIdPattern": "[headful.spec] *", @@ -712,7 +712,7 @@ "testIdPattern": "[jshandle.spec] JSHandle Page.evaluateHandle should use the same JS wrappers", "platforms": ["darwin", "linux", "win32"], "parameters": ["webDriverBiDi"], - "expectations": ["FAIL", "PASS"] + "expectations": ["PASS"] }, { "testIdPattern": "[keyboard.spec] Keyboard should send a character with sendCharacter in iframe", @@ -820,7 +820,8 @@ "testIdPattern": "[navigation.spec] navigation Page.goto should fail when navigating to bad SSL", "platforms": ["darwin", "linux", "win32"], "parameters": ["firefox"], - "expectations": ["FAIL"] + "expectations": ["SKIP"], + "comment": "https://github.com/w3c/webdriver-bidi/issues/657" }, { "testIdPattern": "[navigation.spec] navigation Page.goto should send referer", @@ -846,12 +847,6 @@ "parameters": ["firefox", "webDriverBiDi"], "expectations": ["SKIP"] }, - { - "testIdPattern": "[network.spec] network raw network headers Cross-origin set-cookie", - "platforms": ["darwin", "linux", "win32"], - "parameters": ["webDriverBiDi"], - "expectations": ["FAIL", "PASS"] - }, { "testIdPattern": "[network.spec] network Request.initiator should return the initiator", "platforms": ["darwin", "linux", "win32"], @@ -870,6 +865,13 @@ "parameters": ["webDriverBiDi"], "expectations": ["PASS"] }, + { + "testIdPattern": "[network.spec] network Request.postData should be |undefined| when there is no post data", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"], + "comment": "Unsupported" + }, { "testIdPattern": "[network.spec] network Request.postData should work", "platforms": ["darwin", "linux", "win32"], @@ -1218,7 +1220,7 @@ "testIdPattern": "[target.spec] Target Browser.waitForTarget should timeout waiting for a non-existent target", "platforms": ["darwin", "linux", "win32"], "parameters": ["webDriverBiDi"], - "expectations": ["FAIL", "PASS"] + "expectations": ["PASS"] }, { "testIdPattern": "[target.spec] Target should be able to use async waitForTarget", @@ -1609,7 +1611,7 @@ "testIdPattern": "[click.spec] Page.click should click on checkbox input and toggle", "platforms": ["darwin", "linux", "win32"], "parameters": ["firefox", "webDriverBiDi"], - "expectations": ["FAIL", "PASS"] + "expectations": ["PASS"] }, { "testIdPattern": "[click.spec] Page.click should click on checkbox label and toggle", @@ -1783,13 +1785,13 @@ "testIdPattern": "[coverage.spec] Coverage specs CSSCoverage should work with complicated usecases", "platforms": ["darwin", "linux", "win32"], "parameters": ["chrome", "webDriverBiDi"], - "expectations": ["FAIL", "PASS"] + "expectations": ["PASS"] }, { "testIdPattern": "[coverage.spec] Coverage specs JSCoverage should not ignore eval() scripts if reportAnonymousScripts is true", "platforms": ["darwin", "linux", "win32"], "parameters": ["chrome", "webDriverBiDi"], - "expectations": ["FAIL", "PASS"] + "expectations": ["PASS"] }, { "testIdPattern": "[debugInfo.spec] DebugInfo Browser.debugInfo should work", @@ -2125,7 +2127,7 @@ "testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluate should await promise", "platforms": ["darwin", "linux", "win32"], "parameters": ["firefox", "webDriverBiDi"], - "expectations": ["FAIL", "PASS"] + "expectations": ["PASS"] }, { "testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluate should simulate a user gesture", @@ -2198,7 +2200,8 @@ "testIdPattern": "[frame.spec] Frame specs Frame Management should detach child frames on navigation", "platforms": ["darwin", "linux", "win32"], "parameters": ["firefox", "webDriverBiDi"], - "expectations": ["PASS"] + "expectations": ["FAIL"], + "comment": "https://github.com/w3c/webdriver-bidi/issues/659" }, { "testIdPattern": "[frame.spec] Frame specs Frame Management should report different frame instance when frame re-attaches", @@ -2236,6 +2239,13 @@ "parameters": ["cdp", "firefox"], "expectations": ["SKIP"] }, + { + "testIdPattern": "[frame.spec] Frame specs Frame Management should send \"framenavigated\" when navigating on anchor URLs", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["SKIP"], + "comment": "https://github.com/w3c/webdriver-bidi/issues/657" + }, { "testIdPattern": "[frame.spec] Frame specs Frame Management should send events when frames are manipulated dynamically", "platforms": ["darwin", "linux", "win32"], @@ -2252,7 +2262,8 @@ "testIdPattern": "[frame.spec] Frame specs Frame Management should support framesets", "platforms": ["darwin", "linux", "win32"], "parameters": ["firefox", "webDriverBiDi"], - "expectations": ["PASS"] + "expectations": ["FAIL"], + "comment": "https://github.com/w3c/webdriver-bidi/issues/659" }, { "testIdPattern": "[frame.spec] Frame specs Frame Management should support lazy frames", @@ -3015,7 +3026,7 @@ "testIdPattern": "[navigation.spec] navigation Frame.goto should navigate subframes", "platforms": ["darwin", "linux", "win32"], "parameters": ["firefox", "webDriverBiDi"], - "expectations": ["FAIL", "PASS"] + "expectations": ["PASS"] }, { "testIdPattern": "[navigation.spec] navigation Frame.goto should reject when frame detaches", @@ -3059,11 +3070,33 @@ "parameters": ["cdp", "firefox"], "expectations": ["FAIL"] }, + { + "testIdPattern": "[navigation.spec] navigation Page.goBack should work with HistoryAPI", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["SKIP"], + "comment": "browsingContext.navigationStarted event not emitted for fragment navigation" + }, { "testIdPattern": "[navigation.spec] navigation Page.goto should fail when main resources failed to load", "platforms": ["darwin", "linux", "win32"], "parameters": ["firefox", "webDriverBiDi"], - "expectations": ["FAIL", "PASS"] + "expectations": ["SKIP"], + "comment": "https://github.com/w3c/webdriver-bidi/issues/657" + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should fail when navigating and show the url at the error message", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["SKIP"], + "comment": "https://github.com/w3c/webdriver-bidi/issues/657" + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should fail when navigating to bad SSL after redirects", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["SKIP"], + "comment": "https://github.com/w3c/webdriver-bidi/issues/657" }, { "testIdPattern": "[navigation.spec] navigation Page.goto should fail when server returns 204", @@ -3071,6 +3104,13 @@ "parameters": ["cdp", "firefox"], "expectations": ["SKIP"] }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should fail when server returns 204", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["SKIP"], + "comment": "https://github.com/w3c/webdriver-bidi/issues/657" + }, { "testIdPattern": "[navigation.spec] navigation Page.goto should navigate to dataURL and fire dataURL requests", "platforms": ["darwin", "linux", "win32"], @@ -3087,7 +3127,7 @@ "testIdPattern": "[navigation.spec] navigation Page.goto should navigate to dataURL and fire dataURL requests", "platforms": ["darwin", "linux", "win32"], "parameters": ["chrome", "webDriverBiDi"], - "expectations": ["FAIL", "PASS"] + "expectations": ["PASS"] }, { "testIdPattern": "[navigation.spec] navigation Page.goto should navigate to empty page with networkidle0", @@ -3131,11 +3171,17 @@ "parameters": ["chrome", "webDriverBiDi"], "expectations": ["PASS"] }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should return last response in redirect chain", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["PASS"] + }, { "testIdPattern": "[navigation.spec] navigation Page.goto should return response when page changes its URL after load", "platforms": ["darwin", "linux", "win32"], "parameters": ["firefox", "webDriverBiDi"], - "expectations": ["FAIL", "PASS"] + "expectations": ["PASS"] }, { "testIdPattern": "[navigation.spec] navigation Page.goto should send referer", @@ -3161,18 +3207,19 @@ "parameters": ["cdp", "firefox"], "expectations": ["FAIL"] }, - { - "testIdPattern": "[navigation.spec] navigation Page.goto should work when page calls history API in beforeunload", - "platforms": ["darwin", "linux", "win32"], - "parameters": ["firefox", "webDriverBiDi"], - "expectations": ["FAIL"] - }, { "testIdPattern": "[navigation.spec] navigation Page.goto should work with anchor navigation", "platforms": ["darwin", "linux", "win32"], "parameters": ["chrome", "chrome-headless-shell"], "expectations": ["SKIP"] }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should work with anchor navigation", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["SKIP"], + "comment": "browsingContext.navigationStarted event not emitted for fragment navigation" + }, { "testIdPattern": "[navigation.spec] navigation Page.goto should work with subframes return 204", "platforms": ["darwin", "linux", "win32"], @@ -3209,6 +3256,13 @@ "parameters": ["cdp", "firefox"], "expectations": ["FAIL"] }, + { + "testIdPattern": "[navigation.spec] navigation Page.waitForNavigation should work with DOM history.back()/history.forward()", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["SKIP"], + "comment": "browsingContext.navigationStarted event not emitted for fragment navigation" + }, { "testIdPattern": "[navigation.spec] navigation Page.waitForNavigation should work with history.pushState()", "platforms": ["darwin", "linux", "win32"], @@ -3217,9 +3271,10 @@ }, { "testIdPattern": "[navigation.spec] navigation Page.waitForNavigation should work with history.pushState()", - "platforms": ["linux"], + "platforms": ["darwin", "linux", "win32"], "parameters": ["firefox", "webDriverBiDi"], - "expectations": ["SKIP"] + "expectations": ["SKIP"], + "comment": "browsingContext.navigationStarted event not emitted for fragment navigation" }, { "testIdPattern": "[navigation.spec] navigation Page.waitForNavigation should work with history.replaceState()", @@ -3268,7 +3323,7 @@ "testIdPattern": "[network.spec] network Network Events Page.Events.RequestServedFromCache", "platforms": ["darwin", "linux", "win32"], "parameters": ["chrome", "webDriverBiDi"], - "expectations": ["FAIL", "PASS"] + "expectations": ["FAIL"] }, { "testIdPattern": "[network.spec] network Network Events Page.Events.Response", @@ -3342,6 +3397,12 @@ "parameters": ["cdp", "firefox"], "expectations": ["FAIL"] }, + { + "testIdPattern": "[network.spec] network raw network headers Cross-origin set-cookie", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["FAIL", "PASS"] + }, { "testIdPattern": "[network.spec] network raw network headers Same-origin set-cookie navigation", "platforms": ["darwin", "linux", "win32"], @@ -3388,7 +3449,7 @@ "testIdPattern": "[network.spec] network Request.frame should work for subframe navigation request", "platforms": ["darwin", "linux", "win32"], "parameters": ["firefox", "webDriverBiDi"], - "expectations": ["FAIL", "PASS"] + "expectations": ["PASS"] }, { "testIdPattern": "[network.spec] network Request.initiator should return the initiator", @@ -3455,7 +3516,7 @@ "testIdPattern": "[network.spec] network Response.fromCache should work", "platforms": ["darwin", "linux", "win32"], "parameters": ["firefox", "webDriverBiDi"], - "expectations": ["FAIL", "PASS"] + "expectations": ["FAIL"] }, { "testIdPattern": "[network.spec] network Response.fromCache should work", @@ -3529,12 +3590,6 @@ "parameters": ["firefox", "webDriverBiDi"], "expectations": ["PASS"] }, - { - "testIdPattern": "[oopif.spec] OOPIF should detect existing OOPIFs when Puppeteer connects to an existing page", - "platforms": ["darwin", "linux", "win32"], - "parameters": ["chrome", "webDriverBiDi"], - "expectations": ["FAIL"] - }, { "testIdPattern": "[oopif.spec] OOPIF should detect existing OOPIFs when Puppeteer connects to an existing page", "platforms": ["darwin", "linux", "win32"], @@ -3597,28 +3652,18 @@ "expectations": ["FAIL"] }, { - "testIdPattern": "[oopif.spec] OOPIF should report oopif frames", - "platforms": ["darwin", "linux", "win32"], - "parameters": ["chrome", "webDriverBiDi"], - "expectations": ["FAIL"] - }, - { - "testIdPattern": "[oopif.spec] OOPIF should report oopif frames", + "testIdPattern": "[oopif.spec] OOPIF should support frames within OOP iframes", "platforms": ["darwin", "linux", "win32"], "parameters": ["firefox", "webDriverBiDi"], - "expectations": ["FAIL"] + "expectations": ["SKIP"], + "comment": "Fetch error" }, { "testIdPattern": "[oopif.spec] OOPIF should support lazy OOP frames", "platforms": ["darwin", "linux", "win32"], "parameters": ["firefox", "webDriverBiDi"], - "expectations": ["FAIL", "PASS"] - }, - { - "testIdPattern": "[oopif.spec] OOPIF should support lazy OOP frames", - "platforms": ["darwin", "linux", "win32"], - "parameters": ["chrome", "webDriverBiDi"], - "expectations": ["PASS"] + "expectations": ["FAIL"], + "comment": "https://bugzilla.mozilla.org/show_bug.cgi?id=187816" }, { "testIdPattern": "[oopif.spec] OOPIF should support lazy OOP frames", @@ -3678,7 +3723,8 @@ "testIdPattern": "[oopif.spec] OOPIF should wait for inner OOPIFs", "platforms": ["darwin", "linux", "win32"], "parameters": ["firefox", "webDriverBiDi"], - "expectations": ["FAIL"] + "expectations": ["SKIP"], + "comment": "Fetch error" }, { "testIdPattern": "[page.spec] Page Page.addScriptTag should throw when added with content to the CSP page", @@ -4080,7 +4126,7 @@ "testIdPattern": "[proxy.spec] request proxy should respect proxy bypass list", "platforms": ["darwin", "linux", "win32"], "parameters": ["firefox", "webDriverBiDi"], - "expectations": ["FAIL", "PASS"] + "expectations": ["PASS"] }, { "testIdPattern": "[proxy.spec] request proxy should respect proxy bypass list", @@ -4224,7 +4270,7 @@ "testIdPattern": "[target.spec] Target Browser.waitForTarget should wait for a target", "platforms": ["darwin", "linux", "win32"], "parameters": ["chrome", "webDriverBiDi"], - "expectations": ["FAIL", "PASS"] + "expectations": ["PASS"] }, { "testIdPattern": "[target.spec] Target Browser.waitForTarget should wait for a target", @@ -4232,12 +4278,6 @@ "parameters": ["cdp", "firefox"], "expectations": ["SKIP"] }, - { - "testIdPattern": "[target.spec] Target Browser.waitForTarget should wait for a target", - "platforms": ["darwin", "linux", "win32"], - "parameters": ["firefox", "webDriverBiDi"], - "expectations": ["FAIL"] - }, { "testIdPattern": "[target.spec] Target should be able to use async waitForTarget", "platforms": ["darwin", "linux", "win32"], @@ -4332,7 +4372,7 @@ "testIdPattern": "[target.spec] Target should not crash while redirecting if original request was missed", "platforms": ["darwin", "linux", "win32"], "parameters": ["chrome", "webDriverBiDi"], - "expectations": ["FAIL", "PASS"] + "expectations": ["PASS"] }, { "testIdPattern": "[target.spec] Target should not report uninitialized pages", diff --git a/test/src/ariaqueryhandler.spec.ts b/test/src/ariaqueryhandler.spec.ts index 434d01426ab..0ffb8ae6a51 100644 --- a/test/src/ariaqueryhandler.spec.ts +++ b/test/src/ariaqueryhandler.spec.ts @@ -370,9 +370,10 @@ describe('AriaQueryHandler', () => { await detachFrame(page, 'frame1'); await waitPromise; expect(waitError).toBeTruthy(); - expect(waitError.message).toContain( - 'waitForFunction failed: frame got detached.' - ); + expect(waitError.message).atLeastOneToContain([ + 'waitForFunction failed: frame got detached.', + 'Browsing context already closed.', + ]); }); it('should survive cross-process navigation', async () => { diff --git a/test/src/evaluation.spec.ts b/test/src/evaluation.spec.ts index 3305b59cc21..88cccb82dd2 100644 --- a/test/src/evaluation.spec.ts +++ b/test/src/evaluation.spec.ts @@ -408,9 +408,10 @@ describe('Evaluation specs', function () { return (error = error_); }); expect(error).toBeTruthy(); - expect(error.message).toContain( - 'JSHandles can be evaluated only in the context they were created' - ); + expect(error.message).atLeastOneToContain([ + 'JSHandles can be evaluated only in the context they were created', + "Trying to evaluate JSHandle from different frames. Usually this means you're using a handle from a page on a different page.", + ]); }); it('should simulate a user gesture', async () => { const {page} = await getTestState(); diff --git a/test/src/launcher.spec.ts b/test/src/launcher.spec.ts index e81ded1c663..876f8d16240 100644 --- a/test/src/launcher.spec.ts +++ b/test/src/launcher.spec.ts @@ -102,6 +102,7 @@ describe('Launcher specs', function () { expect(message).atLeastOneToContain([ 'Target closed', 'Page closed!', + 'Browser already closed', ]); expect(message).not.toContain('Timeout'); } diff --git a/test/src/oopif.spec.ts b/test/src/oopif.spec.ts index f55b37ec541..0213e14d5d5 100644 --- a/test/src/oopif.spec.ts +++ b/test/src/oopif.spec.ts @@ -5,10 +5,9 @@ */ import expect from 'expect'; -import type {BrowserContext} from 'puppeteer-core/internal/api/BrowserContext.js'; import type {CDPSession} from 'puppeteer-core/internal/api/CDPSession.js'; import {CDPSessionEvent} from 'puppeteer-core/internal/api/CDPSession.js'; -import type {CdpTarget} from 'puppeteer-core/internal/cdp/Target.js'; +import type {Page} from 'puppeteer-core/internal/api/Page.js'; import {getTestState, launch} from './mocha-utils.js'; import {attachFrame, detachFrame, navigateFrame} from './utils.js'; @@ -266,24 +265,24 @@ describe('OOPIF', function () { await frame.waitForSelector('#clicked'); }); it('should report oopif frames', async () => { - const {server, page, context} = state; + const {server, page} = state; const frame = page.waitForFrame(frame => { return frame.url().endsWith('/oopif.html'); }); await page.goto(server.PREFIX + '/dynamic-oopif.html'); await frame; - expect(oopifs(context)).toHaveLength(1); + expect(await iframes(page)).toHaveLength(1); expect(page.frames()).toHaveLength(2); }); it('should wait for inner OOPIFs', async () => { - const {server, page, context} = state; + const {server, page} = state; await page.goto(`http://mainframe:${server.PORT}/main-frame.html`); const frame2 = await page.waitForFrame(frame => { return frame.url().endsWith('inner-frame2.html'); }); - expect(oopifs(context)).toHaveLength(2); + expect(await iframes(page)).toHaveLength(2); expect( page.frames().filter(frame => { return frame.isOOPFrame(); @@ -297,7 +296,7 @@ describe('OOPIF', function () { }); it('should load oopif iframes with subresources and request interception', async () => { - const {server, page, context} = state; + const {server, page} = state; const framePromise = page.waitForFrame(frame => { return frame.url().endsWith('/oopif.html'); @@ -312,7 +311,7 @@ describe('OOPIF', function () { await page.goto(server.PREFIX + '/dynamic-oopif.html'); const frame = await framePromise; const request = await requestPromise; - expect(oopifs(context)).toHaveLength(1); + expect(await iframes(page)).toHaveLength(1); expect(request.frame()).toBe(frame); }); @@ -394,14 +393,14 @@ describe('OOPIF', function () { }); it('should detect existing OOPIFs when Puppeteer connects to an existing page', async () => { - const {server, puppeteer, page, context} = state; + const {server, puppeteer, page} = state; const frame = page.waitForFrame(frame => { return frame.url().endsWith('/oopif.html'); }); await page.goto(server.PREFIX + '/dynamic-oopif.html'); await frame; - expect(oopifs(context)).toHaveLength(1); + expect(await iframes(page)).toHaveLength(1); expect(page.frames()).toHaveLength(2); const browserURL = 'http://127.0.0.1:21222'; @@ -520,8 +519,13 @@ describe('OOPIF', function () { }); }); -function oopifs(context: BrowserContext) { - return context.targets().filter(target => { - return (target as CdpTarget)._getTargetInfo().type === 'iframe'; +async function iframes(page: Page) { + const iframes = await Promise.all( + page.frames().map(async frame => { + return await frame.frameElement(); + }) + ); + return iframes.filter(frame => { + return frame !== null; }); } diff --git a/test/src/page.spec.ts b/test/src/page.spec.ts index 7ffb77b573d..692188a6f78 100644 --- a/test/src/page.spec.ts +++ b/test/src/page.spec.ts @@ -102,7 +102,11 @@ describe('Page', function () { ]); for (let i = 0; i < 2; i++) { const message = results[i].message; - expect(message).atLeastOneToContain(['Target closed', 'Page closed!']); + expect(message).atLeastOneToContain([ + 'Target closed', + 'Page closed!', + 'Frame detached', + ]); expect(message).not.toContain('Timeout'); } }); diff --git a/test/src/waittask.spec.ts b/test/src/waittask.spec.ts index dd8c55c0bee..b9a28c9e7ae 100644 --- a/test/src/waittask.spec.ts +++ b/test/src/waittask.spec.ts @@ -446,9 +446,10 @@ describe('waittask specs', function () { await detachFrame(page, 'frame1'); await waitPromise; expect(waitError).toBeTruthy(); - expect(waitError?.message).toContain( - 'waitForFunction failed: frame got detached.' - ); + expect(waitError?.message).atLeastOneToContain([ + 'waitForFunction failed: frame got detached.', + 'Browsing context already closed.', + ]); }); it('should survive cross-process navigation', async () => { const {page, server} = await getTestState(); @@ -754,9 +755,10 @@ describe('waittask specs', function () { await detachFrame(page, 'frame1'); await waitPromise; expect(waitError).toBeTruthy(); - expect(waitError?.message).toContain( - 'waitForFunction failed: frame got detached.' - ); + expect(waitError?.message).atLeastOneToContain([ + 'waitForFunction failed: frame got detached.', + 'Browsing context already closed.', + ]); }); it('hidden should wait for display: none', async () => { const {page} = await getTestState();