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 '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} 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 | ['geolocation', 'geolocation'],
|
78 | ['midi', 'midi'],
|
79 | ['notifications', 'notifications'],
|
80 | // TODO: push isn't a valid type?
|
81 | // ['push', 'push'],
|
82 | ['camera', 'videoCapture'],
|
83 | ['microphone', 'audioCapture'],
|
84 | ['background-sync', 'backgroundSync'],
|
85 | ['ambient-light-sensor', 'sensors'],
|
86 | ['accelerometer', 'sensors'],
|
87 | ['gyroscope', 'sensors'],
|
88 | ['magnetometer', 'sensors'],
|
89 | ['accessibility-events', 'accessibilityEvents'],
|
90 | ['clipboard-read', 'clipboardReadWrite'],
|
91 | ['clipboard-write', 'clipboardReadWrite'],
|
92 | ['clipboard-sanitized-write', 'clipboardSanitizedWrite'],
|
93 | ['payment-handler', 'paymentHandler'],
|
94 | ['persistent-storage', 'durableStorage'],
|
95 | ['idle-detection', 'idleDetection'],
|
96 | // chrome-specific permissions we have.
|
97 | ['midi-sysex', 'midiSysex'],
|
98 | ]);
|
99 |
|
100 | /**
|
101 | * @public
|
102 | */
|
103 | export type Permission =
|
104 | | 'geolocation'
|
105 | | 'midi'
|
106 | | 'notifications'
|
107 | | 'camera'
|
108 | | 'microphone'
|
109 | | 'background-sync'
|
110 | | 'ambient-light-sensor'
|
111 | | 'accelerometer'
|
112 | | 'gyroscope'
|
113 | | 'magnetometer'
|
114 | | 'accessibility-events'
|
115 | | 'clipboard-read'
|
116 | | 'clipboard-write'
|
117 | | 'clipboard-sanitized-write'
|
118 | | 'payment-handler'
|
119 | | 'persistent-storage'
|
120 | | 'idle-detection'
|
121 | | 'midi-sysex';
|
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: Cookie[]): 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 | * Whether Puppeteer is connected to this {@link Browser | browser}.
|
466 | *
|
467 | * @deprecated Use {@link Browser | Browser.connected}.
|
468 | */
|
469 | isConnected(): boolean {
|
470 | return this.connected;
|
471 | }
|
472 |
|
473 | /**
|
474 | * Whether Puppeteer is connected to this {@link Browser | browser}.
|
475 | */
|
476 | abstract get connected(): boolean;
|
477 |
|
478 | /** @internal */
|
479 | override [disposeSymbol](): void {
|
480 | if (this.process()) {
|
481 | return void this.close().catch(debugError);
|
482 | }
|
483 | return void this.disconnect().catch(debugError);
|
484 | }
|
485 |
|
486 | /** @internal */
|
487 | [asyncDisposeSymbol](): Promise<void> {
|
488 | if (this.process()) {
|
489 | return this.close();
|
490 | }
|
491 | return this.disconnect();
|
492 | }
|
493 |
|
494 | /**
|
495 | * @internal
|
496 | */
|
497 | abstract get protocol(): ProtocolType;
|
498 |
|
499 | /**
|
500 | * Get debug information from Puppeteer.
|
501 | *
|
502 | * @remarks
|
503 | *
|
504 | * Currently, includes pending protocol calls. In the future, we might add more info.
|
505 | *
|
506 | * @public
|
507 | * @experimental
|
508 | */
|
509 | abstract get debugInfo(): DebugInfo;
|
510 | }
|