import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; import type ProtocolMapping from 'devtools-protocol/types/protocol-mapping.js'; import {CDPSession} from '../api/CDPSession.js'; import type {Connection as CdpConnection} from '../cdp/Connection.js'; import type {PuppeteerLifeCycleEvent} from '../cdp/LifecycleWatcher.js'; import {TargetCloseError} from '../common/Errors.js'; import type {EventType} from '../common/EventEmitter.js'; import {debugError} from '../common/util.js'; import {assert} from '../util/assert.js'; import {Deferred} from '../util/Deferred.js'; import type {BidiConnection} from './Connection.js'; import {BidiRealm} from './Realm.js'; /** * @internal */ export const lifeCycleToSubscribedEvent = new Map< PuppeteerLifeCycleEvent, string >([ ['load', 'browsingContext.load'], ['domcontentloaded', 'browsingContext.domContentLoaded'], ]); /** * @internal */ export const cdpSessions = new Map(); /** * @internal */ export class CdpSessionWrapper extends CDPSession { #context: BrowsingContext; #sessionId = Deferred.create(); #detached = false; constructor(context: BrowsingContext, sessionId?: string) { super(); this.#context = context; if (!this.#context.supportsCdp()) { return; } if (sessionId) { this.#sessionId.resolve(sessionId); cdpSessions.set(sessionId, this); } else { context.connection .send('cdp.getSession', { context: context.id, }) .then(session => { this.#sessionId.resolve(session.result.session!); cdpSessions.set(session.result.session!, this); }) .catch(err => { this.#sessionId.reject(err); }); } } override connection(): CdpConnection | undefined { return undefined; } override async send( method: T, ...paramArgs: ProtocolMapping.Commands[T]['paramsType'] ): Promise { if (!this.#context.supportsCdp()) { throw new Error( 'CDP support is required for this feature. The current browser does not support CDP.' ); } if (this.#detached) { throw new TargetCloseError( `Protocol error (${method}): Session closed. Most likely the page has been closed.` ); } const session = await this.#sessionId.valueOrThrow(); const {result} = await this.#context.connection.send('cdp.sendCommand', { method: method, params: paramArgs[0], session, }); return result.result; } override async detach(): Promise { cdpSessions.delete(this.id()); if (!this.#detached && this.#context.supportsCdp()) { await this.#context.cdpSession.send('Target.detachFromTarget', { sessionId: this.id(), }); } this.#detached = true; } override id(): string { const val = this.#sessionId.value(); return val instanceof Error || val === undefined ? '' : val; } } /** * 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 CdpSessionWrapper(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); } } /** * @internal */ export function getWaitUntilSingle( event: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[] ): Extract { if (Array.isArray(event) && event.length > 1) { throw new Error('BiDi support only single `waitUntil` argument'); } const waitUntilSingle = Array.isArray(event) ? (event.find(lifecycle => { return lifecycle === 'domcontentloaded' || lifecycle === 'load'; }) as PuppeteerLifeCycleEvent) : event; if ( waitUntilSingle === 'networkidle0' || waitUntilSingle === 'networkidle2' ) { throw new Error(`BiDi does not support 'waitUntil' ${waitUntilSingle}`); } assert(waitUntilSingle, `Invalid waitUntil option ${waitUntilSingle}`); return waitUntilSingle; }