Browser.ts (8150B)
1 /** 2 * @license 3 * Copyright 2022 Google Inc. 4 * SPDX-License-Identifier: Apache-2.0 5 */ 6 7 import type {ChildProcess} from 'node:child_process'; 8 9 import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; 10 11 import type {BrowserEvents} from '../api/Browser.js'; 12 import { 13 Browser, 14 BrowserEvent, 15 type BrowserCloseCallback, 16 type BrowserContextOptions, 17 type DebugInfo, 18 } from '../api/Browser.js'; 19 import {BrowserContextEvent} from '../api/BrowserContext.js'; 20 import type {Page} from '../api/Page.js'; 21 import type {Target} from '../api/Target.js'; 22 import type {Connection as CdpConnection} from '../cdp/Connection.js'; 23 import type {SupportedWebDriverCapabilities} from '../common/ConnectOptions.js'; 24 import {EventEmitter} from '../common/EventEmitter.js'; 25 import {debugError} from '../common/util.js'; 26 import type {Viewport} from '../common/Viewport.js'; 27 import {bubble} from '../util/decorators.js'; 28 29 import {BidiBrowserContext} from './BrowserContext.js'; 30 import type {BidiConnection} from './Connection.js'; 31 import type {Browser as BrowserCore} from './core/Browser.js'; 32 import {Session} from './core/Session.js'; 33 import type {UserContext} from './core/UserContext.js'; 34 import {BidiBrowserTarget} from './Target.js'; 35 36 /** 37 * @internal 38 */ 39 export interface BidiBrowserOptions { 40 process?: ChildProcess; 41 closeCallback?: BrowserCloseCallback; 42 connection: BidiConnection; 43 cdpConnection?: CdpConnection; 44 defaultViewport: Viewport | null; 45 acceptInsecureCerts?: boolean; 46 capabilities?: SupportedWebDriverCapabilities; 47 } 48 49 /** 50 * @internal 51 */ 52 export class BidiBrowser extends Browser { 53 readonly protocol = 'webDriverBiDi'; 54 55 static readonly subscribeModules: [string, ...string[]] = [ 56 'browsingContext', 57 'network', 58 'log', 59 'script', 60 'input', 61 ]; 62 static readonly subscribeCdpEvents: Bidi.Cdp.EventNames[] = [ 63 // Coverage 64 'goog:cdp.Debugger.scriptParsed', 65 'goog:cdp.CSS.styleSheetAdded', 66 'goog:cdp.Runtime.executionContextsCleared', 67 // Tracing 68 'goog:cdp.Tracing.tracingComplete', 69 // TODO: subscribe to all CDP events in the future. 70 'goog:cdp.Network.requestWillBeSent', 71 'goog:cdp.Debugger.scriptParsed', 72 'goog:cdp.Page.screencastFrame', 73 ]; 74 75 static async create(opts: BidiBrowserOptions): Promise<BidiBrowser> { 76 const session = await Session.from(opts.connection, { 77 firstMatch: opts.capabilities?.firstMatch, 78 alwaysMatch: { 79 ...opts.capabilities?.alwaysMatch, 80 // Capabilities that come from Puppeteer's API take precedence. 81 acceptInsecureCerts: opts.acceptInsecureCerts, 82 unhandledPromptBehavior: { 83 default: Bidi.Session.UserPromptHandlerType.Ignore, 84 }, 85 webSocketUrl: true, 86 // Puppeteer with WebDriver BiDi does not support prerendering 87 // yet because WebDriver BiDi behavior is not specified. See 88 // https://github.com/w3c/webdriver-bidi/issues/321. 89 'goog:prerenderingDisabled': true, 90 }, 91 }); 92 93 await session.subscribe( 94 session.capabilities.browserName.toLocaleLowerCase().includes('firefox') 95 ? BidiBrowser.subscribeModules 96 : [...BidiBrowser.subscribeModules, ...BidiBrowser.subscribeCdpEvents], 97 ); 98 99 const browser = new BidiBrowser(session.browser, opts); 100 browser.#initialize(); 101 return browser; 102 } 103 104 @bubble() 105 accessor #trustedEmitter = new EventEmitter<BrowserEvents>(); 106 107 #process?: ChildProcess; 108 #closeCallback?: BrowserCloseCallback; 109 #browserCore: BrowserCore; 110 #defaultViewport: Viewport | null; 111 #browserContexts = new WeakMap<UserContext, BidiBrowserContext>(); 112 #target = new BidiBrowserTarget(this); 113 #cdpConnection?: CdpConnection; 114 115 private constructor(browserCore: BrowserCore, opts: BidiBrowserOptions) { 116 super(); 117 this.#process = opts.process; 118 this.#closeCallback = opts.closeCallback; 119 this.#browserCore = browserCore; 120 this.#defaultViewport = opts.defaultViewport; 121 this.#cdpConnection = opts.cdpConnection; 122 } 123 124 #initialize() { 125 // Initializing existing contexts. 126 for (const userContext of this.#browserCore.userContexts) { 127 this.#createBrowserContext(userContext); 128 } 129 130 this.#browserCore.once('disconnected', () => { 131 this.#trustedEmitter.emit(BrowserEvent.Disconnected, undefined); 132 this.#trustedEmitter.removeAllListeners(); 133 }); 134 this.#process?.once('close', () => { 135 this.#browserCore.dispose('Browser process exited.', true); 136 this.connection.dispose(); 137 }); 138 } 139 140 get #browserName() { 141 return this.#browserCore.session.capabilities.browserName; 142 } 143 get #browserVersion() { 144 return this.#browserCore.session.capabilities.browserVersion; 145 } 146 147 get cdpSupported(): boolean { 148 return this.#cdpConnection !== undefined; 149 } 150 151 get cdpConnection(): CdpConnection | undefined { 152 return this.#cdpConnection; 153 } 154 155 override async userAgent(): Promise<string> { 156 return this.#browserCore.session.capabilities.userAgent; 157 } 158 159 #createBrowserContext(userContext: UserContext) { 160 const browserContext = BidiBrowserContext.from(this, userContext, { 161 defaultViewport: this.#defaultViewport, 162 }); 163 this.#browserContexts.set(userContext, browserContext); 164 165 browserContext.trustedEmitter.on( 166 BrowserContextEvent.TargetCreated, 167 target => { 168 this.#trustedEmitter.emit(BrowserEvent.TargetCreated, target); 169 }, 170 ); 171 browserContext.trustedEmitter.on( 172 BrowserContextEvent.TargetChanged, 173 target => { 174 this.#trustedEmitter.emit(BrowserEvent.TargetChanged, target); 175 }, 176 ); 177 browserContext.trustedEmitter.on( 178 BrowserContextEvent.TargetDestroyed, 179 target => { 180 this.#trustedEmitter.emit(BrowserEvent.TargetDestroyed, target); 181 }, 182 ); 183 184 return browserContext; 185 } 186 187 get connection(): BidiConnection { 188 // SAFETY: We only have one implementation. 189 return this.#browserCore.session.connection as BidiConnection; 190 } 191 192 override wsEndpoint(): string { 193 return this.connection.url; 194 } 195 196 override async close(): Promise<void> { 197 if (this.connection.closed) { 198 return; 199 } 200 201 try { 202 await this.#browserCore.close(); 203 await this.#closeCallback?.call(null); 204 } catch (error) { 205 // Fail silently. 206 debugError(error); 207 } finally { 208 this.connection.dispose(); 209 } 210 } 211 212 override get connected(): boolean { 213 return !this.#browserCore.disconnected; 214 } 215 216 override process(): ChildProcess | null { 217 return this.#process ?? null; 218 } 219 220 override async createBrowserContext( 221 _options?: BrowserContextOptions, 222 ): Promise<BidiBrowserContext> { 223 const userContext = await this.#browserCore.createUserContext(); 224 return this.#createBrowserContext(userContext); 225 } 226 227 override async version(): Promise<string> { 228 return `${this.#browserName}/${this.#browserVersion}`; 229 } 230 231 override browserContexts(): BidiBrowserContext[] { 232 return [...this.#browserCore.userContexts].map(context => { 233 return this.#browserContexts.get(context)!; 234 }); 235 } 236 237 override defaultBrowserContext(): BidiBrowserContext { 238 return this.#browserContexts.get(this.#browserCore.defaultUserContext)!; 239 } 240 241 override newPage(): Promise<Page> { 242 return this.defaultBrowserContext().newPage(); 243 } 244 245 override installExtension(path: string): Promise<string> { 246 return this.#browserCore.installExtension(path); 247 } 248 249 override async uninstallExtension(id: string): Promise<void> { 250 await this.#browserCore.uninstallExtension(id); 251 } 252 253 override targets(): Target[] { 254 return [ 255 this.#target, 256 ...this.browserContexts().flatMap(context => { 257 return context.targets(); 258 }), 259 ]; 260 } 261 262 override target(): BidiBrowserTarget { 263 return this.#target; 264 } 265 266 override async disconnect(): Promise<void> { 267 try { 268 await this.#browserCore.session.end(); 269 } catch (error) { 270 // Fail silently. 271 debugError(error); 272 } finally { 273 this.connection.dispose(); 274 } 275 } 276 277 override get debugInfo(): DebugInfo { 278 return { 279 pendingProtocolErrors: this.connection.getPendingProtocolErrors(), 280 }; 281 } 282 }