/** * @license * Copyright 2022 Google Inc. * SPDX-License-Identifier: Apache-2.0 */ import type {ChildProcess} from 'child_process'; import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; import type {BrowserEvents} from '../api/Browser.js'; import { Browser, BrowserEvent, type BrowserCloseCallback, type BrowserContextOptions, type DebugInfo, } from '../api/Browser.js'; import {BrowserContextEvent} from '../api/BrowserContext.js'; import type {Page} from '../api/Page.js'; import type {Target} from '../api/Target.js'; import type {Connection as CdpConnection} from '../cdp/Connection.js'; import type {SupportedWebDriverCapabilities} from '../common/ConnectOptions.js'; import {EventEmitter} from '../common/EventEmitter.js'; import {debugError} from '../common/util.js'; import type {Viewport} from '../common/Viewport.js'; import {bubble} from '../util/decorators.js'; import {BidiBrowserContext} from './BrowserContext.js'; import type {BidiConnection} from './Connection.js'; import type {Browser as BrowserCore} from './core/Browser.js'; import {Session} from './core/Session.js'; import type {UserContext} from './core/UserContext.js'; import {BidiBrowserTarget} from './Target.js'; /** * @internal */ export interface BidiBrowserOptions { process?: ChildProcess; closeCallback?: BrowserCloseCallback; connection: BidiConnection; cdpConnection?: CdpConnection; defaultViewport: Viewport | null; acceptInsecureCerts?: boolean; capabilities?: SupportedWebDriverCapabilities; } /** * @internal */ export class BidiBrowser extends Browser { readonly protocol = 'webDriverBiDi'; static readonly subscribeModules: [string, ...string[]] = [ 'browsingContext', 'network', 'log', 'script', ]; static readonly subscribeCdpEvents: Bidi.Cdp.EventNames[] = [ // Coverage 'cdp.Debugger.scriptParsed', 'cdp.CSS.styleSheetAdded', 'cdp.Runtime.executionContextsCleared', // Tracing 'cdp.Tracing.tracingComplete', // TODO: subscribe to all CDP events in the future. 'cdp.Network.requestWillBeSent', 'cdp.Debugger.scriptParsed', 'cdp.Page.screencastFrame', ]; static async create(opts: BidiBrowserOptions): Promise<BidiBrowser> { const session = await Session.from(opts.connection, { firstMatch: opts.capabilities?.firstMatch, alwaysMatch: { ...opts.capabilities?.alwaysMatch, // Capabilities that come from Puppeteer's API take precedence. acceptInsecureCerts: opts.acceptInsecureCerts, unhandledPromptBehavior: { default: Bidi.Session.UserPromptHandlerType.Ignore, }, webSocketUrl: true, }, }); await session.subscribe( session.capabilities.browserName.toLocaleLowerCase().includes('firefox') ? BidiBrowser.subscribeModules : [...BidiBrowser.subscribeModules, ...BidiBrowser.subscribeCdpEvents] ); const browser = new BidiBrowser(session.browser, opts); browser.#initialize(); return browser; } @bubble() accessor #trustedEmitter = new EventEmitter<BrowserEvents>(); #process?: ChildProcess; #closeCallback?: BrowserCloseCallback; #browserCore: BrowserCore; #defaultViewport: Viewport | null; #browserContexts = new WeakMap<UserContext, BidiBrowserContext>(); #target = new BidiBrowserTarget(this); #cdpConnection?: CdpConnection; private constructor(browserCore: BrowserCore, opts: BidiBrowserOptions) { super(); this.#process = opts.process; this.#closeCallback = opts.closeCallback; this.#browserCore = browserCore; this.#defaultViewport = opts.defaultViewport; this.#cdpConnection = opts.cdpConnection; } #initialize() { // Initializing existing contexts. for (const userContext of this.#browserCore.userContexts) { this.#createBrowserContext(userContext); } this.#browserCore.once('disconnected', () => { this.#trustedEmitter.emit(BrowserEvent.Disconnected, undefined); this.#trustedEmitter.removeAllListeners(); }); this.#process?.once('close', () => { this.#browserCore.dispose('Browser process exited.', true); this.connection.dispose(); }); } get #browserName() { return this.#browserCore.session.capabilities.browserName; } get #browserVersion() { return this.#browserCore.session.capabilities.browserVersion; } get cdpSupported(): boolean { return this.#cdpConnection !== undefined; } get cdpConnection(): CdpConnection | undefined { return this.#cdpConnection; } override async userAgent(): Promise<string> { return this.#browserCore.session.capabilities.userAgent; } #createBrowserContext(userContext: UserContext) { const browserContext = BidiBrowserContext.from(this, userContext, { defaultViewport: this.#defaultViewport, }); this.#browserContexts.set(userContext, browserContext); browserContext.trustedEmitter.on( BrowserContextEvent.TargetCreated, target => { this.#trustedEmitter.emit(BrowserEvent.TargetCreated, target); } ); browserContext.trustedEmitter.on( BrowserContextEvent.TargetChanged, target => { this.#trustedEmitter.emit(BrowserEvent.TargetChanged, target); } ); browserContext.trustedEmitter.on( BrowserContextEvent.TargetDestroyed, target => { this.#trustedEmitter.emit(BrowserEvent.TargetDestroyed, target); } ); return browserContext; } get connection(): BidiConnection { // SAFETY: We only have one implementation. return this.#browserCore.session.connection as BidiConnection; } override wsEndpoint(): string { return this.connection.url; } override async close(): Promise<void> { if (this.connection.closed) { return; } try { await this.#browserCore.close(); await this.#closeCallback?.call(null); } catch (error) { // Fail silently. debugError(error); } finally { this.connection.dispose(); } } override get connected(): boolean { return !this.#browserCore.disconnected; } override process(): ChildProcess | null { return this.#process ?? null; } override async createBrowserContext( _options?: BrowserContextOptions ): Promise<BidiBrowserContext> { const userContext = await this.#browserCore.createUserContext(); return this.#createBrowserContext(userContext); } override async version(): Promise<string> { return `${this.#browserName}/${this.#browserVersion}`; } override browserContexts(): BidiBrowserContext[] { return [...this.#browserCore.userContexts].map(context => { return this.#browserContexts.get(context)!; }); } override defaultBrowserContext(): BidiBrowserContext { return this.#browserContexts.get(this.#browserCore.defaultUserContext)!; } override newPage(): Promise<Page> { return this.defaultBrowserContext().newPage(); } override targets(): Target[] { return [ this.#target, ...this.browserContexts().flatMap(context => { return context.targets(); }), ]; } override target(): BidiBrowserTarget { return this.#target; } override async disconnect(): Promise<void> { try { await this.#browserCore.session.end(); } catch (error) { // Fail silently. debugError(error); } finally { this.connection.dispose(); } } override get debugInfo(): DebugInfo { return { pendingProtocolErrors: this.connection.getPendingProtocolErrors(), }; } }