Browser.ts (14025B)
1 /** 2 * @license 3 * Copyright 2017 Google Inc. 4 * SPDX-License-Identifier: Apache-2.0 5 */ 6 /// <reference types="node" preserve="true"/> 7 import type {ChildProcess} from 'node:child_process'; 8 9 import type {Protocol} from 'devtools-protocol'; 10 11 import { 12 firstValueFrom, 13 from, 14 merge, 15 raceWith, 16 } from '../../third_party/rxjs/rxjs.js'; 17 import type {ProtocolType} from '../common/ConnectOptions.js'; 18 import type {Cookie, CookieData} from '../common/Cookie.js'; 19 import type {DownloadBehavior} from '../common/DownloadBehavior.js'; 20 import {EventEmitter, type EventType} from '../common/EventEmitter.js'; 21 import { 22 debugError, 23 fromEmitterEvent, 24 filterAsync, 25 timeout, 26 fromAbortSignal, 27 } from '../common/util.js'; 28 import {asyncDisposeSymbol, disposeSymbol} from '../util/disposable.js'; 29 30 import type {BrowserContext} from './BrowserContext.js'; 31 import type {Page} from './Page.js'; 32 import type {Target} from './Target.js'; 33 /** 34 * @public 35 */ 36 export interface BrowserContextOptions { 37 /** 38 * Proxy server with optional port to use for all requests. 39 * Username and password can be set in `Page.authenticate`. 40 */ 41 proxyServer?: string; 42 /** 43 * Bypass the proxy for the given list of hosts. 44 */ 45 proxyBypassList?: string[]; 46 /** 47 * Behavior definition for when downloading a file. 48 * 49 * @remarks 50 * If not set, the default behavior will be used. 51 */ 52 downloadBehavior?: DownloadBehavior; 53 } 54 55 /** 56 * @internal 57 */ 58 export type BrowserCloseCallback = () => Promise<void> | void; 59 60 /** 61 * @public 62 */ 63 export type TargetFilterCallback = (target: Target) => boolean; 64 65 /** 66 * @internal 67 */ 68 export type IsPageTargetCallback = (target: Target) => boolean; 69 70 /** 71 * @internal 72 */ 73 export const WEB_PERMISSION_TO_PROTOCOL_PERMISSION = new Map< 74 Permission, 75 Protocol.Browser.PermissionType 76 >([ 77 ['accelerometer', 'sensors'], 78 ['ambient-light-sensor', 'sensors'], 79 ['background-sync', 'backgroundSync'], 80 ['camera', 'videoCapture'], 81 ['clipboard-read', 'clipboardReadWrite'], 82 ['clipboard-sanitized-write', 'clipboardSanitizedWrite'], 83 ['clipboard-write', 'clipboardReadWrite'], 84 ['geolocation', 'geolocation'], 85 ['gyroscope', 'sensors'], 86 ['idle-detection', 'idleDetection'], 87 ['keyboard-lock', 'keyboardLock'], 88 ['magnetometer', 'sensors'], 89 ['microphone', 'audioCapture'], 90 ['midi', 'midi'], 91 ['notifications', 'notifications'], 92 ['payment-handler', 'paymentHandler'], 93 ['persistent-storage', 'durableStorage'], 94 ['pointer-lock', 'pointerLock'], 95 // chrome-specific permissions we have. 96 ['midi-sysex', 'midiSysex'], 97 ]); 98 99 /** 100 * @public 101 */ 102 export type Permission = 103 | 'accelerometer' 104 | 'ambient-light-sensor' 105 | 'background-sync' 106 | 'camera' 107 | 'clipboard-read' 108 | 'clipboard-sanitized-write' 109 | 'clipboard-write' 110 | 'geolocation' 111 | 'gyroscope' 112 | 'idle-detection' 113 | 'keyboard-lock' 114 | 'magnetometer' 115 | 'microphone' 116 | 'midi-sysex' 117 | 'midi' 118 | 'notifications' 119 | 'payment-handler' 120 | 'persistent-storage' 121 | 'pointer-lock'; 122 123 /** 124 * @public 125 */ 126 export interface WaitForTargetOptions { 127 /** 128 * Maximum wait time in milliseconds. Pass `0` to disable the timeout. 129 * 130 * @defaultValue `30_000` 131 */ 132 timeout?: number; 133 134 /** 135 * A signal object that allows you to cancel a waitFor call. 136 */ 137 signal?: AbortSignal; 138 } 139 140 /** 141 * All the events a {@link Browser | browser instance} may emit. 142 * 143 * @public 144 */ 145 export const enum BrowserEvent { 146 /** 147 * Emitted when Puppeteer gets disconnected from the browser instance. This 148 * might happen because either: 149 * 150 * - The browser closes/crashes or 151 * - {@link Browser.disconnect} was called. 152 */ 153 Disconnected = 'disconnected', 154 /** 155 * Emitted when the URL of a target changes. Contains a {@link Target} 156 * instance. 157 * 158 * @remarks Note that this includes target changes in all browser 159 * contexts. 160 */ 161 TargetChanged = 'targetchanged', 162 /** 163 * Emitted when a target is created, for example when a new page is opened by 164 * {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/open | window.open} 165 * or by {@link Browser.newPage | browser.newPage} 166 * 167 * Contains a {@link Target} instance. 168 * 169 * @remarks Note that this includes target creations in all browser 170 * contexts. 171 */ 172 TargetCreated = 'targetcreated', 173 /** 174 * Emitted when a target is destroyed, for example when a page is closed. 175 * Contains a {@link Target} instance. 176 * 177 * @remarks Note that this includes target destructions in all browser 178 * contexts. 179 */ 180 TargetDestroyed = 'targetdestroyed', 181 /** 182 * @internal 183 */ 184 TargetDiscovered = 'targetdiscovered', 185 } 186 187 /** 188 * @public 189 */ 190 export interface BrowserEvents extends Record<EventType, unknown> { 191 [BrowserEvent.Disconnected]: undefined; 192 [BrowserEvent.TargetCreated]: Target; 193 [BrowserEvent.TargetDestroyed]: Target; 194 [BrowserEvent.TargetChanged]: Target; 195 /** 196 * @internal 197 */ 198 [BrowserEvent.TargetDiscovered]: Protocol.Target.TargetInfo; 199 } 200 201 /** 202 * @public 203 * @experimental 204 */ 205 export interface DebugInfo { 206 pendingProtocolErrors: Error[]; 207 } 208 209 /** 210 * {@link Browser} represents a browser instance that is either: 211 * 212 * - connected to via {@link Puppeteer.connect} or 213 * - launched by {@link PuppeteerNode.launch}. 214 * 215 * {@link Browser} {@link EventEmitter.emit | emits} various events which are 216 * documented in the {@link BrowserEvent} enum. 217 * 218 * @example Using a {@link Browser} to create a {@link Page}: 219 * 220 * ```ts 221 * import puppeteer from 'puppeteer'; 222 * 223 * const browser = await puppeteer.launch(); 224 * const page = await browser.newPage(); 225 * await page.goto('https://example.com'); 226 * await browser.close(); 227 * ``` 228 * 229 * @example Disconnecting from and reconnecting to a {@link Browser}: 230 * 231 * ```ts 232 * import puppeteer from 'puppeteer'; 233 * 234 * const browser = await puppeteer.launch(); 235 * // Store the endpoint to be able to reconnect to the browser. 236 * const browserWSEndpoint = browser.wsEndpoint(); 237 * // Disconnect puppeteer from the browser. 238 * await browser.disconnect(); 239 * 240 * // Use the endpoint to reestablish a connection 241 * const browser2 = await puppeteer.connect({browserWSEndpoint}); 242 * // Close the browser. 243 * await browser2.close(); 244 * ``` 245 * 246 * @public 247 */ 248 export abstract class Browser extends EventEmitter<BrowserEvents> { 249 /** 250 * @internal 251 */ 252 constructor() { 253 super(); 254 } 255 256 /** 257 * Gets the associated 258 * {@link https://nodejs.org/api/child_process.html#class-childprocess | ChildProcess}. 259 * 260 * @returns `null` if this instance was connected to via 261 * {@link Puppeteer.connect}. 262 */ 263 abstract process(): ChildProcess | null; 264 265 /** 266 * Creates a new {@link BrowserContext | browser context}. 267 * 268 * This won't share cookies/cache with other {@link BrowserContext | browser contexts}. 269 * 270 * @example 271 * 272 * ```ts 273 * import puppeteer from 'puppeteer'; 274 * 275 * const browser = await puppeteer.launch(); 276 * // Create a new browser context. 277 * const context = await browser.createBrowserContext(); 278 * // Create a new page in a pristine context. 279 * const page = await context.newPage(); 280 * // Do stuff 281 * await page.goto('https://example.com'); 282 * ``` 283 */ 284 abstract createBrowserContext( 285 options?: BrowserContextOptions, 286 ): Promise<BrowserContext>; 287 288 /** 289 * Gets a list of open {@link BrowserContext | browser contexts}. 290 * 291 * In a newly-created {@link Browser | browser}, this will return a single 292 * instance of {@link BrowserContext}. 293 */ 294 abstract browserContexts(): BrowserContext[]; 295 296 /** 297 * Gets the default {@link BrowserContext | browser context}. 298 * 299 * @remarks The default {@link BrowserContext | browser context} cannot be 300 * closed. 301 */ 302 abstract defaultBrowserContext(): BrowserContext; 303 304 /** 305 * Gets the WebSocket URL to connect to this {@link Browser | browser}. 306 * 307 * This is usually used with {@link Puppeteer.connect}. 308 * 309 * You can find the debugger URL (`webSocketDebuggerUrl`) from 310 * `http://HOST:PORT/json/version`. 311 * 312 * See {@link https://chromedevtools.github.io/devtools-protocol/#how-do-i-access-the-browser-target | browser endpoint} 313 * for more information. 314 * 315 * @remarks The format is always `ws://HOST:PORT/devtools/browser/<id>`. 316 */ 317 abstract wsEndpoint(): string; 318 319 /** 320 * Creates a new {@link Page | page} in the 321 * {@link Browser.defaultBrowserContext | default browser context}. 322 */ 323 abstract newPage(): Promise<Page>; 324 325 /** 326 * Gets all active {@link Target | targets}. 327 * 328 * In case of multiple {@link BrowserContext | browser contexts}, this returns 329 * all {@link Target | targets} in all 330 * {@link BrowserContext | browser contexts}. 331 */ 332 abstract targets(): Target[]; 333 334 /** 335 * Gets the {@link Target | target} associated with the 336 * {@link Browser.defaultBrowserContext | default browser context}). 337 */ 338 abstract target(): Target; 339 340 /** 341 * Waits until a {@link Target | target} matching the given `predicate` 342 * appears and returns it. 343 * 344 * This will look all open {@link BrowserContext | browser contexts}. 345 * 346 * @example Finding a target for a page opened via `window.open`: 347 * 348 * ```ts 349 * await page.evaluate(() => window.open('https://www.example.com/')); 350 * const newWindowTarget = await browser.waitForTarget( 351 * target => target.url() === 'https://www.example.com/', 352 * ); 353 * ``` 354 */ 355 async waitForTarget( 356 predicate: (x: Target) => boolean | Promise<boolean>, 357 options: WaitForTargetOptions = {}, 358 ): Promise<Target> { 359 const {timeout: ms = 30000, signal} = options; 360 return await firstValueFrom( 361 merge( 362 fromEmitterEvent(this, BrowserEvent.TargetCreated), 363 fromEmitterEvent(this, BrowserEvent.TargetChanged), 364 from(this.targets()), 365 ).pipe( 366 filterAsync(predicate), 367 raceWith(fromAbortSignal(signal), timeout(ms)), 368 ), 369 ); 370 } 371 372 /** 373 * Gets a list of all open {@link Page | pages} inside this {@link Browser}. 374 * 375 * If there are multiple {@link BrowserContext | browser contexts}, this 376 * returns all {@link Page | pages} in all 377 * {@link BrowserContext | browser contexts}. 378 * 379 * @remarks Non-visible {@link Page | pages}, such as `"background_page"`, 380 * will not be listed here. You can find them using {@link Target.page}. 381 */ 382 async pages(): Promise<Page[]> { 383 const contextPages = await Promise.all( 384 this.browserContexts().map(context => { 385 return context.pages(); 386 }), 387 ); 388 // Flatten array. 389 return contextPages.reduce((acc, x) => { 390 return acc.concat(x); 391 }, []); 392 } 393 394 /** 395 * Gets a string representing this {@link Browser | browser's} name and 396 * version. 397 * 398 * For headless browser, this is similar to `"HeadlessChrome/61.0.3153.0"`. For 399 * non-headless or new-headless, this is similar to `"Chrome/61.0.3153.0"`. For 400 * Firefox, it is similar to `"Firefox/116.0a1"`. 401 * 402 * The format of {@link Browser.version} might change with future releases of 403 * browsers. 404 */ 405 abstract version(): Promise<string>; 406 407 /** 408 * Gets this {@link Browser | browser's} original user agent. 409 * 410 * {@link Page | Pages} can override the user agent with 411 * {@link Page.setUserAgent}. 412 * 413 */ 414 abstract userAgent(): Promise<string>; 415 416 /** 417 * Closes this {@link Browser | browser} and all associated 418 * {@link Page | pages}. 419 */ 420 abstract close(): Promise<void>; 421 422 /** 423 * Disconnects Puppeteer from this {@link Browser | browser}, but leaves the 424 * process running. 425 */ 426 abstract disconnect(): Promise<void>; 427 428 /** 429 * Returns all cookies in the default {@link BrowserContext}. 430 * 431 * @remarks 432 * 433 * Shortcut for 434 * {@link BrowserContext.cookies | browser.defaultBrowserContext().cookies()}. 435 */ 436 async cookies(): Promise<Cookie[]> { 437 return await this.defaultBrowserContext().cookies(); 438 } 439 440 /** 441 * Sets cookies in the default {@link BrowserContext}. 442 * 443 * @remarks 444 * 445 * Shortcut for 446 * {@link BrowserContext.setCookie | browser.defaultBrowserContext().setCookie()}. 447 */ 448 async setCookie(...cookies: CookieData[]): Promise<void> { 449 return await this.defaultBrowserContext().setCookie(...cookies); 450 } 451 452 /** 453 * Removes cookies from the default {@link BrowserContext}. 454 * 455 * @remarks 456 * 457 * Shortcut for 458 * {@link BrowserContext.deleteCookie | browser.defaultBrowserContext().deleteCookie()}. 459 */ 460 async deleteCookie(...cookies: Cookie[]): Promise<void> { 461 return await this.defaultBrowserContext().deleteCookie(...cookies); 462 } 463 464 /** 465 * Installs an extension and returns the ID. In Chrome, this is only 466 * available if the browser was created using `pipe: true` and the 467 * `--enable-unsafe-extension-debugging` flag is set. 468 */ 469 abstract installExtension(path: string): Promise<string>; 470 471 /** 472 * Uninstalls an extension. In Chrome, this is only available if the browser 473 * was created using `pipe: true` and the 474 * `--enable-unsafe-extension-debugging` flag is set. 475 */ 476 abstract uninstallExtension(id: string): Promise<void>; 477 478 /** 479 * Whether Puppeteer is connected to this {@link Browser | browser}. 480 * 481 * @deprecated Use {@link Browser | Browser.connected}. 482 */ 483 isConnected(): boolean { 484 return this.connected; 485 } 486 487 /** 488 * Whether Puppeteer is connected to this {@link Browser | browser}. 489 */ 490 abstract get connected(): boolean; 491 492 /** @internal */ 493 override [disposeSymbol](): void { 494 if (this.process()) { 495 return void this.close().catch(debugError); 496 } 497 return void this.disconnect().catch(debugError); 498 } 499 500 /** @internal */ 501 [asyncDisposeSymbol](): Promise<void> { 502 if (this.process()) { 503 return this.close(); 504 } 505 return this.disconnect(); 506 } 507 508 /** 509 * @internal 510 */ 511 abstract get protocol(): ProtocolType; 512 513 /** 514 * Get debug information from Puppeteer. 515 * 516 * @remarks 517 * 518 * Currently, includes pending protocol calls. In the future, we might add more info. 519 * 520 * @public 521 * @experimental 522 */ 523 abstract get debugInfo(): DebugInfo; 524 }