import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; import ProtocolMapping from 'devtools-protocol/types/protocol-mapping.js'; import {WaitForOptions} from '../../api/Page.js'; import {assert} from '../../util/assert.js'; import {Deferred} from '../../util/Deferred.js'; import type {CDPSession, Connection as CDPConnection} from '../Connection.js'; import {ProtocolError, TimeoutError} from '../Errors.js'; import {EventEmitter} from '../EventEmitter.js'; import {PuppeteerLifeCycleEvent} from '../LifecycleWatcher.js'; import {TimeoutSettings} from '../TimeoutSettings.js'; import {getPageContent, setPageContent, waitWithTimeout} from '../util.js'; import {Connection} from './Connection.js'; import {Realm} from './Realm.js'; /** * @internal */ const lifeCycleToSubscribedEvent = new Map([ ['load', 'browsingContext.load'], ['domcontentloaded', 'browsingContext.domContentLoaded'], ]); /** * @internal */ const lifeCycleToReadinessState = new Map< PuppeteerLifeCycleEvent, Bidi.BrowsingContext.ReadinessState >([ ['load', 'complete'], ['domcontentloaded', 'interactive'], ]); /** * @internal */ export class CDPSessionWrapper extends EventEmitter implements CDPSession { #context: BrowsingContext; #sessionId = Deferred.create(); constructor(context: BrowsingContext) { super(); this.#context = context; context.connection .send('cdp.getSession', { context: context.id, }) .then(session => { this.#sessionId.resolve(session.result.cdpSession); }) .catch(err => { this.#sessionId.reject(err); }); } connection(): CDPConnection | undefined { return undefined; } async send( method: T, ...paramArgs: ProtocolMapping.Commands[T]['paramsType'] ): Promise { const cdpSession = await this.#sessionId.valueOrThrow(); const result = await this.#context.connection.send('cdp.sendCommand', { cdpMethod: method, cdpParams: paramArgs[0] || {}, cdpSession, }); return result.result; } detach(): Promise { throw new Error('Method not implemented.'); } id(): string { const val = this.#sessionId.value(); return val instanceof Error || val === undefined ? '' : val; } } /** * @internal */ export class BrowsingContext extends Realm { #timeoutSettings: TimeoutSettings; #id: string; #url = 'about:blank'; #cdpSession: CDPSession; constructor( connection: Connection, timeoutSettings: TimeoutSettings, info: Bidi.BrowsingContext.Info ) { super(connection, info.context); this.connection = connection; this.#timeoutSettings = timeoutSettings; this.#id = info.context; this.#cdpSession = new CDPSessionWrapper(this); } createSandboxRealm(sandbox: string): Realm { return new Realm(this.connection, this.#id, sandbox); } get url(): string { return this.#url; } get id(): string { return this.#id; } get cdpSession(): CDPSession { return this.#cdpSession; } async goto( url: string, options: { referer?: string; referrerPolicy?: string; timeout?: number; waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[]; } = {} ): Promise { const { waitUntil = 'load', timeout = this.#timeoutSettings.navigationTimeout(), } = options; const readinessState = lifeCycleToReadinessState.get( getWaitUntilSingle(waitUntil) ) as Bidi.BrowsingContext.ReadinessState; try { const {result} = await waitWithTimeout( this.connection.send('browsingContext.navigate', { url: url, context: this.#id, wait: readinessState, }), 'Navigation', timeout ); this.#url = result.url; return result.navigation; } catch (error) { if (error instanceof ProtocolError) { error.message += ` at ${url}`; } else if (error instanceof TimeoutError) { error.message = 'Navigation timeout of ' + timeout + ' ms exceeded'; } throw error; } } async reload(options: WaitForOptions = {}): Promise { const { waitUntil = 'load', timeout = this.#timeoutSettings.navigationTimeout(), } = options; const readinessState = lifeCycleToReadinessState.get( getWaitUntilSingle(waitUntil) ) as Bidi.BrowsingContext.ReadinessState; await waitWithTimeout( this.connection.send('browsingContext.reload', { context: this.#id, wait: readinessState, }), 'Navigation', timeout ); } async setContent( html: string, options: { timeout?: number; waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[]; } ): Promise { const { waitUntil = 'load', timeout = this.#timeoutSettings.navigationTimeout(), } = options; const waitUntilEvent = lifeCycleToSubscribedEvent.get( getWaitUntilSingle(waitUntil) ) as string; await Promise.all([ setPageContent(this, html), waitWithTimeout( new Promise(resolve => { this.once(waitUntilEvent, () => { resolve(); }); }), waitUntilEvent, timeout ), ]); } async content(): Promise { return await this.evaluate(getPageContent); } async sendCDPCommand( method: T, ...paramArgs: ProtocolMapping.Commands[T]['paramsType'] ): Promise { return this.#cdpSession.send(method, ...paramArgs); } title(): Promise { return this.evaluate(() => { return document.title; }); } dispose(): void { this.removeAllListeners(); this.connection.unregisterBrowsingContexts(this.#id); } } /** * @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; }