FRE-709: Document duplicate recovery wake - FRE-635 already recovered via FRE-708

This commit is contained in:
2026-04-26 20:23:14 -04:00
parent e07237b6b0
commit 0ff6c74871
5880 changed files with 1643723 additions and 908 deletions

View File

@@ -0,0 +1,103 @@
/**
* @license
* Copyright 2025 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @public
* Emulated bluetooth adapter state.
*/
export type AdapterState = 'absent' | 'powered-off' | 'powered-on';
/**
* @public
* Represents the simulated bluetooth peripheral's manufacturer data.
*/
export interface BluetoothManufacturerData {
/**
* The company identifier, as defined by the {@link https://www.bluetooth.com/specifications/assigned-numbers/company-identifiers/|Bluetooth SIG}.
*/
key: number;
/**
* The manufacturer-specific data as a base64-encoded string.
*/
data: string;
}
/**
* @public
* A bluetooth peripheral to be simulated.
*/
export interface PreconnectedPeripheral {
address: string;
name: string;
manufacturerData: BluetoothManufacturerData[];
knownServiceUuids: string[];
}
/**
* Exposes the bluetooth emulation abilities.
*
* @remarks {@link https://webbluetoothcg.github.io/web-bluetooth/#simulated-bluetooth-adapter|Web Bluetooth specification}
* requires the emulated adapters should be isolated per top-level navigable. However,
* at the moment Chromium's bluetooth emulation implementation is tight to the browser
* context, not the page. This means the bluetooth emulation exposed from different pages
* of the same browser context would interfere their states.
*
* @example
*
* ```ts
* await page.bluetooth.emulateAdapter('powered-on');
* await page.bluetooth.simulatePreconnectedPeripheral({
* address: '09:09:09:09:09:09',
* name: 'SOME_NAME',
* manufacturerData: [
* {
* key: 17,
* data: 'AP8BAX8=',
* },
* ],
* knownServiceUuids: ['12345678-1234-5678-9abc-def123456789'],
* });
* await page.bluetooth.disableEmulation();
* ```
*
* @experimental
* @public
*/
export interface BluetoothEmulation {
/**
* Emulate Bluetooth adapter. Required for bluetooth simulations
* See {@link https://webbluetoothcg.github.io/web-bluetooth/#bluetooth-simulateAdapter-command|bluetooth.simulateAdapter}.
*
* @param state - The desired bluetooth adapter state.
* @param leSupported - Mark if the adapter supports low-energy bluetooth.
*
* @experimental
* @public
*/
emulateAdapter(state: AdapterState, leSupported?: boolean): Promise<void>;
/**
* Disable emulated bluetooth adapter.
* See {@link https://webbluetoothcg.github.io/web-bluetooth/#bluetooth-disableSimulation-command|bluetooth.disableSimulation}.
*
* @experimental
* @public
*/
disableEmulation(): Promise<void>;
/**
* Simulated preconnected Bluetooth Peripheral.
* See {@link https://webbluetoothcg.github.io/web-bluetooth/#bluetooth-simulateconnectedperipheral-command|bluetooth.simulatePreconnectedPeripheral}.
*
* @param preconnectedPeripheral - The peripheral to simulate.
*
* @experimental
* @public
*/
simulatePreconnectedPeripheral(
preconnectedPeripheral: PreconnectedPeripheral,
): Promise<void>;
}

743
node_modules/puppeteer-core/src/api/Browser.ts generated vendored Normal file
View File

@@ -0,0 +1,743 @@
/**
* @license
* Copyright 2017 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
/// <reference types="node" preserve="true"/>
import type {ChildProcess} from 'node:child_process';
import type {Protocol} from 'devtools-protocol';
import {
firstValueFrom,
from,
merge,
raceWith,
} from '../../third_party/rxjs/rxjs.js';
import type {ProtocolType} from '../common/ConnectOptions.js';
import type {
Cookie,
CookieData,
DeleteCookiesRequest,
} from '../common/Cookie.js';
import type {DownloadBehavior} from '../common/DownloadBehavior.js';
import {EventEmitter, type EventType} from '../common/EventEmitter.js';
import {
debugError,
fromEmitterEvent,
filterAsync,
timeout,
fromAbortSignal,
} from '../common/util.js';
import {asyncDisposeSymbol, disposeSymbol} from '../util/disposable.js';
import type {BrowserContext} from './BrowserContext.js';
import type {Extension} from './Extension.js';
import type {Page} from './Page.js';
import type {Target} from './Target.js';
/**
* @public
*/
export interface BrowserContextOptions {
/**
* Proxy server with optional port to use for all requests.
* Username and password can be set in `Page.authenticate`.
*/
proxyServer?: string;
/**
* Bypass the proxy for the given list of hosts.
*/
proxyBypassList?: string[];
/**
* Behavior definition for when downloading a file.
*
* @remarks
* If not set, the default behavior will be used.
*/
downloadBehavior?: DownloadBehavior;
}
/**
* @internal
*/
export type BrowserCloseCallback = () => Promise<void> | void;
/**
* @public
*/
export type TargetFilterCallback = (target: Target) => boolean;
/**
* @internal
*/
export type IsPageTargetCallback = (target: Target) => boolean;
/**
* @internal
*/
export const WEB_PERMISSION_TO_PROTOCOL_PERMISSION = new Map<
Permission,
Protocol.Browser.PermissionType
>([
['accelerometer', 'sensors'],
['ambient-light-sensor', 'sensors'],
['background-sync', 'backgroundSync'],
['camera', 'videoCapture'],
['clipboard-read', 'clipboardReadWrite'],
['clipboard-sanitized-write', 'clipboardSanitizedWrite'],
['clipboard-write', 'clipboardReadWrite'],
['geolocation', 'geolocation'],
['gyroscope', 'sensors'],
['idle-detection', 'idleDetection'],
['keyboard-lock', 'keyboardLock'],
['magnetometer', 'sensors'],
['microphone', 'audioCapture'],
['midi', 'midi'],
['notifications', 'notifications'],
['payment-handler', 'paymentHandler'],
['persistent-storage', 'durableStorage'],
['pointer-lock', 'pointerLock'],
// chrome-specific permissions we have.
['midi-sysex', 'midiSysex'],
]);
/**
* @public
* @deprecated in favor of {@link PermissionDescriptor}.
*/
export type Permission =
| 'accelerometer'
| 'ambient-light-sensor'
| 'background-sync'
| 'camera'
| 'clipboard-read'
| 'clipboard-sanitized-write'
| 'clipboard-write'
| 'geolocation'
| 'gyroscope'
| 'idle-detection'
| 'keyboard-lock'
| 'magnetometer'
| 'microphone'
| 'midi-sysex'
| 'midi'
| 'notifications'
| 'payment-handler'
| 'persistent-storage'
| 'pointer-lock';
/**
* @public
*/
export interface PermissionDescriptor {
name: string;
userVisibleOnly?: boolean;
sysex?: boolean;
panTiltZoom?: boolean;
allowWithoutSanitization?: boolean;
}
/**
* @public
*/
export type PermissionState = 'granted' | 'denied' | 'prompt';
/**
* @public
*/
export interface WaitForTargetOptions {
/**
* Maximum wait time in milliseconds. Pass `0` to disable the timeout.
*
* @defaultValue `30_000`
*/
timeout?: number;
/**
* A signal object that allows you to cancel a waitFor call.
*/
signal?: AbortSignal;
}
/**
* All the events a {@link Browser | browser instance} may emit.
*
* @public
*/
export const enum BrowserEvent {
/**
* Emitted when Puppeteer gets disconnected from the browser instance. This
* might happen because either:
*
* - The browser closes/crashes or
* - {@link Browser.disconnect} was called.
*/
Disconnected = 'disconnected',
/**
* Emitted when the URL of a target changes. Contains a {@link Target}
* instance.
*
* @remarks Note that this includes target changes in all browser
* contexts.
*/
TargetChanged = 'targetchanged',
/**
* Emitted when a target is created, for example when a new page is opened by
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/open | window.open}
* or by {@link Browser.newPage | browser.newPage}
*
* Contains a {@link Target} instance.
*
* @remarks Note that this includes target creations in all browser
* contexts.
*/
TargetCreated = 'targetcreated',
/**
* Emitted when a target is destroyed, for example when a page is closed.
* Contains a {@link Target} instance.
*
* @remarks Note that this includes target destructions in all browser
* contexts.
*/
TargetDestroyed = 'targetdestroyed',
/**
* @internal
*/
TargetDiscovered = 'targetdiscovered',
}
/**
* @public
*/
export interface BrowserEvents extends Record<EventType, unknown> {
[BrowserEvent.Disconnected]: undefined;
[BrowserEvent.TargetCreated]: Target;
[BrowserEvent.TargetDestroyed]: Target;
[BrowserEvent.TargetChanged]: Target;
/**
* @internal
*/
[BrowserEvent.TargetDiscovered]: Protocol.Target.TargetInfo;
}
/**
* @public
* @experimental
*/
export interface DebugInfo {
pendingProtocolErrors: Error[];
}
/**
* @public
*/
export type WindowState = 'normal' | 'minimized' | 'maximized' | 'fullscreen';
/**
* @public
*/
export interface WindowBounds {
left?: number;
top?: number;
width?: number;
height?: number;
windowState?: WindowState;
}
/**
* @public
*/
export type WindowId = string;
/**
* @public
*/
export type CreatePageOptions = (
| {
type?: 'tab';
}
| {
type: 'window';
windowBounds?: WindowBounds;
}
) & {
/**
* Whether to create the page in the background.
*
* @defaultValue `false`
*/
background?: boolean;
};
/**
* @public
*/
export interface ScreenOrientation {
angle: number;
type: string;
}
/**
* @public
*/
export interface ScreenInfo {
left: number;
top: number;
width: number;
height: number;
availLeft: number;
availTop: number;
availWidth: number;
availHeight: number;
devicePixelRatio: number;
colorDepth: number;
orientation: ScreenOrientation;
isExtended: boolean;
isInternal: boolean;
isPrimary: boolean;
label: string;
id: string;
}
/**
* @public
*/
export interface WorkAreaInsets {
top?: number;
left?: number;
bottom?: number;
right?: number;
}
/**
* @public
*/
export interface AddScreenParams {
left: number;
top: number;
width: number;
height: number;
workAreaInsets?: WorkAreaInsets;
devicePixelRatio?: number;
rotation?: number;
colorDepth?: number;
label?: string;
isInternal?: boolean;
}
/**
* {@link Browser} represents a browser instance that is either:
*
* - connected to via {@link Puppeteer.connect} or
* - launched by {@link PuppeteerNode.launch}.
*
* {@link Browser} {@link EventEmitter.emit | emits} various events which are
* documented in the {@link BrowserEvent} enum.
*
* @example Using a {@link Browser} to create a {@link Page}:
*
* ```ts
* import puppeteer from 'puppeteer';
*
* const browser = await puppeteer.launch();
* const page = await browser.newPage();
* await page.goto('https://example.com');
* await browser.close();
* ```
*
* @example Disconnecting from and reconnecting to a {@link Browser}:
*
* ```ts
* import puppeteer from 'puppeteer';
*
* const browser = await puppeteer.launch();
* // Store the endpoint to be able to reconnect to the browser.
* const browserWSEndpoint = browser.wsEndpoint();
* // Disconnect puppeteer from the browser.
* await browser.disconnect();
*
* // Use the endpoint to reestablish a connection
* const browser2 = await puppeteer.connect({browserWSEndpoint});
* // Close the browser.
* await browser2.close();
* ```
*
* @public
*/
export abstract class Browser extends EventEmitter<BrowserEvents> {
/**
* @internal
*/
constructor() {
super();
}
/**
* Gets the associated
* {@link https://nodejs.org/api/child_process.html#class-childprocess | ChildProcess}.
*
* @returns `null` if this instance was connected to via
* {@link Puppeteer.connect}.
*/
abstract process(): ChildProcess | null;
/**
* Creates a new {@link BrowserContext | browser context}.
*
* This won't share cookies/cache with other {@link BrowserContext | browser contexts}.
*
* @example
*
* ```ts
* import puppeteer from 'puppeteer';
*
* const browser = await puppeteer.launch();
* // Create a new browser context.
* const context = await browser.createBrowserContext();
* // Create a new page in a pristine context.
* const page = await context.newPage();
* // Do stuff
* await page.goto('https://example.com');
* ```
*/
abstract createBrowserContext(
options?: BrowserContextOptions,
): Promise<BrowserContext>;
/**
* Gets a list of open {@link BrowserContext | browser contexts}.
*
* In a newly-created {@link Browser | browser}, this will return a single
* instance of {@link BrowserContext}.
*/
abstract browserContexts(): BrowserContext[];
/**
* Gets the default {@link BrowserContext | browser context}.
*
* @remarks The default {@link BrowserContext | browser context} cannot be
* closed.
*/
abstract defaultBrowserContext(): BrowserContext;
/**
* Gets the WebSocket URL to connect to this {@link Browser | browser}.
*
* This is usually used with {@link Puppeteer.connect}.
*
* You can find the debugger URL (`webSocketDebuggerUrl`) from
* `http://HOST:PORT/json/version`.
*
* See {@link https://chromedevtools.github.io/devtools-protocol/#how-do-i-access-the-browser-target | browser endpoint}
* for more information.
*
* @remarks The format is always `ws://HOST:PORT/devtools/browser/<id>`.
*/
abstract wsEndpoint(): string;
/**
* Creates a new {@link Page | page} in the
* {@link Browser.defaultBrowserContext | default browser context}.
*/
abstract newPage(options?: CreatePageOptions): Promise<Page>;
/**
* Gets the specified window {@link WindowBounds | bounds}.
*/
abstract getWindowBounds(windowId: WindowId): Promise<WindowBounds>;
/**
* Sets the specified window {@link WindowBounds | bounds}.
*/
abstract setWindowBounds(
windowId: WindowId,
windowBounds: WindowBounds,
): Promise<void>;
/**
* Gets all active {@link Target | targets}.
*
* In case of multiple {@link BrowserContext | browser contexts}, this returns
* all {@link Target | targets} in all
* {@link BrowserContext | browser contexts}.
*/
abstract targets(): Target[];
/**
* Gets the {@link Target | target} associated with the
* {@link Browser.defaultBrowserContext | default browser context}).
*/
abstract target(): Target;
/**
* Waits until a {@link Target | target} matching the given `predicate`
* appears and returns it.
*
* This will look all open {@link BrowserContext | browser contexts}.
*
* @example Finding a target for a page opened via `window.open`:
*
* ```ts
* await page.evaluate(() => window.open('https://www.example.com/'));
* const newWindowTarget = await browser.waitForTarget(
* target => target.url() === 'https://www.example.com/',
* );
* ```
*/
async waitForTarget(
predicate: (x: Target) => boolean | Promise<boolean>,
options: WaitForTargetOptions = {},
): Promise<Target> {
const {timeout: ms = 30000, signal} = options;
return await firstValueFrom(
merge(
fromEmitterEvent(this, BrowserEvent.TargetCreated),
fromEmitterEvent(this, BrowserEvent.TargetChanged),
from(this.targets()),
).pipe(
filterAsync(predicate),
raceWith(fromAbortSignal(signal), timeout(ms)),
),
);
}
/**
* Gets a list of all open {@link Page | pages} inside this {@link Browser}.
*
* If there are multiple {@link BrowserContext | browser contexts}, this
* returns all {@link Page | pages} in all
* {@link BrowserContext | browser contexts}.
*
* @param includeAll - experimental, setting to true includes all kinds of pages.
*
* @remarks Non-visible {@link Page | pages}, such as `"background_page"`,
* will not be listed here. You can find them using {@link Target.page}.
*/
async pages(includeAll = false): Promise<Page[]> {
const contextPages = await Promise.all(
this.browserContexts().map(context => {
return context.pages(includeAll);
}),
);
// Flatten array.
return contextPages.reduce((acc, x) => {
return acc.concat(x);
}, []);
}
/**
* Gets a string representing this {@link Browser | browser's} name and
* version.
*
* For headless browser, this is similar to `"HeadlessChrome/61.0.3153.0"`. For
* non-headless or new-headless, this is similar to `"Chrome/61.0.3153.0"`. For
* Firefox, it is similar to `"Firefox/116.0a1"`.
*
* The format of {@link Browser.version} might change with future releases of
* browsers.
*/
abstract version(): Promise<string>;
/**
* Gets this {@link Browser | browser's} original user agent.
*
* {@link Page | Pages} can override the user agent with
* {@link Page.(setUserAgent:2) }.
*
*/
abstract userAgent(): Promise<string>;
/**
* Closes this {@link Browser | browser} and all associated
* {@link Page | pages}.
*/
abstract close(): Promise<void>;
/**
* Disconnects Puppeteer from this {@link Browser | browser}, but leaves the
* process running.
*/
abstract disconnect(): Promise<void>;
/**
* Returns all cookies in the default {@link BrowserContext}.
*
* @remarks
*
* Shortcut for
* {@link BrowserContext.cookies | browser.defaultBrowserContext().cookies()}.
*/
async cookies(): Promise<Cookie[]> {
return await this.defaultBrowserContext().cookies();
}
/**
* Sets cookies in the default {@link BrowserContext}.
*
* @remarks
*
* Shortcut for
* {@link BrowserContext.setCookie | browser.defaultBrowserContext().setCookie()}.
*/
async setCookie(...cookies: CookieData[]): Promise<void> {
return await this.defaultBrowserContext().setCookie(...cookies);
}
/**
* Removes cookies from the default {@link BrowserContext}.
*
* @remarks
*
* Shortcut for
* {@link BrowserContext.deleteCookie | browser.defaultBrowserContext().deleteCookie()}.
*/
async deleteCookie(...cookies: Cookie[]): Promise<void> {
return await this.defaultBrowserContext().deleteCookie(...cookies);
}
/**
* Deletes cookies matching the provided filters from the default
* {@link BrowserContext}.
*
* @remarks
*
* Shortcut for
* {@link BrowserContext.deleteMatchingCookies |
* browser.defaultBrowserContext().deleteMatchingCookies()}.
*/
async deleteMatchingCookies(
...filters: DeleteCookiesRequest[]
): Promise<void> {
return await this.defaultBrowserContext().deleteMatchingCookies(...filters);
}
/**
* Sets the permission for a specific origin in the default
* {@link BrowserContext}.
*
* @remarks
*
* Shortcut for
* {@link BrowserContext.setPermission |
* browser.defaultBrowserContext().setPermission()}.
*
* @param origin - The origin to set the permission for.
* @param permission - The permission descriptor.
* @param state - The state of the permission.
*
* @public
*/
async setPermission(
origin: string,
...permissions: Array<{
permission: PermissionDescriptor;
state: PermissionState;
}>
): Promise<void> {
return await this.defaultBrowserContext().setPermission(
origin,
...permissions,
);
}
/**
* Installs an extension and returns the ID. In Chrome, this is only
* available if the browser was created using `pipe: true` and the
* `--enable-unsafe-extension-debugging` flag is set.
*/
abstract installExtension(path: string): Promise<string>;
/**
* Uninstalls an extension. In Chrome, this is only available if the browser
* was created using `pipe: true` and the
* `--enable-unsafe-extension-debugging` flag is set.
*/
abstract uninstallExtension(id: string): Promise<void>;
/**
* Gets a list of {@link ScreenInfo | screen information objects}.
*/
abstract screens(): Promise<ScreenInfo[]>;
/**
* Adds a new screen, returns the added {@link ScreenInfo | screen information object}.
*
* @remarks
*
* Only supported in headless mode.
*/
abstract addScreen(params: AddScreenParams): Promise<ScreenInfo>;
/**
* Removes a screen.
*
* @remarks
*
* Only supported in headless mode. Fails if the primary screen id is specified.
*/
abstract removeScreen(screenId: string): Promise<void>;
/**
* Whether Puppeteer is connected to this {@link Browser | browser}.
*
* @deprecated Use {@link Browser | Browser.connected}.
*/
isConnected(): boolean {
return this.connected;
}
/**
* Whether Puppeteer is connected to this {@link Browser | browser}.
*/
abstract get connected(): boolean;
/** @internal */
override [disposeSymbol](): void {
if (this.process()) {
return void this.close().catch(debugError);
}
return void this.disconnect().catch(debugError);
}
/** @internal */
[asyncDisposeSymbol](): Promise<void> {
if (this.process()) {
return this.close();
}
return this.disconnect();
}
/**
* @internal
*/
abstract get protocol(): ProtocolType;
/**
* Get debug information from Puppeteer.
*
* @remarks
*
* Currently, includes pending protocol calls. In the future, we might add more info.
*
* @public
* @experimental
*/
abstract get debugInfo(): DebugInfo;
/**
* @internal
*/
abstract isNetworkEnabled(): boolean;
/**
* Retrieves a map of all extensions installed in the browser, where the keys
* are extension IDs and the values are the corresponding {@link Extension} instances.
*
* @public
*/
abstract extensions(): Promise<Map<string, Extension>>;
/**
* @internal
*/
abstract isIssuesEnabled(): boolean;
}

384
node_modules/puppeteer-core/src/api/BrowserContext.ts generated vendored Normal file
View File

@@ -0,0 +1,384 @@
/**
* @license
* Copyright 2017 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import {
firstValueFrom,
from,
merge,
raceWith,
} from '../../third_party/rxjs/rxjs.js';
import type {
Cookie,
CookieData,
DeleteCookiesRequest,
} from '../common/Cookie.js';
import {EventEmitter, type EventType} from '../common/EventEmitter.js';
import {
debugError,
fromEmitterEvent,
filterAsync,
timeout,
} from '../common/util.js';
import {asyncDisposeSymbol, disposeSymbol} from '../util/disposable.js';
import {Mutex} from '../util/Mutex.js';
import type {
Browser,
CreatePageOptions,
Permission,
PermissionDescriptor,
PermissionState,
WaitForTargetOptions,
} from './Browser.js';
import type {Page} from './Page.js';
import type {Target} from './Target.js';
/**
* @public
*/
export const enum BrowserContextEvent {
/**
* Emitted when the url of a target inside the browser context changes.
* Contains a {@link Target} instance.
*/
TargetChanged = 'targetchanged',
/**
* Emitted when a target is created within the browser context, for example
* when a new page is opened by
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/open | window.open}
* or by {@link BrowserContext.newPage | browserContext.newPage}
*
* Contains a {@link Target} instance.
*/
TargetCreated = 'targetcreated',
/**
* Emitted when a target is destroyed within the browser context, for example
* when a page is closed. Contains a {@link Target} instance.
*/
TargetDestroyed = 'targetdestroyed',
}
/**
* @public
*/
export interface BrowserContextEvents extends Record<EventType, unknown> {
[BrowserContextEvent.TargetChanged]: Target;
[BrowserContextEvent.TargetCreated]: Target;
[BrowserContextEvent.TargetDestroyed]: Target;
}
/**
* {@link BrowserContext} represents individual user contexts within a
* {@link Browser | browser}.
*
* When a {@link Browser | browser} is launched, it has at least one default
* {@link BrowserContext | browser context}. Others can be created
* using {@link Browser.createBrowserContext}. Each context has isolated storage
* (cookies/localStorage/etc.)
*
* {@link BrowserContext} {@link EventEmitter | emits} various events which are
* documented in the {@link BrowserContextEvent} enum.
*
* If a {@link Page | page} opens another {@link Page | page}, e.g. using
* `window.open`, the popup will belong to the parent {@link Page.browserContext
* | page's browser context}.
*
* @example Creating a new {@link BrowserContext | browser context}:
*
* ```ts
* // Create a new browser context
* const context = await browser.createBrowserContext();
* // Create a new page inside context.
* const page = await context.newPage();
* // ... do stuff with page ...
* await page.goto('https://example.com');
* // Dispose context once it's no longer needed.
* await context.close();
* ```
*
* @remarks
*
* In Chrome all non-default contexts are incognito,
* and {@link Browser.defaultBrowserContext | default browser context}
* might be incognito if you provide the `--incognito` argument when launching
* the browser.
*
* @public
*/
export abstract class BrowserContext extends EventEmitter<BrowserContextEvents> {
/**
* @internal
*/
constructor() {
super();
}
/**
* Gets all active {@link Target | targets} inside this
* {@link BrowserContext | browser context}.
*/
abstract targets(): Target[];
/**
* If defined, indicates an ongoing screenshot opereation.
*/
#pageScreenshotMutex?: Mutex;
#screenshotOperationsCount = 0;
/**
* @internal
*/
startScreenshot(): Promise<InstanceType<typeof Mutex.Guard>> {
const mutex = this.#pageScreenshotMutex || new Mutex();
this.#pageScreenshotMutex = mutex;
this.#screenshotOperationsCount++;
return mutex.acquire(() => {
this.#screenshotOperationsCount--;
if (this.#screenshotOperationsCount === 0) {
// Remove the mutex to indicate no ongoing screenshot operation.
this.#pageScreenshotMutex = undefined;
}
});
}
/**
* @internal
*/
waitForScreenshotOperations():
| Promise<InstanceType<typeof Mutex.Guard>>
| undefined {
return this.#pageScreenshotMutex?.acquire();
}
/**
* Waits until a {@link Target | target} matching the given `predicate`
* appears and returns it.
*
* This will look all open {@link BrowserContext | browser contexts}.
*
* @example Finding a target for a page opened via `window.open`:
*
* ```ts
* await page.evaluate(() => window.open('https://www.example.com/'));
* const newWindowTarget = await browserContext.waitForTarget(
* target => target.url() === 'https://www.example.com/',
* );
* ```
*/
async waitForTarget(
predicate: (x: Target) => boolean | Promise<boolean>,
options: WaitForTargetOptions = {},
): Promise<Target> {
const {timeout: ms = 30000} = options;
return await firstValueFrom(
merge(
fromEmitterEvent(this, BrowserContextEvent.TargetCreated),
fromEmitterEvent(this, BrowserContextEvent.TargetChanged),
from(this.targets()),
).pipe(filterAsync(predicate), raceWith(timeout(ms))),
);
}
/**
* Gets a list of all open {@link Page | pages} inside this
* {@link BrowserContext | browser context}.
*
* @param includeAll - experimental, setting to true includes all kinds of pages.
*
* @remarks Non-visible {@link Page | pages}, such as `"background_page"`,
* will not be listed here. You can find them using {@link Target.page}.
*/
abstract pages(includeAll?: boolean): Promise<Page[]>;
/**
* Grants this {@link BrowserContext | browser context} the given
* `permissions` within the given `origin`.
*
* @example Overriding permissions in the
* {@link Browser.defaultBrowserContext | default browser context}:
*
* ```ts
* const context = browser.defaultBrowserContext();
* await context.overridePermissions('https://html5demos.com', [
* 'geolocation',
* ]);
* ```
*
* @param origin - The origin to grant permissions to, e.g.
* "https://example.com".
* @param permissions - An array of permissions to grant. All permissions that
* are not listed here will be automatically denied.
*
* @deprecated in favor of {@link BrowserContext.setPermission}.
*/
abstract overridePermissions(
origin: string,
permissions: Permission[],
): Promise<void>;
/**
* Sets the permission for a specific origin.
*
* @param origin - The origin to set the permission for.
* @param permission - The permission descriptor.
* @param state - The state of the permission.
*
* @public
*/
abstract setPermission(
origin: string | '*',
...permissions: Array<{
permission: PermissionDescriptor;
state: PermissionState;
}>
): Promise<void>;
/**
* Clears all permission overrides for this
* {@link BrowserContext | browser context}.
*
* @example Clearing overridden permissions in the
* {@link Browser.defaultBrowserContext | default browser context}:
*
* ```ts
* const context = browser.defaultBrowserContext();
* context.overridePermissions('https://example.com', ['clipboard-read']);
* // do stuff ..
* context.clearPermissionOverrides();
* ```
*/
abstract clearPermissionOverrides(): Promise<void>;
/**
* Creates a new {@link Page | page} in this
* {@link BrowserContext | browser context}.
*/
abstract newPage(options?: CreatePageOptions): Promise<Page>;
/**
* Gets the {@link Browser | browser} associated with this
* {@link BrowserContext | browser context}.
*/
abstract browser(): Browser;
/**
* Closes this {@link BrowserContext | browser context} and all associated
* {@link Page | pages}.
*
* @remarks The
* {@link Browser.defaultBrowserContext | default browser context} cannot be
* closed.
*/
abstract close(): Promise<void>;
/**
* Gets all cookies in the browser context.
*/
abstract cookies(): Promise<Cookie[]>;
/**
* Sets a cookie in the browser context.
*/
abstract setCookie(...cookies: CookieData[]): Promise<void>;
/**
* Removes cookie in this browser context.
*
* @param cookies - Complete {@link Cookie | cookie} object to be removed.
*/
async deleteCookie(...cookies: Cookie[]): Promise<void> {
return await this.setCookie(
...cookies.map(cookie => {
return {
...cookie,
expires: 1,
};
}),
);
}
/**
* Deletes cookies matching the provided filters in this browser context.
*
* @param filters - {@link DeleteCookiesRequest}
*/
async deleteMatchingCookies(
...filters: DeleteCookiesRequest[]
): Promise<void> {
const cookies = await this.cookies();
const cookiesToDelete = cookies.filter(cookie => {
return filters.some(filter => {
if (filter.name === cookie.name) {
if (filter.domain !== undefined && filter.domain === cookie.domain) {
return true;
}
if (filter.path !== undefined && filter.path === cookie.path) {
return true;
}
if (
filter.partitionKey !== undefined &&
cookie.partitionKey !== undefined
) {
if (typeof cookie.partitionKey !== 'object') {
throw new Error('Unexpected string partition key');
}
if (typeof filter.partitionKey === 'string') {
if (filter.partitionKey === cookie.partitionKey?.sourceOrigin) {
return true;
}
} else {
if (
filter.partitionKey.sourceOrigin ===
cookie.partitionKey?.sourceOrigin
) {
return true;
}
}
}
if (filter.url !== undefined) {
const url = new URL(filter.url);
if (
url.hostname === cookie.domain &&
url.pathname === cookie.path
) {
return true;
}
}
return true;
}
return false;
});
});
await this.deleteCookie(...cookiesToDelete);
}
/**
* Whether this {@link BrowserContext | browser context} is closed.
*/
get closed(): boolean {
return !this.browser().browserContexts().includes(this);
}
/**
* Identifier for this {@link BrowserContext | browser context}.
*/
get id(): string | undefined {
return undefined;
}
/** @internal */
override [disposeSymbol](): void {
return void this.close().catch(debugError);
}
/** @internal */
[asyncDisposeSymbol](): Promise<void> {
return this.close();
}
}

137
node_modules/puppeteer-core/src/api/CDPSession.ts generated vendored Normal file
View File

@@ -0,0 +1,137 @@
/**
* @license
* Copyright 2024 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {ProtocolMapping} from 'devtools-protocol/types/protocol-mapping.js';
import type {Connection} from '../cdp/Connection.js';
import {EventEmitter, type EventType} from '../common/EventEmitter.js';
/**
* @public
*/
export type CDPEvents = {
[Property in keyof ProtocolMapping.Events]: ProtocolMapping.Events[Property][0];
};
/**
* Events that the CDPSession class emits.
*
* @public
*/
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace CDPSessionEvent {
/** @internal */
export const Disconnected = Symbol('CDPSession.Disconnected');
/** @internal */
export const Swapped = Symbol('CDPSession.Swapped');
/**
* Emitted when the session is ready to be configured during the auto-attach
* process. Right after the event is handled, the session will be resumed.
*
* @internal
*/
export const Ready = Symbol('CDPSession.Ready');
export const SessionAttached = 'sessionattached' as const;
export const SessionDetached = 'sessiondetached' as const;
}
/**
* @public
*/
export interface CDPSessionEvents
extends CDPEvents, Record<EventType, unknown> {
/** @internal */
[CDPSessionEvent.Disconnected]: undefined;
/** @internal */
[CDPSessionEvent.Swapped]: CDPSession;
/** @internal */
[CDPSessionEvent.Ready]: CDPSession;
[CDPSessionEvent.SessionAttached]: CDPSession;
[CDPSessionEvent.SessionDetached]: CDPSession;
}
/**
* @public
*/
export interface CommandOptions {
timeout: number;
}
/**
* The `CDPSession` instances are used to talk raw Chrome Devtools Protocol.
*
* @remarks
*
* Protocol methods can be called with {@link CDPSession.send} method and protocol
* events can be subscribed to with `CDPSession.on` method.
*
* Useful links: {@link https://chromedevtools.github.io/devtools-protocol/ | DevTools Protocol Viewer}
* and {@link https://github.com/aslushnikov/getting-started-with-cdp/blob/HEAD/README.md | Getting Started with DevTools Protocol}.
*
* @example
*
* ```ts
* const client = await page.createCDPSession();
* await client.send('Animation.enable');
* client.on('Animation.animationCreated', () =>
* console.log('Animation created!'),
* );
* const response = await client.send('Animation.getPlaybackRate');
* console.log('playback rate is ' + response.playbackRate);
* await client.send('Animation.setPlaybackRate', {
* playbackRate: response.playbackRate / 2,
* });
* ```
*
* @public
*/
export abstract class CDPSession extends EventEmitter<CDPSessionEvents> {
/**
* @internal
*/
constructor() {
super();
}
/**
* The underlying connection for this session, if any.
*
* @public
*/
abstract connection(): Connection | undefined;
/**
* True if the session has been detached, false otherwise.
*
* @public
*/
abstract get detached(): boolean;
/**
* Parent session in terms of CDP's auto-attach mechanism.
*
* @internal
*/
parentSession(): CDPSession | undefined {
return undefined;
}
abstract send<T extends keyof ProtocolMapping.Commands>(
method: T,
params?: ProtocolMapping.Commands[T]['paramsType'][0],
options?: CommandOptions,
): Promise<ProtocolMapping.Commands[T]['returnType']>;
/**
* Detaches the cdpSession from the target. Once detached, the cdpSession object
* won't emit any events and can't be used to send messages.
*/
abstract detach(): Promise<void>;
/**
* Returns the session's id.
*/
abstract id(): string;
}

View File

@@ -0,0 +1,71 @@
/**
* @license
* Copyright 2025 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {WaitTimeoutOptions} from './Page.js';
/**
* Device in a request prompt.
*
* @public
*/
export interface DeviceRequestPromptDevice {
/**
* Device id during a prompt.
*/
id: string;
/**
* Device name as it appears in a prompt.
*/
name: string;
}
/**
* Device request prompts let you respond to the page requesting for a device
* through an API like WebBluetooth.
*
* @remarks
* `DeviceRequestPrompt` instances are returned via the
* {@link Page.waitForDevicePrompt} method.
*
* @example
*
* ```ts
* const [devicePrompt] = Promise.all([
* page.waitForDevicePrompt(),
* page.click('#connect-bluetooth'),
* ]);
* await devicePrompt.select(
* await devicePrompt.waitForDevice(({name}) => name.includes('My Device')),
* );
* ```
*
* @public
*/
export abstract class DeviceRequestPrompt {
/**
* Current list of selectable devices.
*/
readonly devices: DeviceRequestPromptDevice[] = [];
/**
* Resolve to the first device in the prompt matching a filter.
*/
abstract waitForDevice(
filter: (device: DeviceRequestPromptDevice) => boolean,
options?: WaitTimeoutOptions,
): Promise<DeviceRequestPromptDevice>;
/**
* Select a device in the prompt's list.
*/
abstract select(device: DeviceRequestPromptDevice): Promise<void>;
/**
* Cancel the prompt.
*/
abstract cancel(): Promise<void>;
}

111
node_modules/puppeteer-core/src/api/Dialog.ts generated vendored Normal file
View File

@@ -0,0 +1,111 @@
/**
* @license
* Copyright 2017 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {Protocol} from 'devtools-protocol';
import {assert} from '../util/assert.js';
/**
* Dialog instances are dispatched by the {@link Page} via the `dialog` event.
*
* @remarks
*
* @example
*
* ```ts
* import puppeteer from 'puppeteer';
*
* const browser = await puppeteer.launch();
* const page = await browser.newPage();
* page.on('dialog', async dialog => {
* console.log(dialog.message());
* await dialog.dismiss();
* await browser.close();
* });
* await page.evaluate(() => alert('1'));
* ```
*
* @public
*/
export abstract class Dialog {
#type: Protocol.Page.DialogType;
#message: string;
#defaultValue: string;
/**
* @internal
*/
protected handled = false;
/**
* @internal
*/
constructor(
type: Protocol.Page.DialogType,
message: string,
defaultValue = '',
) {
this.#type = type;
this.#message = message;
this.#defaultValue = defaultValue;
}
/**
* The type of the dialog.
*/
type(): Protocol.Page.DialogType {
return this.#type;
}
/**
* The message displayed in the dialog.
*/
message(): string {
return this.#message;
}
/**
* The default value of the prompt, or an empty string if the dialog
* is not a `prompt`.
*/
defaultValue(): string {
return this.#defaultValue;
}
/**
* @internal
*/
protected abstract handle(options: {
accept: boolean;
text?: string;
}): Promise<void>;
/**
* A promise that resolves when the dialog has been accepted.
*
* @param promptText - optional text that will be entered in the dialog
* prompt. Has no effect if the dialog's type is not `prompt`.
*
*/
async accept(promptText?: string): Promise<void> {
assert(!this.handled, 'Cannot accept dialog which is already handled!');
this.handled = true;
await this.handle({
accept: true,
text: promptText,
});
}
/**
* A promise which will resolve once the dialog has been dismissed
*/
async dismiss(): Promise<void> {
assert(!this.handled, 'Cannot dismiss dialog which is already handled!');
this.handled = true;
await this.handle({
accept: false,
});
}
}

1708
node_modules/puppeteer-core/src/api/ElementHandle.ts generated vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,10 @@
/**
* @license
* Copyright 2023 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @internal
*/
export const _isElementHandle = Symbol('_isElementHandle');

16
node_modules/puppeteer-core/src/api/Environment.ts generated vendored Normal file
View File

@@ -0,0 +1,16 @@
/**
* @license
* Copyright 2023 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {CDPSession} from './CDPSession.js';
import type {Realm} from './Realm.js';
/**
* @internal
*/
export interface Environment {
get client(): CDPSession;
mainRealm(): Realm;
}

126
node_modules/puppeteer-core/src/api/Extension.ts generated vendored Normal file
View File

@@ -0,0 +1,126 @@
/**
* @license
* Copyright 2026 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {Page} from './Page.js';
import type {WebWorker} from './WebWorker.js';
/**
* {@link Extension} represents a browser extension installed in the browser.
* It provides access to the extension's ID, name, and version, as well as
* methods for interacting with the extension's background workers and pages.
*
* @example
* To get all extensions installed in the browser:
*
* ```ts
* const extensions = await browser.extensions();
* for (const [id, extension] of extensions) {
* console.log(extension.name, id);
* }
* ```
*
* @experimental
* @public
*/
export abstract class Extension {
#id: string;
#version: string;
#name: string;
#path: string;
#enabled: boolean;
/**
* @internal
*/
constructor(
id: string,
version: string,
name: string,
path: string,
enabled: boolean,
) {
if (!id || !version) {
throw new Error('Extension ID and version are required');
}
this.#id = id;
this.#version = version;
this.#name = name;
this.#path = path;
this.#enabled = enabled;
}
/**
* Whether the extension is enabled.
*
* @public
*/
get enabled(): boolean {
return this.#enabled;
}
/**
* The path in the file system where the extension is located.
*
* @public
*/
get path(): string {
return this.#path;
}
/**
* The version of the extension as specified in its manifest.
*
* @public
*/
get version(): string {
return this.#version;
}
/**
* The name of the extension as specified in its manifest.
*
* @public
*/
get name(): string {
return this.#name;
}
/**
* The unique identifier of the extension.
*
* @public
*/
get id(): string {
return this.#id;
}
/**
* Returns a list of the currently active service workers belonging
* to the extension.
*
* @public
*/
abstract workers(): Promise<WebWorker[]>;
/**
* Returns a list of the currently active and visible pages belonging
* to the extension.
*
* @public
*/
abstract pages(): Promise<Page[]>;
/**
* Triggers the default action of the extension for a specified page.
* This typically simulates a user clicking the extension's action icon
* in the browser toolbar, potentially opening a popup or executing an action script.
*
* @param page - The page to trigger the action on.
* @public
*/
abstract triggerAction(page: Page): Promise<void>;
}

1212
node_modules/puppeteer-core/src/api/Frame.ts generated vendored Normal file

File diff suppressed because it is too large Load Diff

750
node_modules/puppeteer-core/src/api/HTTPRequest.ts generated vendored Normal file
View File

@@ -0,0 +1,750 @@
/**
* @license
* Copyright 2020 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {Protocol} from 'devtools-protocol';
import type {ProtocolError} from '../common/Errors.js';
import {debugError, isString} from '../common/util.js';
import {assert} from '../util/assert.js';
import {typedArrayToBase64} from '../util/encoding.js';
import type {CDPSession} from './CDPSession.js';
import type {Frame} from './Frame.js';
import type {HTTPResponse} from './HTTPResponse.js';
/**
* @public
*/
export interface ContinueRequestOverrides {
/**
* If set, the request URL will change. This is not a redirect.
*/
url?: string;
method?: string;
postData?: string;
headers?: Record<string, string>;
}
/**
* @public
*/
export interface InterceptResolutionState {
action: InterceptResolutionAction;
priority?: number;
}
/**
* Required response data to fulfill a request with.
*
* @public
*/
export interface ResponseForRequest {
status: number;
/**
* Optional response headers.
*
* The record values will be converted to string following:
* Arrays' values will be mapped to String
* (Used when you need multiple headers with the same name).
* Non-arrays will be converted to String.
*/
headers: Record<string, string | string[] | unknown>;
contentType: string;
body: string | Uint8Array;
}
/**
* Resource types for HTTPRequests as perceived by the rendering engine.
*
* @public
*/
export type ResourceType = Lowercase<Protocol.Network.ResourceType>;
/**
* The default cooperative request interception resolution priority
*
* @public
*/
export const DEFAULT_INTERCEPT_RESOLUTION_PRIORITY = 0;
/**
* Represents an HTTP request sent by a page.
* @remarks
*
* Whenever the page sends a request, such as for a network resource, the
* following events are emitted by Puppeteer's `page`:
*
* - `request`: emitted when the request is issued by the page.
*
* - `requestfinished` - emitted when the response body is downloaded and the
* request is complete.
*
* If request fails at some point, then instead of `requestfinished` event the
* `requestfailed` event is emitted.
*
* All of these events provide an instance of `HTTPRequest` representing the
* request that occurred:
*
* ```
* page.on('request', request => ...)
* ```
*
* NOTE: HTTP Error responses, such as 404 or 503, are still successful
* responses from HTTP standpoint, so request will complete with
* `requestfinished` event.
*
* If request gets a 'redirect' response, the request is successfully finished
* with the `requestfinished` event, and a new request is issued to a
* redirected url.
*
* @public
*/
export abstract class HTTPRequest {
/**
* @internal
*/
abstract get id(): string;
/**
* @internal
*/
_interceptionId: string | undefined;
/**
* @internal
*/
_failureText: string | null = null;
/**
* @internal
*/
_response: HTTPResponse | null = null;
/**
* @internal
*/
_fromMemoryCache = false;
/**
* @internal
*/
_redirectChain: HTTPRequest[] = [];
/**
* @internal
*/
protected interception: {
enabled: boolean;
handled: boolean;
handlers: Array<() => void | PromiseLike<any>>;
resolutionState: InterceptResolutionState;
requestOverrides: ContinueRequestOverrides;
response: Partial<ResponseForRequest> | null;
abortReason: Protocol.Network.ErrorReason | null;
} = {
enabled: false,
handled: false,
handlers: [],
resolutionState: {
action: InterceptResolutionAction.None,
},
requestOverrides: {},
response: null,
abortReason: null,
};
/**
* Warning! Using this client can break Puppeteer. Use with caution.
*
* @experimental
*/
abstract get client(): CDPSession;
/**
* @internal
*/
constructor() {}
/**
* The URL of the request
*/
abstract url(): string;
/**
* The `ContinueRequestOverrides` that will be used
* if the interception is allowed to continue (ie, `abort()` and
* `respond()` aren't called).
*/
continueRequestOverrides(): ContinueRequestOverrides {
return this.interception.requestOverrides;
}
/**
* The `ResponseForRequest` that gets used if the
* interception is allowed to respond (ie, `abort()` is not called).
*/
responseForRequest(): Partial<ResponseForRequest> | null {
return this.interception.response;
}
/**
* The most recent reason for aborting the request
*/
abortErrorReason(): Protocol.Network.ErrorReason | null {
return this.interception.abortReason;
}
/**
* An InterceptResolutionState object describing the current resolution
* action and priority.
*
* InterceptResolutionState contains:
* action: InterceptResolutionAction
* priority?: number
*
* InterceptResolutionAction is one of: `abort`, `respond`, `continue`,
* `disabled`, `none`, or `already-handled`.
*/
interceptResolutionState(): InterceptResolutionState {
if (!this.interception.enabled) {
return {action: InterceptResolutionAction.Disabled};
}
if (this.interception.handled) {
return {action: InterceptResolutionAction.AlreadyHandled};
}
return {...this.interception.resolutionState};
}
/**
* Is `true` if the intercept resolution has already been handled,
* `false` otherwise.
*/
isInterceptResolutionHandled(): boolean {
return this.interception.handled;
}
/**
* Adds an async request handler to the processing queue.
* Deferred handlers are not guaranteed to execute in any particular order,
* but they are guaranteed to resolve before the request interception
* is finalized.
*/
enqueueInterceptAction(
pendingHandler: () => void | PromiseLike<unknown>,
): void {
this.interception.handlers.push(pendingHandler);
}
/**
* @internal
*/
abstract _abort(
errorReason: Protocol.Network.ErrorReason | null,
): Promise<void>;
/**
* @internal
*/
abstract _respond(response: Partial<ResponseForRequest>): Promise<void>;
/**
* @internal
*/
abstract _continue(overrides: ContinueRequestOverrides): Promise<void>;
/**
* Awaits pending interception handlers and then decides how to fulfill
* the request interception.
*/
async finalizeInterceptions(): Promise<void> {
await this.interception.handlers.reduce((promiseChain, interceptAction) => {
return promiseChain.then(interceptAction);
}, Promise.resolve());
this.interception.handlers = [];
const {action} = this.interceptResolutionState();
switch (action) {
case 'abort':
return await this._abort(this.interception.abortReason);
case 'respond':
if (this.interception.response === null) {
throw new Error('Response is missing for the interception');
}
return await this._respond(this.interception.response);
case 'continue':
return await this._continue(this.interception.requestOverrides);
}
}
/**
* Contains the request's resource type as it was perceived by the rendering
* engine.
*/
abstract resourceType(): ResourceType;
/**
* The method used (`GET`, `POST`, etc.)
*/
abstract method(): string;
/**
* @deprecated Use {@link HTTPRequest.fetchPostData}.
*/
abstract postData(): string | undefined;
/**
* True when the request has POST data. Note that {@link HTTPRequest.postData}
* might still be undefined when this flag is true when the data is too long
* or not readily available in the decoded form. In that case, use
* {@link HTTPRequest.fetchPostData}.
*/
abstract hasPostData(): boolean;
/**
* Fetches the POST data for the request from the browser.
*/
abstract fetchPostData(): Promise<string | undefined>;
/**
* An object with HTTP headers associated with the request. All
* header names are lower-case.
*/
abstract headers(): Record<string, string>;
/**
* A matching `HTTPResponse` object, or null if the response has not
* been received yet.
*/
abstract response(): HTTPResponse | null;
/**
* The frame that initiated the request, or null if navigating to
* error pages.
*/
abstract frame(): Frame | null;
/**
* True if the request is the driver of the current frame's navigation.
*/
abstract isNavigationRequest(): boolean;
/**
* The initiator of the request.
*/
abstract initiator(): Protocol.Network.Initiator | undefined;
/**
* A `redirectChain` is a chain of requests initiated to fetch a resource.
* @remarks
*
* `redirectChain` is shared between all the requests of the same chain.
*
* For example, if the website `http://example.com` has a single redirect to
* `https://example.com`, then the chain will contain one request:
*
* ```ts
* const response = await page.goto('http://example.com');
* const chain = response.request().redirectChain();
* console.log(chain.length); // 1
* console.log(chain[0].url()); // 'http://example.com'
* ```
*
* If the website `https://google.com` has no redirects, then the chain will be empty:
*
* ```ts
* const response = await page.goto('https://google.com');
* const chain = response.request().redirectChain();
* console.log(chain.length); // 0
* ```
*
* @returns the chain of requests - if a server responds with at least a
* single redirect, this chain will contain all requests that were redirected.
*/
abstract redirectChain(): HTTPRequest[];
/**
* Access information about the request's failure.
*
* @remarks
*
* @example
*
* Example of logging all failed requests:
*
* ```ts
* page.on('requestfailed', request => {
* console.log(request.url() + ' ' + request.failure().errorText);
* });
* ```
*
* @returns `null` unless the request failed. If the request fails this can
* return an object with `errorText` containing a human-readable error
* message, e.g. `net::ERR_FAILED`. It is not guaranteed that there will be
* failure text if the request fails.
*/
abstract failure(): {errorText: string} | null;
/**
* @internal
*/
protected verifyInterception(): void {
assert(this.interception.enabled, 'Request Interception is not enabled!');
assert(!this.interception.handled, 'Request is already handled!');
}
/**
* @internal
*/
protected abstract canBeIntercepted(): boolean;
/**
* Continues request with optional request overrides.
*
* @example
*
* ```ts
* await page.setRequestInterception(true);
* page.on('request', request => {
* // Override headers
* const headers = Object.assign({}, request.headers(), {
* foo: 'bar', // set "foo" header
* origin: undefined, // remove "origin" header
* });
* request.continue({headers});
* });
* ```
*
* @param overrides - optional overrides to apply to the request.
* @param priority - If provided, intercept is resolved using cooperative
* handling rules. Otherwise, intercept is resolved immediately.
*
* @remarks
*
* To use this, request interception should be enabled with
* {@link Page.setRequestInterception}.
*
* Exception is immediately thrown if the request interception is not enabled.
*/
async continue(
overrides: ContinueRequestOverrides = {},
priority?: number,
): Promise<void> {
this.verifyInterception();
if (!this.canBeIntercepted()) {
return;
}
if (priority === undefined) {
return await this._continue(overrides);
}
this.interception.requestOverrides = overrides;
if (
this.interception.resolutionState.priority === undefined ||
priority > this.interception.resolutionState.priority
) {
this.interception.resolutionState = {
action: InterceptResolutionAction.Continue,
priority,
};
return;
}
if (priority === this.interception.resolutionState.priority) {
if (
this.interception.resolutionState.action === 'abort' ||
this.interception.resolutionState.action === 'respond'
) {
return;
}
this.interception.resolutionState.action =
InterceptResolutionAction.Continue;
}
return;
}
/**
* Fulfills a request with the given response.
*
* @example
* An example of fulfilling all requests with 404 responses:
*
* ```ts
* await page.setRequestInterception(true);
* page.on('request', request => {
* request.respond({
* status: 404,
* contentType: 'text/plain',
* body: 'Not Found!',
* });
* });
* ```
*
* NOTE: Mocking responses for dataURL requests is not supported.
* Calling `request.respond` for a dataURL request is a noop.
*
* @param response - the response to fulfill the request with.
* @param priority - If provided, intercept is resolved using
* cooperative handling rules. Otherwise, intercept is resolved
* immediately.
*
* @remarks
*
* To use this, request
* interception should be enabled with {@link Page.setRequestInterception}.
*
* Exception is immediately thrown if the request interception is not enabled.
*/
async respond(
response: Partial<ResponseForRequest>,
priority?: number,
): Promise<void> {
this.verifyInterception();
if (!this.canBeIntercepted()) {
return;
}
if (priority === undefined) {
return await this._respond(response);
}
this.interception.response = response;
if (
this.interception.resolutionState.priority === undefined ||
priority > this.interception.resolutionState.priority
) {
this.interception.resolutionState = {
action: InterceptResolutionAction.Respond,
priority,
};
return;
}
if (priority === this.interception.resolutionState.priority) {
if (this.interception.resolutionState.action === 'abort') {
return;
}
this.interception.resolutionState.action =
InterceptResolutionAction.Respond;
}
}
/**
* Aborts a request.
*
* @param errorCode - optional error code to provide.
* @param priority - If provided, intercept is resolved using
* cooperative handling rules. Otherwise, intercept is resolved
* immediately.
*
* @remarks
*
* To use this, request interception should be enabled with
* {@link Page.setRequestInterception}. If it is not enabled, this method will
* throw an exception immediately.
*/
async abort(
errorCode: ErrorCode = 'failed',
priority?: number,
): Promise<void> {
this.verifyInterception();
if (!this.canBeIntercepted()) {
return;
}
const errorReason = errorReasons[errorCode];
assert(errorReason, 'Unknown error code: ' + errorCode);
if (priority === undefined) {
return await this._abort(errorReason);
}
this.interception.abortReason = errorReason;
if (
this.interception.resolutionState.priority === undefined ||
priority >= this.interception.resolutionState.priority
) {
this.interception.resolutionState = {
action: InterceptResolutionAction.Abort,
priority,
};
return;
}
}
/**
* @internal
*/
static getResponse(body: string | Uint8Array): {
contentLength: number;
base64: string;
} {
// Needed to get the correct byteLength
const byteBody: Uint8Array = isString(body)
? new TextEncoder().encode(body)
: body;
return {
contentLength: byteBody.byteLength,
base64: typedArrayToBase64(byteBody),
};
}
}
/**
* @public
*/
export enum InterceptResolutionAction {
Abort = 'abort',
Respond = 'respond',
Continue = 'continue',
Disabled = 'disabled',
None = 'none',
AlreadyHandled = 'already-handled',
}
/**
* @public
*/
export type ErrorCode =
| 'aborted'
| 'accessdenied'
| 'addressunreachable'
| 'blockedbyclient'
| 'blockedbyresponse'
| 'connectionaborted'
| 'connectionclosed'
| 'connectionfailed'
| 'connectionrefused'
| 'connectionreset'
| 'internetdisconnected'
| 'namenotresolved'
| 'timedout'
| 'failed';
/**
* @public
*/
export type ActionResult = 'continue' | 'abort' | 'respond';
/**
* @internal
*/
export function headersArray(
headers: Record<string, string | string[]>,
): Array<{name: string; value: string}> {
const result = [];
for (const name in headers) {
const value = headers[name];
if (!Object.is(value, undefined)) {
const values = Array.isArray(value) ? value : [value];
result.push(
...values.map(value => {
return {name, value: value + ''};
}),
);
}
}
return result;
}
/**
* @internal
*
* @remarks
* List taken from {@link https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml}
* with extra 306 and 418 codes.
*/
export const STATUS_TEXTS: Record<string, string> = {
'100': 'Continue',
'101': 'Switching Protocols',
'102': 'Processing',
'103': 'Early Hints',
'200': 'OK',
'201': 'Created',
'202': 'Accepted',
'203': 'Non-Authoritative Information',
'204': 'No Content',
'205': 'Reset Content',
'206': 'Partial Content',
'207': 'Multi-Status',
'208': 'Already Reported',
'226': 'IM Used',
'300': 'Multiple Choices',
'301': 'Moved Permanently',
'302': 'Found',
'303': 'See Other',
'304': 'Not Modified',
'305': 'Use Proxy',
'306': 'Switch Proxy',
'307': 'Temporary Redirect',
'308': 'Permanent Redirect',
'400': 'Bad Request',
'401': 'Unauthorized',
'402': 'Payment Required',
'403': 'Forbidden',
'404': 'Not Found',
'405': 'Method Not Allowed',
'406': 'Not Acceptable',
'407': 'Proxy Authentication Required',
'408': 'Request Timeout',
'409': 'Conflict',
'410': 'Gone',
'411': 'Length Required',
'412': 'Precondition Failed',
'413': 'Payload Too Large',
'414': 'URI Too Long',
'415': 'Unsupported Media Type',
'416': 'Range Not Satisfiable',
'417': 'Expectation Failed',
'418': "I'm a teapot",
'421': 'Misdirected Request',
'422': 'Unprocessable Entity',
'423': 'Locked',
'424': 'Failed Dependency',
'425': 'Too Early',
'426': 'Upgrade Required',
'428': 'Precondition Required',
'429': 'Too Many Requests',
'431': 'Request Header Fields Too Large',
'451': 'Unavailable For Legal Reasons',
'500': 'Internal Server Error',
'501': 'Not Implemented',
'502': 'Bad Gateway',
'503': 'Service Unavailable',
'504': 'Gateway Timeout',
'505': 'HTTP Version Not Supported',
'506': 'Variant Also Negotiates',
'507': 'Insufficient Storage',
'508': 'Loop Detected',
'510': 'Not Extended',
'511': 'Network Authentication Required',
} as const;
const errorReasons: Record<ErrorCode, Protocol.Network.ErrorReason> = {
aborted: 'Aborted',
accessdenied: 'AccessDenied',
addressunreachable: 'AddressUnreachable',
blockedbyclient: 'BlockedByClient',
blockedbyresponse: 'BlockedByResponse',
connectionaborted: 'ConnectionAborted',
connectionclosed: 'ConnectionClosed',
connectionfailed: 'ConnectionFailed',
connectionrefused: 'ConnectionRefused',
connectionreset: 'ConnectionReset',
internetdisconnected: 'InternetDisconnected',
namenotresolved: 'NameNotResolved',
timedout: 'TimedOut',
failed: 'Failed',
} as const;
/**
* @internal
*/
export function handleError(error: ProtocolError): void {
// Firefox throws an invalid argument error with a message starting with
// 'Expected "header" [...]'.
if (
error.originalMessage.includes('Invalid header') ||
error.originalMessage.includes('Unsafe header') ||
error.originalMessage.includes('Expected "header"') ||
// WebDriver BiDi error for invalid values, for example, headers.
error.originalMessage.includes('invalid argument')
) {
throw error;
}
// In certain cases, protocol will return error if the request was
// already canceled or the page was closed. We should tolerate these
// errors.
debugError(error);
}

148
node_modules/puppeteer-core/src/api/HTTPResponse.ts generated vendored Normal file
View File

@@ -0,0 +1,148 @@
/**
* @license
* Copyright 2023 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type Protocol from 'devtools-protocol';
import type {SecurityDetails} from '../common/SecurityDetails.js';
import type {Frame} from './Frame.js';
import type {HTTPRequest} from './HTTPRequest.js';
/**
* @public
*/
export interface RemoteAddress {
ip?: string;
port?: number;
}
/**
* The HTTPResponse class represents responses which are received by the
* {@link Page} class.
*
* @public
*/
export abstract class HTTPResponse {
/**
* @internal
*/
constructor() {}
/**
* The IP address and port number used to connect to the remote
* server.
*/
abstract remoteAddress(): RemoteAddress;
/**
* The URL of the response.
*/
abstract url(): string;
/**
* True if the response was successful (status in the range 200-299).
*/
ok(): boolean {
// TODO: document === 0 case?
const status = this.status();
return status === 0 || (status >= 200 && status <= 299);
}
/**
* The status code of the response (e.g., 200 for a success).
*/
abstract status(): number;
/**
* The status text of the response (e.g. usually an "OK" for a
* success).
*/
abstract statusText(): string;
/**
* An object with HTTP headers associated with the response. All
* header names are lower-case.
*/
abstract headers(): Record<string, string>;
/**
* {@link SecurityDetails} if the response was received over the
* secure connection, or `null` otherwise.
*/
abstract securityDetails(): SecurityDetails | null;
/**
* Timing information related to the response.
*/
abstract timing(): Protocol.Network.ResourceTiming | null;
/**
* Promise which resolves to a buffer with response body.
*
* @remarks
*
* The buffer might be re-encoded by the browser
* based on HTTP-headers or other heuristics. If the browser
* failed to detect the correct encoding, the buffer might
* be encoded incorrectly. See
* https://github.com/puppeteer/puppeteer/issues/6478.
*/
abstract content(): Promise<Uint8Array>;
/**
* {@inheritDoc HTTPResponse.content}
*/
async buffer(): Promise<Buffer> {
const content = await this.content();
return Buffer.from(content);
}
/**
* Promise which resolves to a text (utf8) representation of response body.
*
* @remarks
*
* This method will throw if the content is not utf-8 string
*/
async text(): Promise<string> {
const content = await this.content();
return new TextDecoder('utf-8', {fatal: true}).decode(content);
}
/**
* Promise which resolves to a JSON representation of response body.
*
* @remarks
*
* This method will throw if the response body is not parsable via
* `JSON.parse`.
*/
async json(): Promise<any> {
const content = await this.text();
return JSON.parse(content);
}
/**
* A matching {@link HTTPRequest} object.
*/
abstract request(): HTTPRequest;
/**
* True if the response was served from either the browser's disk
* cache or memory cache.
*/
abstract fromCache(): boolean;
/**
* True if the response was served by a service worker.
*/
abstract fromServiceWorker(): boolean;
/**
* A {@link Frame} that initiated this response, or `null` if
* navigating to error pages.
*/
abstract frame(): Frame | null;
}

567
node_modules/puppeteer-core/src/api/Input.ts generated vendored Normal file
View File

@@ -0,0 +1,567 @@
/**
* @license
* Copyright 2017 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {Protocol} from 'devtools-protocol';
import {TouchError} from '../common/Errors.js';
import type {KeyInput} from '../common/USKeyboardLayout.js';
import {createIncrementalIdGenerator} from '../util/incremental-id-generator.js';
import type {Point} from './ElementHandle.js';
/**
* @public
*/
export interface KeyDownOptions {
/**
* @deprecated Do not use. This is automatically handled.
*/
text?: string;
/**
* @deprecated Do not use. This is automatically handled.
*/
commands?: string[];
}
/**
* @public
*/
export interface KeyboardTypeOptions {
delay?: number;
}
/**
* @public
*/
export type KeyPressOptions = KeyDownOptions & KeyboardTypeOptions;
/**
* Keyboard provides an api for managing a virtual keyboard.
* The high level api is {@link Keyboard."type"},
* which takes raw characters and generates proper keydown, keypress/input,
* and keyup events on your page.
*
* @remarks
* For finer control, you can use {@link Keyboard.down},
* {@link Keyboard.up}, and {@link Keyboard.sendCharacter}
* to manually fire events as if they were generated from a real keyboard.
*
* On macOS, keyboard shortcuts like `⌘ A` -\> Select All do not work.
* See {@link https://github.com/puppeteer/puppeteer/issues/1313 | #1313}.
*
* @example
* An example of holding down `Shift` in order to select and delete some text:
*
* ```ts
* await page.keyboard.type('Hello World!');
* await page.keyboard.press('ArrowLeft');
*
* await page.keyboard.down('Shift');
* for (let i = 0; i < ' World'.length; i++)
* await page.keyboard.press('ArrowLeft');
* await page.keyboard.up('Shift');
*
* await page.keyboard.press('Backspace');
* // Result text will end up saying 'Hello!'
* ```
*
* @example
* An example of pressing `A`
*
* ```ts
* await page.keyboard.down('Shift');
* await page.keyboard.press('KeyA');
* await page.keyboard.up('Shift');
* ```
*
* @public
*/
export abstract class Keyboard {
/**
* @internal
*/
constructor() {}
/**
* Dispatches a `keydown` event.
*
* @remarks
* If `key` is a single character and no modifier keys besides `Shift`
* are being held down, a `keypress`/`input` event will also generated.
* The `text` option can be specified to force an input event to be generated.
* If `key` is a modifier key, `Shift`, `Meta`, `Control`, or `Alt`,
* subsequent key presses will be sent with that modifier active.
* To release the modifier key, use {@link Keyboard.up}.
*
* After the key is pressed once, subsequent calls to
* {@link Keyboard.down} will have
* {@link https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/repeat | repeat}
* set to true. To release the key, use {@link Keyboard.up}.
*
* Modifier keys DO influence {@link Keyboard.down}.
* Holding down `Shift` will type the text in upper case.
*
* @param key - Name of key to press, such as `ArrowLeft`.
* See {@link KeyInput} for a list of all key names.
*
* @param options - An object of options. Accepts text which, if specified,
* generates an input event with this text. Accepts commands which, if specified,
* is the commands of keyboard shortcuts,
* see {@link https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/editing/commands/editor_command_names.h | Chromium Source Code} for valid command names.
*/
abstract down(
key: KeyInput,
options?: Readonly<KeyDownOptions>,
): Promise<void>;
/**
* Dispatches a `keyup` event.
*
* @param key - Name of key to release, such as `ArrowLeft`.
* See {@link KeyInput | KeyInput}
* for a list of all key names.
*/
abstract up(key: KeyInput): Promise<void>;
/**
* Dispatches a `keypress` and `input` event.
* This does not send a `keydown` or `keyup` event.
*
* @remarks
* Modifier keys DO NOT effect {@link Keyboard.sendCharacter | Keyboard.sendCharacter}.
* Holding down `Shift` will not type the text in upper case.
*
* @example
*
* ```ts
* page.keyboard.sendCharacter('嗨');
* ```
*
* @param char - Character to send into the page.
*/
abstract sendCharacter(char: string): Promise<void>;
/**
* Sends a `keydown`, `keypress`/`input`,
* and `keyup` event for each character in the text.
*
* @remarks
* To press a special key, like `Control` or `ArrowDown`,
* use {@link Keyboard.press}.
*
* Modifier keys DO NOT effect `keyboard.type`.
* Holding down `Shift` will not type the text in upper case.
*
* @example
*
* ```ts
* await page.keyboard.type('Hello'); // Types instantly
* await page.keyboard.type('World', {delay: 100}); // Types slower, like a user
* ```
*
* @param text - A text to type into a focused element.
* @param options - An object of options. Accepts delay which,
* if specified, is the time to wait between `keydown` and `keyup` in milliseconds.
* Defaults to 0.
*/
abstract type(
text: string,
options?: Readonly<KeyboardTypeOptions>,
): Promise<void>;
/**
* Shortcut for {@link Keyboard.down}
* and {@link Keyboard.up}.
*
* @remarks
* If `key` is a single character and no modifier keys besides `Shift`
* are being held down, a `keypress`/`input` event will also generated.
* The `text` option can be specified to force an input event to be generated.
*
* Modifier keys DO effect {@link Keyboard.press}.
* Holding down `Shift` will type the text in upper case.
*
* @param key - Name of key to press, such as `ArrowLeft`.
* See {@link KeyInput} for a list of all key names.
*
* @param options - An object of options. Accepts text which, if specified,
* generates an input event with this text. Accepts delay which,
* if specified, is the time to wait between `keydown` and `keyup` in milliseconds.
* Defaults to 0. Accepts commands which, if specified,
* is the commands of keyboard shortcuts,
* see {@link https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/editing/commands/editor_command_names.h | Chromium Source Code} for valid command names.
*/
abstract press(
key: KeyInput,
options?: Readonly<KeyPressOptions>,
): Promise<void>;
}
/**
* @public
*/
export interface MouseOptions {
/**
* Determines which button will be pressed.
*
* @defaultValue `'left'`
*/
button?: MouseButton;
/**
* Determines the click count for the mouse event. This does not perform
* multiple clicks.
*
* @deprecated Use {@link MouseClickOptions.count}.
* @defaultValue `1`
*/
clickCount?: number;
}
/**
* @public
*/
export interface MouseClickOptions extends MouseOptions {
/**
* Time (in ms) to delay the mouse release after the mouse press.
*/
delay?: number;
/**
* Number of clicks to perform.
*
* @defaultValue `1`
*/
count?: number;
}
/**
* @public
*/
export interface MouseWheelOptions {
deltaX?: number;
deltaY?: number;
}
/**
* @public
*/
export interface MouseMoveOptions {
/**
* Determines the number of movements to make from the current mouse position
* to the new one.
*
* @defaultValue `1`
*/
steps?: number;
}
/**
* Enum of valid mouse buttons.
*
* @public
*/
export const MouseButton = Object.freeze({
Left: 'left',
Right: 'right',
Middle: 'middle',
Back: 'back',
Forward: 'forward',
}) satisfies Record<string, Protocol.Input.MouseButton>;
/**
* @public
*/
export type MouseButton = (typeof MouseButton)[keyof typeof MouseButton];
/**
* The Mouse class operates in main-frame CSS pixels
* relative to the top-left corner of the viewport.
*
* @remarks
* Every `page` object has its own Mouse, accessible with {@link Page.mouse}.
*
* @example
*
* ```ts
* // Using page.mouse to trace a 100x100 square.
* await page.mouse.move(0, 0);
* await page.mouse.down();
* await page.mouse.move(0, 100);
* await page.mouse.move(100, 100);
* await page.mouse.move(100, 0);
* await page.mouse.move(0, 0);
* await page.mouse.up();
* ```
*
* **Note**: The mouse events trigger synthetic `MouseEvent`s.
* This means that it does not fully replicate the functionality of what a normal user
* would be able to do with their mouse.
*
* For example, dragging and selecting text is not possible using `page.mouse`.
* Instead, you can use the {@link https://developer.mozilla.org/en-US/docs/Web/API/DocumentOrShadowRoot/getSelection | `DocumentOrShadowRoot.getSelection()`} functionality implemented in the platform.
*
* @example
* For example, if you want to select all content between nodes:
*
* ```ts
* await page.evaluate(
* (from, to) => {
* const selection = from.getRootNode().getSelection();
* const range = document.createRange();
* range.setStartBefore(from);
* range.setEndAfter(to);
* selection.removeAllRanges();
* selection.addRange(range);
* },
* fromJSHandle,
* toJSHandle,
* );
* ```
*
* If you then would want to copy-paste your selection, you can use the clipboard api:
*
* ```ts
* // The clipboard api does not allow you to copy, unless the tab is focused.
* await page.bringToFront();
* await page.evaluate(() => {
* // Copy the selected content to the clipboard
* document.execCommand('copy');
* // Obtain the content of the clipboard as a string
* return navigator.clipboard.readText();
* });
* ```
*
* **Note**: If you want access to the clipboard API,
* you have to give it permission to do so:
*
* ```ts
* await browser
* .defaultBrowserContext()
* .overridePermissions('<your origin>', [
* 'clipboard-read',
* 'clipboard-write',
* ]);
* ```
*
* @public
*/
export abstract class Mouse {
/**
* @internal
*/
constructor() {}
/**
* Resets the mouse to the default state: No buttons pressed; position at
* (0,0).
*/
abstract reset(): Promise<void>;
/**
* Moves the mouse to the given coordinate.
*
* @param x - Horizontal position of the mouse.
* @param y - Vertical position of the mouse.
* @param options - Options to configure behavior.
*/
abstract move(
x: number,
y: number,
options?: Readonly<MouseMoveOptions>,
): Promise<void>;
/**
* Presses the mouse.
*
* @param options - Options to configure behavior.
*/
abstract down(options?: Readonly<MouseOptions>): Promise<void>;
/**
* Releases the mouse.
*
* @param options - Options to configure behavior.
*/
abstract up(options?: Readonly<MouseOptions>): Promise<void>;
/**
* Shortcut for `mouse.move`, `mouse.down` and `mouse.up`.
*
* @param x - Horizontal position of the mouse.
* @param y - Vertical position of the mouse.
* @param options - Options to configure behavior.
*/
abstract click(
x: number,
y: number,
options?: Readonly<MouseClickOptions>,
): Promise<void>;
/**
* Dispatches a `mousewheel` event.
* @param options - Optional: `MouseWheelOptions`.
*
* @example
* An example of zooming into an element:
*
* ```ts
* await page.goto(
* 'https://mdn.mozillademos.org/en-US/docs/Web/API/Element/wheel_event$samples/Scaling_an_element_via_the_wheel?revision=1587366',
* );
*
* const elem = await page.$('div');
* const boundingBox = await elem.boundingBox();
* await page.mouse.move(
* boundingBox.x + boundingBox.width / 2,
* boundingBox.y + boundingBox.height / 2,
* );
*
* await page.mouse.wheel({deltaY: -100});
* ```
*/
abstract wheel(options?: Readonly<MouseWheelOptions>): Promise<void>;
/**
* Dispatches a `drag` event.
* @param start - starting point for drag
* @param target - point to drag to
*/
abstract drag(start: Point, target: Point): Promise<Protocol.Input.DragData>;
/**
* Dispatches a `dragenter` event.
* @param target - point for emitting `dragenter` event
* @param data - drag data containing items and operations mask
*/
abstract dragEnter(
target: Point,
data: Protocol.Input.DragData,
): Promise<void>;
/**
* Dispatches a `dragover` event.
* @param target - point for emitting `dragover` event
* @param data - drag data containing items and operations mask
*/
abstract dragOver(
target: Point,
data: Protocol.Input.DragData,
): Promise<void>;
/**
* Performs a dragenter, dragover, and drop in sequence.
* @param target - point to drop on
* @param data - drag data containing items and operations mask
*/
abstract drop(target: Point, data: Protocol.Input.DragData): Promise<void>;
/**
* Performs a drag, dragenter, dragover, and drop in sequence.
* @param start - point to drag from
* @param target - point to drop on
* @param options - An object of options. Accepts delay which,
* if specified, is the time to wait between `dragover` and `drop` in milliseconds.
* Defaults to 0.
*/
abstract dragAndDrop(
start: Point,
target: Point,
options?: {delay?: number},
): Promise<void>;
}
/**
* The TouchHandle interface exposes methods to manipulate touches that have been started
* @public
*/
export interface TouchHandle {
/**
* Dispatches a `touchMove` event for this touch.
* @param x - Horizontal position of the move.
* @param y - Vertical position of the move.
*/
move(x: number, y: number): Promise<void>;
/**
* Dispatches a `touchend` event for this touch.
*/
end(): Promise<void>;
}
/**
* The Touchscreen class exposes touchscreen events.
* @public
*/
export abstract class Touchscreen {
/**
* @internal
*/
idGenerator = createIncrementalIdGenerator();
/**
* @internal
*/
touches: TouchHandle[] = [];
/**
* @internal
*/
constructor() {}
/**
* @internal
*/
removeHandle(handle: TouchHandle): void {
const index = this.touches.indexOf(handle);
if (index === -1) {
return;
}
this.touches.splice(index, 1);
}
/**
* Dispatches a `touchstart` and `touchend` event.
* @param x - Horizontal position of the tap.
* @param y - Vertical position of the tap.
*/
async tap(x: number, y: number): Promise<void> {
const touch = await this.touchStart(x, y);
await touch.end();
}
/**
* Dispatches a `touchstart` event.
* @param x - Horizontal position of the tap.
* @param y - Vertical position of the tap.
* @returns A handle for the touch that was started.
*/
abstract touchStart(x: number, y: number): Promise<TouchHandle>;
/**
* Dispatches a `touchMove` event on the first touch that is active.
* @param x - Horizontal position of the move.
* @param y - Vertical position of the move.
*
* @remarks
*
* Not every `touchMove` call results in a `touchmove` event being emitted,
* depending on the browser's optimizations. For example, Chrome
* {@link https://developer.chrome.com/blog/a-more-compatible-smoother-touch/#chromes-new-model-the-throttled-async-touchmove-model | throttles}
* touch move events.
*/
async touchMove(x: number, y: number): Promise<void> {
const touch = this.touches[0];
if (!touch) {
throw new TouchError('Must start a new Touch first');
}
return await touch.move(x, y);
}
/**
* Dispatches a `touchend` event on the first touch that is active.
*/
async touchEnd(): Promise<void> {
const touch = this.touches.shift();
if (!touch) {
throw new TouchError('Must start a new Touch first');
}
await touch.end();
}
}

24
node_modules/puppeteer-core/src/api/Issue.ts generated vendored Normal file
View File

@@ -0,0 +1,24 @@
/**
* @license
* Copyright 2026 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type Protocol from 'devtools-protocol';
/**
* The Issue interface represents a DevTools issue.
*
* @public
*/
export interface Issue {
/**
* The code of the issue.
*/
code: string;
/**
* The details of the issue.
*/
details: Protocol.Audits.InspectorIssueDetails;
}

212
node_modules/puppeteer-core/src/api/JSHandle.ts generated vendored Normal file
View File

@@ -0,0 +1,212 @@
/**
* @license
* Copyright 2023 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type Protocol from 'devtools-protocol';
import type {EvaluateFuncWith, HandleFor, HandleOr} from '../common/types.js';
import {debugError, withSourcePuppeteerURLIfNone} from '../common/util.js';
import {moveable, throwIfDisposed} from '../util/decorators.js';
import {disposeSymbol, asyncDisposeSymbol} from '../util/disposable.js';
import type {ElementHandle} from './ElementHandle.js';
import type {Realm} from './Realm.js';
/**
* Represents a reference to a JavaScript object. Instances can be created using
* {@link Page.evaluateHandle}.
*
* Handles prevent the referenced JavaScript object from being garbage-collected
* unless the handle is purposely {@link JSHandle.dispose | disposed}. JSHandles
* are auto-disposed when their associated frame is navigated away or the parent
* context gets destroyed.
*
* Handles can be used as arguments for any evaluation function such as
* {@link Page.$eval}, {@link Page.evaluate}, and {@link Page.evaluateHandle}.
* They are resolved to their referenced object.
*
* @example
*
* ```ts
* const windowHandle = await page.evaluateHandle(() => window);
* ```
*
* @public
*/
@moveable
export abstract class JSHandle<T = unknown> {
declare move: () => this;
/**
* Used for nominally typing {@link JSHandle}.
*/
declare _?: T;
/**
* @internal
*/
constructor() {}
/**
* @internal
*/
abstract get realm(): Realm;
/**
* @internal
*/
abstract get disposed(): boolean;
/**
* Evaluates the given function with the current handle as its first argument.
*/
async evaluate<
Params extends unknown[],
Func extends EvaluateFuncWith<T, Params> = EvaluateFuncWith<T, Params>,
>(
pageFunction: Func | string,
...args: Params
): Promise<Awaited<ReturnType<Func>>> {
pageFunction = withSourcePuppeteerURLIfNone(
this.evaluate.name,
pageFunction,
);
return await this.realm.evaluate(pageFunction, this, ...args);
}
/**
* Evaluates the given function with the current handle as its first argument.
*
*/
async evaluateHandle<
Params extends unknown[],
Func extends EvaluateFuncWith<T, Params> = EvaluateFuncWith<T, Params>,
>(
pageFunction: Func | string,
...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
pageFunction = withSourcePuppeteerURLIfNone(
this.evaluateHandle.name,
pageFunction,
);
return await this.realm.evaluateHandle(pageFunction, this, ...args);
}
/**
* Fetches a single property from the referenced object.
*/
getProperty<K extends keyof T>(
propertyName: HandleOr<K>,
): Promise<HandleFor<T[K]>>;
getProperty(propertyName: string): Promise<JSHandle<unknown>>;
/**
* @internal
*/
@throwIfDisposed()
async getProperty<K extends keyof T>(
propertyName: HandleOr<K>,
): Promise<HandleFor<T[K]>> {
return await this.evaluateHandle((object, propertyName) => {
return object[propertyName as K];
}, propertyName);
}
/**
* Gets a map of handles representing the properties of the current handle.
*
* @example
*
* ```ts
* const listHandle = await page.evaluateHandle(() => document.body.children);
* const properties = await listHandle.getProperties();
* const children = [];
* for (const property of properties.values()) {
* const element = property.asElement();
* if (element) {
* children.push(element);
* }
* }
* children; // holds elementHandles to all children of document.body
* ```
*/
@throwIfDisposed()
async getProperties(): Promise<Map<string, JSHandle>> {
const propertyNames = await this.evaluate(object => {
const enumerableProperties = [];
const descriptors = Object.getOwnPropertyDescriptors(object);
for (const propertyName in descriptors) {
if (descriptors[propertyName]?.enumerable) {
enumerableProperties.push(propertyName);
}
}
return enumerableProperties;
});
const map = new Map<string, JSHandle>();
const results = await Promise.all(
propertyNames.map(key => {
return this.getProperty(key);
}),
);
for (const [key, value] of Object.entries(propertyNames)) {
using handle = results[key as any];
if (handle) {
map.set(value, handle.move());
}
}
return map;
}
/**
* A vanilla object representing the serializable portions of the
* referenced object.
* @throws Throws if the object cannot be serialized due to circularity.
*
* @remarks
* If the object has a `toJSON` function, it **will not** be called.
*/
abstract jsonValue(): Promise<T>;
/**
* Either `null` or the handle itself if the handle is an
* instance of {@link ElementHandle}.
*/
abstract asElement(): ElementHandle<Node> | null;
/**
* Releases the object referenced by the handle for garbage collection.
*/
abstract dispose(): Promise<void>;
/**
* Returns a string representation of the JSHandle.
*
* @remarks
* Useful during debugging.
*/
abstract toString(): string;
/**
* @internal
*/
abstract get id(): string | undefined;
/**
* Provides access to the
* {@link https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#type-RemoteObject | Protocol.Runtime.RemoteObject}
* backing this handle.
*/
abstract remoteObject(): Protocol.Runtime.RemoteObject;
/** @internal */
[disposeSymbol](): void {
return void this.dispose().catch(debugError);
}
/** @internal */
[asyncDisposeSymbol](): Promise<void> {
return this.dispose();
}
}

3328
node_modules/puppeteer-core/src/api/Page.ts generated vendored Normal file

File diff suppressed because it is too large Load Diff

207
node_modules/puppeteer-core/src/api/Realm.ts generated vendored Normal file
View File

@@ -0,0 +1,207 @@
/**
* @license
* Copyright 2023 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {TimeoutSettings} from '../common/TimeoutSettings.js';
import type {EvaluateFunc, HandleFor} from '../common/types.js';
import {TaskManager, WaitTask} from '../common/WaitTask.js';
import {disposeSymbol} from '../util/disposable.js';
import type {ElementHandle} from './ElementHandle.js';
import type {Environment} from './Environment.js';
import type {Extension} from './Extension.js';
import type {JSHandle} from './JSHandle.js';
/**
* @public
*/
export abstract class Realm {
/** @internal */
protected readonly timeoutSettings: TimeoutSettings;
/** @internal */
readonly taskManager = new TaskManager();
/** @internal */
constructor(timeoutSettings: TimeoutSettings) {
this.timeoutSettings = timeoutSettings;
}
/** @internal */
abstract get environment(): Environment;
/**
* Returns the origin that created the Realm.
* For example, if the realm was created by an extension content script,
* this will return the origin of the extension
* (e.g., `chrome-extension://<extension-id>`).
*
* @experimental
* @example
* `chrome-extension://<chrome-extension-id>`
*/
abstract get origin(): string | undefined;
/**
* Returns the {@link Extension} that created this realm, if applicable.
* This is typically populated when the realm was created by an extension
* content script injected into a page.
*
* @returns A promise that resolves to the {@link Extension}
* or `null` if not created by an extension.
* @experimental
*/
abstract extension(): Promise<Extension | null>;
/** @internal */
abstract adoptHandle<T extends JSHandle<Node>>(handle: T): Promise<T>;
/** @internal */
abstract transferHandle<T extends JSHandle<Node>>(handle: T): Promise<T>;
/**
* Evaluates a function in the realm's context and returns a
* {@link JSHandle} to the result.
*
* If the function passed to `realm.evaluateHandle` returns a Promise,
* the method will wait for the promise to resolve and return its value.
*
* {@link JSHandle} instances can be passed as arguments to the function.
*
* @example
*
* ```ts
* const aHandle = await realm.evaluateHandle(() => document.body);
* const resultHandle = await realm.evaluateHandle(
* body => body.innerHTML,
* aHandle,
* );
* ```
*
* @param pageFunction - A function to be evaluated in the realm.
* @param args - Arguments to be passed to the `pageFunction`.
* @returns A promise that resolves to a {@link JSHandle} containing
* the result.
* @public
*/
abstract evaluateHandle<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
>(
pageFunction: Func | string,
...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>>;
/**
* Evaluates a function in the realm's context and returns the
* resulting value.
*
* If the function passed to `realm.evaluate` returns a Promise,
* the method will wait for the promise to resolve and return its value.
*
* {@link JSHandle} instances can be passed as arguments to the function.
*
* @example
*
* ```ts
* const result = await realm.evaluate(() => {
* return Promise.resolve(8 * 7);
* });
* console.log(result); // prints "56"
* ```
*
* @param pageFunction - A function to be evaluated in the realm.
* @param args - Arguments to be passed to the `pageFunction`.
* @returns A promise that resolves to the return value of the function.
* @public
*/
abstract evaluate<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
>(
pageFunction: Func | string,
...args: Params
): Promise<Awaited<ReturnType<Func>>>;
/**
* Waits for a function to return a truthy value when evaluated in
* the realm's context.
*
* Arguments can be passed from Node.js to `pageFunction`.
*
* @example
*
* ```ts
* const selector = '.foo';
* await realm.waitForFunction(
* selector => !!document.querySelector(selector),
* {},
* selector,
* );
* ```
*
* @param pageFunction - A function to evaluate in the realm.
* @param options - Options for polling and timeouts.
* @param args - Arguments to pass to the function.
* @returns A promise that resolves when the function returns a truthy
* value.
* @public
*/
async waitForFunction<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
>(
pageFunction: Func | string,
options: {
polling?: 'raf' | 'mutation' | number;
timeout?: number;
root?: ElementHandle<Node>;
signal?: AbortSignal;
} = {},
...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
const {
polling = 'raf',
timeout = this.timeoutSettings.timeout(),
root,
signal,
} = options;
if (typeof polling === 'number' && polling < 0) {
throw new Error('Cannot poll with non-positive interval');
}
const waitTask = new WaitTask(
this,
{
polling,
root,
timeout,
signal,
},
pageFunction as unknown as
| ((...args: unknown[]) => Promise<Awaited<ReturnType<Func>>>)
| string,
...args,
);
return await waitTask.result;
}
/** @internal */
abstract adoptBackendNode(backendNodeId?: number): Promise<JSHandle<Node>>;
/** @internal */
get disposed(): boolean {
return this.#disposed;
}
#disposed = false;
/** @internal */
dispose(): void {
this.#disposed = true;
this.taskManager.terminateAll(
new Error('waitForFunction failed: frame got detached.'),
);
}
/** @internal */
[disposeSymbol](): void {
this.dispose();
}
}

95
node_modules/puppeteer-core/src/api/Target.ts generated vendored Normal file
View File

@@ -0,0 +1,95 @@
/**
* @license
* Copyright 2023 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {Browser} from './Browser.js';
import type {BrowserContext} from './BrowserContext.js';
import type {CDPSession} from './CDPSession.js';
import type {Page} from './Page.js';
import type {WebWorker} from './WebWorker.js';
/**
* @public
*/
export enum TargetType {
PAGE = 'page',
BACKGROUND_PAGE = 'background_page',
SERVICE_WORKER = 'service_worker',
SHARED_WORKER = 'shared_worker',
BROWSER = 'browser',
WEBVIEW = 'webview',
OTHER = 'other',
/**
* @internal
*/
TAB = 'tab',
}
/**
* Target represents a
* {@link https://chromedevtools.github.io/devtools-protocol/tot/Target/ | CDP target}.
* In CDP a target is something that can be debugged such a frame, a page or a
* worker.
* @public
*/
export abstract class Target {
/**
* @internal
*/
protected constructor() {}
/**
* If the target is not of type `"service_worker"` or `"shared_worker"`, returns `null`.
*/
async worker(): Promise<WebWorker | null> {
return null;
}
/**
* If the target is not of type `"page"`, `"webview"` or `"background_page"`,
* returns `null`.
*/
async page(): Promise<Page | null> {
return null;
}
/**
* Forcefully creates a page for a target of any type. It is useful if you
* want to handle a CDP target of type `other` as a page. If you deal with a
* regular page target, use {@link Target.page}.
*/
abstract asPage(): Promise<Page>;
abstract url(): string;
/**
* Creates a Chrome Devtools Protocol session attached to the target.
*/
abstract createCDPSession(): Promise<CDPSession>;
/**
* Identifies what kind of target this is.
*
* @remarks
*
* See {@link https://developer.chrome.com/extensions/background_pages | docs} for more info about background pages.
*/
abstract type(): TargetType;
/**
* Get the browser the target belongs to.
*/
abstract browser(): Browser;
/**
* Get the browser context the target belongs to.
*/
abstract browserContext(): BrowserContext;
/**
* Get the target that opened this target. Top-level targets return `null`.
*/
abstract opener(): Target | undefined;
}

161
node_modules/puppeteer-core/src/api/WebWorker.ts generated vendored Normal file
View File

@@ -0,0 +1,161 @@
/**
* @license
* Copyright 2018 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {ConsoleMessage} from '../common/ConsoleMessage.js';
import {UnsupportedOperation} from '../common/Errors.js';
import type {EventType} from '../common/EventEmitter.js';
import {EventEmitter} from '../common/EventEmitter.js';
import {TimeoutSettings} from '../common/TimeoutSettings.js';
import type {EvaluateFunc, HandleFor} from '../common/types.js';
import {withSourcePuppeteerURLIfNone} from '../common/util.js';
import type {CDPSession} from './CDPSession.js';
import type {Realm} from './Realm.js';
/**
* @public
*/
export enum WebWorkerEvent {
/**
* Emitted when the worker calls a console API.
*/
Console = 'console',
/**
* Emitted when the worker throws an exception.
*/
Error = 'error',
}
/**
* @public
*/
export interface WebWorkerEvents extends Record<EventType, unknown> {
[WebWorkerEvent.Console]: ConsoleMessage;
[WebWorkerEvent.Error]: Error;
}
/**
* This class represents a
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API | WebWorker}.
*
* @remarks
* The events `workercreated` and `workerdestroyed` are emitted on the page
* object to signal the worker lifecycle.
*
* @example
*
* ```ts
* page.on('workercreated', worker =>
* console.log('Worker created: ' + worker.url()),
* );
* page.on('workerdestroyed', worker =>
* console.log('Worker destroyed: ' + worker.url()),
* );
*
* console.log('Current workers:');
* for (const worker of page.workers()) {
* console.log(' ' + worker.url());
* }
* ```
*
* @public
*/
export abstract class WebWorker extends EventEmitter<WebWorkerEvents> {
/**
* @internal
*/
readonly timeoutSettings = new TimeoutSettings();
readonly #url: string;
/**
* @internal
*/
constructor(url: string) {
super();
this.#url = url;
}
/**
* @internal
*/
abstract mainRealm(): Realm;
/**
* The URL of this web worker.
*/
url(): string {
return this.#url;
}
/**
* The CDP session client the WebWorker belongs to.
*/
abstract get client(): CDPSession;
/**
* Evaluates a given function in the {@link WebWorker | worker}.
*
* @remarks If the given function returns a promise,
* {@link WebWorker.evaluate | evaluate} will wait for the promise to resolve.
*
* As a rule of thumb, if the return value of the given function is more
* complicated than a JSON object (e.g. most classes), then
* {@link WebWorker.evaluate | evaluate} will _likely_ return some truncated
* value (or `{}`). This is because we are not returning the actual return
* value, but a deserialized version as a result of transferring the return
* value through a protocol to Puppeteer.
*
* In general, you should use
* {@link WebWorker.evaluateHandle | evaluateHandle} if
* {@link WebWorker.evaluate | evaluate} cannot serialize the return value
* properly or you need a mutable {@link JSHandle | handle} to the return
* object.
*
* @param func - Function to be evaluated.
* @param args - Arguments to pass into `func`.
* @returns The result of `func`.
*/
async evaluate<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
>(func: Func | string, ...args: Params): Promise<Awaited<ReturnType<Func>>> {
func = withSourcePuppeteerURLIfNone(this.evaluate.name, func);
return await this.mainRealm().evaluate(func, ...args);
}
/**
* Evaluates a given function in the {@link WebWorker | worker}.
*
* @remarks If the given function returns a promise,
* {@link WebWorker.evaluate | evaluate} will wait for the promise to resolve.
*
* In general, you should use
* {@link WebWorker.evaluateHandle | evaluateHandle} if
* {@link WebWorker.evaluate | evaluate} cannot serialize the return value
* properly or you need a mutable {@link JSHandle | handle} to the return
* object.
*
* @param func - Function to be evaluated.
* @param args - Arguments to pass into `func`.
* @returns A {@link JSHandle | handle} to the return value of `func`.
*/
async evaluateHandle<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
>(
func: Func | string,
...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
func = withSourcePuppeteerURLIfNone(this.evaluateHandle.name, func);
return await this.mainRealm().evaluateHandle(func, ...args);
}
async close(): Promise<void> {
throw new UnsupportedOperation('WebWorker.close() is not supported');
}
}

26
node_modules/puppeteer-core/src/api/api.ts generated vendored Normal file
View File

@@ -0,0 +1,26 @@
/**
* @license
* Copyright 2022 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
export type * from './BluetoothEmulation.js';
export * from './Browser.js';
export * from './BrowserContext.js';
export * from './CDPSession.js';
export * from './DeviceRequestPrompt.js';
export * from './Dialog.js';
export * from './ElementHandle.js';
export * from './Extension.js';
export type * from './Environment.js';
export * from './Frame.js';
export * from './HTTPRequest.js';
export * from './HTTPResponse.js';
export * from './Input.js';
export type * from './Issue.js';
export * from './JSHandle.js';
export * from './Page.js';
export * from './Realm.js';
export * from './Target.js';
export * from './WebWorker.js';
export * from './locators/locators.js';

1149
node_modules/puppeteer-core/src/api/locators/locators.ts generated vendored Normal file

File diff suppressed because it is too large Load Diff

208
node_modules/puppeteer-core/src/bidi/BidiOverCdp.ts generated vendored Normal file
View File

@@ -0,0 +1,208 @@
/**
* @license
* Copyright 2023 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import * as BidiMapper from 'chromium-bidi/lib/cjs/bidiMapper/BidiMapper.js';
import type {ProtocolMapping} from 'devtools-protocol/types/protocol-mapping.js';
import type {CDPEvents, CDPSession} from '../api/CDPSession.js';
import type {Connection as CdpConnection} from '../cdp/Connection.js';
import {debug} from '../common/Debug.js';
import {TargetCloseError} from '../common/Errors.js';
import type {Handler} from '../common/EventEmitter.js';
import {BidiConnection} from './Connection.js';
const bidiServerLogger = (prefix: string, ...args: unknown[]): void => {
debug(`bidi:${prefix}`)(args);
};
/**
* @internal
*/
export async function connectBidiOverCdp(
cdp: CdpConnection,
): Promise<BidiConnection> {
const transportBiDi = new NoOpTransport();
const cdpConnectionAdapter = new CdpConnectionAdapter(cdp);
const pptrTransport = {
send(message: string): void {
// Forwards a BiDi command sent by Puppeteer to the input of the BidiServer.
transportBiDi.emitMessage(JSON.parse(message));
},
close(): void {
bidiServer.close();
cdpConnectionAdapter.close();
cdp.dispose();
},
onmessage(_message: string): void {
// The method is overridden by the Connection.
},
};
transportBiDi.on('bidiResponse', (message: object) => {
// Forwards a BiDi event sent by BidiServer to Puppeteer.
pptrTransport.onmessage(JSON.stringify(message));
});
const pptrBiDiConnection = new BidiConnection(
cdp.url(),
pptrTransport,
cdp._idGenerator,
cdp.delay,
cdp.timeout,
);
const bidiServer = await BidiMapper.BidiServer.createAndStart(
transportBiDi,
cdpConnectionAdapter,
cdpConnectionAdapter.browserClient(),
/* selfTargetId= */ '',
undefined,
bidiServerLogger,
);
return pptrBiDiConnection;
}
/**
* Manages CDPSessions for BidiServer.
* @internal
*/
class CdpConnectionAdapter {
#cdp: CdpConnection;
#adapters = new Map<CDPSession, CDPClientAdapter<CDPSession>>();
#browserCdpConnection: CDPClientAdapter<CdpConnection>;
constructor(cdp: CdpConnection) {
this.#cdp = cdp;
this.#browserCdpConnection = new CDPClientAdapter(cdp);
}
browserClient(): CDPClientAdapter<CdpConnection> {
return this.#browserCdpConnection;
}
getCdpClient(id: string) {
const session = this.#cdp.session(id);
if (!session) {
throw new Error(`Unknown CDP session with id ${id}`);
}
if (!this.#adapters.has(session)) {
const adapter = new CDPClientAdapter(
session,
id,
this.#browserCdpConnection,
);
this.#adapters.set(session, adapter);
return adapter;
}
return this.#adapters.get(session)!;
}
close() {
this.#browserCdpConnection.close();
for (const adapter of this.#adapters.values()) {
adapter.close();
}
}
}
/**
* Wrapper on top of CDPSession/CDPConnection to satisfy CDP interface that
* BidiServer needs.
*
* @internal
*/
class CDPClientAdapter<T extends CDPSession | CdpConnection>
extends BidiMapper.EventEmitter<CDPEvents>
implements BidiMapper.CdpClient
{
#closed = false;
#client: T;
sessionId: string | undefined = undefined;
#browserClient?: BidiMapper.CdpClient;
constructor(
client: T,
sessionId?: string,
browserClient?: BidiMapper.CdpClient,
) {
super();
this.#client = client;
this.sessionId = sessionId;
this.#browserClient = browserClient;
this.#client.on('*', this.#forwardMessage as Handler<any>);
}
browserClient(): BidiMapper.CdpClient {
return this.#browserClient!;
}
#forwardMessage = <T extends keyof CDPEvents>(
method: T,
event: CDPEvents[T],
) => {
this.emit(method, event);
};
async sendCommand<T extends keyof ProtocolMapping.Commands>(
method: T,
...params: ProtocolMapping.Commands[T]['paramsType']
): Promise<ProtocolMapping.Commands[T]['returnType']> {
if (this.#closed) {
return;
}
try {
return await this.#client.send(method, ...params);
} catch (err) {
if (this.#closed) {
return;
}
throw err;
}
}
close() {
this.#client.off('*', this.#forwardMessage as Handler<any>);
this.#closed = true;
}
isCloseError(error: unknown): boolean {
return error instanceof TargetCloseError;
}
}
/**
* This transport is given to the BiDi server instance and allows Puppeteer
* to send and receive commands to the BiDiServer.
* @internal
*/
class NoOpTransport
extends BidiMapper.EventEmitter<{
bidiResponse: any;
}>
implements BidiMapper.BidiTransport
{
#onMessage: (message: any) => Promise<void> | void = async (
_m: any,
): Promise<void> => {
return;
};
emitMessage(message: any) {
void this.#onMessage(message);
}
setOnMessage(onMessage: (message: any) => Promise<void> | void): void {
this.#onMessage = onMessage;
}
async sendMessage(message: any): Promise<void> {
this.emit('bidiResponse', message);
}
close() {
this.#onMessage = async (_m: any): Promise<void> => {
return;
};
}
}

View File

@@ -0,0 +1,52 @@
/**
* @license
* Copyright 2025 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {
AdapterState,
BluetoothEmulation,
PreconnectedPeripheral,
} from '../api/BluetoothEmulation.js';
import type {Session} from './core/Session.js';
/**
* @internal
*/
export class BidiBluetoothEmulation implements BluetoothEmulation {
readonly #session: Session;
readonly #contextId: string;
constructor(contextId: string, session: Session) {
this.#contextId = contextId;
this.#session = session;
}
async emulateAdapter(state: AdapterState, leSupported = true): Promise<void> {
await this.#session.send('bluetooth.simulateAdapter', {
context: this.#contextId,
state,
leSupported,
});
}
async disableEmulation(): Promise<void> {
await this.#session.send('bluetooth.disableSimulation', {
context: this.#contextId,
});
}
async simulatePreconnectedPeripheral(
preconnectedPeripheral: PreconnectedPeripheral,
): Promise<void> {
await this.#session.send('bluetooth.simulatePreconnectedPeripheral', {
context: this.#contextId,
address: preconnectedPeripheral.address,
name: preconnectedPeripheral.name,
manufacturerData: preconnectedPeripheral.manufacturerData,
knownServiceUuids: preconnectedPeripheral.knownServiceUuids,
});
}
}

390
node_modules/puppeteer-core/src/bidi/Browser.ts generated vendored Normal file
View File

@@ -0,0 +1,390 @@
/**
* @license
* Copyright 2022 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {ChildProcess} from 'node:child_process';
import * as Bidi from 'webdriver-bidi-protocol';
import type {BrowserEvents, CreatePageOptions} from '../api/Browser.js';
import {
Browser,
BrowserEvent,
type BrowserCloseCallback,
type BrowserContextOptions,
type ScreenInfo,
type AddScreenParams,
type WindowBounds,
type WindowId,
type DebugInfo,
} from '../api/Browser.js';
import {BrowserContextEvent} from '../api/BrowserContext.js';
import type {Extension} from '../api/Extension.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 {ProtocolError, UnsupportedOperation} from '../common/Errors.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, CdpEvent} 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;
networkEnabled: boolean;
issuesEnabled: boolean;
}
/**
* @internal
*/
export class BidiBrowser extends Browser {
readonly protocol = 'webDriverBiDi';
static readonly subscribeModules: [string, ...string[]] = [
'browsingContext',
'network',
'log',
'script',
'input',
];
static readonly subscribeCdpEvents: Array<CdpEvent['method']> = [
// Coverage
'goog:cdp.Debugger.scriptParsed',
'goog:cdp.CSS.styleSheetAdded',
'goog:cdp.Runtime.executionContextsCleared',
// Tracing
'goog:cdp.Tracing.tracingComplete',
// TODO: subscribe to all CDP events in the future.
'goog:cdp.Network.requestWillBeSent',
'goog:cdp.Debugger.scriptParsed',
'goog: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,
// Puppeteer with WebDriver BiDi does not support prerendering
// yet because WebDriver BiDi behavior is not specified. See
// https://github.com/w3c/webdriver-bidi/issues/321.
'goog:prerenderingDisabled': true,
// TODO: remove after Puppeteer rolled Chrome to 142 after Oct 28, 2025.
'goog:disableNetworkDurableMessages': true,
},
});
// Subscribe to all WebDriver BiDi events. Also subscribe to CDP events if CDP
// connection is available.
await session.subscribe(
(opts.cdpConnection
? [...BidiBrowser.subscribeModules, ...BidiBrowser.subscribeCdpEvents]
: BidiBrowser.subscribeModules
).filter(module => {
if (!opts.networkEnabled) {
return (
module !== 'network' &&
module !== 'goog:cdp.Network.requestWillBeSent'
);
}
return true;
}) as [string, ...string[]],
);
await Promise.all(
[Bidi.Network.DataType.Request, Bidi.Network.DataType.Response].map(
// Data collectors might be not implemented for specific data type, so create them
// separately and ignore protocol errors.
async dataType => {
try {
await session.send('network.addDataCollector', {
dataTypes: [dataType],
// Buffer size of 20 MB is equivalent to the CDP:
maxEncodedDataSize: 20_000_000,
});
} catch (err) {
if (err instanceof ProtocolError) {
debugError(err);
} else {
throw err;
}
}
},
),
);
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;
#networkEnabled: boolean;
#issuesEnabled: boolean;
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;
this.#networkEnabled = opts.networkEnabled;
this.#issuesEnabled = opts.issuesEnabled;
}
#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(options);
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(options?: CreatePageOptions): Promise<Page> {
return this.defaultBrowserContext().newPage(options);
}
override installExtension(path: string): Promise<string> {
return this.#browserCore.installExtension(path);
}
override async uninstallExtension(id: string): Promise<void> {
await this.#browserCore.uninstallExtension(id);
}
override screens(): Promise<ScreenInfo[]> {
throw new UnsupportedOperation();
}
override addScreen(_params: AddScreenParams): Promise<ScreenInfo> {
throw new UnsupportedOperation();
}
override removeScreen(_screenId: string): Promise<void> {
throw new UnsupportedOperation();
}
override async getWindowBounds(windowId: WindowId): Promise<WindowBounds> {
const clientWindowInfo =
await this.#browserCore.getClientWindowInfo(windowId);
return {
left: clientWindowInfo.x,
top: clientWindowInfo.y,
width: clientWindowInfo.width,
height: clientWindowInfo.height,
windowState: clientWindowInfo.state,
};
}
override async setWindowBounds(
windowId: WindowId,
windowBounds: WindowBounds,
): Promise<void> {
let params: Bidi.Browser.SetClientWindowStateParameters | undefined;
const windowState = windowBounds.windowState ?? 'normal';
if (windowState === 'normal') {
params = {
clientWindow: windowId,
state: 'normal',
x: windowBounds.left,
y: windowBounds.top,
width: windowBounds.width,
height: windowBounds.height,
};
} else {
params = {
clientWindow: windowId,
state: windowState,
};
}
await this.#browserCore.setClientWindowState(params);
}
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(),
};
}
override isNetworkEnabled(): boolean {
return this.#networkEnabled;
}
override extensions(): Promise<Map<string, Extension>> {
throw new UnsupportedOperation();
}
override isIssuesEnabled(): boolean {
return this.#issuesEnabled;
}
}

View File

@@ -0,0 +1,132 @@
/**
* @license
* Copyright 2023 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {BrowserCloseCallback} from '../api/Browser.js';
import {Connection} from '../cdp/Connection.js';
import type {ConnectionTransport} from '../common/ConnectionTransport.js';
import type {ConnectOptions} from '../common/ConnectOptions.js';
import {ProtocolError, UnsupportedOperation} from '../common/Errors.js';
import {debugError, DEFAULT_VIEWPORT} from '../common/util.js';
import {createIncrementalIdGenerator} from '../util/incremental-id-generator.js';
import type {BidiBrowser} from './Browser.js';
import type {BidiConnection} from './Connection.js';
/**
* Users should never call this directly; it's called when calling `puppeteer.connect`
* with `protocol: 'webDriverBiDi'`. This method attaches Puppeteer to an existing browser
* instance. First it tries to connect to the browser using pure BiDi. If the protocol is
* not supported, connects to the browser using BiDi over CDP.
*
* @internal
*/
export async function _connectToBiDiBrowser(
connectionTransport: ConnectionTransport,
url: string,
options: ConnectOptions,
): Promise<BidiBrowser> {
const {
acceptInsecureCerts = false,
networkEnabled = true,
issuesEnabled = true,
defaultViewport = DEFAULT_VIEWPORT,
} = options;
const {bidiConnection, cdpConnection, closeCallback} =
await getBiDiConnection(connectionTransport, url, options);
const BiDi = await import(/* webpackIgnore: true */ './bidi.js');
const bidiBrowser = await BiDi.BidiBrowser.create({
connection: bidiConnection,
cdpConnection,
closeCallback,
process: undefined,
defaultViewport: defaultViewport,
acceptInsecureCerts: acceptInsecureCerts,
networkEnabled,
issuesEnabled,
capabilities: options.capabilities,
});
return bidiBrowser;
}
/**
* Returns a BiDiConnection established to the endpoint specified by the options and a
* callback closing the browser. Callback depends on whether the connection is pure BiDi
* or BiDi over CDP.
* The method tries to connect to the browser using pure BiDi protocol, and falls back
* to BiDi over CDP.
*/
async function getBiDiConnection(
connectionTransport: ConnectionTransport,
url: string,
options: ConnectOptions,
): Promise<{
cdpConnection?: Connection;
bidiConnection: BidiConnection;
closeCallback: BrowserCloseCallback;
}> {
const BiDi = await import(/* webpackIgnore: true */ './bidi.js');
const {
slowMo = 0,
protocolTimeout,
idGenerator = createIncrementalIdGenerator(),
} = options;
// Try pure BiDi first.
const pureBidiConnection = new BiDi.BidiConnection(
url,
connectionTransport,
idGenerator,
slowMo,
protocolTimeout,
);
try {
const result = await pureBidiConnection.send('session.status', {});
if ('type' in result && result.type === 'success') {
// The `browserWSEndpoint` points to an endpoint supporting pure WebDriver BiDi.
return {
bidiConnection: pureBidiConnection,
closeCallback: async () => {
await pureBidiConnection.send('browser.close', {}).catch(debugError);
},
};
}
} catch (e) {
if (!(e instanceof ProtocolError)) {
// Unexpected exception not related to BiDi / CDP. Rethrow.
throw e;
}
}
// Unbind the connection to avoid memory leaks.
pureBidiConnection.unbind();
// Fall back to CDP over BiDi reusing the WS connection.
const cdpConnection = new Connection(
url,
connectionTransport,
slowMo,
protocolTimeout,
/* rawErrors= */ true,
idGenerator,
);
const version = await cdpConnection.send('Browser.getVersion');
if (version.product.toLowerCase().includes('firefox')) {
throw new UnsupportedOperation(
'Firefox is not supported in BiDi over CDP mode.',
);
}
const bidiOverCdpConnection = await BiDi.connectBidiOverCdp(cdpConnection);
return {
cdpConnection,
bidiConnection: bidiOverCdpConnection,
closeCallback: async () => {
// In case of BiDi over CDP, we need to close browser via CDP.
await cdpConnection.send('Browser.close').catch(debugError);
},
};
}

401
node_modules/puppeteer-core/src/bidi/BrowserContext.ts generated vendored Normal file
View File

@@ -0,0 +1,401 @@
/**
* @license
* Copyright 2022 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import * as Bidi from 'webdriver-bidi-protocol';
import type {
CreatePageOptions,
Permission,
PermissionDescriptor,
PermissionState,
} from '../api/Browser.js';
import {WEB_PERMISSION_TO_PROTOCOL_PERMISSION} from '../api/Browser.js';
import type {BrowserContextEvents} from '../api/BrowserContext.js';
import {BrowserContext, BrowserContextEvent} from '../api/BrowserContext.js';
import {PageEvent, type Page} from '../api/Page.js';
import type {Target} from '../api/Target.js';
import type {Cookie, CookieData} from '../common/Cookie.js';
import {UnsupportedOperation} from '../common/Errors.js';
import {EventEmitter} from '../common/EventEmitter.js';
import {debugError} from '../common/util.js';
import type {Viewport} from '../common/Viewport.js';
import {assert} from '../util/assert.js';
import {bubble} from '../util/decorators.js';
import type {BidiBrowser} from './Browser.js';
import type {BrowsingContext} from './core/BrowsingContext.js';
import {UserContext} from './core/UserContext.js';
import type {BidiFrame} from './Frame.js';
import {
BidiPage,
bidiToPuppeteerCookie,
cdpSpecificCookiePropertiesFromPuppeteerToBidi,
convertCookiesExpiryCdpToBiDi,
convertCookiesPartitionKeyFromPuppeteerToBiDi,
convertCookiesSameSiteCdpToBiDi,
} from './Page.js';
import {BidiWorkerTarget} from './Target.js';
import {BidiFrameTarget, BidiPageTarget} from './Target.js';
import type {BidiWebWorker} from './WebWorker.js';
/**
* @internal
*/
export interface BidiBrowserContextOptions {
defaultViewport: Viewport | null;
}
/**
* @internal
*/
export class BidiBrowserContext extends BrowserContext {
static from(
browser: BidiBrowser,
userContext: UserContext,
options: BidiBrowserContextOptions,
): BidiBrowserContext {
const context = new BidiBrowserContext(browser, userContext, options);
context.#initialize();
return context;
}
@bubble()
accessor trustedEmitter = new EventEmitter<BrowserContextEvents>();
readonly #browser: BidiBrowser;
readonly #defaultViewport: Viewport | null;
// This is public because of cookies.
readonly userContext: UserContext;
readonly #pages = new WeakMap<BrowsingContext, BidiPage>();
readonly #targets = new Map<
BidiPage,
[
BidiPageTarget,
Map<BidiFrame | BidiWebWorker, BidiFrameTarget | BidiWorkerTarget>,
]
>();
#overrides: Array<{origin: string; permission: Permission}> = [];
private constructor(
browser: BidiBrowser,
userContext: UserContext,
options: BidiBrowserContextOptions,
) {
super();
this.#browser = browser;
this.userContext = userContext;
this.#defaultViewport = options.defaultViewport;
}
#initialize() {
// Create targets for existing browsing contexts.
for (const browsingContext of this.userContext.browsingContexts) {
this.#createPage(browsingContext);
}
this.userContext.on('browsingcontext', ({browsingContext}) => {
const page = this.#createPage(browsingContext);
// We need to wait for the DOMContentLoaded as the
// browsingContext still may be navigating from the about:blank
if (browsingContext.originalOpener) {
for (const context of this.userContext.browsingContexts) {
if (context.id === browsingContext.originalOpener) {
this.#pages
.get(context)!
.trustedEmitter.emit(PageEvent.Popup, page);
}
}
}
});
this.userContext.on('closed', () => {
this.trustedEmitter.removeAllListeners();
});
}
#createPage(browsingContext: BrowsingContext): BidiPage {
const page = BidiPage.from(this, browsingContext);
this.#pages.set(browsingContext, page);
page.trustedEmitter.on(PageEvent.Close, () => {
this.#pages.delete(browsingContext);
});
// -- Target stuff starts here --
const pageTarget = new BidiPageTarget(page);
const pageTargets = new Map();
this.#targets.set(page, [pageTarget, pageTargets]);
page.trustedEmitter.on(PageEvent.FrameAttached, frame => {
const bidiFrame = frame as BidiFrame;
const target = new BidiFrameTarget(bidiFrame);
pageTargets.set(bidiFrame, target);
this.trustedEmitter.emit(BrowserContextEvent.TargetCreated, target);
});
page.trustedEmitter.on(PageEvent.FrameNavigated, frame => {
const bidiFrame = frame as BidiFrame;
const target = pageTargets.get(bidiFrame);
// If there is no target, then this is the page's frame.
if (target === undefined) {
this.trustedEmitter.emit(BrowserContextEvent.TargetChanged, pageTarget);
} else {
this.trustedEmitter.emit(BrowserContextEvent.TargetChanged, target);
}
});
page.trustedEmitter.on(PageEvent.FrameDetached, frame => {
const bidiFrame = frame as BidiFrame;
const target = pageTargets.get(bidiFrame);
if (target === undefined) {
return;
}
pageTargets.delete(bidiFrame);
this.trustedEmitter.emit(BrowserContextEvent.TargetDestroyed, target);
});
page.trustedEmitter.on(PageEvent.WorkerCreated, worker => {
const bidiWorker = worker as BidiWebWorker;
const target = new BidiWorkerTarget(bidiWorker);
pageTargets.set(bidiWorker, target);
this.trustedEmitter.emit(BrowserContextEvent.TargetCreated, target);
});
page.trustedEmitter.on(PageEvent.WorkerDestroyed, worker => {
const bidiWorker = worker as BidiWebWorker;
const target = pageTargets.get(bidiWorker);
if (target === undefined) {
return;
}
pageTargets.delete(worker);
this.trustedEmitter.emit(BrowserContextEvent.TargetDestroyed, target);
});
page.trustedEmitter.on(PageEvent.Close, () => {
this.#targets.delete(page);
this.trustedEmitter.emit(BrowserContextEvent.TargetDestroyed, pageTarget);
});
this.trustedEmitter.emit(BrowserContextEvent.TargetCreated, pageTarget);
// -- Target stuff ends here --
return page;
}
override targets(): Target[] {
return [...this.#targets.values()].flatMap(([target, frames]) => {
return [target, ...frames.values()];
});
}
/**
* @internal
*/
getTargetForPage(page: BidiPage): BidiPageTarget | undefined {
return this.#targets.get(page)?.[0];
}
override async newPage(options?: CreatePageOptions): Promise<Page> {
using _guard = await this.waitForScreenshotOperations();
const type =
options?.type === 'window'
? Bidi.BrowsingContext.CreateType.Window
: Bidi.BrowsingContext.CreateType.Tab;
const context = await this.userContext.createBrowsingContext(type, {
background: options?.background,
});
const page = this.#pages.get(context)!;
if (!page) {
throw new Error('Page is not found');
}
if (this.#defaultViewport) {
try {
await page.setViewport(this.#defaultViewport);
} catch (error) {
// Tolerate not supporting `browsingContext.setViewport`. Only log it.
debugError(error);
}
}
if (options?.type === 'window' && options?.windowBounds !== undefined) {
try {
await this.browser().setWindowBounds(
context.windowId,
options.windowBounds,
);
} catch (error) {
// Tolerate not supporting `browser.setClientWindowState`. Only log it.
debugError(error);
}
}
return page;
}
override async close(): Promise<void> {
assert(
this.userContext.id !== UserContext.DEFAULT,
'Default BrowserContext cannot be closed!',
);
try {
await this.userContext.remove();
} catch (error) {
debugError(error);
}
this.#targets.clear();
}
override browser(): BidiBrowser {
return this.#browser;
}
override async pages(_includeAll = false): Promise<BidiPage[]> {
return [...this.userContext.browsingContexts].map(context => {
return this.#pages.get(context)!;
});
}
override async overridePermissions(
origin: string,
permissions: Permission[],
): Promise<void> {
const permissionsSet = new Set(
permissions.map(permission => {
const protocolPermission =
WEB_PERMISSION_TO_PROTOCOL_PERMISSION.get(permission);
if (!protocolPermission) {
throw new Error('Unknown permission: ' + permission);
}
return permission;
}),
);
await Promise.all(
Array.from(WEB_PERMISSION_TO_PROTOCOL_PERMISSION.keys()).map(
permission => {
const result = this.userContext.setPermissions(
origin,
{
name: permission,
},
permissionsSet.has(permission)
? Bidi.Permissions.PermissionState.Granted
: Bidi.Permissions.PermissionState.Denied,
);
this.#overrides.push({origin, permission});
// TODO: some permissions are outdated and setting them to denied does
// not work.
if (!permissionsSet.has(permission)) {
return result.catch(debugError);
}
return result;
},
),
);
}
override async setPermission(
origin: string | '*',
...permissions: Array<{
permission: PermissionDescriptor;
state: PermissionState;
}>
): Promise<void> {
if (origin === '*') {
throw new UnsupportedOperation(
'Origin (*) is not supported by WebDriver BiDi',
);
}
await Promise.all(
permissions.map(permission => {
if (permission.permission.allowWithoutSanitization) {
throw new UnsupportedOperation(
'allowWithoutSanitization is not supported by WebDriver BiDi',
);
}
if (permission.permission.panTiltZoom) {
throw new UnsupportedOperation(
'panTiltZoom is not supported by WebDriver BiDi',
);
}
if (permission.permission.userVisibleOnly) {
throw new UnsupportedOperation(
'userVisibleOnly is not supported by WebDriver BiDi',
);
}
return this.userContext.setPermissions(
origin,
{
name: permission.permission.name,
},
permission.state as Bidi.Permissions.PermissionState,
);
}),
);
}
override async clearPermissionOverrides(): Promise<void> {
const promises = this.#overrides.map(({permission, origin}) => {
return this.userContext
.setPermissions(
origin,
{
name: permission,
},
Bidi.Permissions.PermissionState.Prompt,
)
.catch(debugError);
});
this.#overrides = [];
await Promise.all(promises);
}
override get id(): string | undefined {
if (this.userContext.id === UserContext.DEFAULT) {
return undefined;
}
return this.userContext.id;
}
override async cookies(): Promise<Cookie[]> {
const cookies = await this.userContext.getCookies();
return cookies.map(cookie => {
return bidiToPuppeteerCookie(cookie, true);
});
}
override async setCookie(...cookies: CookieData[]): Promise<void> {
await Promise.all(
cookies.map(async cookie => {
const bidiCookie: Bidi.Storage.PartialCookie = {
domain: cookie.domain,
name: cookie.name,
value: {
type: 'string',
value: cookie.value,
},
...(cookie.path !== undefined ? {path: cookie.path} : {}),
...(cookie.httpOnly !== undefined ? {httpOnly: cookie.httpOnly} : {}),
...(cookie.secure !== undefined ? {secure: cookie.secure} : {}),
...(cookie.sameSite !== undefined
? {sameSite: convertCookiesSameSiteCdpToBiDi(cookie.sameSite)}
: {}),
...{expiry: convertCookiesExpiryCdpToBiDi(cookie.expires)},
// Chrome-specific properties.
...cdpSpecificCookiePropertiesFromPuppeteerToBidi(
cookie,
'sameParty',
'sourceScheme',
'priority',
'url',
),
};
return await this.userContext.setCookie(
bidiCookie,
convertCookiesPartitionKeyFromPuppeteerToBiDi(cookie.partitionKey),
);
}),
);
}
}

124
node_modules/puppeteer-core/src/bidi/CDPSession.ts generated vendored Normal file
View File

@@ -0,0 +1,124 @@
/**
* @license
* Copyright 2024 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type ProtocolMapping from 'devtools-protocol/types/protocol-mapping.js';
import type {CommandOptions} from '../api/CDPSession.js';
import {CDPSession} from '../api/CDPSession.js';
import type {Connection as CdpConnection} from '../cdp/Connection.js';
import {TargetCloseError, UnsupportedOperation} from '../common/Errors.js';
import {Deferred} from '../util/Deferred.js';
import type {BidiConnection} from './Connection.js';
import type {BidiFrame} from './Frame.js';
/**
* @internal
*/
export class BidiCdpSession extends CDPSession {
static sessions = new Map<string, BidiCdpSession>();
#detached = false;
readonly #connection?: BidiConnection;
readonly #sessionId = Deferred.create<string>();
readonly frame: BidiFrame;
constructor(frame: BidiFrame, sessionId?: string) {
super();
this.frame = frame;
if (!this.frame.page().browser().cdpSupported) {
return;
}
const connection = this.frame.page().browser().connection;
this.#connection = connection;
if (sessionId) {
this.#sessionId.resolve(sessionId);
BidiCdpSession.sessions.set(sessionId, this);
} else {
(async () => {
try {
const {result} = await connection.send('goog:cdp.getSession', {
context: frame._id,
});
this.#sessionId.resolve(result.session!);
BidiCdpSession.sessions.set(result.session!, this);
} catch (error) {
this.#sessionId.reject(error as Error);
}
})();
}
// SAFETY: We never throw #sessionId.
BidiCdpSession.sessions.set(this.#sessionId.value() as string, this);
}
override connection(): CdpConnection | undefined {
return undefined;
}
override get detached(): boolean {
return this.#detached;
}
override async send<T extends keyof ProtocolMapping.Commands>(
method: T,
params?: ProtocolMapping.Commands[T]['paramsType'][0],
options?: CommandOptions,
): Promise<ProtocolMapping.Commands[T]['returnType']> {
if (this.#connection === undefined) {
throw new UnsupportedOperation(
'CDP support is required for this feature. The current browser does not support CDP.',
);
}
if (this.#detached) {
throw new TargetCloseError(
`Protocol error (${method}): Session closed. Most likely the page has been closed.`,
);
}
const session = await this.#sessionId.valueOrThrow();
const {result} = await this.#connection.send(
'goog:cdp.sendCommand',
{
method: method,
params: params,
session,
},
options?.timeout,
);
return result.result;
}
override async detach(): Promise<void> {
if (
this.#connection === undefined ||
this.#connection.closed ||
this.#detached
) {
return;
}
try {
await this.frame.client.send('Target.detachFromTarget', {
sessionId: this.id(),
});
} finally {
this.onClose();
}
}
/**
* @internal
*/
onClose = (): void => {
BidiCdpSession.sessions.delete(this.id());
this.#detached = true;
};
override id(): string {
const value = this.#sessionId.value();
return typeof value === 'string' ? value : '';
}
}

232
node_modules/puppeteer-core/src/bidi/Connection.ts generated vendored Normal file
View File

@@ -0,0 +1,232 @@
/**
* @license
* Copyright 2017 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type * as ChromiumBidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import type * as Bidi from 'webdriver-bidi-protocol';
import {CallbackRegistry} from '../common/CallbackRegistry.js';
import type {ConnectionTransport} from '../common/ConnectionTransport.js';
import {debug} from '../common/Debug.js';
import {ConnectionClosedError} from '../common/Errors.js';
import type {EventsWithWildcard} from '../common/EventEmitter.js';
import {EventEmitter} from '../common/EventEmitter.js';
import {debugError} from '../common/util.js';
import type {GetIdFn} from '../util/incremental-id-generator.js';
import {BidiCdpSession} from './CDPSession.js';
import type {
BidiEvents,
Commands as BidiCommands,
Connection,
} from './core/Connection.js';
const debugProtocolSend = debug('puppeteer:webDriverBiDi:SEND ►');
const debugProtocolReceive = debug('puppeteer:webDriverBiDi:RECV ◀');
export type CdpEvent = ChromiumBidi.Cdp.Event;
/**
* @internal
*/
export interface Commands extends BidiCommands {
'goog:cdp.sendCommand': {
params: ChromiumBidi.Cdp.SendCommandParameters;
returnType: ChromiumBidi.Cdp.SendCommandResult;
};
'goog:cdp.getSession': {
params: ChromiumBidi.Cdp.GetSessionParameters;
returnType: ChromiumBidi.Cdp.GetSessionResult;
};
'goog:cdp.resolveRealm': {
params: ChromiumBidi.Cdp.ResolveRealmParameters;
returnType: ChromiumBidi.Cdp.ResolveRealmResult;
};
}
/**
* @internal
*/
export class BidiConnection
extends EventEmitter<BidiEvents>
implements Connection
{
#url: string;
#transport: ConnectionTransport;
#delay: number;
#timeout = 0;
#closed = false;
#callbacks: CallbackRegistry;
#emitters: Array<EventEmitter<any>> = [];
constructor(
url: string,
transport: ConnectionTransport,
idGenerator: GetIdFn,
delay = 0,
timeout?: number,
) {
super();
this.#url = url;
this.#delay = delay;
this.#timeout = timeout ?? 180_000;
this.#callbacks = new CallbackRegistry(idGenerator);
this.#transport = transport;
this.#transport.onmessage = this.onMessage.bind(this);
this.#transport.onclose = this.unbind.bind(this);
}
get closed(): boolean {
return this.#closed;
}
get url(): string {
return this.#url;
}
pipeTo<Events extends BidiEvents>(emitter: EventEmitter<Events>): void {
this.#emitters.push(emitter);
}
#toWebDriverOnlyEvent(event: Record<string, any>) {
for (const key in event) {
if (key.startsWith('goog:')) {
delete event[key];
} else {
if (typeof event[key] === 'object' && event[key] !== null) {
this.#toWebDriverOnlyEvent(event[key]);
}
}
}
}
override emit<Key extends keyof EventsWithWildcard<BidiEvents>>(
type: Key,
event: EventsWithWildcard<BidiEvents>[Key],
): boolean {
if (process.env['PUPPETEER_WEBDRIVER_BIDI_ONLY'] === 'true') {
// Required for WebDriver-only testing.
this.#toWebDriverOnlyEvent(event);
}
for (const emitter of this.#emitters) {
emitter.emit(type, event);
}
return super.emit(type, event);
}
send<T extends keyof Commands>(
method: T,
params: Commands[T]['params'],
timeout?: number,
): Promise<{result: Commands[T]['returnType']}> {
if (this.#closed) {
return Promise.reject(new ConnectionClosedError('Connection closed.'));
}
return this.#callbacks.create(method, timeout ?? this.#timeout, id => {
const stringifiedMessage = JSON.stringify({
id,
method,
params,
} as Bidi.Command);
debugProtocolSend(stringifiedMessage);
this.#transport.send(stringifiedMessage);
}) as Promise<{result: Commands[T]['returnType']}>;
}
/**
* @internal
*/
protected async onMessage(message: string): Promise<void> {
if (this.#delay) {
await new Promise(f => {
return setTimeout(f, this.#delay);
});
}
debugProtocolReceive(message);
const object: Bidi.Message | CdpEvent = JSON.parse(message);
if ('type' in object) {
switch (object.type) {
case 'success':
this.#callbacks.resolve(object.id, object);
return;
case 'error':
if (object.id === null) {
break;
}
this.#callbacks.reject(
object.id,
createProtocolError(object),
`${object.error}: ${object.message}`,
);
return;
case 'event':
if (isCdpEvent(object)) {
BidiCdpSession.sessions
.get(object.params.session)
?.emit(object.params.event, object.params.params);
return;
}
// SAFETY: We know the method and parameter still match here.
this.emit(object.method, object.params);
return;
}
}
// Even if the response in not in BiDi protocol format but `id` is provided, reject
// the callback. This can happen if the endpoint supports CDP instead of BiDi.
if ('id' in object) {
this.#callbacks.reject(
(object as {id: number}).id,
`Protocol Error. Message is not in BiDi protocol format: '${message}'`,
object.message,
);
}
debugError(object);
}
/**
* Unbinds the connection, but keeps the transport open. Useful when the transport will
* be reused by other connection e.g. with different protocol.
* @internal
*/
unbind(): void {
if (this.#closed) {
return;
}
this.#closed = true;
// Both may still be invoked and produce errors
this.#transport.onmessage = () => {};
this.#transport.onclose = () => {};
this.#callbacks.clear();
}
/**
* Unbinds the connection and closes the transport.
*/
dispose(): void {
this.unbind();
this.#transport.close();
}
getPendingProtocolErrors(): Error[] {
return this.#callbacks.getPendingProtocolErrors();
}
}
/**
* @internal
*/
function createProtocolError(object: Bidi.ErrorResponse): string {
let message = `${object.error} ${object.message}`;
if (object.stacktrace) {
message += ` ${object.stacktrace}`;
}
return message;
}
function isCdpEvent(event: Bidi.Event | CdpEvent): event is CdpEvent {
return event.method.startsWith('goog:cdp.');
}

92
node_modules/puppeteer-core/src/bidi/Deserializer.ts generated vendored Normal file
View File

@@ -0,0 +1,92 @@
/**
* @license
* Copyright 2023 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type * as Bidi from 'webdriver-bidi-protocol';
import {debugError} from '../common/util.js';
/**
* @internal
*/
export class BidiDeserializer {
static deserialize(result: Bidi.Script.RemoteValue): any {
if (!result) {
debugError('Service did not produce a result.');
return undefined;
}
switch (result.type) {
case 'array':
return result.value?.map(value => {
return this.deserialize(value);
});
case 'set':
return result.value?.reduce((acc: Set<unknown>, value) => {
return acc.add(this.deserialize(value));
}, new Set());
case 'object':
return result.value?.reduce((acc: Record<any, unknown>, tuple) => {
const {key, value} = this.#deserializeTuple(tuple);
acc[key as any] = value;
return acc;
}, {});
case 'map':
return result.value?.reduce((acc: Map<unknown, unknown>, tuple) => {
const {key, value} = this.#deserializeTuple(tuple);
return acc.set(key, value);
}, new Map());
case 'promise':
return {};
case 'regexp':
return new RegExp(result.value.pattern, result.value.flags);
case 'date':
return new Date(result.value);
case 'undefined':
return undefined;
case 'null':
return null;
case 'number':
return this.#deserializeNumber(result.value);
case 'bigint':
return BigInt(result.value);
case 'boolean':
return Boolean(result.value);
case 'string':
return result.value;
}
debugError(`Deserialization of type ${result.type} not supported.`);
return undefined;
}
static #deserializeNumber(value: Bidi.Script.SpecialNumber | number): number {
switch (value) {
case '-0':
return -0;
case 'NaN':
return NaN;
case 'Infinity':
return Infinity;
case '-Infinity':
return -Infinity;
default:
return value;
}
}
static #deserializeTuple([serializedKey, serializedValue]: [
Bidi.Script.RemoteValue | string,
Bidi.Script.RemoteValue,
]): {key: unknown; value: unknown} {
const key =
typeof serializedKey === 'string'
? serializedKey
: this.deserialize(serializedKey);
const value = this.deserialize(serializedValue);
return {key, value};
}
}

View File

@@ -0,0 +1,138 @@
/**
* @license
* Copyright 2025 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type * as Bidi from 'webdriver-bidi-protocol';
import {
DeviceRequestPrompt,
type DeviceRequestPromptDevice,
} from '../api/DeviceRequestPrompt.js';
import {UnsupportedOperation} from '../common/Errors.js';
import {Deferred} from '../util/Deferred.js';
import type {Session} from './core/Session.js';
/**
* @internal
*/
export class BidiDeviceRequestPromptManager {
readonly #session: Session;
readonly #contextId: string;
#enabled = false;
constructor(contextId: string, session: Session) {
this.#session = session;
this.#contextId = contextId;
}
async #enableIfNeeded(): Promise<void> {
if (!this.#enabled) {
this.#enabled = true;
await this.#session.subscribe(
['bluetooth.requestDevicePromptUpdated'],
[this.#contextId],
);
}
}
async waitForDevicePrompt(
timeout: number,
signal: AbortSignal | undefined,
): Promise<DeviceRequestPrompt> {
const deferred = Deferred.create<DeviceRequestPrompt>({
message: `Waiting for \`DeviceRequestPrompt\` failed: ${timeout}ms exceeded`,
timeout,
});
const onRequestDevicePromptUpdated = (
params: Bidi.Bluetooth.RequestDevicePromptUpdatedParameters,
) => {
if (params.context === this.#contextId) {
deferred.resolve(
new BidiDeviceRequestPrompt(
this.#contextId,
params.prompt,
this.#session,
params.devices,
),
);
this.#session.off(
'bluetooth.requestDevicePromptUpdated',
onRequestDevicePromptUpdated,
);
}
};
this.#session.on(
'bluetooth.requestDevicePromptUpdated',
onRequestDevicePromptUpdated,
);
if (signal) {
signal.addEventListener(
'abort',
() => {
deferred.reject(signal.reason);
},
{once: true},
);
}
await this.#enableIfNeeded();
return await deferred.valueOrThrow();
}
}
/**
* @internal
*/
export class BidiDeviceRequestPrompt extends DeviceRequestPrompt {
readonly #session: Session;
#promptId: string;
#contextId: string;
constructor(
contextId: string,
promptId: string,
session: Session,
devices: Bidi.Bluetooth.RequestDeviceInfo[],
) {
super();
this.#session = session;
this.#promptId = promptId;
this.#contextId = contextId;
this.devices.push(
...devices.map(d => {
return {
id: d.id,
name: d.name ?? 'UNKNOWN',
};
}),
);
}
async cancel(): Promise<void> {
await this.#session.send('bluetooth.handleRequestDevicePrompt', {
context: this.#contextId,
prompt: this.#promptId,
accept: false,
});
}
async select(device: DeviceRequestPromptDevice): Promise<void> {
await this.#session.send('bluetooth.handleRequestDevicePrompt', {
context: this.#contextId,
prompt: this.#promptId,
accept: true,
device: device.id,
});
}
waitForDevice(): never {
throw new UnsupportedOperation();
}
}

32
node_modules/puppeteer-core/src/bidi/Dialog.ts generated vendored Normal file
View File

@@ -0,0 +1,32 @@
/**
* @license
* Copyright 2017 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import {Dialog} from '../api/Dialog.js';
import type {UserPrompt} from './core/UserPrompt.js';
export class BidiDialog extends Dialog {
static from(prompt: UserPrompt): BidiDialog {
return new BidiDialog(prompt);
}
#prompt: UserPrompt;
private constructor(prompt: UserPrompt) {
super(prompt.info.type, prompt.info.message, prompt.info.defaultValue);
this.#prompt = prompt;
this.handled = prompt.handled;
}
override async handle(options: {
accept: boolean;
text?: string;
}): Promise<void> {
await this.#prompt.handle({
accept: options.accept,
userText: options.text,
});
}
}

153
node_modules/puppeteer-core/src/bidi/ElementHandle.ts generated vendored Normal file
View File

@@ -0,0 +1,153 @@
/**
* @license
* Copyright 2023 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type * as Bidi from 'webdriver-bidi-protocol';
import {
bindIsolatedHandle,
ElementHandle,
type AutofillData,
} from '../api/ElementHandle.js';
import {UnsupportedOperation} from '../common/Errors.js';
import type {AwaitableIterable} from '../common/types.js';
import {environment} from '../environment.js';
import {AsyncIterableUtil} from '../util/AsyncIterableUtil.js';
import {throwIfDisposed} from '../util/decorators.js';
import type {BidiFrame} from './Frame.js';
import {BidiJSHandle} from './JSHandle.js';
import type {BidiFrameRealm} from './Realm.js';
/**
* @internal
*/
export class BidiElementHandle<
ElementType extends Node = Element,
> extends ElementHandle<ElementType> {
#backendNodeId?: number;
static from<ElementType extends Node = Element>(
value: Bidi.Script.RemoteValue,
realm: BidiFrameRealm,
): BidiElementHandle<ElementType> {
return new BidiElementHandle(value, realm);
}
declare handle: BidiJSHandle<ElementType>;
constructor(value: Bidi.Script.RemoteValue, realm: BidiFrameRealm) {
super(BidiJSHandle.from(value, realm));
}
override get realm(): BidiFrameRealm {
// SAFETY: See the super call in the constructor.
return this.handle.realm as BidiFrameRealm;
}
override get frame(): BidiFrame {
return this.realm.environment;
}
remoteValue(): Bidi.Script.RemoteValue {
return this.handle.remoteValue();
}
@throwIfDisposed()
override async autofill(data: AutofillData): Promise<void> {
const client = this.frame.client;
const nodeInfo = await client.send('DOM.describeNode', {
objectId: this.handle.id,
});
const fieldId = nodeInfo.node.backendNodeId;
const frameId = this.frame._id;
await client.send('Autofill.trigger', {
fieldId,
frameId,
card: data.creditCard,
address: data.address,
});
}
override async contentFrame(
this: BidiElementHandle<HTMLIFrameElement>,
): Promise<BidiFrame>;
@throwIfDisposed()
@bindIsolatedHandle
override async contentFrame(): Promise<BidiFrame | null> {
using handle = (await this.evaluateHandle(element => {
if (
element instanceof HTMLIFrameElement ||
element instanceof HTMLFrameElement
) {
return element.contentWindow;
}
return;
})) as BidiJSHandle;
const value = handle.remoteValue();
if (value.type === 'window') {
return (
this.frame
.page()
.frames()
.find(frame => {
return frame._id === value.value.context;
}) ?? null
);
}
return null;
}
override async uploadFile(
this: BidiElementHandle<HTMLInputElement>,
...files: string[]
): Promise<void> {
// Locate all files and confirm that they exist.
const path = environment.value.path;
if (path) {
files = files.map(file => {
if (path.win32.isAbsolute(file) || path.posix.isAbsolute(file)) {
return file;
} else {
return path.resolve(file);
}
});
}
await this.frame.setFiles(this, files);
}
override async *queryAXTree(
this: BidiElementHandle<HTMLElement>,
name?: string | undefined,
role?: string | undefined,
): AwaitableIterable<ElementHandle<Node>> {
const results = await this.frame.locateNodes(this, {
type: 'accessibility',
value: {
role,
name,
},
});
return yield* AsyncIterableUtil.map(results, node => {
// TODO: maybe change ownership since the default ownership is probably none.
return Promise.resolve(BidiElementHandle.from(node, this.realm));
});
}
override async backendNodeId(): Promise<number> {
if (!this.frame.page().browser().cdpSupported) {
throw new UnsupportedOperation();
}
if (this.#backendNodeId) {
return this.#backendNodeId;
}
const {node} = await this.frame.client.send('DOM.describeNode', {
objectId: this.handle.id,
});
this.#backendNodeId = node.backendNodeId;
return this.#backendNodeId;
}
}

256
node_modules/puppeteer-core/src/bidi/ExposedFunction.ts generated vendored Normal file
View File

@@ -0,0 +1,256 @@
/**
* @license
* Copyright 2023 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import * as Bidi from 'webdriver-bidi-protocol';
import {EventEmitter} from '../common/EventEmitter.js';
import type {Awaitable, FlattenHandle} from '../common/types.js';
import {debugError} from '../common/util.js';
import {DisposableStack} from '../util/disposable.js';
import {interpolateFunction, stringifyFunction} from '../util/Function.js';
import type {Connection} from './core/Connection.js';
import {BidiElementHandle} from './ElementHandle.js';
import type {BidiFrame} from './Frame.js';
import {BidiJSHandle} from './JSHandle.js';
type CallbackChannel<Args, Ret> = (
value: [
resolve: (ret: FlattenHandle<Awaited<Ret>>) => void,
reject: (error: unknown) => void,
args: Args,
],
) => void;
/**
* @internal
*/
export class ExposableFunction<Args extends unknown[], Ret> {
static async from<Args extends unknown[], Ret>(
frame: BidiFrame,
name: string,
apply: (...args: Args) => Awaitable<Ret>,
isolate = false,
): Promise<ExposableFunction<Args, Ret>> {
const func = new ExposableFunction(frame, name, apply, isolate);
await func.#initialize();
return func;
}
readonly #frame;
readonly name;
readonly #apply;
readonly #isolate;
readonly #channel;
#scripts: Array<[BidiFrame, Bidi.Script.PreloadScript]> = [];
#disposables = new DisposableStack();
constructor(
frame: BidiFrame,
name: string,
apply: (...args: Args) => Awaitable<Ret>,
isolate = false,
) {
this.#frame = frame;
this.name = name;
this.#apply = apply;
this.#isolate = isolate;
this.#channel = `__puppeteer__${this.#frame._id}_page_exposeFunction_${this.name}`;
}
async #initialize() {
const connection = this.#connection;
const channel = {
type: 'channel' as const,
value: {
channel: this.#channel,
ownership: Bidi.Script.ResultOwnership.Root,
},
};
const connectionEmitter = this.#disposables.use(
new EventEmitter(connection),
);
connectionEmitter.on('script.message', this.#handleMessage);
const functionDeclaration = stringifyFunction(
interpolateFunction(
(callback: CallbackChannel<Args, Ret>) => {
Object.assign(globalThis, {
[PLACEHOLDER('name') as string]: function (...args: Args) {
return new Promise<FlattenHandle<Awaited<Ret>>>(
(resolve, reject) => {
callback([resolve, reject, args]);
},
);
},
});
},
{name: JSON.stringify(this.name)},
),
);
const frames = [this.#frame];
for (const frame of frames) {
frames.push(...frame.childFrames());
}
await Promise.all(
frames.map(async frame => {
const realm = this.#isolate ? frame.isolatedRealm() : frame.mainRealm();
try {
const [script] = await Promise.all([
frame.browsingContext.addPreloadScript(functionDeclaration, {
arguments: [channel],
sandbox: realm.sandbox,
}),
realm.realm.callFunction(functionDeclaration, false, {
arguments: [channel],
}),
]);
this.#scripts.push([frame, script]);
} catch (error) {
// If it errors, the frame probably doesn't support call function. We
// fail gracefully.
debugError(error);
}
}),
);
}
get #connection(): Connection {
return this.#frame.page().browser().connection;
}
#handleMessage = async (params: Bidi.Script.MessageParameters) => {
if (params.channel !== this.#channel) {
return;
}
const realm = this.#getRealm(params.source);
if (!realm) {
// Unrelated message.
return;
}
using dataHandle = BidiJSHandle.from<
[
resolve: (ret: FlattenHandle<Awaited<Ret>>) => void,
reject: (error: unknown) => void,
args: Args,
]
>(params.data, realm);
using stack = new DisposableStack();
const args = [];
let result;
try {
using argsHandle = await dataHandle.evaluateHandle(([, , args]) => {
return args;
});
for (const [index, handle] of await argsHandle.getProperties()) {
stack.use(handle);
// Element handles are passed as is.
if (handle instanceof BidiElementHandle) {
args[+index] = handle;
stack.use(handle);
continue;
}
// Everything else is passed as the JS value.
args[+index] = handle.jsonValue();
}
result = await this.#apply(...((await Promise.all(args)) as Args));
} catch (error) {
try {
if (error instanceof Error) {
await dataHandle.evaluate(
([, reject], name, message, stack) => {
const error = new Error(message);
error.name = name;
if (stack) {
error.stack = stack;
}
reject(error);
},
error.name,
error.message,
error.stack,
);
} else {
await dataHandle.evaluate(([, reject], error) => {
reject(error);
}, error);
}
} catch (error) {
debugError(error);
}
return;
}
try {
await dataHandle.evaluate(([resolve], result) => {
resolve(result);
}, result);
} catch (error) {
debugError(error);
}
};
#getRealm(source: Bidi.Script.Source) {
const frame = this.#findFrame(source.context as string);
if (!frame) {
// Unrelated message.
return;
}
return frame.realm(source.realm);
}
#findFrame(id: string) {
const frames = [this.#frame];
for (const frame of frames) {
if (frame._id === id) {
return frame;
}
frames.push(...frame.childFrames());
}
return;
}
[Symbol.dispose](): void {
void this[Symbol.asyncDispose]().catch(debugError);
}
async [Symbol.asyncDispose](): Promise<void> {
this.#disposables.dispose();
await Promise.all(
this.#scripts.map(async ([frame, script]) => {
const realm = this.#isolate ? frame.isolatedRealm() : frame.mainRealm();
try {
await Promise.all([
realm.evaluate(name => {
delete (globalThis as any)[name];
}, this.name),
...frame.childFrames().map(childFrame => {
return childFrame.evaluate(name => {
delete (globalThis as any)[name];
}, this.name);
}),
frame.browsingContext.removePreloadScript(script),
]);
} catch (error) {
debugError(error);
}
}),
);
}
}

615
node_modules/puppeteer-core/src/bidi/Frame.ts generated vendored Normal file
View File

@@ -0,0 +1,615 @@
/**
* @license
* Copyright 2023 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import * as Bidi from 'webdriver-bidi-protocol';
import type {Observable} from '../../third_party/rxjs/rxjs.js';
import {
combineLatest,
defer,
delayWhen,
filter,
first,
firstValueFrom,
map,
of,
race,
raceWith,
switchMap,
} from '../../third_party/rxjs/rxjs.js';
import type {CDPSession} from '../api/CDPSession.js';
import type {DeviceRequestPrompt} from '../api/DeviceRequestPrompt.js';
import {
Frame,
throwIfDetached,
type GoToOptions,
type WaitForOptions,
} from '../api/Frame.js';
import {PageEvent, type WaitTimeoutOptions} from '../api/Page.js';
import type {Realm} from '../api/Realm.js';
import {Accessibility} from '../cdp/Accessibility.js';
import {TargetCloseError, UnsupportedOperation} from '../common/Errors.js';
import type {TimeoutSettings} from '../common/TimeoutSettings.js';
import type {Awaitable, HandleFor} from '../common/types.js';
import {
debugError,
fromAbortSignal,
fromEmitterEvent,
timeout,
} from '../common/util.js';
import {isErrorLike} from '../util/ErrorLike.js';
import {BidiCdpSession} from './CDPSession.js';
import type {BrowsingContext} from './core/BrowsingContext.js';
import type {Navigation} from './core/Navigation.js';
import type {Request} from './core/Request.js';
import {BidiDialog} from './Dialog.js';
import {BidiElementHandle} from './ElementHandle.js';
import {ExposableFunction} from './ExposedFunction.js';
import {BidiHTTPRequest, requests} from './HTTPRequest.js';
import type {BidiHTTPResponse} from './HTTPResponse.js';
import type {BidiPage} from './Page.js';
import type {BidiRealm} from './Realm.js';
import {BidiFrameRealm} from './Realm.js';
import {
getConsoleMessage,
isConsoleLogEntry,
isJavaScriptLogEntry,
rewriteNavigationError,
} from './util.js';
import {BidiWebWorker} from './WebWorker.js';
export class BidiFrame extends Frame {
static from(
parent: BidiPage | BidiFrame,
browsingContext: BrowsingContext,
): BidiFrame {
const frame = new BidiFrame(parent, browsingContext);
frame.#initialize();
return frame;
}
readonly #parent: BidiPage | BidiFrame;
readonly browsingContext: BrowsingContext;
readonly #frames = new WeakMap<BrowsingContext, BidiFrame>();
readonly realms: {default: BidiFrameRealm; internal: BidiFrameRealm};
override readonly _id: string;
override readonly client: BidiCdpSession;
override readonly accessibility: Accessibility;
private constructor(
parent: BidiPage | BidiFrame,
browsingContext: BrowsingContext,
) {
super();
this.#parent = parent;
this.browsingContext = browsingContext;
this._id = browsingContext.id;
this.client = new BidiCdpSession(this);
this.realms = {
default: BidiFrameRealm.from(this.browsingContext.defaultRealm, this),
internal: BidiFrameRealm.from(
this.browsingContext.createWindowRealm(
`__puppeteer_internal_${Math.ceil(Math.random() * 10000)}`,
),
this,
),
};
this.accessibility = new Accessibility(this.realms.default, this._id);
}
#initialize(): void {
for (const browsingContext of this.browsingContext.children) {
this.#createFrameTarget(browsingContext);
}
this.browsingContext.on('browsingcontext', ({browsingContext}) => {
this.#createFrameTarget(browsingContext);
});
this.browsingContext.on('closed', () => {
for (const session of BidiCdpSession.sessions.values()) {
if (session.frame === this) {
session.onClose();
}
}
this.page().trustedEmitter.emit(PageEvent.FrameDetached, this);
});
this.browsingContext.on('request', ({request}) => {
const httpRequest = BidiHTTPRequest.from(
request,
this,
this.page().isNetworkInterceptionEnabled,
);
request.once('success', () => {
this.page().trustedEmitter.emit(PageEvent.RequestFinished, httpRequest);
});
request.once('error', () => {
this.page().trustedEmitter.emit(PageEvent.RequestFailed, httpRequest);
});
void httpRequest.finalizeInterceptions();
});
this.browsingContext.on('navigation', ({navigation}) => {
navigation.once('fragment', () => {
this.page().trustedEmitter.emit(PageEvent.FrameNavigated, this);
});
});
this.browsingContext.on('load', () => {
this.page().trustedEmitter.emit(PageEvent.Load, undefined);
});
this.browsingContext.on('DOMContentLoaded', () => {
this._hasStartedLoading = true;
this.page().trustedEmitter.emit(PageEvent.DOMContentLoaded, undefined);
this.page().trustedEmitter.emit(PageEvent.FrameNavigated, this);
});
this.browsingContext.on('userprompt', ({userPrompt}) => {
this.page().trustedEmitter.emit(
PageEvent.Dialog,
BidiDialog.from(userPrompt),
);
});
this.browsingContext.on('log', ({entry}) => {
if (this._id !== entry.source.context) {
return;
}
if (isConsoleLogEntry(entry)) {
if (!this.page().listenerCount(PageEvent.Console)) {
return;
}
const args = entry.args.map(arg => {
return this.mainRealm().createHandle(arg);
});
this.page().trustedEmitter.emit(
PageEvent.Console,
getConsoleMessage(entry, args, this),
);
} else if (isJavaScriptLogEntry(entry)) {
const error = new Error(entry.text ?? '');
const messageHeight = error.message.split('\n').length;
const messageLines = error.stack!.split('\n').splice(0, messageHeight);
const stackLines = [];
if (entry.stackTrace) {
for (const frame of entry.stackTrace.callFrames) {
// Note we need to add `1` because the values are 0-indexed.
stackLines.push(
` at ${frame.functionName || '<anonymous>'} (${frame.url}:${
frame.lineNumber + 1
}:${frame.columnNumber + 1})`,
);
if (stackLines.length >= Error.stackTraceLimit) {
break;
}
}
}
error.stack = [...messageLines, ...stackLines].join('\n');
this.page().trustedEmitter.emit(PageEvent.PageError, error);
} else {
debugError(
`Unhandled LogEntry with type "${entry.type}", text "${entry.text}" and level "${entry.level}"`,
);
}
});
this.browsingContext.on('worker', ({realm}) => {
const worker = BidiWebWorker.from(this, realm);
realm.on('destroyed', () => {
this.page().trustedEmitter.emit(PageEvent.WorkerDestroyed, worker);
});
this.page().trustedEmitter.emit(PageEvent.WorkerCreated, worker);
});
}
#createFrameTarget(browsingContext: BrowsingContext) {
const frame = BidiFrame.from(this, browsingContext);
this.#frames.set(browsingContext, frame);
this.page().trustedEmitter.emit(PageEvent.FrameAttached, frame);
browsingContext.on('closed', () => {
this.#frames.delete(browsingContext);
});
return frame;
}
get timeoutSettings(): TimeoutSettings {
return this.page()._timeoutSettings;
}
override mainRealm(): BidiFrameRealm {
return this.realms.default;
}
override isolatedRealm(): BidiFrameRealm {
return this.realms.internal;
}
realm(id: string): BidiRealm | undefined {
for (const realm of Object.values(this.realms)) {
if (realm.realm.id === id) {
return realm;
}
}
return;
}
override page(): BidiPage {
let parent = this.#parent;
while (parent instanceof BidiFrame) {
parent = parent.#parent;
}
return parent;
}
override url(): string {
return this.browsingContext.url;
}
override parentFrame(): BidiFrame | null {
if (this.#parent instanceof BidiFrame) {
return this.#parent;
}
return null;
}
override childFrames(): BidiFrame[] {
return [...this.browsingContext.children].map(child => {
return this.#frames.get(child)!;
});
}
#detached$() {
return defer(() => {
if (this.detached) {
return of(this as Frame);
}
return fromEmitterEvent(
this.page().trustedEmitter,
PageEvent.FrameDetached,
).pipe(
filter(detachedFrame => {
return detachedFrame === this;
}),
);
});
}
@throwIfDetached
override async goto(
url: string,
options: GoToOptions = {},
): Promise<BidiHTTPResponse | null> {
const [response] = await Promise.all([
this.waitForNavigation(options),
// Some implementations currently only report errors when the
// readiness=interactive.
//
// Related: https://bugzilla.mozilla.org/show_bug.cgi?id=1846601
this.browsingContext
.navigate(url, Bidi.BrowsingContext.ReadinessState.Interactive)
.catch(error => {
if (
isErrorLike(error) &&
error.message.includes('net::ERR_HTTP_RESPONSE_CODE_FAILURE')
) {
return;
}
if (error.message.includes('navigation canceled')) {
return;
}
if (
error.message.includes(
'Navigation was aborted by another navigation',
)
) {
return;
}
throw error;
}),
]).catch(
rewriteNavigationError(
url,
options.timeout ?? this.timeoutSettings.navigationTimeout(),
),
);
return response;
}
@throwIfDetached
override async setContent(
html: string,
options: WaitForOptions = {},
): Promise<void> {
await Promise.all([
this.setFrameContent(html),
firstValueFrom(
combineLatest([
this.#waitForLoad$(options),
this.#waitForNetworkIdle$(options),
]),
),
]);
}
@throwIfDetached
override async waitForNavigation(
options: WaitForOptions = {},
): Promise<BidiHTTPResponse | null> {
const {timeout: ms = this.timeoutSettings.navigationTimeout(), signal} =
options;
const frames = this.childFrames().map(frame => {
return frame.#detached$();
});
return await firstValueFrom(
combineLatest([
race(
fromEmitterEvent(this.browsingContext, 'navigation'),
fromEmitterEvent(this.browsingContext, 'historyUpdated').pipe(
map(() => {
return {navigation: null};
}),
),
)
.pipe(first())
.pipe(
switchMap(({navigation}) => {
if (navigation === null) {
return of(null);
}
return this.#waitForLoad$(options).pipe(
delayWhen(() => {
if (frames.length === 0) {
return of(undefined);
}
return combineLatest(frames);
}),
raceWith(
fromEmitterEvent(navigation, 'fragment'),
fromEmitterEvent(navigation, 'failed'),
fromEmitterEvent(navigation, 'aborted'),
),
switchMap(() => {
if (navigation.request) {
function requestFinished$(
request: Request,
): Observable<Navigation | null> {
if (navigation === null) {
return of(null);
}
// Reduces flakiness if the response events arrive after
// the load event.
// Usually, the response or error is already there at this point.
if (request.response || request.error) {
return of(navigation);
}
if (request.redirect) {
return requestFinished$(request.redirect);
}
return fromEmitterEvent(request, 'success')
.pipe(
raceWith(fromEmitterEvent(request, 'error')),
raceWith(fromEmitterEvent(request, 'redirect')),
)
.pipe(
switchMap(() => {
return requestFinished$(request);
}),
);
}
return requestFinished$(navigation.request);
}
return of(navigation);
}),
);
}),
),
this.#waitForNetworkIdle$(options),
]).pipe(
map(([navigation]) => {
if (!navigation) {
return null;
}
const request = navigation.request;
if (!request) {
return null;
}
const lastRequest = request.lastRedirect ?? request;
const httpRequest = requests.get(lastRequest)!;
return httpRequest.response();
}),
raceWith(
timeout(ms),
fromAbortSignal(signal),
this.#detached$().pipe(
map(() => {
throw new TargetCloseError('Frame detached.');
}),
),
),
),
);
}
override waitForDevicePrompt(
options: WaitTimeoutOptions = {},
): Promise<DeviceRequestPrompt> {
const {timeout = this.timeoutSettings.timeout(), signal} = options;
return this.browsingContext.waitForDevicePrompt(timeout, signal);
}
override get detached(): boolean {
return this.browsingContext.closed;
}
#exposedFunctions = new Map<string, ExposableFunction<never[], unknown>>();
async exposeFunction<Args extends unknown[], Ret>(
name: string,
apply: (...args: Args) => Awaitable<Ret>,
): Promise<void> {
if (this.#exposedFunctions.has(name)) {
throw new Error(
`Failed to add page binding with name ${name}: globalThis['${name}'] already exists!`,
);
}
const exposable = await ExposableFunction.from(this, name, apply);
this.#exposedFunctions.set(name, exposable);
}
async removeExposedFunction(name: string): Promise<void> {
const exposedFunction = this.#exposedFunctions.get(name);
if (!exposedFunction) {
throw new Error(
`Failed to remove page binding with name ${name}: window['${name}'] does not exists!`,
);
}
this.#exposedFunctions.delete(name);
await exposedFunction[Symbol.asyncDispose]();
}
async createCDPSession(): Promise<CDPSession> {
if (!this.page().browser().cdpSupported) {
throw new UnsupportedOperation();
}
const cdpConnection = this.page().browser().cdpConnection!;
return await cdpConnection._createSession({targetId: this._id});
}
@throwIfDetached
#waitForLoad$(options: WaitForOptions = {}): Observable<void> {
let {waitUntil = 'load'} = options;
const {timeout: ms = this.timeoutSettings.navigationTimeout()} = options;
if (!Array.isArray(waitUntil)) {
waitUntil = [waitUntil];
}
const events = new Set<'load' | 'DOMContentLoaded'>();
for (const lifecycleEvent of waitUntil) {
switch (lifecycleEvent) {
case 'load': {
events.add('load');
break;
}
case 'domcontentloaded': {
events.add('DOMContentLoaded');
break;
}
}
}
if (events.size === 0) {
return of(undefined);
}
return combineLatest(
[...events].map(event => {
return fromEmitterEvent(this.browsingContext, event);
}),
).pipe(
map(() => {}),
first(),
raceWith(
timeout(ms),
this.#detached$().pipe(
map(() => {
throw new Error('Frame detached.');
}),
),
),
);
}
@throwIfDetached
#waitForNetworkIdle$(options: WaitForOptions = {}): Observable<void> {
let {waitUntil = 'load'} = options;
if (!Array.isArray(waitUntil)) {
waitUntil = [waitUntil];
}
let concurrency = Infinity;
for (const event of waitUntil) {
switch (event) {
case 'networkidle0': {
concurrency = Math.min(0, concurrency);
break;
}
case 'networkidle2': {
concurrency = Math.min(2, concurrency);
break;
}
}
}
if (concurrency === Infinity) {
return of(undefined);
}
return this.page().waitForNetworkIdle$({
idleTime: 500,
timeout: options.timeout ?? this.timeoutSettings.timeout(),
concurrency,
});
}
@throwIfDetached
async setFiles(element: BidiElementHandle, files: string[]): Promise<void> {
await this.browsingContext.setFiles(
// SAFETY: ElementHandles are always remote references.
element.remoteValue() as Bidi.Script.SharedReference,
files,
);
}
@throwIfDetached
override async frameElement(): Promise<HandleFor<HTMLIFrameElement> | null> {
const parentFrame = this.parentFrame();
if (!parentFrame) {
return null;
}
const [node] = await parentFrame.browsingContext.locateNodes({
type: 'context',
value: {
context: this._id,
},
});
if (!node) {
return null;
}
return BidiElementHandle.from(
node,
parentFrame.mainRealm(),
) as HandleFor<HTMLIFrameElement>;
}
@throwIfDetached
async locateNodes(
element: BidiElementHandle,
locator: Bidi.BrowsingContext.Locator,
): Promise<Bidi.Script.NodeRemoteValue[]> {
return await this.browsingContext.locateNodes(
locator,
// SAFETY: ElementHandles are always remote references.
[element.remoteValue() as Bidi.Script.SharedReference],
);
}
override extensionRealms(): Realm[] {
throw new UnsupportedOperation();
}
}

354
node_modules/puppeteer-core/src/bidi/HTTPRequest.ts generated vendored Normal file
View File

@@ -0,0 +1,354 @@
/**
* @license
* Copyright 2020 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {Protocol} from 'devtools-protocol';
import type * as Bidi from 'webdriver-bidi-protocol';
import type {CDPSession} from '../api/CDPSession.js';
import type {
ContinueRequestOverrides,
InterceptResolutionState,
ResponseForRequest,
} from '../api/HTTPRequest.js';
import {
HTTPRequest,
STATUS_TEXTS,
type ResourceType,
handleError,
InterceptResolutionAction,
} from '../api/HTTPRequest.js';
import {PageEvent} from '../api/Page.js';
import {UnsupportedOperation} from '../common/Errors.js';
import {stringToBase64} from '../util/encoding.js';
import type {Request} from './core/Request.js';
import type {BidiFrame} from './Frame.js';
import {BidiHTTPResponse} from './HTTPResponse.js';
export const requests = new WeakMap<Request, BidiHTTPRequest>();
/**
* @internal
*/
export class BidiHTTPRequest extends HTTPRequest {
static from(
bidiRequest: Request,
frame: BidiFrame,
isNetworkInterceptionEnabled: boolean,
redirect?: BidiHTTPRequest,
): BidiHTTPRequest {
const request = new BidiHTTPRequest(
bidiRequest,
frame,
isNetworkInterceptionEnabled,
redirect,
);
request.#initialize();
return request;
}
#redirectChain: BidiHTTPRequest[];
#response: BidiHTTPResponse | null = null;
override readonly id: string;
readonly #frame: BidiFrame;
readonly #request: Request;
private constructor(
request: Request,
frame: BidiFrame,
isNetworkInterceptionEnabled: boolean,
redirect?: BidiHTTPRequest,
) {
super();
requests.set(request, this);
this.interception.enabled = isNetworkInterceptionEnabled;
this.#request = request;
this.#frame = frame;
this.#redirectChain = redirect ? redirect.#redirectChain : [];
this.id = request.id;
}
override get client(): CDPSession {
return this.#frame.client;
}
#initialize() {
this.#request.on('redirect', request => {
const httpRequest = BidiHTTPRequest.from(
request,
this.#frame,
this.interception.enabled,
this,
);
this.#redirectChain.push(this);
request.once('success', () => {
this.#frame
.page()
.trustedEmitter.emit(PageEvent.RequestFinished, httpRequest);
});
request.once('error', () => {
this.#frame
.page()
.trustedEmitter.emit(PageEvent.RequestFailed, httpRequest);
});
void httpRequest.finalizeInterceptions();
});
this.#request.once('response', data => {
// Create new response with the initial data. Note: the data can be updated later
// on, when the `success` event is received.
this.#response = BidiHTTPResponse.from(
data,
this,
this.#frame.page().browser().cdpSupported,
);
});
this.#request.once('success', data => {
// The `network.responseCompleted` event (mapped to `success` here)
// contains the most up-to-date and complete response data, including
// headers that might be missing from `network.responseStarted`
// (e.g., `Set-Cookie` for navigation requests in Chrome).
this.#response = BidiHTTPResponse.from(
data,
this,
this.#frame.page().browser().cdpSupported,
);
});
this.#request.on('authenticate', this.#handleAuthentication);
this.#frame.page().trustedEmitter.emit(PageEvent.Request, this);
}
protected canBeIntercepted(): boolean {
return this.#request.isBlocked;
}
override interceptResolutionState(): InterceptResolutionState {
if (!this.#request.isBlocked) {
return {action: InterceptResolutionAction.Disabled};
}
return super.interceptResolutionState();
}
override url(): string {
return this.#request.url;
}
override resourceType(): ResourceType {
if (!this.#frame.page().browser().cdpSupported) {
throw new UnsupportedOperation();
}
return (
this.#request.resourceType || 'other'
).toLowerCase() as ResourceType;
}
override method(): string {
return this.#request.method;
}
override postData(): string | undefined {
if (!this.#frame.page().browser().cdpSupported) {
throw new UnsupportedOperation();
}
return this.#request.postData;
}
override hasPostData(): boolean {
return this.#request.hasPostData;
}
override async fetchPostData(): Promise<string | undefined> {
return await this.#request.fetchPostData();
}
override headers(): Record<string, string> {
// Callers should not be allowed to mutate internal structure.
const headers: Record<string, string> = {};
for (const header of this.#request.headers) {
headers[header.name.toLowerCase()] = header.value.value;
}
return {
...headers,
};
}
override response(): BidiHTTPResponse | null {
return this.#response;
}
override failure(): {errorText: string} | null {
if (this.#request.error === undefined) {
return null;
}
return {errorText: this.#request.error};
}
override isNavigationRequest(): boolean {
return this.#request.navigation !== undefined;
}
override initiator(): Protocol.Network.Initiator | undefined {
return {
...this.#request.initiator,
type: this.#request.initiator?.type ?? 'other',
};
}
override redirectChain(): BidiHTTPRequest[] {
return this.#redirectChain.slice();
}
override frame(): BidiFrame {
return this.#frame;
}
override async _continue(
overrides: ContinueRequestOverrides = {},
): Promise<void> {
const headers: Bidi.Network.Header[] = getBidiHeaders(overrides.headers);
this.interception.handled = true;
return await this.#request
.continueRequest({
url: overrides.url,
method: overrides.method,
body: overrides.postData
? {
type: 'base64',
value: stringToBase64(overrides.postData),
}
: undefined,
headers: headers.length > 0 ? headers : undefined,
})
.catch(error => {
this.interception.handled = false;
return handleError(error);
});
}
override async _abort(): Promise<void> {
this.interception.handled = true;
return await this.#request.failRequest().catch(error => {
this.interception.handled = false;
throw error;
});
}
override async _respond(
response: Partial<ResponseForRequest>,
_priority?: number,
): Promise<void> {
this.interception.handled = true;
let parsedBody:
| {
contentLength: number;
base64: string;
}
| undefined;
if (response.body) {
parsedBody = HTTPRequest.getResponse(response.body);
}
const headers: Bidi.Network.Header[] = getBidiHeaders(response.headers);
const hasContentLength = headers.some(header => {
return header.name === 'content-length';
});
if (response.contentType) {
headers.push({
name: 'content-type',
value: {
type: 'string',
value: response.contentType,
},
});
}
if (parsedBody?.contentLength && !hasContentLength) {
headers.push({
name: 'content-length',
value: {
type: 'string',
value: String(parsedBody.contentLength),
},
});
}
const status = response.status || 200;
return await this.#request
.provideResponse({
statusCode: status,
headers: headers.length > 0 ? headers : undefined,
reasonPhrase: STATUS_TEXTS[status],
body: parsedBody?.base64
? {
type: 'base64',
value: parsedBody?.base64,
}
: undefined,
})
.catch(error => {
this.interception.handled = false;
throw error;
});
}
#authenticationHandled = false;
#handleAuthentication = async () => {
if (!this.#frame) {
return;
}
const credentials = this.#frame.page()._credentials;
if (credentials && !this.#authenticationHandled) {
this.#authenticationHandled = true;
void this.#request.continueWithAuth({
action: 'provideCredentials',
credentials: {
type: 'password',
username: credentials.username,
password: credentials.password,
},
});
} else {
void this.#request.continueWithAuth({
action: 'cancel',
});
}
};
timing(): Bidi.Network.FetchTimingInfo {
return this.#request.timing();
}
getResponseContent(): Promise<Uint8Array> {
return this.#request.getResponseContent();
}
}
function getBidiHeaders(rawHeaders?: Record<string, unknown>) {
const headers: Bidi.Network.Header[] = [];
for (const [name, value] of Object.entries(rawHeaders ?? [])) {
if (!Object.is(value, undefined)) {
const values = Array.isArray(value) ? value : [value];
for (const value of values) {
headers.push({
name: name.toLowerCase(),
value: {
type: 'string',
value: String(value),
},
});
}
}
}
return headers;
}

162
node_modules/puppeteer-core/src/bidi/HTTPResponse.ts generated vendored Normal file
View File

@@ -0,0 +1,162 @@
/**
* @license
* Copyright 2020 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {Protocol} from 'devtools-protocol';
import type * as Bidi from 'webdriver-bidi-protocol';
import type {Frame} from '../api/Frame.js';
import {HTTPResponse, type RemoteAddress} from '../api/HTTPResponse.js';
import {PageEvent} from '../api/Page.js';
import {UnsupportedOperation} from '../common/Errors.js';
import {SecurityDetails} from '../common/SecurityDetails.js';
import {invokeAtMostOnceForArguments} from '../util/decorators.js';
import type {BidiHTTPRequest} from './HTTPRequest.js';
/**
* @internal
*/
export class BidiHTTPResponse extends HTTPResponse {
/**
* Returns a new BidiHTTPResponse or updates the existing one if it already exists.
*/
static from(
data: Bidi.Network.ResponseData,
request: BidiHTTPRequest,
cdpSupported: boolean,
): BidiHTTPResponse {
const existingResponse = request.response();
if (existingResponse) {
// Update existing response data with up-to-date data.
existingResponse.#data = data;
return existingResponse;
}
const response = new BidiHTTPResponse(data, request, cdpSupported);
response.#initialize();
return response;
}
#data: Bidi.Network.ResponseData;
#request: BidiHTTPRequest;
#securityDetails?: SecurityDetails;
#cdpSupported = false;
private constructor(
data: Bidi.Network.ResponseData,
request: BidiHTTPRequest,
cdpSupported: boolean,
) {
super();
this.#data = data;
this.#request = request;
this.#cdpSupported = cdpSupported;
// @ts-expect-error non-standard property.
const securityDetails = data['goog:securityDetails'];
if (cdpSupported && securityDetails) {
this.#securityDetails = new SecurityDetails(
securityDetails as Protocol.Network.SecurityDetails,
);
}
}
#initialize() {
if (this.#data.fromCache) {
this.#request._fromMemoryCache = true;
this.#request
.frame()
?.page()
.trustedEmitter.emit(PageEvent.RequestServedFromCache, this.#request);
}
this.#request.frame()?.page().trustedEmitter.emit(PageEvent.Response, this);
}
@invokeAtMostOnceForArguments
override remoteAddress(): RemoteAddress {
return {
ip: '',
port: -1,
};
}
override url(): string {
return this.#data.url;
}
override status(): number {
return this.#data.status;
}
override statusText(): string {
return this.#data.statusText;
}
override headers(): Record<string, string> {
const headers: Record<string, string> = {};
for (const header of this.#data.headers) {
// TODO: How to handle Binary Headers
// https://w3c.github.io/webdriver-bidi/#type-network-Header
if (header.value.type === 'string') {
headers[header.name.toLowerCase()] = header.value.value;
}
}
return headers;
}
override request(): BidiHTTPRequest {
return this.#request;
}
override fromCache(): boolean {
return this.#data.fromCache;
}
override timing(): Protocol.Network.ResourceTiming | null {
const bidiTiming = this.#request.timing();
return {
requestTime: bidiTiming.requestTime,
proxyStart: -1,
proxyEnd: -1,
dnsStart: bidiTiming.dnsStart,
dnsEnd: bidiTiming.dnsEnd,
connectStart: bidiTiming.connectStart,
connectEnd: bidiTiming.connectEnd,
sslStart: bidiTiming.tlsStart,
sslEnd: -1,
workerStart: -1,
workerReady: -1,
workerFetchStart: -1,
workerRespondWithSettled: -1,
workerRouterEvaluationStart: -1,
workerCacheLookupStart: -1,
sendStart: bidiTiming.requestStart,
sendEnd: -1,
pushStart: -1,
pushEnd: -1,
receiveHeadersStart: bidiTiming.responseStart,
receiveHeadersEnd: bidiTiming.responseEnd,
};
}
override frame(): Frame | null {
return this.#request.frame();
}
override fromServiceWorker(): boolean {
return false;
}
override securityDetails(): SecurityDetails | null {
if (!this.#cdpSupported) {
throw new UnsupportedOperation();
}
return this.#securityDetails ?? null;
}
async content(): Promise<Uint8Array> {
return await this.#request.getResponseContent();
}
}

741
node_modules/puppeteer-core/src/bidi/Input.ts generated vendored Normal file
View File

@@ -0,0 +1,741 @@
/**
* @license
* Copyright 2017 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import * as Bidi from 'webdriver-bidi-protocol';
import type {Point} from '../api/ElementHandle.js';
import {
Keyboard,
Mouse,
MouseButton,
Touchscreen,
type TouchHandle,
type KeyboardTypeOptions,
type KeyDownOptions,
type KeyPressOptions,
type MouseClickOptions,
type MouseMoveOptions,
type MouseOptions,
type MouseWheelOptions,
} from '../api/Input.js';
import {UnsupportedOperation} from '../common/Errors.js';
import {TouchError} from '../common/Errors.js';
import type {KeyInput} from '../common/USKeyboardLayout.js';
import type {BidiPage} from './Page.js';
const enum InputId {
Mouse = '__puppeteer_mouse',
Keyboard = '__puppeteer_keyboard',
Wheel = '__puppeteer_wheel',
Finger = '__puppeteer_finger',
}
enum SourceActionsType {
None = 'none',
Key = 'key',
Pointer = 'pointer',
Wheel = 'wheel',
}
enum ActionType {
Pause = 'pause',
KeyDown = 'keyDown',
KeyUp = 'keyUp',
PointerUp = 'pointerUp',
PointerDown = 'pointerDown',
PointerMove = 'pointerMove',
Scroll = 'scroll',
}
const getBidiKeyValue = (key: KeyInput) => {
switch (key) {
case '\r':
case '\n':
key = 'Enter';
break;
}
// Measures the number of code points rather than UTF-16 code units.
if ([...key].length === 1) {
return key;
}
switch (key) {
case 'Cancel':
return '\uE001';
case 'Help':
return '\uE002';
case 'Backspace':
return '\uE003';
case 'Tab':
return '\uE004';
case 'Clear':
return '\uE005';
case 'Enter':
return '\uE007';
case 'Shift':
case 'ShiftLeft':
return '\uE008';
case 'Control':
case 'ControlLeft':
return '\uE009';
case 'Alt':
case 'AltLeft':
return '\uE00A';
case 'Pause':
return '\uE00B';
case 'Escape':
return '\uE00C';
case 'PageUp':
return '\uE00E';
case 'PageDown':
return '\uE00F';
case 'End':
return '\uE010';
case 'Home':
return '\uE011';
case 'ArrowLeft':
return '\uE012';
case 'ArrowUp':
return '\uE013';
case 'ArrowRight':
return '\uE014';
case 'ArrowDown':
return '\uE015';
case 'Insert':
return '\uE016';
case 'Delete':
return '\uE017';
case 'NumpadEqual':
return '\uE019';
case 'Numpad0':
return '\uE01A';
case 'Numpad1':
return '\uE01B';
case 'Numpad2':
return '\uE01C';
case 'Numpad3':
return '\uE01D';
case 'Numpad4':
return '\uE01E';
case 'Numpad5':
return '\uE01F';
case 'Numpad6':
return '\uE020';
case 'Numpad7':
return '\uE021';
case 'Numpad8':
return '\uE022';
case 'Numpad9':
return '\uE023';
case 'NumpadMultiply':
return '\uE024';
case 'NumpadAdd':
return '\uE025';
case 'NumpadSubtract':
return '\uE027';
case 'NumpadDecimal':
return '\uE028';
case 'NumpadDivide':
return '\uE029';
case 'F1':
return '\uE031';
case 'F2':
return '\uE032';
case 'F3':
return '\uE033';
case 'F4':
return '\uE034';
case 'F5':
return '\uE035';
case 'F6':
return '\uE036';
case 'F7':
return '\uE037';
case 'F8':
return '\uE038';
case 'F9':
return '\uE039';
case 'F10':
return '\uE03A';
case 'F11':
return '\uE03B';
case 'F12':
return '\uE03C';
case 'Meta':
case 'MetaLeft':
return '\uE03D';
case 'ShiftRight':
return '\uE050';
case 'ControlRight':
return '\uE051';
case 'AltRight':
return '\uE052';
case 'MetaRight':
return '\uE053';
case 'Digit0':
return '0';
case 'Digit1':
return '1';
case 'Digit2':
return '2';
case 'Digit3':
return '3';
case 'Digit4':
return '4';
case 'Digit5':
return '5';
case 'Digit6':
return '6';
case 'Digit7':
return '7';
case 'Digit8':
return '8';
case 'Digit9':
return '9';
case 'KeyA':
return 'a';
case 'KeyB':
return 'b';
case 'KeyC':
return 'c';
case 'KeyD':
return 'd';
case 'KeyE':
return 'e';
case 'KeyF':
return 'f';
case 'KeyG':
return 'g';
case 'KeyH':
return 'h';
case 'KeyI':
return 'i';
case 'KeyJ':
return 'j';
case 'KeyK':
return 'k';
case 'KeyL':
return 'l';
case 'KeyM':
return 'm';
case 'KeyN':
return 'n';
case 'KeyO':
return 'o';
case 'KeyP':
return 'p';
case 'KeyQ':
return 'q';
case 'KeyR':
return 'r';
case 'KeyS':
return 's';
case 'KeyT':
return 't';
case 'KeyU':
return 'u';
case 'KeyV':
return 'v';
case 'KeyW':
return 'w';
case 'KeyX':
return 'x';
case 'KeyY':
return 'y';
case 'KeyZ':
return 'z';
case 'Semicolon':
return ';';
case 'Equal':
return '=';
case 'Comma':
return ',';
case 'Minus':
return '-';
case 'Period':
return '.';
case 'Slash':
return '/';
case 'Backquote':
return '`';
case 'BracketLeft':
return '[';
case 'Backslash':
return '\\';
case 'BracketRight':
return ']';
case 'Quote':
return '"';
default:
throw new Error(`Unknown key: "${key}"`);
}
};
/**
* @internal
*/
export class BidiKeyboard extends Keyboard {
#page: BidiPage;
constructor(page: BidiPage) {
super();
this.#page = page;
}
override async down(
key: KeyInput,
_options?: Readonly<KeyDownOptions>,
): Promise<void> {
await this.#page.mainFrame().browsingContext.performActions([
{
type: SourceActionsType.Key,
id: InputId.Keyboard,
actions: [
{
type: ActionType.KeyDown,
value: getBidiKeyValue(key),
},
],
},
]);
}
override async up(key: KeyInput): Promise<void> {
await this.#page.mainFrame().browsingContext.performActions([
{
type: SourceActionsType.Key,
id: InputId.Keyboard,
actions: [
{
type: ActionType.KeyUp,
value: getBidiKeyValue(key),
},
],
},
]);
}
override async press(
key: KeyInput,
options: Readonly<KeyPressOptions> = {},
): Promise<void> {
const {delay = 0} = options;
const actions: Bidi.Input.KeySourceAction[] = [
{
type: ActionType.KeyDown,
value: getBidiKeyValue(key),
},
];
if (delay > 0) {
actions.push({
type: ActionType.Pause,
duration: delay,
});
}
actions.push({
type: ActionType.KeyUp,
value: getBidiKeyValue(key),
});
await this.#page.mainFrame().browsingContext.performActions([
{
type: SourceActionsType.Key,
id: InputId.Keyboard,
actions,
},
]);
}
override async type(
text: string,
options: Readonly<KeyboardTypeOptions> = {},
): Promise<void> {
const {delay = 0} = options;
// This spread separates the characters into code points rather than UTF-16
// code units.
const values = ([...text] as KeyInput[]).map(getBidiKeyValue);
const actions: Bidi.Input.KeySourceAction[] = [];
if (delay <= 0) {
for (const value of values) {
actions.push(
{
type: ActionType.KeyDown,
value,
},
{
type: ActionType.KeyUp,
value,
},
);
}
} else {
for (const value of values) {
actions.push(
{
type: ActionType.KeyDown,
value,
},
{
type: ActionType.Pause,
duration: delay,
},
{
type: ActionType.KeyUp,
value,
},
);
}
}
await this.#page.mainFrame().browsingContext.performActions([
{
type: SourceActionsType.Key,
id: InputId.Keyboard,
actions,
},
]);
}
override async sendCharacter(char: string): Promise<void> {
// Measures the number of code points rather than UTF-16 code units.
if ([...char].length > 1) {
throw new Error('Cannot send more than 1 character.');
}
const frame = await this.#page.focusedFrame();
await frame.isolatedRealm().evaluate(async char => {
document.execCommand('insertText', false, char);
}, char);
}
}
/**
* @internal
*/
export interface BidiMouseClickOptions extends MouseClickOptions {
origin?: Bidi.Input.Origin;
}
/**
* @internal
*/
export interface BidiMouseMoveOptions extends MouseMoveOptions {
origin?: Bidi.Input.Origin;
}
/**
* @internal
*/
export interface BidiTouchMoveOptions {
origin?: Bidi.Input.Origin;
}
const getBidiButton = (button: MouseButton) => {
switch (button) {
case MouseButton.Left:
return 0;
case MouseButton.Middle:
return 1;
case MouseButton.Right:
return 2;
case MouseButton.Back:
return 3;
case MouseButton.Forward:
return 4;
}
};
/**
* @internal
*/
export class BidiMouse extends Mouse {
#page: BidiPage;
#lastMovePoint: Point = {x: 0, y: 0};
constructor(page: BidiPage) {
super();
this.#page = page;
}
override async reset(): Promise<void> {
this.#lastMovePoint = {x: 0, y: 0};
await this.#page.mainFrame().browsingContext.releaseActions();
}
override async move(
x: number,
y: number,
options: Readonly<BidiMouseMoveOptions> = {},
): Promise<void> {
const from = this.#lastMovePoint;
const to = {
x: Math.round(x),
y: Math.round(y),
};
const actions: Bidi.Input.PointerSourceAction[] = [];
const steps = options.steps ?? 0;
for (let i = 0; i < steps; ++i) {
actions.push({
type: ActionType.PointerMove,
x: from.x + (to.x - from.x) * (i / steps),
y: from.y + (to.y - from.y) * (i / steps),
origin: options.origin,
});
}
actions.push({
type: ActionType.PointerMove,
...to,
origin: options.origin,
});
// https://w3c.github.io/webdriver-bidi/#command-input-performActions:~:text=input.PointerMoveAction%20%3D%20%7B%0A%20%20type%3A%20%22pointerMove%22%2C%0A%20%20x%3A%20js%2Dint%2C
this.#lastMovePoint = to;
await this.#page.mainFrame().browsingContext.performActions([
{
type: SourceActionsType.Pointer,
id: InputId.Mouse,
actions,
},
]);
}
override async down(options: Readonly<MouseOptions> = {}): Promise<void> {
await this.#page.mainFrame().browsingContext.performActions([
{
type: SourceActionsType.Pointer,
id: InputId.Mouse,
actions: [
{
type: ActionType.PointerDown,
button: getBidiButton(options.button ?? MouseButton.Left),
},
],
},
]);
}
override async up(options: Readonly<MouseOptions> = {}): Promise<void> {
await this.#page.mainFrame().browsingContext.performActions([
{
type: SourceActionsType.Pointer,
id: InputId.Mouse,
actions: [
{
type: ActionType.PointerUp,
button: getBidiButton(options.button ?? MouseButton.Left),
},
],
},
]);
}
override async click(
x: number,
y: number,
options: Readonly<BidiMouseClickOptions> = {},
): Promise<void> {
const actions: Bidi.Input.PointerSourceAction[] = [
{
type: ActionType.PointerMove,
x: Math.round(x),
y: Math.round(y),
origin: options.origin,
},
];
const pointerDownAction = {
type: ActionType.PointerDown,
button: getBidiButton(options.button ?? MouseButton.Left),
} as const;
const pointerUpAction = {
type: ActionType.PointerUp,
button: pointerDownAction.button,
} as const;
for (let i = 1; i < (options.count ?? 1); ++i) {
actions.push(pointerDownAction, pointerUpAction);
}
actions.push(pointerDownAction);
if (options.delay) {
actions.push({
type: ActionType.Pause,
duration: options.delay,
});
}
actions.push(pointerUpAction);
await this.#page.mainFrame().browsingContext.performActions([
{
type: SourceActionsType.Pointer,
id: InputId.Mouse,
actions,
},
]);
}
override async wheel(
options: Readonly<MouseWheelOptions> = {},
): Promise<void> {
await this.#page.mainFrame().browsingContext.performActions([
{
type: SourceActionsType.Wheel,
id: InputId.Wheel,
actions: [
{
type: ActionType.Scroll,
...(this.#lastMovePoint ?? {
x: 0,
y: 0,
}),
deltaX: options.deltaX ?? 0,
deltaY: options.deltaY ?? 0,
},
],
},
]);
}
override drag(): never {
throw new UnsupportedOperation();
}
override dragOver(): never {
throw new UnsupportedOperation();
}
override dragEnter(): never {
throw new UnsupportedOperation();
}
override drop(): never {
throw new UnsupportedOperation();
}
override dragAndDrop(): never {
throw new UnsupportedOperation();
}
}
/**
* @internal
*/
class BidiTouchHandle implements TouchHandle {
#started = false;
#x: number;
#y: number;
#bidiId: string;
#page: BidiPage;
#touchScreen: BidiTouchscreen;
#properties: Bidi.Input.PointerCommonProperties;
constructor(
page: BidiPage,
touchScreen: BidiTouchscreen,
id: number,
x: number,
y: number,
properties: Bidi.Input.PointerCommonProperties,
) {
this.#page = page;
this.#touchScreen = touchScreen;
this.#x = Math.round(x);
this.#y = Math.round(y);
this.#properties = properties;
this.#bidiId = `${InputId.Finger}_${id}`;
}
async start(options: BidiTouchMoveOptions = {}): Promise<void> {
if (this.#started) {
throw new TouchError('Touch has already started');
}
await this.#page.mainFrame().browsingContext.performActions([
{
type: SourceActionsType.Pointer,
id: this.#bidiId,
parameters: {
pointerType: Bidi.Input.PointerType.Touch,
},
actions: [
{
type: ActionType.PointerMove,
x: this.#x,
y: this.#y,
origin: options.origin,
},
{
...this.#properties,
type: ActionType.PointerDown,
button: 0,
},
],
},
]);
this.#started = true;
}
move(x: number, y: number): Promise<void> {
const newX = Math.round(x);
const newY = Math.round(y);
return this.#page.mainFrame().browsingContext.performActions([
{
type: SourceActionsType.Pointer,
id: this.#bidiId,
parameters: {
pointerType: Bidi.Input.PointerType.Touch,
},
actions: [
{
...this.#properties,
type: ActionType.PointerMove,
x: newX,
y: newY,
},
],
},
]);
}
async end(): Promise<void> {
await this.#page.mainFrame().browsingContext.performActions([
{
type: SourceActionsType.Pointer,
id: this.#bidiId,
parameters: {
pointerType: Bidi.Input.PointerType.Touch,
},
actions: [
{
type: ActionType.PointerUp,
button: 0,
},
],
},
]);
this.#touchScreen.removeHandle(this);
}
}
/**
* @internal
*/
export class BidiTouchscreen extends Touchscreen {
#page: BidiPage;
declare touches: BidiTouchHandle[];
constructor(page: BidiPage) {
super();
this.#page = page;
}
override async touchStart(
x: number,
y: number,
options: BidiTouchMoveOptions = {},
): Promise<TouchHandle> {
const id = this.idGenerator();
const properties: Bidi.Input.PointerCommonProperties = {
width: 0.5 * 2, // 2 times default touch radius.
height: 0.5 * 2, // 2 times default touch radius.
pressure: 0.5,
};
const touch = new BidiTouchHandle(this.#page, this, id, x, y, properties);
await touch.start(options);
this.touches.push(touch);
return touch;
}
}

95
node_modules/puppeteer-core/src/bidi/JSHandle.ts generated vendored Normal file
View File

@@ -0,0 +1,95 @@
/**
* @license
* Copyright 2023 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type * as Bidi from 'webdriver-bidi-protocol';
import type {ElementHandle} from '../api/ElementHandle.js';
import {JSHandle} from '../api/JSHandle.js';
import {UnsupportedOperation} from '../common/Errors.js';
import {BidiDeserializer} from './Deserializer.js';
import type {BidiRealm} from './Realm.js';
/**
* @internal
*/
export class BidiJSHandle<T = unknown> extends JSHandle<T> {
static from<T>(
value: Bidi.Script.RemoteValue,
realm: BidiRealm,
): BidiJSHandle<T> {
return new BidiJSHandle(value, realm);
}
readonly #remoteValue: Bidi.Script.RemoteValue;
override readonly realm: BidiRealm;
#disposed = false;
constructor(value: Bidi.Script.RemoteValue, realm: BidiRealm) {
super();
this.#remoteValue = value;
this.realm = realm;
}
override get disposed(): boolean {
return this.#disposed;
}
override async jsonValue(): Promise<T> {
return await this.evaluate(value => {
return value;
});
}
override asElement(): ElementHandle<Node> | null {
return null;
}
override async dispose(): Promise<void> {
if (this.#disposed) {
return;
}
this.#disposed = true;
await this.realm.destroyHandles([this]);
}
get isPrimitiveValue(): boolean {
switch (this.#remoteValue.type) {
case 'string':
case 'number':
case 'bigint':
case 'boolean':
case 'undefined':
case 'null':
return true;
default:
return false;
}
}
override toString(): string {
if (this.isPrimitiveValue) {
return 'JSHandle:' + BidiDeserializer.deserialize(this.#remoteValue);
}
return 'JSHandle@' + this.#remoteValue.type;
}
override get id(): string | undefined {
return 'handle' in this.#remoteValue ? this.#remoteValue.handle : undefined;
}
remoteValue(): Bidi.Script.RemoteValue {
return this.#remoteValue;
}
override remoteObject(): never {
throw new UnsupportedOperation('Not available in WebDriver BiDi');
}
}

1231
node_modules/puppeteer-core/src/bidi/Page.ts generated vendored Normal file

File diff suppressed because it is too large Load Diff

448
node_modules/puppeteer-core/src/bidi/Realm.ts generated vendored Normal file
View File

@@ -0,0 +1,448 @@
/**
* @license
* Copyright 2024 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import * as Bidi from 'webdriver-bidi-protocol';
import type {Extension} from '../api/Extension.js';
import type {JSHandle} from '../api/JSHandle.js';
import {Realm} from '../api/Realm.js';
import {WebWorkerEvent} from '../api/WebWorker.js';
import {ARIAQueryHandler} from '../common/AriaQueryHandler.js';
import {LazyArg} from '../common/LazyArg.js';
import {scriptInjector} from '../common/ScriptInjector.js';
import type {TimeoutSettings} from '../common/TimeoutSettings.js';
import type {EvaluateFunc, HandleFor} from '../common/types.js';
import {
debugError,
getSourcePuppeteerURLIfAvailable,
getSourceUrlComment,
isString,
PuppeteerURL,
SOURCE_URL_REGEX,
} from '../common/util.js';
import {UnsupportedOperation} from '../index-browser.js';
import type {PuppeteerInjectedUtil} from '../injected/injected.js';
import {AsyncIterableUtil} from '../util/AsyncIterableUtil.js';
import {stringifyFunction} from '../util/Function.js';
import type {
Realm as BidiRealmCore,
DedicatedWorkerRealm,
SharedWorkerRealm,
} from './core/Realm.js';
import type {WindowRealm} from './core/Realm.js';
import {BidiDeserializer} from './Deserializer.js';
import {BidiElementHandle} from './ElementHandle.js';
import {ExposableFunction} from './ExposedFunction.js';
import type {BidiFrame} from './Frame.js';
import {BidiJSHandle} from './JSHandle.js';
import {BidiSerializer} from './Serializer.js';
import {
createEvaluationError,
getConsoleMessage,
isConsoleLogEntry,
rewriteEvaluationError,
} from './util.js';
import type {BidiWebWorker} from './WebWorker.js';
/**
* @internal
*/
export abstract class BidiRealm extends Realm {
readonly realm: BidiRealmCore;
constructor(realm: BidiRealmCore, timeoutSettings: TimeoutSettings) {
super(timeoutSettings);
this.realm = realm;
}
protected initialize(): void {
this.realm.on('destroyed', ({reason}) => {
this.taskManager.terminateAll(new Error(reason));
this.dispose();
});
this.realm.on('updated', () => {
this.internalPuppeteerUtil = undefined;
void this.taskManager.rerunAll();
});
}
protected internalPuppeteerUtil?: Promise<
BidiJSHandle<PuppeteerInjectedUtil>
>;
get puppeteerUtil(): Promise<BidiJSHandle<PuppeteerInjectedUtil>> {
const promise = Promise.resolve() as Promise<unknown>;
scriptInjector.inject(script => {
if (this.internalPuppeteerUtil) {
void this.internalPuppeteerUtil.then(handle => {
void handle.dispose();
});
}
this.internalPuppeteerUtil = promise.then(() => {
return this.evaluateHandle(script) as Promise<
BidiJSHandle<PuppeteerInjectedUtil>
>;
});
}, !this.internalPuppeteerUtil);
return this.internalPuppeteerUtil as Promise<
BidiJSHandle<PuppeteerInjectedUtil>
>;
}
override async evaluateHandle<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
>(
pageFunction: Func | string,
...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
return await this.#evaluate(false, pageFunction, ...args);
}
override async evaluate<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
>(
pageFunction: Func | string,
...args: Params
): Promise<Awaited<ReturnType<Func>>> {
return await this.#evaluate(true, pageFunction, ...args);
}
async #evaluate<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
>(
returnByValue: true,
pageFunction: Func | string,
...args: Params
): Promise<Awaited<ReturnType<Func>>>;
async #evaluate<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
>(
returnByValue: false,
pageFunction: Func | string,
...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>>;
async #evaluate<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
>(
returnByValue: boolean,
pageFunction: Func | string,
...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>> | Awaited<ReturnType<Func>>> {
const sourceUrlComment = getSourceUrlComment(
getSourcePuppeteerURLIfAvailable(pageFunction)?.toString() ??
PuppeteerURL.INTERNAL_URL,
);
let responsePromise;
const resultOwnership = returnByValue
? Bidi.Script.ResultOwnership.None
: Bidi.Script.ResultOwnership.Root;
const serializationOptions: Bidi.Script.SerializationOptions = returnByValue
? {}
: {
maxObjectDepth: 0,
maxDomDepth: 0,
};
if (isString(pageFunction)) {
const expression = SOURCE_URL_REGEX.test(pageFunction)
? pageFunction
: `${pageFunction}\n${sourceUrlComment}\n`;
responsePromise = this.realm.evaluate(expression, true, {
resultOwnership,
userActivation: true,
serializationOptions,
});
} else {
let functionDeclaration = stringifyFunction(pageFunction);
functionDeclaration = SOURCE_URL_REGEX.test(functionDeclaration)
? functionDeclaration
: `${functionDeclaration}\n${sourceUrlComment}\n`;
responsePromise = this.realm.callFunction(
functionDeclaration,
/* awaitPromise= */ true,
{
// LazyArgs are used only internally and should not affect the order
// evaluate calls for the public APIs.
arguments: args.some(arg => {
return arg instanceof LazyArg;
})
? await Promise.all(
args.map(arg => {
return this.serializeAsync(arg);
}),
)
: args.map(arg => {
return this.serialize(arg);
}),
resultOwnership,
userActivation: true,
serializationOptions,
},
);
}
const result = await responsePromise.catch(rewriteEvaluationError);
if ('type' in result && result.type === 'exception') {
throw createEvaluationError(result.exceptionDetails);
}
if (returnByValue) {
return BidiDeserializer.deserialize(result.result);
}
return this.createHandle(result.result) as unknown as HandleFor<
Awaited<ReturnType<Func>>
>;
}
createHandle(
result: Bidi.Script.RemoteValue,
): BidiJSHandle<unknown> | BidiElementHandle<Node> {
if (
(result.type === 'node' || result.type === 'window') &&
this instanceof BidiFrameRealm
) {
return BidiElementHandle.from(result, this);
}
return BidiJSHandle.from(result, this);
}
async serializeAsync(arg: unknown): Promise<Bidi.Script.LocalValue> {
if (arg instanceof LazyArg) {
arg = await arg.get(this);
}
return this.serialize(arg);
}
serialize(arg: unknown): Bidi.Script.LocalValue {
if (arg instanceof BidiJSHandle || arg instanceof BidiElementHandle) {
if (arg.realm !== this) {
if (
!(arg.realm instanceof BidiFrameRealm) ||
!(this instanceof BidiFrameRealm)
) {
throw new Error(
"Trying to evaluate JSHandle from different global types. Usually this means you're using a handle from a worker in a page or vice versa.",
);
}
if (arg.realm.environment !== this.environment) {
throw new Error(
"Trying to evaluate JSHandle from different frames. Usually this means you're using a handle from a page on a different page.",
);
}
}
if (arg.disposed) {
throw new Error('JSHandle is disposed!');
}
return arg.remoteValue() as Bidi.Script.RemoteReference;
}
return BidiSerializer.serialize(arg);
}
async destroyHandles(handles: Array<BidiJSHandle<unknown>>): Promise<void> {
if (this.disposed) {
return;
}
const handleIds = handles
.map(({id}) => {
return id;
})
.filter((id): id is string => {
return id !== undefined;
});
if (handleIds.length === 0) {
return;
}
await this.realm.disown(handleIds).catch(error => {
// Exceptions might happen in case of a page been navigated or closed.
// Swallow these since they are harmless and we don't leak anything in this case.
debugError(error);
});
}
override async adoptHandle<T extends JSHandle<Node>>(handle: T): Promise<T> {
return (await this.evaluateHandle(node => {
return node;
}, handle)) as unknown as T;
}
override async transferHandle<T extends JSHandle<Node>>(
handle: T,
): Promise<T> {
if (handle.realm === this) {
return handle;
}
const transferredHandle = this.adoptHandle(handle);
await handle.dispose();
return await transferredHandle;
}
extension(): Promise<Extension | null> {
throw new UnsupportedOperation();
}
override get origin(): string {
throw new UnsupportedOperation();
}
}
/**
* @internal
*/
export class BidiFrameRealm extends BidiRealm {
static from(realm: WindowRealm, frame: BidiFrame): BidiFrameRealm {
const frameRealm = new BidiFrameRealm(realm, frame);
frameRealm.#initialize();
return frameRealm;
}
declare readonly realm: WindowRealm;
readonly #frame: BidiFrame;
private constructor(realm: WindowRealm, frame: BidiFrame) {
super(realm, frame.timeoutSettings);
this.#frame = frame;
}
#initialize() {
super.initialize();
// This should run first.
this.realm.on('updated', () => {
this.environment.clearDocumentHandle();
this.#bindingsInstalled = false;
});
}
#bindingsInstalled = false;
override get puppeteerUtil(): Promise<BidiJSHandle<PuppeteerInjectedUtil>> {
let promise = Promise.resolve() as Promise<unknown>;
if (!this.#bindingsInstalled) {
promise = Promise.all([
ExposableFunction.from(
this.environment,
'__ariaQuerySelector',
ARIAQueryHandler.queryOne,
!!this.sandbox,
),
ExposableFunction.from(
this.environment,
'__ariaQuerySelectorAll',
async (
element: BidiElementHandle<Node>,
selector: string,
): Promise<JSHandle<Node[]>> => {
const results = ARIAQueryHandler.queryAll(element, selector);
return await element.realm.evaluateHandle(
(...elements) => {
return elements;
},
...(await AsyncIterableUtil.collect(results)),
);
},
!!this.sandbox,
),
]);
this.#bindingsInstalled = true;
}
return promise.then(() => {
return super.puppeteerUtil;
});
}
get sandbox(): string | undefined {
return this.realm.sandbox;
}
override get environment(): BidiFrame {
return this.#frame;
}
override async adoptBackendNode(
backendNodeId?: number | undefined,
): Promise<JSHandle<Node>> {
const {object} = await this.#frame.client.send('DOM.resolveNode', {
backendNodeId,
executionContextId: await this.realm.resolveExecutionContextId(),
});
using handle = BidiElementHandle.from(
{
handle: object.objectId,
type: 'node',
},
this,
);
// We need the sharedId, so we perform the following to obtain it.
return await handle.evaluateHandle(element => {
return element;
});
}
}
/**
* @internal
*/
export class BidiWorkerRealm extends BidiRealm {
static from(
realm: DedicatedWorkerRealm | SharedWorkerRealm,
worker: BidiWebWorker,
): BidiWorkerRealm {
const workerRealm = new BidiWorkerRealm(realm, worker);
workerRealm.initialize();
return workerRealm;
}
declare readonly realm: DedicatedWorkerRealm | SharedWorkerRealm;
readonly #worker: BidiWebWorker;
private constructor(
realm: DedicatedWorkerRealm | SharedWorkerRealm,
frame: BidiWebWorker,
) {
super(realm, frame.timeoutSettings);
this.#worker = frame;
}
override initialize(): void {
super.initialize();
this.realm.on('log', entry => {
if (
isConsoleLogEntry(entry) &&
this.#worker.listenerCount(WebWorkerEvent.Console)
) {
const args = entry.args.map(arg => {
return this.createHandle(arg);
});
const message = getConsoleMessage(
entry,
args,
undefined,
this.realm.id,
);
this.#worker.emit(WebWorkerEvent.Console, message);
}
});
}
override get environment(): BidiWebWorker {
return this.#worker;
}
override async adoptBackendNode(): Promise<JSHandle<Node>> {
throw new Error('Cannot adopt DOM nodes into a worker.');
}
}

126
node_modules/puppeteer-core/src/bidi/Serializer.ts generated vendored Normal file
View File

@@ -0,0 +1,126 @@
/**
* @license
* Copyright 2023 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type * as Bidi from 'webdriver-bidi-protocol';
import {isDate, isPlainObject, isRegExp} from '../common/util.js';
/**
* @internal
*/
class UnserializableError extends Error {}
/**
* @internal
*/
export class BidiSerializer {
static serialize(arg: unknown): Bidi.Script.LocalValue {
switch (typeof arg) {
case 'symbol':
case 'function':
throw new UnserializableError(`Unable to serializable ${typeof arg}`);
case 'object':
return this.#serializeObject(arg);
case 'undefined':
return {
type: 'undefined',
};
case 'number':
return this.#serializeNumber(arg);
case 'bigint':
return {
type: 'bigint',
value: arg.toString(),
};
case 'string':
return {
type: 'string',
value: arg,
};
case 'boolean':
return {
type: 'boolean',
value: arg,
};
}
}
static #serializeNumber(arg: number): Bidi.Script.LocalValue {
let value: Bidi.Script.SpecialNumber | number;
if (Object.is(arg, -0)) {
value = '-0';
} else if (Object.is(arg, Infinity)) {
value = 'Infinity';
} else if (Object.is(arg, -Infinity)) {
value = '-Infinity';
} else if (Object.is(arg, NaN)) {
value = 'NaN';
} else {
value = arg;
}
return {
type: 'number',
value,
};
}
static #serializeObject(arg: object | null): Bidi.Script.LocalValue {
if (arg === null) {
return {
type: 'null',
};
} else if (Array.isArray(arg)) {
const parsedArray = arg.map(subArg => {
return this.serialize(subArg);
});
return {
type: 'array',
value: parsedArray,
};
} else if (isPlainObject(arg)) {
try {
JSON.stringify(arg);
} catch (error) {
if (
error instanceof TypeError &&
error.message.startsWith('Converting circular structure to JSON')
) {
error.message += ' Recursive objects are not allowed.';
}
throw error;
}
const parsedObject: Bidi.Script.MappingLocalValue = [];
for (const key in arg) {
parsedObject.push([this.serialize(key), this.serialize(arg[key])]);
}
return {
type: 'object',
value: parsedObject,
};
} else if (isRegExp(arg)) {
return {
type: 'regexp',
value: {
pattern: arg.source,
flags: arg.flags,
},
};
} else if (isDate(arg)) {
return {
type: 'date',
value: arg.toISOString(),
};
}
throw new UnserializableError(
'Custom object serialization not possible. Use plain objects instead.',
);
}
}

167
node_modules/puppeteer-core/src/bidi/Target.ts generated vendored Normal file
View File

@@ -0,0 +1,167 @@
/**
* @license
* Copyright 2023 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import {Target, TargetType} from '../api/Target.js';
import {UnsupportedOperation} from '../common/Errors.js';
import type {CDPSession} from '../puppeteer-core.js';
import type {BidiBrowser} from './Browser.js';
import type {BidiBrowserContext} from './BrowserContext.js';
import type {BidiFrame} from './Frame.js';
import {BidiPage} from './Page.js';
import type {BidiWebWorker} from './WebWorker.js';
/**
* @internal
*/
export class BidiBrowserTarget extends Target {
#browser: BidiBrowser;
constructor(browser: BidiBrowser) {
super();
this.#browser = browser;
}
override asPage(): Promise<BidiPage> {
throw new UnsupportedOperation();
}
override url(): string {
return '';
}
override createCDPSession(): Promise<CDPSession> {
throw new UnsupportedOperation();
}
override type(): TargetType {
return TargetType.BROWSER;
}
override browser(): BidiBrowser {
return this.#browser;
}
override browserContext(): BidiBrowserContext {
return this.#browser.defaultBrowserContext();
}
override opener(): Target | undefined {
throw new UnsupportedOperation();
}
}
/**
* @internal
*/
export class BidiPageTarget extends Target {
#page: BidiPage;
constructor(page: BidiPage) {
super();
this.#page = page;
}
override async page(): Promise<BidiPage> {
return this.#page;
}
override async asPage(): Promise<BidiPage> {
return await this.page();
}
override url(): string {
return this.#page.url();
}
override createCDPSession(): Promise<CDPSession> {
return this.#page.createCDPSession();
}
override type(): TargetType {
return TargetType.PAGE;
}
override browser(): BidiBrowser {
return this.browserContext().browser();
}
override browserContext(): BidiBrowserContext {
return this.#page.browserContext();
}
override opener(): Target | undefined {
throw new UnsupportedOperation();
}
}
/**
* @internal
*/
export class BidiFrameTarget extends Target {
#frame: BidiFrame;
#page: BidiPage | undefined;
constructor(frame: BidiFrame) {
super();
this.#frame = frame;
}
override async page(): Promise<BidiPage> {
if (this.#page === undefined) {
this.#page = BidiPage.from(
this.browserContext(),
this.#frame.browsingContext,
);
}
return this.#page;
}
override async asPage(): Promise<BidiPage> {
return await this.page();
}
override url(): string {
return this.#frame.url();
}
override createCDPSession(): Promise<CDPSession> {
return this.#frame.createCDPSession();
}
override type(): TargetType {
return TargetType.PAGE;
}
override browser(): BidiBrowser {
return this.browserContext().browser();
}
override browserContext(): BidiBrowserContext {
return this.#frame.page().browserContext();
}
override opener(): Target | undefined {
throw new UnsupportedOperation();
}
}
/**
* @internal
*/
export class BidiWorkerTarget extends Target {
#worker: BidiWebWorker;
constructor(worker: BidiWebWorker) {
super();
this.#worker = worker;
}
override async page(): Promise<BidiPage> {
throw new UnsupportedOperation();
}
override async asPage(): Promise<BidiPage> {
throw new UnsupportedOperation();
}
override url(): string {
return this.#worker.url();
}
override createCDPSession(): Promise<CDPSession> {
throw new UnsupportedOperation();
}
override type(): TargetType {
return TargetType.OTHER;
}
override browser(): BidiBrowser {
return this.browserContext().browser();
}
override browserContext(): BidiBrowserContext {
return this.#worker.frame.page().browserContext();
}
override opener(): Target | undefined {
throw new UnsupportedOperation();
}
}

48
node_modules/puppeteer-core/src/bidi/WebWorker.ts generated vendored Normal file
View File

@@ -0,0 +1,48 @@
/**
* @license
* Copyright 2024 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import {WebWorker} from '../api/WebWorker.js';
import {UnsupportedOperation} from '../common/Errors.js';
import type {CDPSession} from '../puppeteer-core.js';
import type {DedicatedWorkerRealm, SharedWorkerRealm} from './core/Realm.js';
import type {BidiFrame} from './Frame.js';
import {BidiWorkerRealm} from './Realm.js';
/**
* @internal
*/
export class BidiWebWorker extends WebWorker {
static from(
frame: BidiFrame,
realm: DedicatedWorkerRealm | SharedWorkerRealm,
): BidiWebWorker {
const worker = new BidiWebWorker(frame, realm);
return worker;
}
readonly #frame: BidiFrame;
readonly #realm: BidiWorkerRealm;
private constructor(
frame: BidiFrame,
realm: DedicatedWorkerRealm | SharedWorkerRealm,
) {
super(realm.origin);
this.#frame = frame;
this.#realm = BidiWorkerRealm.from(realm, this);
}
get frame(): BidiFrame {
return this.#frame;
}
mainRealm(): BidiWorkerRealm {
return this.#realm;
}
get client(): CDPSession {
throw new UnsupportedOperation();
}
}

18
node_modules/puppeteer-core/src/bidi/bidi.ts generated vendored Normal file
View File

@@ -0,0 +1,18 @@
/**
* @license
* Copyright 2022 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
export * from './BidiOverCdp.js';
export * from './Browser.js';
export * from './BrowserContext.js';
export * from './Connection.js';
export * from './ElementHandle.js';
export * from './Frame.js';
export * from './HTTPRequest.js';
export * from './HTTPResponse.js';
export * from './Input.js';
export * from './JSHandle.js';
export * from './Page.js';
export * from './Realm.js';

330
node_modules/puppeteer-core/src/bidi/core/Browser.ts generated vendored Normal file
View File

@@ -0,0 +1,330 @@
/**
* @license
* Copyright 2024 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type * as Bidi from 'webdriver-bidi-protocol';
import type {BrowserContextOptions} from '../../api/Browser.js';
import {UnsupportedOperation} from '../../common/Errors.js';
import {EventEmitter} from '../../common/EventEmitter.js';
import {inertIfDisposed, throwIfDisposed} from '../../util/decorators.js';
import {DisposableStack, disposeSymbol} from '../../util/disposable.js';
import type {BrowsingContext} from './BrowsingContext.js';
import {SharedWorkerRealm} from './Realm.js';
import type {Session} from './Session.js';
import {UserContext} from './UserContext.js';
/**
* @internal
*/
export type AddPreloadScriptOptions = Omit<
Bidi.Script.AddPreloadScriptParameters,
'functionDeclaration' | 'contexts'
> & {
contexts?: [BrowsingContext, ...BrowsingContext[]];
};
/**
* @internal
*/
export class Browser extends EventEmitter<{
/** Emitted before the browser closes. */
closed: {
/** The reason for closing the browser. */
reason: string;
};
/** Emitted after the browser disconnects. */
disconnected: {
/** The reason for disconnecting the browser. */
reason: string;
};
/** Emitted when a shared worker is created. */
sharedworker: {
/** The realm of the shared worker. */
realm: SharedWorkerRealm;
};
}> {
static async from(session: Session): Promise<Browser> {
const browser = new Browser(session);
await browser.#initialize();
return browser;
}
#closed = false;
#reason: string | undefined;
readonly #disposables = new DisposableStack();
readonly #userContexts = new Map<string, UserContext>();
readonly session: Session;
readonly #sharedWorkers = new Map<string, SharedWorkerRealm>();
private constructor(session: Session) {
super();
this.session = session;
}
async #initialize() {
const sessionEmitter = this.#disposables.use(
new EventEmitter(this.session),
);
sessionEmitter.once('ended', ({reason}) => {
this.dispose(reason);
});
sessionEmitter.on('script.realmCreated', info => {
if (info.type !== 'shared-worker') {
return;
}
this.#sharedWorkers.set(
info.realm,
SharedWorkerRealm.from(this, info.realm, info.origin),
);
});
await this.#syncUserContexts();
await this.#syncBrowsingContexts();
}
async #syncUserContexts() {
const {
result: {userContexts},
} = await this.session.send('browser.getUserContexts', {});
for (const context of userContexts) {
this.#createUserContext(context.userContext);
}
}
async #syncBrowsingContexts() {
// In case contexts are created or destroyed during `getTree`, we use this
// set to detect them.
const contextIds = new Set<string>();
let contexts: Bidi.BrowsingContext.Info[];
{
using sessionEmitter = new EventEmitter(this.session);
sessionEmitter.on('browsingContext.contextCreated', info => {
contextIds.add(info.context);
});
const {result} = await this.session.send('browsingContext.getTree', {});
contexts = result.contexts;
}
// Simulating events so contexts are created naturally.
for (const info of contexts) {
if (!contextIds.has(info.context)) {
this.session.emit('browsingContext.contextCreated', info);
}
if (info.children) {
contexts.push(...info.children);
}
}
}
#createUserContext(id: string) {
const userContext = UserContext.create(this, id);
this.#userContexts.set(userContext.id, userContext);
const userContextEmitter = this.#disposables.use(
new EventEmitter(userContext),
);
userContextEmitter.once('closed', () => {
userContextEmitter.removeAllListeners();
this.#userContexts.delete(userContext.id);
});
return userContext;
}
get closed(): boolean {
return this.#closed;
}
get defaultUserContext(): UserContext {
// SAFETY: A UserContext is always created for the default context.
return this.#userContexts.get(UserContext.DEFAULT)!;
}
get disconnected(): boolean {
return this.#reason !== undefined;
}
get disposed(): boolean {
return this.disconnected;
}
get userContexts(): Iterable<UserContext> {
return this.#userContexts.values();
}
@inertIfDisposed
dispose(reason?: string, closed = false): void {
this.#closed = closed;
this.#reason = reason;
this[disposeSymbol]();
}
@throwIfDisposed<Browser>(browser => {
// SAFETY: By definition of `disposed`, `#reason` is defined.
return browser.#reason!;
})
async close(): Promise<void> {
try {
await this.session.send('browser.close', {});
} finally {
this.dispose('Browser already closed.', true);
}
}
@throwIfDisposed<Browser>(browser => {
// SAFETY: By definition of `disposed`, `#reason` is defined.
return browser.#reason!;
})
async addPreloadScript(
functionDeclaration: string,
options: AddPreloadScriptOptions = {},
): Promise<string> {
const {
result: {script},
} = await this.session.send('script.addPreloadScript', {
functionDeclaration,
...options,
contexts: options.contexts?.map(context => {
return context.id;
}) as [string, ...string[]],
});
return script;
}
@throwIfDisposed<Browser>(browser => {
// SAFETY: By definition of `disposed`, `#reason` is defined.
return browser.#reason!;
})
async removeIntercept(intercept: Bidi.Network.Intercept): Promise<void> {
await this.session.send('network.removeIntercept', {
intercept,
});
}
@throwIfDisposed<Browser>(browser => {
// SAFETY: By definition of `disposed`, `#reason` is defined.
return browser.#reason!;
})
async removePreloadScript(script: string): Promise<void> {
await this.session.send('script.removePreloadScript', {
script,
});
}
@throwIfDisposed<Browser>(browser => {
// SAFETY: By definition of `disposed`, `#reason` is defined.
return browser.#reason!;
})
async createUserContext(
options: BrowserContextOptions,
): Promise<UserContext> {
const proxyConfig: Bidi.Session.ProxyConfiguration | undefined =
options.proxyServer === undefined
? undefined
: {
proxyType: 'manual',
httpProxy: options.proxyServer,
sslProxy: options.proxyServer,
noProxy: options.proxyBypassList,
};
const {
result: {userContext},
} = await this.session.send('browser.createUserContext', {
proxy: proxyConfig,
});
if (options.downloadBehavior?.policy === 'allowAndName') {
throw new UnsupportedOperation(
'`allowAndName` is not supported in WebDriver BiDi',
);
}
if (options.downloadBehavior?.policy === 'allow') {
if (options.downloadBehavior.downloadPath === undefined) {
throw new UnsupportedOperation(
'`downloadPath` is required in `allow` download behavior',
);
}
await this.session.send('browser.setDownloadBehavior', {
downloadBehavior: {
type: 'allowed',
destinationFolder: options.downloadBehavior.downloadPath,
},
userContexts: [userContext],
});
}
if (options.downloadBehavior?.policy === 'deny') {
await this.session.send('browser.setDownloadBehavior', {
downloadBehavior: {type: 'denied'},
userContexts: [userContext],
});
}
return this.#createUserContext(userContext);
}
@throwIfDisposed<Browser>(browser => {
// SAFETY: By definition of `disposed`, `#reason` is defined.
return browser.#reason!;
})
async installExtension(path: string): Promise<string> {
const {
result: {extension},
} = await this.session.send('webExtension.install', {
extensionData: {type: 'path', path},
});
return extension;
}
@throwIfDisposed<Browser>(browser => {
// SAFETY: By definition of `disposed`, `#reason` is defined.
return browser.#reason!;
})
async uninstallExtension(id: string): Promise<void> {
await this.session.send('webExtension.uninstall', {extension: id});
}
@throwIfDisposed<Browser>(browser => {
// SAFETY: By definition of `disposed`, `#reason` is defined.
return browser.#reason!;
})
async setClientWindowState(
params: Bidi.Browser.SetClientWindowStateParameters,
): Promise<void> {
await this.session.send('browser.setClientWindowState', params);
}
@throwIfDisposed<Browser>(browser => {
// SAFETY: By definition of `disposed`, `#reason` is defined.
return browser.#reason!;
})
async getClientWindowInfo(
windowId: string,
): Promise<Bidi.Browser.ClientWindowInfo> {
const {
result: {clientWindows},
} = await this.session.send('browser.getClientWindows', {});
const window = clientWindows.find(window => {
return window.clientWindow === windowId;
});
if (!window) {
throw new Error('Window not found');
}
return window;
}
override [disposeSymbol](): void {
this.#reason ??=
'Browser was disconnected, probably because the session ended.';
if (this.closed) {
this.emit('closed', {reason: this.#reason});
}
this.emit('disconnected', {reason: this.#reason});
this.#disposables.dispose();
super[disposeSymbol]();
}
}

View File

@@ -0,0 +1,845 @@
/**
* @license
* Copyright 2024 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type * as Bidi from 'webdriver-bidi-protocol';
import type {BluetoothEmulation} from '../../api/BluetoothEmulation.js';
import type {DeviceRequestPrompt} from '../../api/DeviceRequestPrompt.js';
import {EventEmitter} from '../../common/EventEmitter.js';
import {isString} from '../../common/util.js';
import {assert} from '../../util/assert.js';
import {inertIfDisposed, throwIfDisposed} from '../../util/decorators.js';
import {DisposableStack, disposeSymbol} from '../../util/disposable.js';
import {BidiBluetoothEmulation} from '../BluetoothEmulation.js';
import {BidiDeviceRequestPromptManager} from '../DeviceRequestPrompt.js';
import type {AddPreloadScriptOptions} from './Browser.js';
import {Navigation} from './Navigation.js';
import type {DedicatedWorkerRealm} from './Realm.js';
import {WindowRealm} from './Realm.js';
import {Request} from './Request.js';
import type {UserContext} from './UserContext.js';
import {UserPrompt} from './UserPrompt.js';
/**
* @internal
*/
export type AddInterceptOptions = Omit<
Bidi.Network.AddInterceptParameters,
'contexts'
>;
/**
* @internal
*/
export type CaptureScreenshotOptions = Omit<
Bidi.BrowsingContext.CaptureScreenshotParameters,
'context'
>;
/**
* @internal
*/
export type ReloadOptions = Omit<
Bidi.BrowsingContext.ReloadParameters,
'context'
>;
/**
* @internal
*/
export type PrintOptions = Omit<
Bidi.BrowsingContext.PrintParameters,
'context'
>;
/**
* @internal
*/
export type HandleUserPromptOptions = Omit<
Bidi.BrowsingContext.HandleUserPromptParameters,
'context'
>;
/**
* @internal
*/
export type SetViewportOptions = Omit<
Bidi.BrowsingContext.SetViewportParameters,
'context'
>;
/**
* @internal
*/
export type GetCookiesOptions = Omit<
Bidi.Storage.GetCookiesParameters,
'partition'
>;
/**
* @internal
*/
export type SetGeoLocationOverrideOptions =
Bidi.Emulation.SetGeolocationOverrideParameters;
/**
* @internal
*/
export class BrowsingContext extends EventEmitter<{
/** Emitted when this context is closed. */
closed: {
/** The reason the browsing context was closed */
reason: string;
};
/** Emitted when a child browsing context is created. */
browsingcontext: {
/** The newly created child browsing context. */
browsingContext: BrowsingContext;
};
/** Emitted whenever a navigation occurs. */
navigation: {
/** The navigation that occurred. */
navigation: Navigation;
};
/** Emitted whenever a file dialog is opened occurs. */
filedialogopened: Bidi.Input.FileDialogInfo;
/** Emitted whenever a request is made. */
request: {
/** The request that was made. */
request: Request;
};
/** Emitted whenever a log entry is added. */
log: {
/** Entry added to the log. */
entry: Bidi.Log.Entry;
};
/** Emitted whenever a prompt is opened. */
userprompt: {
/** The prompt that was opened. */
userPrompt: UserPrompt;
};
/** Emitted whenever the frame history is updated. */
historyUpdated: void;
/** Emitted whenever the frame emits `DOMContentLoaded` */
DOMContentLoaded: void;
/** Emitted whenever the frame emits `load` */
load: void;
/** Emitted whenever a dedicated worker is created */
worker: {
/** The realm for the new dedicated worker */
realm: DedicatedWorkerRealm;
};
}> {
static from(
userContext: UserContext,
parent: BrowsingContext | undefined,
id: string,
url: string,
originalOpener: string | null,
clientWindow: string,
): BrowsingContext {
const browsingContext = new BrowsingContext(
userContext,
parent,
id,
url,
originalOpener,
clientWindow,
);
browsingContext.#initialize();
return browsingContext;
}
#navigation: Navigation | undefined;
#reason?: string;
#url: string;
// Indicated whether client hints have been set to non-default.
#clientHintsAreSet = false;
readonly #children = new Map<string, BrowsingContext>();
readonly #disposables = new DisposableStack();
readonly #realms = new Map<string, WindowRealm>();
readonly #requests = new Map<string, Request>();
readonly defaultRealm: WindowRealm;
readonly id: string;
readonly parent: BrowsingContext | undefined;
readonly userContext: UserContext;
readonly originalOpener: string | null;
readonly windowId: string;
readonly #emulationState: {
javaScriptEnabled: boolean;
} = {javaScriptEnabled: true};
readonly #bluetoothEmulation: BluetoothEmulation;
readonly #deviceRequestPromptManager: BidiDeviceRequestPromptManager;
private constructor(
userContext: UserContext,
parent: BrowsingContext | undefined,
id: string,
url: string,
originalOpener: string | null,
clientWindow: string,
) {
super();
this.#url = url;
this.id = id;
this.parent = parent;
this.userContext = userContext;
this.originalOpener = originalOpener;
this.windowId = clientWindow;
this.defaultRealm = this.#createWindowRealm();
this.#bluetoothEmulation = new BidiBluetoothEmulation(
this.id,
this.#session,
);
this.#deviceRequestPromptManager = new BidiDeviceRequestPromptManager(
this.id,
this.#session,
);
}
#initialize() {
const userContextEmitter = this.#disposables.use(
new EventEmitter(this.userContext),
);
userContextEmitter.once('closed', ({reason}) => {
this.dispose(`Browsing context already closed: ${reason}`);
});
const sessionEmitter = this.#disposables.use(
new EventEmitter(this.#session),
);
sessionEmitter.on('input.fileDialogOpened', info => {
if (this.id !== info.context) {
return;
}
this.emit('filedialogopened', info);
});
sessionEmitter.on('browsingContext.contextCreated', info => {
if (info.parent !== this.id) {
return;
}
const browsingContext = BrowsingContext.from(
this.userContext,
this,
info.context,
info.url,
info.originalOpener,
info.clientWindow,
);
this.#children.set(info.context, browsingContext);
const browsingContextEmitter = this.#disposables.use(
new EventEmitter(browsingContext),
);
browsingContextEmitter.once('closed', () => {
browsingContextEmitter.removeAllListeners();
this.#children.delete(browsingContext.id);
});
this.emit('browsingcontext', {browsingContext});
});
sessionEmitter.on('browsingContext.contextDestroyed', info => {
if (info.context !== this.id) {
return;
}
this.dispose('Browsing context already closed.');
});
sessionEmitter.on('browsingContext.historyUpdated', info => {
if (info.context !== this.id) {
return;
}
this.#url = info.url;
this.emit('historyUpdated', undefined);
});
sessionEmitter.on('browsingContext.domContentLoaded', info => {
if (info.context !== this.id) {
return;
}
this.#url = info.url;
this.emit('DOMContentLoaded', undefined);
});
sessionEmitter.on('browsingContext.load', info => {
if (info.context !== this.id) {
return;
}
this.#url = info.url;
this.emit('load', undefined);
});
sessionEmitter.on('browsingContext.navigationStarted', info => {
if (info.context !== this.id) {
return;
}
// Note: we should not update this.#url at this point since the context
// has not finished navigating to the info.url yet.
for (const [id, request] of this.#requests) {
if (request.disposed) {
this.#requests.delete(id);
}
}
// If the navigation hasn't finished, then this is nested navigation. The
// current navigation will handle this.
if (this.#navigation !== undefined && !this.#navigation.disposed) {
return;
}
// Note the navigation ID is null for this event.
this.#navigation = Navigation.from(this);
const navigationEmitter = this.#disposables.use(
new EventEmitter(this.#navigation),
);
for (const eventName of ['fragment', 'failed', 'aborted'] as const) {
navigationEmitter.once(eventName, ({url}) => {
navigationEmitter[disposeSymbol]();
this.#url = url;
});
}
this.emit('navigation', {navigation: this.#navigation});
});
sessionEmitter.on('network.beforeRequestSent', event => {
if (event.context !== this.id) {
return;
}
if (this.#requests.has(event.request.request)) {
// Means the request is a redirect. This is handled in Request.
// Or an Auth event was issued
return;
}
const request = Request.from(this, event);
this.#requests.set(request.id, request);
this.emit('request', {request});
});
sessionEmitter.on('log.entryAdded', entry => {
if (entry.source.context !== this.id) {
return;
}
this.emit('log', {entry});
});
sessionEmitter.on('browsingContext.userPromptOpened', info => {
if (info.context !== this.id) {
return;
}
const userPrompt = UserPrompt.from(this, info);
this.emit('userprompt', {userPrompt});
});
}
get #session() {
return this.userContext.browser.session;
}
get children(): Iterable<BrowsingContext> {
return this.#children.values();
}
get closed(): boolean {
return this.#reason !== undefined;
}
get disposed(): boolean {
return this.closed;
}
get realms(): Iterable<WindowRealm> {
// eslint-disable-next-line @typescript-eslint/no-this-alias -- Required
const self = this;
return (function* () {
yield self.defaultRealm;
yield* self.#realms.values();
})();
}
get top(): BrowsingContext {
let context = this as BrowsingContext;
for (let {parent} = context; parent; {parent} = context) {
context = parent;
}
return context;
}
get url(): string {
return this.#url;
}
#createWindowRealm(sandbox?: string) {
const realm = WindowRealm.from(this, sandbox);
realm.on('worker', realm => {
this.emit('worker', {realm});
});
return realm;
}
@inertIfDisposed
private dispose(reason?: string): void {
this.#reason = reason;
for (const context of this.#children.values()) {
context.dispose('Parent browsing context was disposed');
}
this[disposeSymbol]();
}
@throwIfDisposed<BrowsingContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
async activate(): Promise<void> {
await this.#session.send('browsingContext.activate', {
context: this.id,
});
}
@throwIfDisposed<BrowsingContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
async captureScreenshot(
options: CaptureScreenshotOptions = {},
): Promise<string> {
const {
result: {data},
} = await this.#session.send('browsingContext.captureScreenshot', {
context: this.id,
...options,
});
return data;
}
@throwIfDisposed<BrowsingContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
async close(promptUnload?: boolean): Promise<void> {
// The WebDriver BiDi specification only allows closing top-level browsing contexts.
// Closing a top-level context automatically closes all its children, so there is
// no need to explicitly close nested contexts.
await this.#session.send('browsingContext.close', {
context: this.id,
promptUnload,
});
}
@throwIfDisposed<BrowsingContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
async traverseHistory(delta: number): Promise<void> {
await this.#session.send('browsingContext.traverseHistory', {
context: this.id,
delta,
});
}
@throwIfDisposed<BrowsingContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
async navigate(
url: string,
wait?: Bidi.BrowsingContext.ReadinessState,
): Promise<void> {
await this.#session.send('browsingContext.navigate', {
context: this.id,
url,
wait,
});
}
@throwIfDisposed<BrowsingContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
async reload(options: ReloadOptions = {}): Promise<void> {
await this.#session.send('browsingContext.reload', {
context: this.id,
...options,
});
}
@throwIfDisposed<BrowsingContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
async setCacheBehavior(cacheBehavior: 'default' | 'bypass'): Promise<void> {
await this.#session.send('network.setCacheBehavior', {
contexts: [this.id],
cacheBehavior,
});
}
@throwIfDisposed<BrowsingContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
async print(options: PrintOptions = {}): Promise<string> {
const {
result: {data},
} = await this.#session.send('browsingContext.print', {
context: this.id,
...options,
});
return data;
}
@throwIfDisposed<BrowsingContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
async handleUserPrompt(options: HandleUserPromptOptions = {}): Promise<void> {
await this.#session.send('browsingContext.handleUserPrompt', {
context: this.id,
...options,
});
}
@throwIfDisposed<BrowsingContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
async setViewport(options: SetViewportOptions = {}): Promise<void> {
await this.#session.send('browsingContext.setViewport', {
context: this.id,
...options,
});
}
@throwIfDisposed<BrowsingContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
async setTouchOverride(maxTouchPoints: number | null): Promise<void> {
await this.#session.send('emulation.setTouchOverride', {
contexts: [this.id],
maxTouchPoints,
});
}
@throwIfDisposed<BrowsingContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
async performActions(actions: Bidi.Input.SourceActions[]): Promise<void> {
await this.#session.send('input.performActions', {
context: this.id,
actions,
});
}
@throwIfDisposed<BrowsingContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
async releaseActions(): Promise<void> {
await this.#session.send('input.releaseActions', {
context: this.id,
});
}
@throwIfDisposed<BrowsingContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
createWindowRealm(sandbox: string): WindowRealm {
return this.#createWindowRealm(sandbox);
}
@throwIfDisposed<BrowsingContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
async addPreloadScript(
functionDeclaration: string,
options: AddPreloadScriptOptions = {},
): Promise<string> {
return await this.userContext.browser.addPreloadScript(
functionDeclaration,
{
...options,
contexts: [this],
},
);
}
@throwIfDisposed<BrowsingContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
async addIntercept(options: AddInterceptOptions): Promise<string> {
const {
result: {intercept},
} = await this.userContext.browser.session.send('network.addIntercept', {
...options,
contexts: [this.id],
});
return intercept;
}
@throwIfDisposed<BrowsingContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
async removePreloadScript(script: string): Promise<void> {
await this.userContext.browser.removePreloadScript(script);
}
@throwIfDisposed<BrowsingContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
async setGeolocationOverride(
options: SetGeoLocationOverrideOptions,
): Promise<void> {
if (!('coordinates' in options)) {
throw new Error('Missing coordinates');
}
await this.userContext.browser.session.send(
'emulation.setGeolocationOverride',
{
coordinates: options.coordinates,
contexts: [this.id],
},
);
}
@throwIfDisposed<BrowsingContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
async setTimezoneOverride(timezoneId?: string): Promise<void> {
if (timezoneId?.startsWith('GMT')) {
// CDP requires `GMT` prefix before timezone offset, while BiDi does not. Remove the
// `GMT` for interop between CDP and BiDi.
timezoneId = timezoneId?.replace('GMT', '');
}
await this.userContext.browser.session.send(
'emulation.setTimezoneOverride',
{
timezone: timezoneId ?? null,
contexts: [this.id],
},
);
}
@throwIfDisposed<BrowsingContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
async setScreenOrientationOverride(
screenOrientation: Bidi.Emulation.ScreenOrientation | null,
): Promise<void> {
await this.#session.send('emulation.setScreenOrientationOverride', {
screenOrientation,
contexts: [this.id],
});
}
@throwIfDisposed<BrowsingContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
async getCookies(
options: GetCookiesOptions = {},
): Promise<Bidi.Network.Cookie[]> {
const {
result: {cookies},
} = await this.#session.send('storage.getCookies', {
...options,
partition: {
type: 'context',
context: this.id,
},
});
return cookies;
}
@throwIfDisposed<BrowsingContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
async setCookie(cookie: Bidi.Storage.PartialCookie): Promise<void> {
await this.#session.send('storage.setCookie', {
cookie,
partition: {
type: 'context',
context: this.id,
},
});
}
@throwIfDisposed<BrowsingContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
async setFiles(
element: Bidi.Script.SharedReference,
files: string[],
): Promise<void> {
await this.#session.send('input.setFiles', {
context: this.id,
element,
files,
});
}
@throwIfDisposed<BrowsingContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
async subscribe(events: [string, ...string[]]): Promise<void> {
await this.#session.subscribe(events, [this.id]);
}
@throwIfDisposed<BrowsingContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
async addInterception(events: [string, ...string[]]): Promise<void> {
await this.#session.subscribe(events, [this.id]);
}
override [disposeSymbol](): void {
this.#reason ??=
'Browsing context already closed, probably because the user context closed.';
this.emit('closed', {reason: this.#reason});
this.#disposables.dispose();
super[disposeSymbol]();
}
@throwIfDisposed<BrowsingContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
async deleteCookie(
...cookieFilters: Bidi.Storage.CookieFilter[]
): Promise<void> {
await Promise.all(
cookieFilters.map(async filter => {
await this.#session.send('storage.deleteCookies', {
filter: filter,
partition: {
type: 'context',
context: this.id,
},
});
}),
);
}
@throwIfDisposed<BrowsingContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
async locateNodes(
locator: Bidi.BrowsingContext.Locator,
startNodes: Bidi.Script.SharedReference[] = [],
): Promise<Bidi.Script.NodeRemoteValue[]> {
// TODO: add other locateNodes options if needed.
const result = await this.#session.send('browsingContext.locateNodes', {
context: this.id,
locator,
startNodes: startNodes.length
? (startNodes as [
Bidi.Script.SharedReference,
...Bidi.Script.SharedReference[],
])
: undefined,
});
return result.result.nodes;
}
async setJavaScriptEnabled(enabled: boolean): Promise<void> {
await this.userContext.browser.session.send(
'emulation.setScriptingEnabled',
{
// Enabled `null` means `default`, `false` means `disabled`.
enabled: enabled ? null : false,
contexts: [this.id],
},
);
this.#emulationState.javaScriptEnabled = enabled;
}
isJavaScriptEnabled(): boolean {
return this.#emulationState.javaScriptEnabled;
}
async setUserAgent(userAgent: string | null): Promise<void> {
await this.#session.send('emulation.setUserAgentOverride', {
userAgent,
contexts: [this.id],
});
}
async setClientHintsOverride(
clientHints: Bidi.BidiUaClientHints.UserAgentClientHints.ClientHintsMetadata | null,
): Promise<void> {
if (clientHints === null && !this.#clientHintsAreSet) {
// Ignore the call, as the client hints are not supposed to be changed. Required to
// avoid breakage with browsers that don't support client hints emulation.
return;
}
this.#clientHintsAreSet = true;
await this.#session.send('userAgentClientHints.setClientHintsOverride', {
clientHints,
contexts: [this.id],
});
}
async setOfflineMode(enabled: boolean): Promise<void> {
await this.#session.send('emulation.setNetworkConditions', {
networkConditions: enabled
? {
type: 'offline',
}
: null,
contexts: [this.id],
});
}
get bluetooth(): BluetoothEmulation {
return this.#bluetoothEmulation;
}
async waitForDevicePrompt(
timeout: number,
signal?: AbortSignal,
): Promise<DeviceRequestPrompt> {
return await this.#deviceRequestPromptManager.waitForDevicePrompt(
timeout,
signal,
);
}
async setExtraHTTPHeaders(headers: Record<string, string>): Promise<void> {
await this.#session.send('network.setExtraHeaders', {
headers: Object.entries(headers).map(([key, value]) => {
assert(
isString(value),
`Expected value of header "${key}" to be String, but "${typeof value}" is found.`,
);
return {
name: key.toLowerCase(),
value: {type: 'string', value: value},
};
}),
contexts: [this.id],
});
}
}

View File

@@ -0,0 +1,31 @@
/**
* @license
* Copyright 2024 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {Event} from 'webdriver-bidi-protocol';
import type {Commands} from 'webdriver-bidi-protocol';
import type {EventEmitter} from '../../common/EventEmitter.js';
export type {Commands};
/**
* @internal
*/
export type BidiEvents = {
[K in Event['method']]: Extract<Event, {method: K}>['params'];
};
/**
* @internal
*/
export interface Connection<
Events extends BidiEvents = BidiEvents,
> extends EventEmitter<Events> {
send<T extends keyof Commands>(
method: T,
params: Commands[T]['params'],
): Promise<{result: Commands[T]['returnType']}>;
}

174
node_modules/puppeteer-core/src/bidi/core/Navigation.ts generated vendored Normal file
View File

@@ -0,0 +1,174 @@
/**
* @license
* Copyright 2024 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import {EventEmitter} from '../../common/EventEmitter.js';
import {inertIfDisposed} from '../../util/decorators.js';
import {DisposableStack, disposeSymbol} from '../../util/disposable.js';
import type {BrowsingContext} from './BrowsingContext.js';
import type {Request} from './Request.js';
/**
* @internal
*/
export interface NavigationInfo {
url: string;
timestamp: Date;
}
/**
* @internal
*/
export class Navigation extends EventEmitter<{
/** Emitted when navigation has a request associated with it. */
request: Request;
/** Emitted when fragment navigation occurred. */
fragment: NavigationInfo;
/** Emitted when navigation failed. */
failed: NavigationInfo;
/** Emitted when navigation was aborted. */
aborted: NavigationInfo;
}> {
static from(context: BrowsingContext): Navigation {
const navigation = new Navigation(context);
navigation.#initialize();
return navigation;
}
#request: Request | undefined;
#navigation: Navigation | undefined;
readonly #browsingContext: BrowsingContext;
readonly #disposables = new DisposableStack();
#id?: string | null;
private constructor(context: BrowsingContext) {
super();
this.#browsingContext = context;
}
#initialize() {
const browsingContextEmitter = this.#disposables.use(
new EventEmitter(this.#browsingContext),
);
browsingContextEmitter.once('closed', () => {
this.emit('failed', {
url: this.#browsingContext.url,
timestamp: new Date(),
});
this.dispose();
});
browsingContextEmitter.on('request', ({request}) => {
if (
request.navigation === undefined ||
// If a request with a navigation ID comes in, then the navigation ID is
// for this navigation.
!this.#matches(request.navigation)
) {
return;
}
this.#request = request;
this.emit('request', request);
const requestEmitter = this.#disposables.use(
new EventEmitter(this.#request),
);
requestEmitter.on('redirect', request => {
this.#request = request;
});
});
const sessionEmitter = this.#disposables.use(
new EventEmitter(this.#session),
);
sessionEmitter.on('browsingContext.navigationStarted', info => {
if (
info.context !== this.#browsingContext.id ||
this.#navigation !== undefined
) {
return;
}
this.#navigation = Navigation.from(this.#browsingContext);
});
for (const eventName of [
'browsingContext.domContentLoaded',
'browsingContext.load',
'browsingContext.navigationCommitted',
] as const) {
sessionEmitter.on(eventName, info => {
if (
info.context !== this.#browsingContext.id ||
info.navigation === null ||
!this.#matches(info.navigation)
) {
return;
}
this.dispose();
});
}
for (const [eventName, event] of [
['browsingContext.fragmentNavigated', 'fragment'],
['browsingContext.navigationFailed', 'failed'],
['browsingContext.navigationAborted', 'aborted'],
] as const) {
sessionEmitter.on(eventName, info => {
if (
info.context !== this.#browsingContext.id ||
// Note we don't check if `navigation` is null since `null` means the
// fragment navigated.
!this.#matches(info.navigation)
) {
return;
}
this.emit(event, {
url: info.url,
timestamp: new Date(info.timestamp),
});
this.dispose();
});
}
}
#matches(navigation: string | null): boolean {
if (this.#navigation !== undefined && !this.#navigation.disposed) {
return false;
}
if (this.#id === undefined) {
this.#id = navigation;
return true;
}
return this.#id === navigation;
}
get #session() {
return this.#browsingContext.userContext.browser.session;
}
get disposed(): boolean {
return this.#disposables.disposed;
}
get request(): Request | undefined {
return this.#request;
}
get navigation(): Navigation | undefined {
return this.#navigation;
}
@inertIfDisposed
private dispose(): void {
this[disposeSymbol]();
}
override [disposeSymbol](): void {
this.#disposables.dispose();
super[disposeSymbol]();
}
}

52
node_modules/puppeteer-core/src/bidi/core/README.md generated vendored Normal file
View File

@@ -0,0 +1,52 @@
# `bidi/core`
`bidi/core` is a low-level layer that sits above the WebDriver BiDi transport to
provide a structured API to WebDriver BiDi's flat API. In particular,
`bidi/core` provides object-oriented semantics around WebDriver BiDi resources
and automatically carries out the correct order of events in WebDriver BiDi through
the use of events.
## Tips
There are a few design decisions in this library that should be considered when
developing `bidi/core`:
- Required arguments are inlined as function arguments while optional arguments
are put into an options object.
- Function arguments are implicitly required in TypeScript, so by putting
required arguments as function arguments, the semantic is automatically
inherited.
- The session shall never be exposed on any public method/getter on any
object except the browser. Private getters are allowed.
- Passing around the session is dangerous as it obfuscates the origin of the
session. By only allowing it on the browser, the origin is well-defined.
- `bidi/core` implements WebDriver BiDi plus its surrounding specifications.
- A lot of WebDriver BiDi is not strictly written in WebDriver BiDi. Since WebDriver
BiDi interacts with several other specs, there are other considerations that
also influence the design of `bidi/core`. For example, for navigation,
WebDriver BiDi doesn't have a concept of "nested navigation", but in
practice this exists if a fragment navigation happens in a `beforeunload`
hook.
- `bidi/core` always follow the spec and never Puppeteer's needs.
- By ensuring `bidi/core` follows the spec rather than Puppeteer's needs, we
can identify the source of a bug precisely (i.e. whether the spec needs to
be updated or Puppeteer needs to work around it).
- `bidi/core` attempts to implement WebDriver BiDi comprehensively, but
minimally.
- Imagine the objects and events in WebDriver BiDi as a large
[graph](<https://en.wikipedia.org/wiki/Graph_(discrete_mathematics)>) where
objects are nodes and events are edges. In `bidi/core`, we always implement
all edges and nodes required by a feature without skipping nodes and events
(e.g. [fragment navigation -> navigation -> browsing context]; not [fragment
navigation -> browsing context]). We also never compose edges (e.g. both
[fragment navigation -> navigation -> browsing context] and [fragment
navigation -> browsing context] must not exist; i.e. a fragment navigation
event should not occur on the browsing context). This ensures that the
semantics of WebDriver BiDi is carried out correctly.
- This point reinforces `bidi/core` should not follow Puppeteer's needs since
Puppeteer typically composes a lot of events to satisfy its needs.

361
node_modules/puppeteer-core/src/bidi/core/Realm.ts generated vendored Normal file
View File

@@ -0,0 +1,361 @@
/**
* @license
* Copyright 2024 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type * as Bidi from 'webdriver-bidi-protocol';
import {EventEmitter} from '../../common/EventEmitter.js';
import {inertIfDisposed, throwIfDisposed} from '../../util/decorators.js';
import {DisposableStack, disposeSymbol} from '../../util/disposable.js';
import type {BidiConnection} from '../Connection.js';
import type {Browser} from './Browser.js';
import type {BrowsingContext} from './BrowsingContext.js';
import type {Session} from './Session.js';
/**
* @internal
*/
export type CallFunctionOptions = Omit<
Bidi.Script.CallFunctionParameters,
'functionDeclaration' | 'awaitPromise' | 'target'
>;
/**
* @internal
*/
export type EvaluateOptions = Omit<
Bidi.Script.EvaluateParameters,
'expression' | 'awaitPromise' | 'target'
>;
/**
* @internal
*/
export abstract class Realm extends EventEmitter<{
/** Emitted whenever the realm has updated. */
updated: Realm;
/** Emitted when the realm is destroyed. */
destroyed: {reason: string};
/** Emitted when a dedicated worker is created in the realm. */
worker: DedicatedWorkerRealm;
/** Emitted when a shared worker is created in the realm. */
sharedworker: SharedWorkerRealm;
/** Emitted whenever a log entry is added to the realm. */
log: Bidi.Log.Entry;
}> {
#reason?: string;
protected readonly disposables = new DisposableStack();
readonly id: string;
readonly origin: string;
protected executionContextId?: number;
protected constructor(id: string, origin: string) {
super();
this.id = id;
this.origin = origin;
}
get disposed(): boolean {
return this.#reason !== undefined;
}
protected abstract get session(): Session;
get target(): Bidi.Script.Target {
return {realm: this.id};
}
@inertIfDisposed
protected dispose(reason?: string): void {
this.#reason = reason;
this[disposeSymbol]();
}
@throwIfDisposed<Realm>(realm => {
// SAFETY: Disposal implies this exists.
return realm.#reason!;
})
async disown(handles: string[]): Promise<void> {
await this.session.send('script.disown', {
target: this.target,
handles,
});
}
@throwIfDisposed<Realm>(realm => {
// SAFETY: Disposal implies this exists.
return realm.#reason!;
})
async callFunction(
functionDeclaration: string,
awaitPromise: boolean,
options: CallFunctionOptions = {},
): Promise<Bidi.Script.EvaluateResult> {
const {result} = await this.session.send('script.callFunction', {
functionDeclaration,
awaitPromise,
target: this.target,
...options,
});
return result;
}
@throwIfDisposed<Realm>(realm => {
// SAFETY: Disposal implies this exists.
return realm.#reason!;
})
async evaluate(
expression: string,
awaitPromise: boolean,
options: EvaluateOptions = {},
): Promise<Bidi.Script.EvaluateResult> {
const {result} = await this.session.send('script.evaluate', {
expression,
awaitPromise,
target: this.target,
...options,
});
return result;
}
@throwIfDisposed<Realm>(realm => {
// SAFETY: Disposal implies this exists.
return realm.#reason!;
})
async resolveExecutionContextId(): Promise<number> {
if (!this.executionContextId) {
const {result} = await (this.session.connection as BidiConnection).send(
'goog:cdp.resolveRealm',
{realm: this.id},
);
this.executionContextId = result.executionContextId;
}
return this.executionContextId;
}
override [disposeSymbol](): void {
this.#reason ??=
'Realm already destroyed, probably because all associated browsing contexts closed.';
this.emit('destroyed', {reason: this.#reason});
this.disposables.dispose();
super[disposeSymbol]();
}
}
/**
* @internal
*/
export class WindowRealm extends Realm {
static from(context: BrowsingContext, sandbox?: string): WindowRealm {
const realm = new WindowRealm(context, sandbox);
realm.#initialize();
return realm;
}
readonly browsingContext: BrowsingContext;
readonly sandbox?: string;
readonly #workers = new Map<string, DedicatedWorkerRealm>();
private constructor(context: BrowsingContext, sandbox?: string) {
super('', '');
this.browsingContext = context;
this.sandbox = sandbox;
}
#initialize(): void {
const browsingContextEmitter = this.disposables.use(
new EventEmitter(this.browsingContext),
);
browsingContextEmitter.on('closed', ({reason}) => {
this.dispose(reason);
});
const sessionEmitter = this.disposables.use(new EventEmitter(this.session));
sessionEmitter.on('script.realmCreated', info => {
if (
info.type !== 'window' ||
info.context !== this.browsingContext.id ||
info.sandbox !== this.sandbox
) {
return;
}
(this as any).id = info.realm;
(this as any).origin = info.origin;
this.executionContextId = undefined;
this.emit('updated', this);
});
sessionEmitter.on('script.realmCreated', info => {
if (info.type !== 'dedicated-worker') {
return;
}
if (!info.owners.includes(this.id)) {
return;
}
const realm = DedicatedWorkerRealm.from(this, info.realm, info.origin);
this.#workers.set(realm.id, realm);
const realmEmitter = this.disposables.use(new EventEmitter(realm));
realmEmitter.once('destroyed', () => {
realmEmitter.removeAllListeners();
this.#workers.delete(realm.id);
});
this.emit('worker', realm);
});
sessionEmitter.on('log.entryAdded', entry => {
if (entry.source.realm !== this.id) {
return;
}
this.emit('log', entry);
});
}
override get session(): Session {
return this.browsingContext.userContext.browser.session;
}
override get target(): Bidi.Script.Target {
return {context: this.browsingContext.id, sandbox: this.sandbox};
}
}
/**
* @internal
*/
export type DedicatedWorkerOwnerRealm =
| DedicatedWorkerRealm
| SharedWorkerRealm
| WindowRealm;
/**
* @internal
*/
export class DedicatedWorkerRealm extends Realm {
static from(
owner: DedicatedWorkerOwnerRealm,
id: string,
origin: string,
): DedicatedWorkerRealm {
const realm = new DedicatedWorkerRealm(owner, id, origin);
realm.#initialize();
return realm;
}
readonly #workers = new Map<string, DedicatedWorkerRealm>();
readonly owners: Set<DedicatedWorkerOwnerRealm>;
private constructor(
owner: DedicatedWorkerOwnerRealm,
id: string,
origin: string,
) {
super(id, origin);
this.owners = new Set([owner]);
}
#initialize(): void {
const sessionEmitter = this.disposables.use(new EventEmitter(this.session));
sessionEmitter.on('script.realmDestroyed', info => {
if (info.realm !== this.id) {
return;
}
this.dispose('Realm already destroyed.');
});
sessionEmitter.on('script.realmCreated', info => {
if (info.type !== 'dedicated-worker') {
return;
}
if (!info.owners.includes(this.id)) {
return;
}
const realm = DedicatedWorkerRealm.from(this, info.realm, info.origin);
this.#workers.set(realm.id, realm);
const realmEmitter = this.disposables.use(new EventEmitter(realm));
realmEmitter.once('destroyed', () => {
this.#workers.delete(realm.id);
});
this.emit('worker', realm);
});
sessionEmitter.on('log.entryAdded', entry => {
if (entry.source.realm !== this.id) {
return;
}
this.emit('log', entry);
});
}
override get session(): Session {
// SAFETY: At least one owner will exist.
return this.owners.values().next().value!.session;
}
}
/**
* @internal
*/
export class SharedWorkerRealm extends Realm {
static from(browser: Browser, id: string, origin: string): SharedWorkerRealm {
const realm = new SharedWorkerRealm(browser, id, origin);
realm.#initialize();
return realm;
}
readonly #workers = new Map<string, DedicatedWorkerRealm>();
readonly browser: Browser;
private constructor(browser: Browser, id: string, origin: string) {
super(id, origin);
this.browser = browser;
}
#initialize(): void {
const sessionEmitter = this.disposables.use(new EventEmitter(this.session));
sessionEmitter.on('script.realmDestroyed', info => {
if (info.realm !== this.id) {
return;
}
this.dispose('Realm already destroyed.');
});
sessionEmitter.on('script.realmCreated', info => {
if (info.type !== 'dedicated-worker') {
return;
}
if (!info.owners.includes(this.id)) {
return;
}
const realm = DedicatedWorkerRealm.from(this, info.realm, info.origin);
this.#workers.set(realm.id, realm);
const realmEmitter = this.disposables.use(new EventEmitter(realm));
realmEmitter.once('destroyed', () => {
this.#workers.delete(realm.id);
});
this.emit('worker', realm);
});
sessionEmitter.on('log.entryAdded', entry => {
if (entry.source.realm !== this.id) {
return;
}
this.emit('log', entry);
});
}
override get session(): Session {
return this.browser.session;
}
}

349
node_modules/puppeteer-core/src/bidi/core/Request.ts generated vendored Normal file
View File

@@ -0,0 +1,349 @@
/**
* @license
* Copyright 2024 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import * as Bidi from 'webdriver-bidi-protocol';
import {ProtocolError, UnsupportedOperation} from '../../common/Errors.js';
import {EventEmitter} from '../../common/EventEmitter.js';
import {inertIfDisposed} from '../../util/decorators.js';
import {DisposableStack, disposeSymbol} from '../../util/disposable.js';
import {stringToTypedArray} from '../../util/encoding.js';
import type {BrowsingContext} from './BrowsingContext.js';
/**
* @internal
*/
export class Request extends EventEmitter<{
/** Emitted when the request is redirected. */
redirect: Request;
/** Emitted when the request succeeds. */
authenticate: void;
/** Emitted when the request succeeds. */
success: Bidi.Network.ResponseData;
/** Analog of WebDriver BiDi event `network.responseStarted`. Emitted when a
* response is received. */
response: Bidi.Network.ResponseData;
/** Emitted when the request fails. */
error: string;
}> {
static from(
browsingContext: BrowsingContext,
event: Bidi.Network.BeforeRequestSentParameters,
): Request {
const request = new Request(browsingContext, event);
request.#initialize();
return request;
}
#responseContentPromise: Promise<Uint8Array<ArrayBufferLike>> | null = null;
#requestBodyPromise: Promise<string> | null = null;
#error?: string;
#redirect?: Request;
#response?: Bidi.Network.ResponseData;
readonly #browsingContext: BrowsingContext;
readonly #disposables = new DisposableStack();
readonly #event: Bidi.Network.BeforeRequestSentParameters;
private constructor(
browsingContext: BrowsingContext,
event: Bidi.Network.BeforeRequestSentParameters,
) {
super();
this.#browsingContext = browsingContext;
this.#event = event;
}
#initialize() {
const browsingContextEmitter = this.#disposables.use(
new EventEmitter(this.#browsingContext),
);
browsingContextEmitter.once('closed', ({reason}) => {
this.#error = reason;
this.emit('error', this.#error);
this.dispose();
});
const sessionEmitter = this.#disposables.use(
new EventEmitter(this.#session),
);
sessionEmitter.on('network.beforeRequestSent', event => {
if (
event.context !== this.#browsingContext.id ||
event.request.request !== this.id
) {
return;
}
// This is a workaround to detect if a beforeRequestSent is for a request
// sent after continueWithAuth. Currently, only emitted in Firefox.
const previousRequestHasAuth = this.#event.request.headers.find(
header => {
return header.name.toLowerCase() === 'authorization';
},
);
const newRequestHasAuth = event.request.headers.find(header => {
return header.name.toLowerCase() === 'authorization';
});
const isAfterAuth = newRequestHasAuth && !previousRequestHasAuth;
if (
event.redirectCount !== this.#event.redirectCount + 1 &&
!isAfterAuth
) {
return;
}
this.#redirect = Request.from(this.#browsingContext, event);
this.emit('redirect', this.#redirect);
this.dispose();
});
sessionEmitter.on('network.authRequired', event => {
if (
event.context !== this.#browsingContext.id ||
event.request.request !== this.id ||
// Don't try to authenticate for events that are not blocked
!event.isBlocked
) {
return;
}
this.emit('authenticate', undefined);
});
sessionEmitter.on('network.fetchError', event => {
if (
event.context !== this.#browsingContext.id ||
event.request.request !== this.id ||
this.#event.redirectCount !== event.redirectCount
) {
return;
}
this.#error = event.errorText;
this.emit('error', this.#error);
this.dispose();
});
sessionEmitter.on('network.responseStarted', event => {
if (
event.context !== this.#browsingContext.id ||
event.request.request !== this.id ||
this.#event.redirectCount !== event.redirectCount
) {
return;
}
this.#response = event.response;
this.#event.request.timings = event.request.timings;
this.emit('response', this.#response);
});
sessionEmitter.on('network.responseCompleted', event => {
if (
event.context !== this.#browsingContext.id ||
event.request.request !== this.id ||
this.#event.redirectCount !== event.redirectCount
) {
return;
}
this.#response = event.response;
this.#event.request.timings = event.request.timings;
this.emit('success', this.#response);
// In case this is a redirect.
if (this.#response.status >= 300 && this.#response.status < 400) {
return;
}
this.dispose();
});
}
get #session() {
return this.#browsingContext.userContext.browser.session;
}
get disposed(): boolean {
return this.#disposables.disposed;
}
get error(): string | undefined {
return this.#error;
}
get headers(): Bidi.Network.Header[] {
return this.#event.request.headers;
}
get id(): string {
return this.#event.request.request;
}
get initiator(): Bidi.Network.Initiator | undefined {
return {
...this.#event.initiator,
// Initiator URL is not specified in BiDi.
// @ts-expect-error non-standard property.
url: this.#event.request['goog:resourceInitiator']?.url,
// @ts-expect-error non-standard property.
stack: this.#event.request['goog:resourceInitiator']?.stack,
};
}
get method(): string {
return this.#event.request.method;
}
get navigation(): string | undefined {
return this.#event.navigation ?? undefined;
}
get redirect(): Request | undefined {
return this.#redirect;
}
get lastRedirect(): Request | undefined {
let redirect = this.#redirect;
while (redirect) {
if (redirect && !redirect.#redirect) {
return redirect;
}
redirect = redirect.#redirect;
}
return redirect;
}
get response(): Bidi.Network.ResponseData | undefined {
return this.#response;
}
get url(): string {
return this.#event.request.url;
}
get isBlocked(): boolean {
return this.#event.isBlocked;
}
get resourceType(): string | undefined {
// @ts-expect-error non-standard attribute.
return this.#event.request['goog:resourceType'] ?? undefined;
}
get postData(): string | undefined {
// @ts-expect-error non-standard attribute.
return this.#event.request['goog:postData'] ?? undefined;
}
get hasPostData(): boolean {
return (this.#event.request.bodySize ?? 0) > 0;
}
async continueRequest({
url,
method,
headers,
cookies,
body,
}: Omit<Bidi.Network.ContinueRequestParameters, 'request'>): Promise<void> {
await this.#session.send('network.continueRequest', {
request: this.id,
url,
method,
headers,
body,
cookies,
});
}
async failRequest(): Promise<void> {
await this.#session.send('network.failRequest', {
request: this.id,
});
}
async provideResponse({
statusCode,
reasonPhrase,
headers,
body,
}: Omit<Bidi.Network.ProvideResponseParameters, 'request'>): Promise<void> {
await this.#session.send('network.provideResponse', {
request: this.id,
statusCode,
reasonPhrase,
headers,
body,
});
}
async fetchPostData(): Promise<string | undefined> {
if (!this.hasPostData) {
return undefined;
}
if (!this.#requestBodyPromise) {
this.#requestBodyPromise = (async () => {
const data = await this.#session.send('network.getData', {
dataType: Bidi.Network.DataType.Request,
request: this.id,
});
if (data.result.bytes.type === 'string') {
return data.result.bytes.value;
}
// TODO: support base64 response.
throw new UnsupportedOperation(
`Collected request body data of type ${data.result.bytes.type} is not supported`,
);
})();
}
return await this.#requestBodyPromise;
}
async getResponseContent(): Promise<Uint8Array> {
if (!this.#responseContentPromise) {
this.#responseContentPromise = (async () => {
try {
const data = await this.#session.send('network.getData', {
dataType: Bidi.Network.DataType.Response,
request: this.id,
});
return stringToTypedArray(
data.result.bytes.value,
data.result.bytes.type === 'base64',
);
} catch (error) {
if (
error instanceof ProtocolError &&
error.originalMessage.includes(
'No resource with given identifier found',
)
) {
throw new ProtocolError(
'Could not load response body for this request. This might happen if the request is a preflight request.',
);
}
throw error;
}
})();
}
return await this.#responseContentPromise;
}
async continueWithAuth(
parameters:
| Bidi.Network.ContinueWithAuthCredentials
| Bidi.Network.ContinueWithAuthNoCredentials,
): Promise<void> {
if (parameters.action === 'provideCredentials') {
await this.#session.send('network.continueWithAuth', {
request: this.id,
action: parameters.action,
credentials: parameters.credentials,
});
} else {
await this.#session.send('network.continueWithAuth', {
request: this.id,
action: parameters.action,
});
}
}
@inertIfDisposed
private dispose(): void {
this[disposeSymbol]();
}
override [disposeSymbol](): void {
this.#disposables.dispose();
super[disposeSymbol]();
}
timing(): Bidi.Network.FetchTimingInfo {
return this.#event.request.timings;
}
}

162
node_modules/puppeteer-core/src/bidi/core/Session.ts generated vendored Normal file
View File

@@ -0,0 +1,162 @@
/**
* @license
* Copyright 2024 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type * as Bidi from 'webdriver-bidi-protocol';
import {EventEmitter} from '../../common/EventEmitter.js';
import {
bubble,
inertIfDisposed,
throwIfDisposed,
} from '../../util/decorators.js';
import {DisposableStack, disposeSymbol} from '../../util/disposable.js';
import {Browser} from './Browser.js';
import type {BidiEvents, Commands, Connection} from './Connection.js';
/**
* @internal
*/
export class Session
extends EventEmitter<BidiEvents & {ended: {reason: string}}>
implements Connection<BidiEvents & {ended: {reason: string}}>
{
static async from(
connection: Connection,
capabilities: Bidi.Session.CapabilitiesRequest,
): Promise<Session> {
const {result} = await connection.send('session.new', {
capabilities,
});
const session = new Session(connection, result);
await session.#initialize();
return session;
}
#reason: string | undefined;
readonly #disposables = new DisposableStack();
readonly #info: Bidi.Session.NewResult;
readonly browser!: Browser;
@bubble()
accessor connection: Connection;
private constructor(connection: Connection, info: Bidi.Session.NewResult) {
super();
this.#info = info;
this.connection = connection;
}
async #initialize(): Promise<void> {
// SAFETY: We use `any` to allow assignment of the readonly property.
(this as any).browser = await Browser.from(this);
const browserEmitter = this.#disposables.use(this.browser);
browserEmitter.once('closed', ({reason}) => {
this.dispose(reason);
});
// TODO: Currently, some implementations do not emit navigationStarted event
// for fragment navigations (as per spec) and some do. This could emits a
// synthetic navigationStarted to work around this inconsistency.
const seen = new WeakSet();
this.on('browsingContext.fragmentNavigated', info => {
if (seen.has(info)) {
return;
}
seen.add(info);
this.emit('browsingContext.navigationStarted', info);
this.emit('browsingContext.fragmentNavigated', info);
});
}
get capabilities(): Bidi.Session.NewResult['capabilities'] {
return this.#info.capabilities;
}
get disposed(): boolean {
return this.ended;
}
get ended(): boolean {
return this.#reason !== undefined;
}
get id(): string {
return this.#info.sessionId;
}
@inertIfDisposed
private dispose(reason?: string): void {
this.#reason = reason;
this[disposeSymbol]();
}
/**
* Currently, there is a 1:1 relationship between the session and the
* session. In the future, we might support multiple sessions and in that
* case we always needs to make sure that the session for the right session
* object is used, so we implement this method here, although it's not defined
* in the spec.
*/
@throwIfDisposed<Session>(session => {
// SAFETY: By definition of `disposed`, `#reason` is defined.
return session.#reason!;
})
async send<T extends keyof Commands>(
method: T,
params: Commands[T]['params'],
): Promise<{result: Commands[T]['returnType']}> {
return await this.connection.send(method, params);
}
@throwIfDisposed<Session>(session => {
// SAFETY: By definition of `disposed`, `#reason` is defined.
return session.#reason!;
})
async subscribe(
events: [string, ...string[]],
contexts?: [string, ...string[]],
): Promise<void> {
await this.send('session.subscribe', {
events,
contexts,
});
}
@throwIfDisposed<Session>(session => {
// SAFETY: By definition of `disposed`, `#reason` is defined.
return session.#reason!;
})
async addIntercepts(
events: [string, ...string[]],
contexts?: [string, ...string[]],
): Promise<void> {
await this.send('session.subscribe', {
events,
contexts,
});
}
@throwIfDisposed<Session>(session => {
// SAFETY: By definition of `disposed`, `#reason` is defined.
return session.#reason!;
})
async end(): Promise<void> {
try {
await this.send('session.end', {});
} finally {
this.dispose(`Session already ended.`);
}
}
override [disposeSymbol](): void {
this.#reason ??=
'Session already destroyed, probably because the connection broke.';
this.emit('ended', {reason: this.#reason});
this.#disposables.dispose();
super[disposeSymbol]();
}
}

View File

@@ -0,0 +1,244 @@
/**
* @license
* Copyright 2024 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type * as Bidi from 'webdriver-bidi-protocol';
import {EventEmitter} from '../../common/EventEmitter.js';
import {assert} from '../../util/assert.js';
import {inertIfDisposed, throwIfDisposed} from '../../util/decorators.js';
import {DisposableStack, disposeSymbol} from '../../util/disposable.js';
import type {Browser} from './Browser.js';
import type {GetCookiesOptions} from './BrowsingContext.js';
import {BrowsingContext} from './BrowsingContext.js';
/**
* @internal
*/
export type CreateBrowsingContextOptions = Omit<
Bidi.BrowsingContext.CreateParameters,
'type' | 'referenceContext'
> & {
referenceContext?: BrowsingContext;
background?: boolean;
};
/**
* @internal
*/
export class UserContext extends EventEmitter<{
/**
* Emitted when a new browsing context is created.
*/
browsingcontext: {
/** The new browsing context. */
browsingContext: BrowsingContext;
};
/**
* Emitted when the user context is closed.
*/
closed: {
/** The reason the user context was closed. */
reason: string;
};
}> {
static DEFAULT = 'default' as const;
static create(browser: Browser, id: string): UserContext {
const context = new UserContext(browser, id);
context.#initialize();
return context;
}
#reason?: string;
// Note these are only top-level contexts.
readonly #browsingContexts = new Map<string, BrowsingContext>();
readonly #disposables = new DisposableStack();
readonly #id: string;
readonly browser: Browser;
private constructor(browser: Browser, id: string) {
super();
this.#id = id;
this.browser = browser;
}
#initialize() {
const browserEmitter = this.#disposables.use(
new EventEmitter(this.browser),
);
browserEmitter.once('closed', ({reason}) => {
this.dispose(`User context was closed: ${reason}`);
});
browserEmitter.once('disconnected', ({reason}) => {
this.dispose(`User context was closed: ${reason}`);
});
const sessionEmitter = this.#disposables.use(
new EventEmitter(this.#session),
);
sessionEmitter.on('browsingContext.contextCreated', info => {
if (info.parent) {
return;
}
if (info.userContext !== this.#id) {
return;
}
const browsingContext = BrowsingContext.from(
this,
undefined,
info.context,
info.url,
info.originalOpener,
info.clientWindow,
);
this.#browsingContexts.set(browsingContext.id, browsingContext);
const browsingContextEmitter = this.#disposables.use(
new EventEmitter(browsingContext),
);
browsingContextEmitter.on('closed', () => {
browsingContextEmitter.removeAllListeners();
this.#browsingContexts.delete(browsingContext.id);
});
this.emit('browsingcontext', {browsingContext});
});
}
get #session() {
return this.browser.session;
}
get browsingContexts(): Iterable<BrowsingContext> {
return this.#browsingContexts.values();
}
get closed(): boolean {
return this.#reason !== undefined;
}
get disposed(): boolean {
return this.closed;
}
get id(): string {
return this.#id;
}
@inertIfDisposed
private dispose(reason?: string): void {
this.#reason = reason;
this[disposeSymbol]();
}
@throwIfDisposed<UserContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
async createBrowsingContext(
type: Bidi.BrowsingContext.CreateType,
options: CreateBrowsingContextOptions = {},
): Promise<BrowsingContext> {
const {
result: {context: contextId},
} = await this.#session.send('browsingContext.create', {
type,
...options,
referenceContext: options.referenceContext?.id,
background: options.background,
userContext: this.#id,
});
const browsingContext = this.#browsingContexts.get(contextId);
assert(
browsingContext,
'The WebDriver BiDi implementation is failing to create a browsing context correctly.',
);
// We use an array to avoid the promise from being awaited.
return browsingContext;
}
@throwIfDisposed<UserContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
async remove(): Promise<void> {
try {
await this.#session.send('browser.removeUserContext', {
userContext: this.#id,
});
} finally {
this.dispose('User context already closed.');
}
}
@throwIfDisposed<UserContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
async getCookies(
options: GetCookiesOptions = {},
sourceOrigin: string | undefined = undefined,
): Promise<Bidi.Network.Cookie[]> {
const {
result: {cookies},
} = await this.#session.send('storage.getCookies', {
...options,
partition: {
type: 'storageKey',
userContext: this.#id,
sourceOrigin,
},
});
return cookies;
}
@throwIfDisposed<UserContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
async setCookie(
cookie: Bidi.Storage.PartialCookie,
sourceOrigin?: string,
): Promise<void> {
await this.#session.send('storage.setCookie', {
cookie,
partition: {
type: 'storageKey',
sourceOrigin,
userContext: this.id,
},
});
}
@throwIfDisposed<UserContext>(context => {
// SAFETY: Disposal implies this exists.
return context.#reason!;
})
async setPermissions(
origin: string,
descriptor: Bidi.Permissions.PermissionDescriptor,
state: Bidi.Permissions.PermissionState,
): Promise<void> {
await this.#session.send('permissions.setPermission', {
origin,
descriptor,
state,
userContext: this.#id,
});
}
override [disposeSymbol](): void {
this.#reason ??=
'User context already closed, probably because the browser disconnected/closed.';
this.emit('closed', {reason: this.#reason});
this.#disposables.dispose();
super[disposeSymbol]();
}
}

138
node_modules/puppeteer-core/src/bidi/core/UserPrompt.ts generated vendored Normal file
View File

@@ -0,0 +1,138 @@
/**
* @license
* Copyright 2024 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import * as Bidi from 'webdriver-bidi-protocol';
import {EventEmitter} from '../../common/EventEmitter.js';
import {inertIfDisposed, throwIfDisposed} from '../../util/decorators.js';
import {DisposableStack, disposeSymbol} from '../../util/disposable.js';
import type {BrowsingContext} from './BrowsingContext.js';
/**
* @internal
*/
export type HandleOptions = Omit<
Bidi.BrowsingContext.HandleUserPromptParameters,
'context'
>;
/**
* @internal
*/
export type UserPromptResult = Omit<
Bidi.BrowsingContext.UserPromptClosedParameters,
'context'
>;
/**
* @internal
*/
export class UserPrompt extends EventEmitter<{
/** Emitted when the user prompt is handled. */
handled: UserPromptResult;
/** Emitted when the user prompt is closed. */
closed: {
/** The reason the user prompt was closed. */
reason: string;
};
}> {
static from(
browsingContext: BrowsingContext,
info: Bidi.BrowsingContext.UserPromptOpenedParameters,
): UserPrompt {
const userPrompt = new UserPrompt(browsingContext, info);
userPrompt.#initialize();
return userPrompt;
}
#reason?: string;
#result?: UserPromptResult;
readonly #disposables = new DisposableStack();
readonly browsingContext: BrowsingContext;
readonly info: Bidi.BrowsingContext.UserPromptOpenedParameters;
private constructor(
context: BrowsingContext,
info: Bidi.BrowsingContext.UserPromptOpenedParameters,
) {
super();
this.browsingContext = context;
this.info = info;
}
#initialize() {
const browserContextEmitter = this.#disposables.use(
new EventEmitter(this.browsingContext),
);
browserContextEmitter.once('closed', ({reason}) => {
this.dispose(`User prompt already closed: ${reason}`);
});
const sessionEmitter = this.#disposables.use(
new EventEmitter(this.#session),
);
sessionEmitter.on('browsingContext.userPromptClosed', parameters => {
if (parameters.context !== this.browsingContext.id) {
return;
}
this.#result = parameters;
this.emit('handled', parameters);
this.dispose('User prompt already handled.');
});
}
get #session() {
return this.browsingContext.userContext.browser.session;
}
get closed(): boolean {
return this.#reason !== undefined;
}
get disposed(): boolean {
return this.closed;
}
get handled(): boolean {
if (
this.info.handler === Bidi.Session.UserPromptHandlerType.Accept ||
this.info.handler === Bidi.Session.UserPromptHandlerType.Dismiss
) {
return true;
}
return this.#result !== undefined;
}
get result(): UserPromptResult | undefined {
return this.#result;
}
@inertIfDisposed
private dispose(reason?: string): void {
this.#reason = reason;
this[disposeSymbol]();
}
@throwIfDisposed<UserPrompt>(prompt => {
// SAFETY: Disposal implies this exists.
return prompt.#reason!;
})
async handle(options: HandleOptions = {}): Promise<UserPromptResult> {
await this.#session.send('browsingContext.handleUserPrompt', {
...options,
context: this.info.context,
});
// SAFETY: `handled` is triggered before the above promise resolved.
return this.#result!;
}
override [disposeSymbol](): void {
this.#reason ??=
'User prompt already closed, probably because the associated browsing context was destroyed.';
this.emit('closed', {reason: this.#reason});
this.#disposables.dispose();
super[disposeSymbol]();
}
}

15
node_modules/puppeteer-core/src/bidi/core/core.ts generated vendored Normal file
View File

@@ -0,0 +1,15 @@
/**
* @license
* Copyright 2024 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
export * from './Browser.js';
export * from './BrowsingContext.js';
export type * from './Connection.js';
export * from './Navigation.js';
export * from './Realm.js';
export * from './Request.js';
export * from './Session.js';
export * from './UserContext.js';
export * from './UserPrompt.js';

195
node_modules/puppeteer-core/src/bidi/util.ts generated vendored Normal file
View File

@@ -0,0 +1,195 @@
/**
* @license
* Copyright 2023 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type * as Bidi from 'webdriver-bidi-protocol';
import type {Frame} from '../api/Frame.js';
import {ConsoleMessage} from '../common/ConsoleMessage.js';
import type {
ConsoleMessageLocation,
ConsoleMessageType,
} from '../common/ConsoleMessage.js';
import {ProtocolError, TimeoutError} from '../common/Errors.js';
import {PuppeteerURL} from '../common/util.js';
import type {BidiElementHandle} from './bidi.js';
import {BidiDeserializer} from './Deserializer.js';
import {BidiJSHandle} from './JSHandle.js';
/**
* @internal
*
* TODO: Remove this and map CDP the correct method.
* Requires breaking change.
*/
export function convertConsoleMessageLevel(method: string): ConsoleMessageType {
switch (method) {
case 'group':
return 'startGroup';
case 'groupCollapsed':
return 'startGroupCollapsed';
case 'groupEnd':
return 'endGroup';
default:
return method as ConsoleMessageType;
}
}
/**
* @internal
*/
export function getStackTraceLocations(
stackTrace?: Bidi.Script.StackTrace,
): ConsoleMessageLocation[] {
const stackTraceLocations: ConsoleMessageLocation[] = [];
if (stackTrace) {
for (const callFrame of stackTrace.callFrames) {
stackTraceLocations.push({
url: callFrame.url,
lineNumber: callFrame.lineNumber,
columnNumber: callFrame.columnNumber,
});
}
}
return stackTraceLocations;
}
/**
* @internal
*/
export function getConsoleMessage(
entry: Bidi.Log.ConsoleLogEntry,
args: Array<BidiJSHandle<unknown> | BidiElementHandle<Node>>,
frame?: Frame,
targetId?: string,
): ConsoleMessage {
const text = args
.reduce((value, arg) => {
const parsedValue =
arg instanceof BidiJSHandle && arg.isPrimitiveValue
? BidiDeserializer.deserialize(arg.remoteValue())
: arg.toString();
return `${value} ${parsedValue}`;
}, '')
.slice(1);
return new ConsoleMessage(
convertConsoleMessageLevel(entry.method),
text,
args,
getStackTraceLocations(entry.stackTrace),
frame,
undefined,
targetId,
);
}
/**
* @internal
*/
export function isConsoleLogEntry(
event: Bidi.Log.Entry,
): event is Bidi.Log.ConsoleLogEntry {
return event.type === 'console';
}
/**
* @internal
*/
export function isJavaScriptLogEntry(
event: Bidi.Log.Entry,
): event is Bidi.Log.JavascriptLogEntry {
return event.type === 'javascript';
}
/**
* @internal
*/
export function createEvaluationError(
details: Bidi.Script.ExceptionDetails,
): unknown {
if (details.exception.type === 'object' && !('value' in details.exception)) {
// Heuristic detecting a platform object was thrown. WebDriver BiDi serializes
// platform objects without value. If so, throw a generic error with the actual
// exception's message, as there is no way to restore the original exception's
// constructor.
return new Error(details.text);
}
if (details.exception.type !== 'error') {
return BidiDeserializer.deserialize(details.exception);
}
const [name = '', ...parts] = details.text.split(': ');
const message = parts.join(': ');
const error = new Error(message);
error.name = name;
// The first line is this function which we ignore.
const stackLines = [];
if (details.stackTrace && stackLines.length < Error.stackTraceLimit) {
for (const frame of details.stackTrace.callFrames.reverse()) {
if (
PuppeteerURL.isPuppeteerURL(frame.url) &&
frame.url !== PuppeteerURL.INTERNAL_URL
) {
const url = PuppeteerURL.parse(frame.url);
stackLines.unshift(
` at ${frame.functionName || url.functionName} (${
url.functionName
} at ${url.siteString}, <anonymous>:${frame.lineNumber}:${
frame.columnNumber
})`,
);
} else {
stackLines.push(
` at ${frame.functionName || '<anonymous>'} (${frame.url}:${
frame.lineNumber
}:${frame.columnNumber})`,
);
}
if (stackLines.length >= Error.stackTraceLimit) {
break;
}
}
}
error.stack = [details.text, ...stackLines].join('\n');
return error;
}
/**
* @internal
*/
export function rewriteNavigationError(
message: string,
ms: number,
): (error: unknown) => never {
return error => {
if (error instanceof ProtocolError) {
error.message += ` at ${message}`;
} else if (error instanceof TimeoutError) {
error.message = `Navigation timeout of ${ms} ms exceeded`;
}
throw error;
};
}
/**
* @internal
*/
export function rewriteEvaluationError(error: unknown): never {
if (error instanceof Error) {
if (
error.message.includes('ExecutionContext was destroyed') ||
error.message.includes('Inspected target navigated or closed')
) {
throw new Error(
'Execution context was destroyed, most likely because of a navigation.',
);
}
}
throw error;
}

784
node_modules/puppeteer-core/src/cdp/Accessibility.ts generated vendored Normal file
View File

@@ -0,0 +1,784 @@
/**
* @license
* Copyright 2018 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {Protocol} from 'devtools-protocol';
import type {ElementHandle} from '../api/ElementHandle.js';
import type {Realm} from '../api/Realm.js';
import type {CdpFrame} from '../cdp/Frame.js';
import {debugError} from '../common/util.js';
/**
* Represents a Node and the properties of it that are relevant to Accessibility.
* @public
*/
export interface SerializedAXNode {
/**
* The {@link https://www.w3.org/TR/wai-aria/#usage_intro | role} of the node.
*/
role: string;
/**
* A human readable name for the node.
*/
name?: string;
/**
* The current value of the node.
*/
value?: string | number;
/**
* An additional human readable description of the node.
*/
description?: string;
/**
* Any keyboard shortcuts associated with this node.
*/
keyshortcuts?: string;
/**
* A human readable alternative to the role.
*/
roledescription?: string;
/**
* A description of the current value.
*/
valuetext?: string;
disabled?: boolean;
expanded?: boolean;
focused?: boolean;
modal?: boolean;
multiline?: boolean;
/**
* Whether more than one child can be selected.
*/
multiselectable?: boolean;
readonly?: boolean;
required?: boolean;
selected?: boolean;
/**
* Whether the checkbox is checked, or in a
* {@link https://www.w3.org/TR/wai-aria-practices/examples/checkbox/checkbox-2/checkbox-2.html | mixed state}.
*/
checked?: boolean | 'mixed';
/**
* Whether the node is checked or in a mixed state.
*/
pressed?: boolean | 'mixed';
/**
* The level of a heading.
*/
level?: number;
valuemin?: number;
valuemax?: number;
autocomplete?: string;
haspopup?: string;
/**
* Whether and in what way this node's value is invalid.
*/
invalid?: string;
orientation?: string;
/**
* Whether the node is {@link https://www.w3.org/TR/wai-aria/#aria-busy | busy}.
*/
busy?: boolean;
/**
* The {@link https://www.w3.org/TR/wai-aria/#aria-live | live} status of the
* node.
*/
live?: string;
/**
* Whether the live region is
* {@link https://www.w3.org/TR/wai-aria/#aria-atomic | atomic}.
*/
atomic?: boolean;
/**
* The {@link https://www.w3.org/TR/wai-aria/#aria-relevant | relevant}
* changes for the live region.
*/
relevant?: string;
/**
* The {@link https://www.w3.org/TR/wai-aria/#aria-errormessage | error message}
* for the node.
*/
errormessage?: string;
/**
* The {@link https://www.w3.org/TR/wai-aria/#aria-details | details} for the
* node.
*/
details?: string;
/**
* Url for link elements.
*/
url?: string;
/**
* Children of this node, if there are any.
*/
children?: SerializedAXNode[];
/**
* CDP-specific ID to reference the DOM node.
*
* @internal
*/
backendNodeId?: number;
/**
* CDP-specific documentId.
*
* @internal
*/
loaderId: string;
/**
* Get an ElementHandle for this AXNode if available.
*
* If the underlying DOM element has been disposed, the method might return an
* error.
*/
elementHandle(): Promise<ElementHandle | null>;
}
/**
* @public
*/
export interface SnapshotOptions {
/**
* Prune uninteresting nodes from the tree.
* @defaultValue `true`
*/
interestingOnly?: boolean;
/**
* If true, gets accessibility trees for each of the iframes in the frame
* subtree.
*
* @defaultValue `false`
*/
includeIframes?: boolean;
/**
* Root node to get the accessibility tree for
* @defaultValue The root node of the entire page.
*/
root?: ElementHandle<Node>;
}
/**
* The Accessibility class provides methods for inspecting the browser's
* accessibility tree. The accessibility tree is used by assistive technology
* such as {@link https://en.wikipedia.org/wiki/Screen_reader | screen readers} or
* {@link https://en.wikipedia.org/wiki/Switch_access | switches}.
*
* @remarks
*
* Accessibility is a very platform-specific thing. On different platforms,
* there are different screen readers that might have wildly different output.
*
* Blink - Chrome's rendering engine - has a concept of "accessibility tree",
* which is then translated into different platform-specific APIs. Accessibility
* namespace gives users access to the Blink Accessibility Tree.
*
* Most of the accessibility tree gets filtered out when converting from Blink
* AX Tree to Platform-specific AX-Tree or by assistive technologies themselves.
* By default, Puppeteer tries to approximate this filtering, exposing only
* the "interesting" nodes of the tree.
*
* @public
*/
export class Accessibility {
#realm: Realm;
#frameId: string;
/**
* @internal
*/
constructor(realm: Realm, frameId = '') {
this.#realm = realm;
this.#frameId = frameId;
}
/**
* Captures the current state of the accessibility tree.
* The returned object represents the root accessible node of the page.
*
* @remarks
*
* **NOTE** The Chrome accessibility tree contains nodes that go unused on
* most platforms and by most screen readers. Puppeteer will discard them as
* well for an easier to process tree, unless `interestingOnly` is set to
* `false`.
*
* @example
* An example of dumping the entire accessibility tree:
*
* ```ts
* const snapshot = await page.accessibility.snapshot();
* console.log(snapshot);
* ```
*
* @example
* An example of logging the focused node's name:
*
* ```ts
* const snapshot = await page.accessibility.snapshot();
* const node = findFocusedNode(snapshot);
* console.log(node && node.name);
*
* function findFocusedNode(node) {
* if (node.focused) return node;
* for (const child of node.children || []) {
* const foundNode = findFocusedNode(child);
* return foundNode;
* }
* return null;
* }
* ```
*
* @returns An AXNode object representing the snapshot.
*/
public async snapshot(
options: SnapshotOptions = {},
): Promise<SerializedAXNode | null> {
const {
interestingOnly = true,
root = null,
includeIframes = false,
} = options;
const {nodes} = await this.#realm.environment.client.send(
'Accessibility.getFullAXTree',
{
frameId: this.#frameId,
},
);
let backendNodeId: number | undefined;
if (root) {
const {node} = await this.#realm.environment.client.send(
'DOM.describeNode',
{
objectId: root.id,
},
);
backendNodeId = node.backendNodeId;
}
const defaultRoot = AXNode.createTree(this.#realm, nodes);
const populateIframes = async (root: AXNode): Promise<void> => {
if (root.payload.role?.value === 'Iframe') {
if (!root.payload.backendDOMNodeId) {
return;
}
using handle = (await this.#realm.adoptBackendNode(
root.payload.backendDOMNodeId,
)) as ElementHandle<Element>;
if (!handle || !('contentFrame' in handle)) {
return;
}
const frame = await handle.contentFrame();
if (!frame) {
return;
}
try {
const iframeSnapshot = await frame.accessibility.snapshot(options);
root.iframeSnapshot = iframeSnapshot ?? undefined;
} catch (error) {
// Frames can get detached at any time resulting in errors.
debugError(error);
}
}
for (const child of root.children) {
await populateIframes(child);
}
};
let needle: AXNode | null = defaultRoot;
if (!defaultRoot) {
return null;
}
if (includeIframes) {
await populateIframes(defaultRoot);
}
if (backendNodeId) {
needle = defaultRoot.find(node => {
return node.payload.backendDOMNodeId === backendNodeId;
});
}
if (!needle) {
return null;
}
if (!interestingOnly) {
return this.serializeTree(needle)[0] ?? null;
}
const interestingNodes = new Set<AXNode>();
this.collectInterestingNodes(interestingNodes, defaultRoot, false);
return this.serializeTree(needle, interestingNodes)[0] ?? null;
}
private serializeTree(
node: AXNode,
interestingNodes?: Set<AXNode>,
): SerializedAXNode[] {
const children: SerializedAXNode[] = [];
for (const child of node.children) {
children.push(...this.serializeTree(child, interestingNodes));
}
if (interestingNodes && !interestingNodes.has(node)) {
return children;
}
const serializedNode = node.serialize();
if (children.length) {
serializedNode.children = children;
}
if (node.iframeSnapshot) {
if (!serializedNode.children) {
serializedNode.children = [];
}
serializedNode.children.push(node.iframeSnapshot);
}
return [serializedNode];
}
private collectInterestingNodes(
collection: Set<AXNode>,
node: AXNode,
insideControl: boolean,
): void {
if (node.isInteresting(insideControl) || node.iframeSnapshot) {
collection.add(node);
}
if (node.isLeafNode()) {
return;
}
insideControl = insideControl || node.isControl();
for (const child of node.children) {
this.collectInterestingNodes(collection, child, insideControl);
}
}
}
class AXNode {
public payload: Protocol.Accessibility.AXNode;
public children: AXNode[] = [];
public iframeSnapshot?: SerializedAXNode;
#richlyEditable = false;
#editable = false;
#focusable = false;
#hidden = false;
#busy = false;
#modal = false;
#hasErrormessage = false;
#hasDetails = false;
#name: string;
#role: string;
#description?: string;
#roledescription?: string;
#live?: string;
#ignored: boolean;
#cachedHasFocusableChild?: boolean;
#realm: Realm;
constructor(realm: Realm, payload: Protocol.Accessibility.AXNode) {
this.payload = payload;
this.#role = this.payload.role ? this.payload.role.value : 'Unknown';
this.#ignored = this.payload.ignored;
this.#name = this.payload.name ? this.payload.name.value : '';
this.#description = this.payload.description
? this.payload.description.value
: undefined;
this.#realm = realm;
for (const property of this.payload.properties || []) {
if (property.name === 'editable') {
this.#richlyEditable = property.value.value === 'richtext';
this.#editable = true;
}
if (property.name === 'focusable') {
this.#focusable = property.value.value;
}
if (property.name === 'hidden') {
this.#hidden = property.value.value;
}
if (property.name === 'busy') {
this.#busy = property.value.value;
}
if (property.name === 'live') {
this.#live = property.value.value;
}
if (property.name === 'modal') {
this.#modal = property.value.value;
}
if (property.name === 'roledescription') {
this.#roledescription = property.value.value;
}
if (property.name === 'errormessage') {
this.#hasErrormessage = true;
}
if (property.name === 'details') {
this.#hasDetails = true;
}
}
}
#isPlainTextField(): boolean {
if (this.#richlyEditable) {
return false;
}
if (this.#editable) {
return true;
}
return this.#role === 'textbox' || this.#role === 'searchbox';
}
#isTextOnlyObject(): boolean {
const role = this.#role;
return (
role === 'LineBreak' ||
role === 'text' ||
role === 'InlineTextBox' ||
role === 'StaticText'
);
}
#hasFocusableChild(): boolean {
if (this.#cachedHasFocusableChild === undefined) {
this.#cachedHasFocusableChild = false;
for (const child of this.children) {
if (child.#focusable || child.#hasFocusableChild()) {
this.#cachedHasFocusableChild = true;
break;
}
}
}
return this.#cachedHasFocusableChild;
}
public find(predicate: (x: AXNode) => boolean): AXNode | null {
if (predicate(this)) {
return this;
}
for (const child of this.children) {
const result = child.find(predicate);
if (result) {
return result;
}
}
return null;
}
public isLeafNode(): boolean {
if (!this.children.length) {
return true;
}
// These types of objects may have children that we use as internal
// implementation details, but we want to expose them as leaves to platform
// accessibility APIs because screen readers might be confused if they find
// any children.
if (this.#isPlainTextField() || this.#isTextOnlyObject()) {
return true;
}
// Roles whose children are only presentational according to the ARIA and
// HTML5 Specs should be hidden from screen readers.
// (Note that whilst ARIA buttons can have only presentational children, HTML5
// buttons are allowed to have content.)
switch (this.#role) {
case 'doc-cover':
case 'graphics-symbol':
case 'img':
case 'image':
case 'Meter':
case 'scrollbar':
case 'slider':
case 'separator':
case 'progressbar':
return true;
default:
break;
}
if (this.#hasFocusableChild()) {
return false;
}
if (this.#role === 'heading' && this.#name) {
return true;
}
return false;
}
public isControl(): boolean {
switch (this.#role) {
case 'button':
case 'checkbox':
case 'ColorWell':
case 'combobox':
case 'DisclosureTriangle':
case 'listbox':
case 'menu':
case 'menubar':
case 'menuitem':
case 'menuitemcheckbox':
case 'menuitemradio':
case 'radio':
case 'scrollbar':
case 'searchbox':
case 'slider':
case 'spinbutton':
case 'switch':
case 'tab':
case 'textbox':
case 'tree':
case 'treeitem':
return true;
default:
return false;
}
}
public isLandmark(): boolean {
switch (this.#role) {
case 'banner':
case 'complementary':
case 'contentinfo':
case 'form':
case 'main':
case 'navigation':
case 'region':
case 'search':
return true;
default:
return false;
}
}
public isInteresting(insideControl: boolean): boolean {
const role = this.#role;
if (role === 'Ignored' || this.#hidden || this.#ignored) {
return false;
}
if (this.isLandmark()) {
return true;
}
if (
this.#focusable ||
this.#richlyEditable ||
this.#busy ||
(this.#live && this.#live !== 'off') ||
this.#modal ||
this.#hasErrormessage ||
this.#hasDetails ||
this.#roledescription
) {
return true;
}
// If it's not focusable but has a control role, then it's interesting.
if (this.isControl()) {
return true;
}
// A non focusable child of a control is not interesting
if (insideControl) {
return false;
}
return this.isLeafNode() && (!!this.#name || !!this.#description);
}
public serialize(): SerializedAXNode {
const properties = new Map<string, number | string | boolean>();
for (const property of this.payload.properties || []) {
properties.set(property.name.toLowerCase(), property.value.value);
}
if (this.payload.name) {
properties.set('name', this.payload.name.value);
}
if (this.payload.value) {
properties.set('value', this.payload.value.value);
}
if (this.payload.description) {
properties.set('description', this.payload.description.value);
}
const node: SerializedAXNode = {
role: this.#role,
elementHandle: async (): Promise<ElementHandle | null> => {
if (!this.payload.backendDOMNodeId) {
return null;
}
using handle = await this.#realm.adoptBackendNode(
this.payload.backendDOMNodeId,
);
// Since Text nodes are not elements, we want to
// return a handle to the parent element for them.
return (await handle.evaluateHandle(node => {
return node.nodeType === Node.TEXT_NODE ? node.parentElement : node;
})) as ElementHandle<Element>;
},
backendNodeId: this.payload.backendDOMNodeId,
// LoaderId is an experimental mechanism to establish unique IDs across
// navigations.
loaderId: (this.#realm.environment as CdpFrame)._loaderId,
};
type UserStringProperty =
| 'name'
| 'value'
| 'description'
| 'keyshortcuts'
| 'roledescription'
| 'valuetext'
| 'url';
const userStringProperties: UserStringProperty[] = [
'name',
'value',
'description',
'keyshortcuts',
'roledescription',
'valuetext',
'url',
];
const getUserStringPropertyValue = (key: UserStringProperty): string => {
return properties.get(key) as string;
};
for (const userStringProperty of userStringProperties) {
if (!properties.has(userStringProperty)) {
continue;
}
node[userStringProperty] = getUserStringPropertyValue(userStringProperty);
}
type BooleanProperty =
| 'disabled'
| 'expanded'
| 'focused'
| 'modal'
| 'multiline'
| 'multiselectable'
| 'readonly'
| 'required'
| 'selected'
| 'busy'
| 'atomic';
const booleanProperties: BooleanProperty[] = [
'disabled',
'expanded',
'focused',
'modal',
'multiline',
'multiselectable',
'readonly',
'required',
'selected',
'busy',
'atomic',
];
const getBooleanPropertyValue = (key: BooleanProperty): boolean => {
return !!properties.get(key);
};
for (const booleanProperty of booleanProperties) {
// RootWebArea's treat focus differently than other nodes. They report whether
// their frame has focus, not whether focus is specifically on the root
// node.
if (booleanProperty === 'focused' && this.#role === 'RootWebArea') {
continue;
}
if (!properties.has(booleanProperty)) {
continue;
}
node[booleanProperty] = getBooleanPropertyValue(booleanProperty);
}
type TristateProperty = 'checked' | 'pressed';
const tristateProperties: TristateProperty[] = ['checked', 'pressed'];
for (const tristateProperty of tristateProperties) {
if (!properties.has(tristateProperty)) {
continue;
}
const value = properties.get(tristateProperty);
node[tristateProperty] =
value === 'mixed' ? 'mixed' : value === 'true' ? true : false;
}
type NumbericalProperty = 'level' | 'valuemax' | 'valuemin';
const numericalProperties: NumbericalProperty[] = [
'level',
'valuemax',
'valuemin',
];
const getNumericalPropertyValue = (key: NumbericalProperty): number => {
return properties.get(key) as number;
};
for (const numericalProperty of numericalProperties) {
if (!properties.has(numericalProperty)) {
continue;
}
node[numericalProperty] = getNumericalPropertyValue(numericalProperty);
}
type TokenProperty =
| 'autocomplete'
| 'haspopup'
| 'invalid'
| 'orientation'
| 'live'
| 'relevant'
| 'errormessage'
| 'details';
const tokenProperties: TokenProperty[] = [
'autocomplete',
'haspopup',
'invalid',
'orientation',
'live',
'relevant',
'errormessage',
'details',
];
const getTokenPropertyValue = (key: TokenProperty): string => {
return properties.get(key) as string;
};
for (const tokenProperty of tokenProperties) {
const value = getTokenPropertyValue(tokenProperty);
if (!value || value === 'false') {
continue;
}
node[tokenProperty] = getTokenPropertyValue(tokenProperty);
}
return node;
}
public static createTree(
realm: Realm,
payloads: Protocol.Accessibility.AXNode[],
): AXNode | null {
const nodeById = new Map<string, AXNode>();
for (const payload of payloads) {
nodeById.set(payload.nodeId, new AXNode(realm, payload));
}
for (const node of nodeById.values()) {
for (const childId of node.payload.childIds || []) {
const child = nodeById.get(childId);
if (child) {
node.children.push(child);
}
}
}
return nodeById.values().next().value ?? null;
}
}

133
node_modules/puppeteer-core/src/cdp/Binding.ts generated vendored Normal file
View File

@@ -0,0 +1,133 @@
/**
* @license
* Copyright 2024 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import {JSHandle} from '../api/JSHandle.js';
import {debugError} from '../common/util.js';
import {DisposableStack} from '../util/disposable.js';
import {isErrorLike} from '../util/ErrorLike.js';
import type {ExecutionContext} from './ExecutionContext.js';
/**
* @internal
*/
export class Binding {
#name: string;
#fn: (...args: unknown[]) => unknown;
#initSource: string;
constructor(
name: string,
fn: (...args: unknown[]) => unknown,
initSource: string,
) {
this.#name = name;
this.#fn = fn;
this.#initSource = initSource;
}
get name(): string {
return this.#name;
}
get initSource(): string {
return this.#initSource;
}
/**
* @param context - Context to run the binding in; the context should have
* the binding added to it beforehand.
* @param id - ID of the call. This should come from the CDP
* `onBindingCalled` response.
* @param args - Plain arguments from CDP.
*/
async run(
context: ExecutionContext,
id: number,
args: unknown[],
isTrivial: boolean,
): Promise<void> {
const stack = new DisposableStack();
try {
if (!isTrivial) {
// Getting non-trivial arguments.
using handles = await context.evaluateHandle(
(name, seq) => {
// @ts-expect-error Code is evaluated in a different context.
return globalThis[name].args.get(seq);
},
this.#name,
id,
);
const properties = await handles.getProperties();
for (const [index, handle] of properties) {
// This is not straight-forward since some arguments can stringify, but
// aren't plain objects so add subtypes when the use-case arises.
if (index in args) {
switch (handle.remoteObject().subtype) {
case 'node':
args[+index] = handle;
break;
default:
stack.use(handle);
}
} else {
stack.use(handle);
}
}
}
await context.evaluate(
(name, seq, result) => {
// @ts-expect-error Code is evaluated in a different context.
const callbacks = globalThis[name].callbacks;
callbacks.get(seq).resolve(result);
callbacks.delete(seq);
},
this.#name,
id,
await this.#fn(...args),
);
for (const arg of args) {
if (arg instanceof JSHandle) {
stack.use(arg);
}
}
} catch (error) {
if (isErrorLike(error)) {
await context
.evaluate(
(name, seq, message, stack) => {
const error = new Error(message);
error.stack = stack;
// @ts-expect-error Code is evaluated in a different context.
const callbacks = globalThis[name].callbacks;
callbacks.get(seq).reject(error);
callbacks.delete(seq);
},
this.#name,
id,
error.message,
error.stack,
)
.catch(debugError);
} else {
await context
.evaluate(
(name, seq, error) => {
// @ts-expect-error Code is evaluated in a different context.
const callbacks = globalThis[name].callbacks;
callbacks.get(seq).reject(error);
callbacks.delete(seq);
},
this.#name,
id,
error,
)
.catch(debugError);
}
}
}
}

View File

@@ -0,0 +1,47 @@
/**
* @license
* Copyright 2025 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {
AdapterState,
BluetoothEmulation,
PreconnectedPeripheral,
} from '../api/BluetoothEmulation.js';
import type {Connection} from './Connection.js';
/**
* @internal
*/
export class CdpBluetoothEmulation implements BluetoothEmulation {
#connection: Connection;
constructor(connection: Connection) {
this.#connection = connection;
}
async emulateAdapter(state: AdapterState, leSupported = true): Promise<void> {
// Bluetooth spec requires overriding the existing adapter (step 6). From the CDP
// perspective, it means disabling the emulation first.
// https://webbluetoothcg.github.io/web-bluetooth/#bluetooth-simulateAdapter-command
await this.#connection.send('BluetoothEmulation.disable');
await this.#connection.send('BluetoothEmulation.enable', {
state,
leSupported,
});
}
async disableEmulation(): Promise<void> {
await this.#connection.send('BluetoothEmulation.disable');
}
async simulatePreconnectedPeripheral(
preconnectedPeripheral: PreconnectedPeripheral,
): Promise<void> {
await this.#connection.send(
'BluetoothEmulation.simulatePreconnectedPeripheral',
preconnectedPeripheral,
);
}
}

627
node_modules/puppeteer-core/src/cdp/Browser.ts generated vendored Normal file
View File

@@ -0,0 +1,627 @@
/**
* @license
* Copyright 2017 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {ChildProcess} from 'node:child_process';
import type {Protocol} from 'devtools-protocol';
import type {CreatePageOptions, DebugInfo} from '../api/Browser.js';
import {
Browser as BrowserBase,
BrowserEvent,
type BrowserCloseCallback,
type BrowserContextOptions,
type IsPageTargetCallback,
type TargetFilterCallback,
type ScreenInfo,
type AddScreenParams,
type WindowBounds,
type WindowId,
} from '../api/Browser.js';
import {BrowserContextEvent} from '../api/BrowserContext.js';
import {CDPSessionEvent} from '../api/CDPSession.js';
import type {Extension} from '../api/Extension.js';
import type {Page} from '../api/Page.js';
import type {Target} from '../api/Target.js';
import type {DownloadBehavior} from '../common/DownloadBehavior.js';
import type {Viewport} from '../common/Viewport.js';
import {CdpBrowserContext} from './BrowserContext.js';
import type {CdpCDPSession} from './CdpSession.js';
import type {Connection} from './Connection.js';
import {CdpExtension} from './Extension.js';
import {
DevToolsTarget,
InitializationStatus,
OtherTarget,
PageTarget,
WorkerTarget,
type CdpTarget,
} from './Target.js';
import {TargetManagerEvent} from './TargetManageEvents.js';
import {TargetManager} from './TargetManager.js';
/**
* @internal
*/
function isDevToolsPageTarget(url: string): boolean {
return url.startsWith('devtools://devtools/bundled/devtools_app.html');
}
/**
* @internal
*/
export class CdpBrowser extends BrowserBase {
readonly protocol = 'cdp';
static async _create(
connection: Connection,
contextIds: string[],
acceptInsecureCerts: boolean,
defaultViewport?: Viewport | null,
downloadBehavior?: DownloadBehavior,
process?: ChildProcess,
closeCallback?: BrowserCloseCallback,
targetFilterCallback?: TargetFilterCallback,
isPageTargetCallback?: IsPageTargetCallback,
waitForInitiallyDiscoveredTargets = true,
networkEnabled = true,
issuesEnabled = true,
handleDevToolsAsPage = false,
blockList?: string[],
): Promise<CdpBrowser> {
const browser = new CdpBrowser(
connection,
contextIds,
defaultViewport,
process,
closeCallback,
targetFilterCallback,
isPageTargetCallback,
waitForInitiallyDiscoveredTargets,
networkEnabled,
issuesEnabled,
handleDevToolsAsPage,
blockList,
);
if (acceptInsecureCerts) {
await connection.send('Security.setIgnoreCertificateErrors', {
ignore: true,
});
}
await browser._attach(downloadBehavior);
return browser;
}
#defaultViewport?: Viewport | null;
#process?: ChildProcess;
#connection: Connection;
#closeCallback: BrowserCloseCallback;
#targetFilterCallback: TargetFilterCallback;
#isPageTargetCallback!: IsPageTargetCallback;
#defaultContext: CdpBrowserContext;
#contexts = new Map<string, CdpBrowserContext>();
#networkEnabled = true;
#issuesEnabled = true;
#targetManager: TargetManager;
#handleDevToolsAsPage = false;
#extensions = new Map<string, Extension>();
constructor(
connection: Connection,
contextIds: string[],
defaultViewport?: Viewport | null,
process?: ChildProcess,
closeCallback?: BrowserCloseCallback,
targetFilterCallback?: TargetFilterCallback,
isPageTargetCallback?: IsPageTargetCallback,
waitForInitiallyDiscoveredTargets = true,
networkEnabled = true,
issuesEnabled = true,
handleDevToolsAsPage = false,
networkConditions?: string[],
) {
super();
this.#networkEnabled = networkEnabled;
this.#issuesEnabled = issuesEnabled;
this.#defaultViewport = defaultViewport;
this.#process = process;
this.#connection = connection;
this.#closeCallback = closeCallback || (() => {});
this.#targetFilterCallback =
targetFilterCallback ||
(() => {
return true;
});
this.#handleDevToolsAsPage = handleDevToolsAsPage;
this.#setIsPageTargetCallback(isPageTargetCallback);
this.#targetManager = new TargetManager(
connection,
this.#createTarget,
this.#targetFilterCallback,
waitForInitiallyDiscoveredTargets,
networkConditions,
);
this.#defaultContext = new CdpBrowserContext(this.#connection, this);
for (const contextId of contextIds) {
this.#contexts.set(
contextId,
new CdpBrowserContext(this.#connection, this, contextId),
);
}
}
#emitDisconnected = () => {
this.emit(BrowserEvent.Disconnected, undefined);
};
async _attach(downloadBehavior: DownloadBehavior | undefined): Promise<void> {
this.#connection.on(CDPSessionEvent.Disconnected, this.#emitDisconnected);
if (downloadBehavior) {
await this.#defaultContext.setDownloadBehavior(downloadBehavior);
}
this.#targetManager.on(
TargetManagerEvent.TargetAvailable,
this.#onAttachedToTarget,
);
this.#targetManager.on(
TargetManagerEvent.TargetGone,
this.#onDetachedFromTarget,
);
this.#targetManager.on(
TargetManagerEvent.TargetChanged,
this.#onTargetChanged,
);
this.#targetManager.on(
TargetManagerEvent.TargetDiscovered,
this.#onTargetDiscovered,
);
await this.#targetManager.initialize();
}
_detach(): void {
this.#connection.off(CDPSessionEvent.Disconnected, this.#emitDisconnected);
this.#targetManager.off(
TargetManagerEvent.TargetAvailable,
this.#onAttachedToTarget,
);
this.#targetManager.off(
TargetManagerEvent.TargetGone,
this.#onDetachedFromTarget,
);
this.#targetManager.off(
TargetManagerEvent.TargetChanged,
this.#onTargetChanged,
);
this.#targetManager.off(
TargetManagerEvent.TargetDiscovered,
this.#onTargetDiscovered,
);
}
override process(): ChildProcess | null {
return this.#process ?? null;
}
_targetManager(): TargetManager {
return this.#targetManager;
}
#setIsPageTargetCallback(isPageTargetCallback?: IsPageTargetCallback): void {
this.#isPageTargetCallback =
isPageTargetCallback ||
((target: Target): boolean => {
return (
target.type() === 'page' ||
target.type() === 'background_page' ||
target.type() === 'webview' ||
(this.#handleDevToolsAsPage &&
target.type() === 'other' &&
isDevToolsPageTarget(target.url()))
);
});
}
_getIsPageTargetCallback(): IsPageTargetCallback | undefined {
return this.#isPageTargetCallback;
}
override async createBrowserContext(
options: BrowserContextOptions = {},
): Promise<CdpBrowserContext> {
const {proxyServer, proxyBypassList, downloadBehavior} = options;
const {browserContextId} = await this.#connection.send(
'Target.createBrowserContext',
{
proxyServer,
proxyBypassList: proxyBypassList && proxyBypassList.join(','),
},
);
const context = new CdpBrowserContext(
this.#connection,
this,
browserContextId,
);
if (downloadBehavior) {
await context.setDownloadBehavior(downloadBehavior);
}
this.#contexts.set(browserContextId, context);
return context;
}
override browserContexts(): CdpBrowserContext[] {
return [this.#defaultContext, ...Array.from(this.#contexts.values())];
}
override defaultBrowserContext(): CdpBrowserContext {
return this.#defaultContext;
}
async _disposeContext(contextId?: string): Promise<void> {
if (!contextId) {
return;
}
await this.#connection.send('Target.disposeBrowserContext', {
browserContextId: contextId,
});
this.#contexts.delete(contextId);
}
#createTarget = (
targetInfo: Protocol.Target.TargetInfo,
session?: CdpCDPSession,
) => {
const {browserContextId} = targetInfo;
const context =
browserContextId && this.#contexts.has(browserContextId)
? this.#contexts.get(browserContextId)
: this.#defaultContext;
if (!context) {
throw new Error('Missing browser context');
}
const createSession = (isAutoAttachEmulated: boolean) => {
return this.#connection._createSession(targetInfo, isAutoAttachEmulated);
};
const otherTarget = new OtherTarget(
targetInfo,
session,
context,
this.#targetManager,
createSession,
);
if (targetInfo.url && isDevToolsPageTarget(targetInfo.url)) {
return new DevToolsTarget(
targetInfo,
session,
context,
this.#targetManager,
createSession,
this.#defaultViewport ?? null,
);
}
if (this.#isPageTargetCallback(otherTarget)) {
return new PageTarget(
targetInfo,
session,
context,
this.#targetManager,
createSession,
this.#defaultViewport ?? null,
);
}
if (
targetInfo.type === 'service_worker' ||
targetInfo.type === 'shared_worker'
) {
return new WorkerTarget(
targetInfo,
session,
context,
this.#targetManager,
createSession,
);
}
return otherTarget;
};
#onAttachedToTarget = async (target: CdpTarget) => {
if (
target._isTargetExposed() &&
(await target._initializedDeferred.valueOrThrow()) ===
InitializationStatus.SUCCESS
) {
this.emit(BrowserEvent.TargetCreated, target);
target.browserContext().emit(BrowserContextEvent.TargetCreated, target);
}
};
#onDetachedFromTarget = async (target: CdpTarget): Promise<void> => {
target._initializedDeferred.resolve(InitializationStatus.ABORTED);
target._isClosedDeferred.resolve();
if (
target._isTargetExposed() &&
(await target._initializedDeferred.valueOrThrow()) ===
InitializationStatus.SUCCESS
) {
this.emit(BrowserEvent.TargetDestroyed, target);
target.browserContext().emit(BrowserContextEvent.TargetDestroyed, target);
}
};
#onTargetChanged = ({target}: {target: CdpTarget}): void => {
this.emit(BrowserEvent.TargetChanged, target);
target.browserContext().emit(BrowserContextEvent.TargetChanged, target);
};
#onTargetDiscovered = (targetInfo: Protocol.Target.TargetInfo): void => {
this.emit(BrowserEvent.TargetDiscovered, targetInfo);
};
override wsEndpoint(): string {
return this.#connection.url();
}
override async newPage(options?: CreatePageOptions): Promise<Page> {
return await this.#defaultContext.newPage(options);
}
async _createPageInContext(
contextId?: string,
options?: CreatePageOptions,
): Promise<Page> {
const hasTargets =
this.targets().filter(t => {
return t.browserContext().id === contextId;
}).length > 0;
const windowBounds =
options?.type === 'window' ? options.windowBounds : undefined;
const {targetId} = await this.#connection.send('Target.createTarget', {
url: 'about:blank',
browserContextId: contextId || undefined,
left: windowBounds?.left,
top: windowBounds?.top,
width: windowBounds?.width,
height: windowBounds?.height,
windowState: windowBounds?.windowState,
// Works around crbug.com/454825274.
newWindow: hasTargets && options?.type === 'window' ? true : undefined,
background: options?.background,
});
const target = (await this.waitForTarget(t => {
return (t as CdpTarget)._targetId === targetId;
})) as CdpTarget;
if (!target) {
throw new Error(`Missing target for page (id = ${targetId})`);
}
const initialized =
(await target._initializedDeferred.valueOrThrow()) ===
InitializationStatus.SUCCESS;
if (!initialized) {
throw new Error(`Failed to create target for page (id = ${targetId})`);
}
const page = await target.page();
if (!page) {
throw new Error(
`Failed to create a page for context (id = ${contextId})`,
);
}
return page;
}
async _createDevToolsPage(pageTargetId: string): Promise<Page> {
const openDevToolsResponse = await this.#connection.send(
'Target.openDevTools',
{
targetId: pageTargetId,
},
);
const target = (await this.waitForTarget(t => {
return (t as CdpTarget)._targetId === openDevToolsResponse.targetId;
})) as CdpTarget;
if (!target) {
throw new Error(
`Missing target for DevTools page (id = ${pageTargetId})`,
);
}
const initialized =
(await target._initializedDeferred.valueOrThrow()) ===
InitializationStatus.SUCCESS;
if (!initialized) {
throw new Error(
`Failed to create target for DevTools page (id = ${pageTargetId})`,
);
}
const page = await target.page();
if (!page) {
throw new Error(
`Failed to create a DevTools Page for target (id = ${pageTargetId})`,
);
}
return page;
}
async _hasDevToolsTarget(pageTargetId: string): Promise<string | undefined> {
const response = await this.#connection.send('Target.getDevToolsTarget', {
targetId: pageTargetId,
});
return response.targetId;
}
override async installExtension(path: string): Promise<string> {
const {id} = await this.#connection.send('Extensions.loadUnpacked', {path});
this.#extensions.delete(id);
return id;
}
override async uninstallExtension(id: string): Promise<void> {
await this.#connection.send('Extensions.uninstall', {id});
// Currently sending the Extensions.uninstall command does not trigger
// the Target.targetDestroyed event for service workers. This causes
// flakiness in the extension tests.
// TODO(nroscino): Remove this once the event is correctly emitted.
const targetDestroyedPromises = [];
for (const [targetId, targetInfo] of this._targetManager()
.getDiscoveredTargetInfos()
.entries()) {
if (targetInfo.url.includes(id) && targetInfo.type === 'service_worker') {
this._targetManager().addToIgnoreTarget(targetId);
targetDestroyedPromises.push(
new Promise(resolve => {
return setTimeout(() => {
this.#connection.emit('Target.targetDestroyed', {
targetId: targetId,
});
resolve(null);
}, 0);
}),
);
}
}
await Promise.all(targetDestroyedPromises);
this.#extensions.delete(id);
}
override async screens(): Promise<ScreenInfo[]> {
const {screenInfos} = await this.#connection.send(
'Emulation.getScreenInfos',
);
return screenInfos;
}
override async addScreen(params: AddScreenParams): Promise<ScreenInfo> {
const {screenInfo} = await this.#connection.send(
'Emulation.addScreen',
params,
);
return screenInfo;
}
override async removeScreen(screenId: string): Promise<void> {
return await this.#connection.send('Emulation.removeScreen', {screenId});
}
override async getWindowBounds(windowId: WindowId): Promise<WindowBounds> {
const {bounds} = await this.#connection.send('Browser.getWindowBounds', {
windowId: Number(windowId),
});
return bounds;
}
override async setWindowBounds(
windowId: WindowId,
windowBounds: WindowBounds,
): Promise<void> {
await this.#connection.send('Browser.setWindowBounds', {
windowId: Number(windowId),
bounds: windowBounds,
});
}
override targets(): CdpTarget[] {
return Array.from(
this.#targetManager.getAvailableTargets().values(),
).filter(target => {
return (
target._isTargetExposed() &&
target._initializedDeferred.value() === InitializationStatus.SUCCESS
);
});
}
override target(): CdpTarget {
const browserTarget = this.targets().find(target => {
return target.type() === 'browser';
});
if (!browserTarget) {
throw new Error('Browser target is not found');
}
return browserTarget;
}
override async version(): Promise<string> {
const version = await this.#getVersion();
return version.product;
}
override async userAgent(): Promise<string> {
const version = await this.#getVersion();
return version.userAgent;
}
override async close(): Promise<void> {
await this.#closeCallback.call(null);
await this.disconnect();
}
override disconnect(): Promise<void> {
this.#targetManager.dispose();
this.#connection.dispose();
this._detach();
return Promise.resolve();
}
/**
* @internal
*/
get _connection(): Connection {
return this.#connection;
}
override get connected(): boolean {
return !this.#connection._closed;
}
#getVersion(): Promise<Protocol.Browser.GetVersionResponse> {
return this.#connection.send('Browser.getVersion');
}
override get debugInfo(): DebugInfo {
return {
pendingProtocolErrors: this.#connection.getPendingProtocolErrors(),
};
}
override isNetworkEnabled(): boolean {
return this.#networkEnabled;
}
override async extensions(): Promise<Map<string, Extension>> {
const response = await this.#connection.send('Extensions.getExtensions');
const extensionsMap = new Map<string, Extension>();
for (const currExtension of response.extensions) {
if (this.#extensions.has(currExtension.id)) {
extensionsMap.set(
currExtension.id,
this.#extensions.get(currExtension.id)!,
);
} else {
const newExtension = new CdpExtension(
currExtension.id,
currExtension.version,
currExtension.name,
currExtension.path,
currExtension.enabled,
this,
);
extensionsMap.set(currExtension.id, newExtension);
}
}
this.#extensions = extensionsMap;
return this.#extensions;
}
override isIssuesEnabled(): boolean {
return this.#issuesEnabled;
}
}

View File

@@ -0,0 +1,72 @@
/**
* @license
* Copyright 2020 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {ConnectionTransport} from '../common/ConnectionTransport.js';
import type {ConnectOptions} from '../common/ConnectOptions.js';
import {debugError, DEFAULT_VIEWPORT} from '../common/util.js';
import {createIncrementalIdGenerator} from '../util/incremental-id-generator.js';
import {CdpBrowser} from './Browser.js';
import {Connection} from './Connection.js';
/**
* Users should never call this directly; it's called when calling
* `puppeteer.connect` with `protocol: 'cdp'`.
*
* @internal
*/
export async function _connectToCdpBrowser(
connectionTransport: ConnectionTransport,
url: string,
options: ConnectOptions,
): Promise<CdpBrowser> {
const {
acceptInsecureCerts = false,
networkEnabled = true,
issuesEnabled = true,
defaultViewport = DEFAULT_VIEWPORT,
downloadBehavior,
targetFilter,
_isPageTarget: isPageTarget,
slowMo = 0,
protocolTimeout,
handleDevToolsAsPage,
idGenerator = createIncrementalIdGenerator(),
blockList,
} = options;
const connection = new Connection(
url,
connectionTransport,
slowMo,
protocolTimeout,
/* rawErrors */ false,
idGenerator,
);
const {browserContextIds} = await connection.send(
'Target.getBrowserContexts',
);
const browser = await CdpBrowser._create(
connection,
browserContextIds,
acceptInsecureCerts,
defaultViewport,
downloadBehavior,
undefined,
() => {
return connection.send('Browser.close').catch(debugError);
},
targetFilter,
isPageTarget,
undefined,
networkEnabled,
issuesEnabled,
handleDevToolsAsPage,
blockList,
);
return browser;
}

183
node_modules/puppeteer-core/src/cdp/BrowserContext.ts generated vendored Normal file
View File

@@ -0,0 +1,183 @@
/**
* @license
* Copyright 2024 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {Protocol} from 'devtools-protocol';
import type {CreatePageOptions} from '../api/Browser.js';
import {
WEB_PERMISSION_TO_PROTOCOL_PERMISSION,
type Permission,
type PermissionDescriptor,
type PermissionState,
} from '../api/Browser.js';
import {BrowserContext} from '../api/BrowserContext.js';
import type {Page} from '../api/Page.js';
import type {Cookie, CookieData} from '../common/Cookie.js';
import type {DownloadBehavior} from '../common/DownloadBehavior.js';
import {assert} from '../util/assert.js';
import type {CdpBrowser} from './Browser.js';
import type {Connection} from './Connection.js';
import {
convertCookiesPartitionKeyFromPuppeteerToCdp,
convertSameSiteFromPuppeteerToCdp,
} from './Page.js';
import type {CdpTarget} from './Target.js';
/**
* @internal
*/
export class CdpBrowserContext extends BrowserContext {
#connection: Connection;
#browser: CdpBrowser;
#id?: string;
constructor(connection: Connection, browser: CdpBrowser, contextId?: string) {
super();
this.#connection = connection;
this.#browser = browser;
this.#id = contextId;
}
override get id(): string | undefined {
return this.#id;
}
override targets(): CdpTarget[] {
return this.#browser.targets().filter(target => {
return target.browserContext() === this;
});
}
override async pages(includeAll = false): Promise<Page[]> {
const pages = await Promise.all(
this.targets()
.filter(target => {
return (
target.type() === 'page' ||
((target.type() === 'other' || includeAll) &&
this.#browser._getIsPageTargetCallback()?.(target))
);
})
.map(target => {
return target.page();
}),
);
return pages.filter((page): page is Page => {
return !!page;
});
}
override async overridePermissions(
origin: string,
permissions: Permission[],
): Promise<void> {
const protocolPermissions = permissions.map(permission => {
const protocolPermission =
WEB_PERMISSION_TO_PROTOCOL_PERMISSION.get(permission);
if (!protocolPermission) {
throw new Error('Unknown permission: ' + permission);
}
return protocolPermission;
});
await this.#connection.send('Browser.grantPermissions', {
origin,
browserContextId: this.#id || undefined,
permissions: protocolPermissions,
});
}
override async setPermission(
origin: string | '*',
...permissions: Array<{
permission: PermissionDescriptor;
state: PermissionState;
}>
): Promise<void> {
await Promise.all(
permissions.map(async permission => {
const protocolPermission: Protocol.Browser.PermissionDescriptor = {
name: permission.permission.name,
userVisibleOnly: permission.permission.userVisibleOnly,
sysex: permission.permission.sysex,
allowWithoutSanitization:
permission.permission.allowWithoutSanitization,
panTiltZoom: permission.permission.panTiltZoom,
};
await this.#connection.send('Browser.setPermission', {
origin: origin === '*' ? undefined : origin,
browserContextId: this.#id || undefined,
permission: protocolPermission,
setting: permission.state as Protocol.Browser.PermissionSetting,
});
}),
);
}
override async clearPermissionOverrides(): Promise<void> {
await this.#connection.send('Browser.resetPermissions', {
browserContextId: this.#id || undefined,
});
}
override async newPage(options?: CreatePageOptions): Promise<Page> {
using _guard = await this.waitForScreenshotOperations();
return await this.#browser._createPageInContext(this.#id, options);
}
override browser(): CdpBrowser {
return this.#browser;
}
override async close(): Promise<void> {
assert(this.#id, 'Default BrowserContext cannot be closed!');
await this.#browser._disposeContext(this.#id);
}
override async cookies(): Promise<Cookie[]> {
const {cookies} = await this.#connection.send('Storage.getCookies', {
browserContextId: this.#id,
});
return cookies.map(cookie => {
return {
...cookie,
partitionKey: cookie.partitionKey
? {
sourceOrigin: cookie.partitionKey.topLevelSite,
hasCrossSiteAncestor: cookie.partitionKey.hasCrossSiteAncestor,
}
: undefined,
// TODO: remove sameParty as it is removed from Chrome.
sameParty: false,
};
});
}
override async setCookie(...cookies: CookieData[]): Promise<void> {
return await this.#connection.send('Storage.setCookies', {
browserContextId: this.#id,
cookies: cookies.map(cookie => {
return {
...cookie,
partitionKey: convertCookiesPartitionKeyFromPuppeteerToCdp(
cookie.partitionKey,
),
sameSite: convertSameSiteFromPuppeteerToCdp(cookie.sameSite),
};
}),
});
}
public async setDownloadBehavior(
downloadBehavior: DownloadBehavior,
): Promise<void> {
await this.#connection.send('Browser.setDownloadBehavior', {
behavior: downloadBehavior.policy,
downloadPath: downloadBehavior.downloadPath,
browserContextId: this.#id,
});
}
}

30
node_modules/puppeteer-core/src/cdp/CdpIssue.ts generated vendored Normal file
View File

@@ -0,0 +1,30 @@
/**
* @license
* Copyright 2026 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {Protocol} from 'devtools-protocol';
import type {Issue} from '../api/Issue.js';
/**
* @internal
*/
export class CdpIssue implements Issue {
#code: Protocol.Audits.InspectorIssueCode;
#details: Protocol.Audits.InspectorIssueDetails;
constructor(issue: Protocol.Audits.InspectorIssue) {
this.#code = issue.code;
this.#details = issue.details;
}
get code(): Protocol.Audits.InspectorIssueCode {
return this.#code;
}
get details(): Protocol.Audits.InspectorIssueDetails {
return this.#details;
}
}

View File

@@ -0,0 +1,46 @@
/**
* @license
* Copyright 2024 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {CdpFrame} from './Frame.js';
/**
* @internal
*/
export class CdpPreloadScript {
/**
* This is the ID of the preload script returned by
* Page.addScriptToEvaluateOnNewDocument in the main frame.
*
* Sub-frames would get a different CDP ID because
* addScriptToEvaluateOnNewDocument is called for each subframe. But
* users only see this ID and subframe IDs are internal to Puppeteer.
*/
#id: string;
#source: string;
#frameToId = new WeakMap<CdpFrame, string>();
constructor(mainFrame: CdpFrame, id: string, source: string) {
this.#id = id;
this.#source = source;
this.#frameToId.set(mainFrame, id);
}
get id(): string {
return this.#id;
}
get source(): string {
return this.#source;
}
getIdForFrame(frame: CdpFrame): string | undefined {
return this.#frameToId.get(frame);
}
setIdForFrame(frame: CdpFrame, identifier: string): void {
this.#frameToId.set(frame, identifier);
}
}

181
node_modules/puppeteer-core/src/cdp/CdpSession.ts generated vendored Normal file
View File

@@ -0,0 +1,181 @@
/**
* @license
* Copyright 2017 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {ProtocolMapping} from 'devtools-protocol/types/protocol-mapping.js';
import {
type CDPEvents,
CDPSession,
CDPSessionEvent,
type CommandOptions,
} from '../api/CDPSession.js';
import {CallbackRegistry} from '../common/CallbackRegistry.js';
import {TargetCloseError} from '../common/Errors.js';
import {assert} from '../util/assert.js';
import {createProtocolErrorMessage} from '../util/ErrorLike.js';
import type {Connection} from './Connection.js';
import type {CdpTarget} from './Target.js';
/**
* @internal
*/
export class CdpCDPSession extends CDPSession {
#sessionId: string;
#targetType: string;
#callbacks: CallbackRegistry;
#connection: Connection;
#parentSessionId?: string;
#target?: CdpTarget;
#rawErrors = false;
#detached = false;
/**
* @internal
*/
constructor(
connection: Connection,
targetType: string,
sessionId: string,
parentSessionId: string | undefined,
rawErrors: boolean,
) {
super();
this.#connection = connection;
this.#targetType = targetType;
this.#callbacks = new CallbackRegistry(connection._idGenerator);
this.#sessionId = sessionId;
this.#parentSessionId = parentSessionId;
this.#rawErrors = rawErrors;
}
/**
* Sets the {@link CdpTarget} associated with the session instance.
*
* @internal
*/
setTarget(target: CdpTarget): void {
this.#target = target;
}
/**
* Gets the {@link CdpTarget} associated with the session instance.
*
* @internal
*/
target(): CdpTarget {
assert(this.#target, 'Target must exist');
return this.#target;
}
override connection(): Connection {
return this.#connection;
}
override get detached(): boolean {
return this.#connection._closed || this.#detached;
}
override parentSession(): CDPSession | undefined {
if (!this.#parentSessionId) {
// In some cases, e.g., DevTools pages there is no parent session. In this
// case, we treat the current session as the parent session.
return this;
}
const parent = this.#connection?.session(this.#parentSessionId);
return parent ?? undefined;
}
override send<T extends keyof ProtocolMapping.Commands>(
method: T,
params?: ProtocolMapping.Commands[T]['paramsType'][0],
options?: CommandOptions,
): Promise<ProtocolMapping.Commands[T]['returnType']> {
if (this.detached) {
return Promise.reject(
new TargetCloseError(
`Protocol error (${method}): Session closed. Most likely the ${this.#targetType} has been closed.`,
),
);
}
return this.#connection._rawSend(
this.#callbacks,
method,
params,
this.#sessionId,
options,
);
}
/**
* @internal
*/
onMessage(object: {
id?: number;
method: keyof CDPEvents;
params: CDPEvents[keyof CDPEvents];
error: {message: string; data: any; code: number};
result?: any;
}): void {
if (object.id) {
if (object.error) {
if (this.#rawErrors) {
this.#callbacks.rejectRaw(object.id, object.error);
} else {
this.#callbacks.reject(
object.id,
createProtocolErrorMessage(object),
object.error.message,
);
}
} else {
this.#callbacks.resolve(object.id, object.result);
}
} else {
assert(!object.id);
this.emit(object.method, object.params);
}
}
/**
* Detaches the cdpSession from the target. Once detached, the cdpSession object
* won't emit any events and can't be used to send messages.
*/
override async detach(): Promise<void> {
if (this.detached) {
throw new Error(
`Session already detached. Most likely the ${this.#targetType} has been closed.`,
);
}
await this.#connection.send('Target.detachFromTarget', {
sessionId: this.#sessionId,
});
this.#detached = true;
}
/**
* @internal
*/
onClosed(): void {
this.#callbacks.clear();
this.#detached = true;
this.emit(CDPSessionEvent.Disconnected, undefined);
}
/**
* Returns the session's id.
*/
override id(): string {
return this.#sessionId;
}
/**
* @internal
*/
getPendingProtocolErrors(): Error[] {
return this.#callbacks.getPendingProtocolErrors();
}
}

308
node_modules/puppeteer-core/src/cdp/Connection.ts generated vendored Normal file
View File

@@ -0,0 +1,308 @@
/**
* @license
* Copyright 2017 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {Protocol} from 'devtools-protocol';
import type {ProtocolMapping} from 'devtools-protocol/types/protocol-mapping.js';
import type {CommandOptions} from '../api/CDPSession.js';
import {
CDPSessionEvent,
type CDPSession,
type CDPSessionEvents,
} from '../api/CDPSession.js';
import {CallbackRegistry} from '../common/CallbackRegistry.js';
import type {ConnectionTransport} from '../common/ConnectionTransport.js';
import {debug} from '../common/Debug.js';
import {ConnectionClosedError, TargetCloseError} from '../common/Errors.js';
import {EventEmitter} from '../common/EventEmitter.js';
import {createProtocolErrorMessage} from '../util/ErrorLike.js';
import {
createIncrementalIdGenerator,
type GetIdFn,
} from '../util/incremental-id-generator.js';
import {CdpCDPSession} from './CdpSession.js';
const debugProtocolSend = debug('puppeteer:protocol:SEND ►');
const debugProtocolReceive = debug('puppeteer:protocol:RECV ◀');
/**
* @public
*/
export class Connection extends EventEmitter<CDPSessionEvents> {
#url: string;
#transport: ConnectionTransport;
#delay: number;
#timeout: number;
#sessions = new Map<string, CdpCDPSession>();
#closed = false;
#manuallyAttached = new Set<string>();
#callbacks: CallbackRegistry;
#rawErrors = false;
#idGenerator: GetIdFn;
constructor(
url: string,
transport: ConnectionTransport,
delay = 0,
timeout?: number,
rawErrors = false,
idGenerator: () => number = createIncrementalIdGenerator(),
) {
super();
this.#rawErrors = rawErrors;
this.#idGenerator = idGenerator;
this.#callbacks = new CallbackRegistry(idGenerator);
this.#url = url;
this.#delay = delay;
this.#timeout = timeout ?? 180_000;
this.#transport = transport;
this.#transport.onmessage = this.onMessage.bind(this);
this.#transport.onclose = this.#onClose.bind(this);
}
static fromSession(session: CDPSession): Connection | undefined {
return session.connection();
}
/**
* @internal
*/
get delay(): number {
return this.#delay;
}
get timeout(): number {
return this.#timeout;
}
/**
* @internal
*/
get _closed(): boolean {
return this.#closed;
}
/**
* @internal
*/
get _idGenerator(): GetIdFn {
return this.#idGenerator;
}
/**
* @internal
*/
get _sessions(): Map<string, CdpCDPSession> {
return this.#sessions;
}
/**
* @internal
*/
_session(sessionId: string): CdpCDPSession | null {
return this.#sessions.get(sessionId) || null;
}
/**
* @param sessionId - The session id
* @returns The current CDP session if it exists
*/
session(sessionId: string): CDPSession | null {
return this._session(sessionId);
}
url(): string {
return this.#url;
}
send<T extends keyof ProtocolMapping.Commands>(
method: T,
params?: ProtocolMapping.Commands[T]['paramsType'][0],
options?: CommandOptions,
): Promise<ProtocolMapping.Commands[T]['returnType']> {
// There is only ever 1 param arg passed, but the Protocol defines it as an
// array of 0 or 1 items See this comment:
// https://github.com/ChromeDevTools/devtools-protocol/pull/113#issuecomment-412603285
// which explains why the protocol defines the params this way for better
// type-inference.
// So now we check if there are any params or not and deal with them accordingly.
return this._rawSend(this.#callbacks, method, params, undefined, options);
}
/**
* @internal
*/
_rawSend<T extends keyof ProtocolMapping.Commands>(
callbacks: CallbackRegistry,
method: T,
params: ProtocolMapping.Commands[T]['paramsType'][0],
sessionId?: string,
options?: CommandOptions,
): Promise<ProtocolMapping.Commands[T]['returnType']> {
if (this.#closed) {
return Promise.reject(new ConnectionClosedError('Connection closed.'));
}
return callbacks.create(method, options?.timeout ?? this.#timeout, id => {
const stringifiedMessage = JSON.stringify({
method,
params,
id,
sessionId,
});
debugProtocolSend(stringifiedMessage);
this.#transport.send(stringifiedMessage);
}) as Promise<ProtocolMapping.Commands[T]['returnType']>;
}
/**
* @internal
*/
async closeBrowser(): Promise<void> {
await this.send('Browser.close');
}
/**
* @internal
*/
protected async onMessage(message: string): Promise<void> {
if (this.#delay) {
await new Promise(r => {
return setTimeout(r, this.#delay);
});
}
debugProtocolReceive(message);
const object = JSON.parse(message);
if (object.method === 'Target.attachedToTarget') {
const sessionId = object.params.sessionId;
const session = new CdpCDPSession(
this,
object.params.targetInfo.type,
sessionId,
object.sessionId,
this.#rawErrors,
);
this.#sessions.set(sessionId, session);
this.emit(CDPSessionEvent.SessionAttached, session);
const parentSession = this.#sessions.get(object.sessionId);
if (parentSession) {
parentSession.emit(CDPSessionEvent.SessionAttached, session);
}
} else if (object.method === 'Target.detachedFromTarget') {
const session = this.#sessions.get(object.params.sessionId);
if (session) {
session.onClosed();
this.#sessions.delete(object.params.sessionId);
this.emit(CDPSessionEvent.SessionDetached, session);
const parentSession = this.#sessions.get(object.sessionId);
if (parentSession) {
parentSession.emit(CDPSessionEvent.SessionDetached, session);
}
}
}
if (object.sessionId) {
const session = this.#sessions.get(object.sessionId);
if (session) {
session.onMessage(object);
}
} else if (object.id) {
if (object.error) {
if (this.#rawErrors) {
this.#callbacks.rejectRaw(object.id, object.error);
} else {
this.#callbacks.reject(
object.id,
createProtocolErrorMessage(object),
object.error.message,
);
}
} else {
this.#callbacks.resolve(object.id, object.result);
}
} else {
this.emit(object.method, object.params);
}
}
#onClose(): void {
if (this.#closed) {
return;
}
this.#closed = true;
this.#transport.onmessage = undefined;
this.#transport.onclose = undefined;
this.#callbacks.clear();
for (const session of this.#sessions.values()) {
session.onClosed();
}
this.#sessions.clear();
this.emit(CDPSessionEvent.Disconnected, undefined);
}
dispose(): void {
this.#onClose();
this.#transport.close();
}
/**
* @internal
*/
isAutoAttached(targetId: string): boolean {
return !this.#manuallyAttached.has(targetId);
}
/**
* @internal
*/
async _createSession(
targetInfo: {targetId: string},
isAutoAttachEmulated = true,
): Promise<CdpCDPSession> {
if (!isAutoAttachEmulated) {
this.#manuallyAttached.add(targetInfo.targetId);
}
const {sessionId} = await this.send('Target.attachToTarget', {
targetId: targetInfo.targetId,
flatten: true,
});
this.#manuallyAttached.delete(targetInfo.targetId);
const session = this.#sessions.get(sessionId);
if (!session) {
throw new Error('CDPSession creation failed.');
}
return session;
}
/**
* @param targetInfo - The target info
* @returns The CDP session that is created
*/
async createSession(
targetInfo: Protocol.Target.TargetInfo,
): Promise<CDPSession> {
return await this._createSession(targetInfo, false);
}
/**
* @internal
*/
getPendingProtocolErrors(): Error[] {
const result: Error[] = [];
result.push(...this.#callbacks.getPendingProtocolErrors());
for (const session of this.#sessions.values()) {
result.push(...session.getPendingProtocolErrors());
}
return result;
}
}
/**
* @internal
*/
export function isTargetClosedError(error: Error): boolean {
return error instanceof TargetCloseError;
}

508
node_modules/puppeteer-core/src/cdp/Coverage.ts generated vendored Normal file
View File

@@ -0,0 +1,508 @@
/**
* @license
* Copyright 2017 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {Protocol} from 'devtools-protocol';
import type {CDPSession} from '../api/CDPSession.js';
import {EventEmitter} from '../common/EventEmitter.js';
import {debugError, PuppeteerURL} from '../common/util.js';
import {assert} from '../util/assert.js';
import {DisposableStack} from '../util/disposable.js';
/**
* The CoverageEntry class represents one entry of the coverage report.
* @public
*/
export interface CoverageEntry {
/**
* The URL of the style sheet or script.
*/
url: string;
/**
* The content of the style sheet or script.
*/
text: string;
/**
* The covered range as start and end positions.
*/
ranges: Array<{start: number; end: number}>;
}
/**
* The CoverageEntry class for JavaScript
* @public
*/
export interface JSCoverageEntry extends CoverageEntry {
/**
* Raw V8 script coverage entry.
*/
rawScriptCoverage?: Protocol.Profiler.ScriptCoverage;
}
/**
* Set of configurable options for JS coverage.
* @public
*/
export interface JSCoverageOptions {
/**
* Whether to reset coverage on every navigation.
*/
resetOnNavigation?: boolean;
/**
* Whether anonymous scripts generated by the page should be reported.
*/
reportAnonymousScripts?: boolean;
/**
* Whether the result includes raw V8 script coverage entries.
*/
includeRawScriptCoverage?: boolean;
/**
* Whether to collect coverage information at the block level.
* If true, coverage will be collected at the block level (this is the default).
* If false, coverage will be collected at the function level.
*/
useBlockCoverage?: boolean;
}
/**
* Set of configurable options for CSS coverage.
* @public
*/
export interface CSSCoverageOptions {
/**
* Whether to reset coverage on every navigation.
*/
resetOnNavigation?: boolean;
}
/**
* The Coverage class provides methods to gather information about parts of
* JavaScript and CSS that were used by the page.
*
* @remarks
* To output coverage in a form consumable by {@link https://github.com/istanbuljs | Istanbul},
* see {@link https://github.com/istanbuljs/puppeteer-to-istanbul | puppeteer-to-istanbul}.
*
* @example
* An example of using JavaScript and CSS coverage to get percentage of initially
* executed code:
*
* ```ts
* // Enable both JavaScript and CSS coverage
* await Promise.all([
* page.coverage.startJSCoverage(),
* page.coverage.startCSSCoverage(),
* ]);
* // Navigate to page
* await page.goto('https://example.com');
* // Disable both JavaScript and CSS coverage
* const [jsCoverage, cssCoverage] = await Promise.all([
* page.coverage.stopJSCoverage(),
* page.coverage.stopCSSCoverage(),
* ]);
* let totalBytes = 0;
* let usedBytes = 0;
* const coverage = [...jsCoverage, ...cssCoverage];
* for (const entry of coverage) {
* totalBytes += entry.text.length;
* for (const range of entry.ranges) usedBytes += range.end - range.start - 1;
* }
* console.log(`Bytes used: ${(usedBytes / totalBytes) * 100}%`);
* ```
*
* @public
*/
export class Coverage {
#jsCoverage: JSCoverage;
#cssCoverage: CSSCoverage;
/**
* @internal
*/
constructor(client: CDPSession) {
this.#jsCoverage = new JSCoverage(client);
this.#cssCoverage = new CSSCoverage(client);
}
/**
* @internal
*/
updateClient(client: CDPSession): void {
this.#jsCoverage.updateClient(client);
this.#cssCoverage.updateClient(client);
}
/**
* @param options - Set of configurable options for coverage defaults to
* `resetOnNavigation : true, reportAnonymousScripts : false,`
* `includeRawScriptCoverage : false, useBlockCoverage : true`
* @returns Promise that resolves when coverage is started.
*
* @remarks
* Anonymous scripts are ones that don't have an associated url. These are
* scripts that are dynamically created on the page using `eval` or
* `new Function`. If `reportAnonymousScripts` is set to `true`, anonymous
* scripts URL will start with `debugger://VM` (unless a magic //# sourceURL
* comment is present, in which case that will the be URL).
*/
async startJSCoverage(options: JSCoverageOptions = {}): Promise<void> {
return await this.#jsCoverage.start(options);
}
/**
* Promise that resolves to the array of coverage reports for
* all scripts.
*
* @remarks
* JavaScript Coverage doesn't include anonymous scripts by default.
* However, scripts with sourceURLs are reported.
*/
async stopJSCoverage(): Promise<JSCoverageEntry[]> {
return await this.#jsCoverage.stop();
}
/**
* @param options - Set of configurable options for coverage, defaults to
* `resetOnNavigation : true`
* @returns Promise that resolves when coverage is started.
*/
async startCSSCoverage(options: CSSCoverageOptions = {}): Promise<void> {
return await this.#cssCoverage.start(options);
}
/**
* Promise that resolves to the array of coverage reports
* for all stylesheets.
*
* @remarks
* CSS Coverage doesn't include dynamically injected style tags
* without sourceURLs.
*/
async stopCSSCoverage(): Promise<CoverageEntry[]> {
return await this.#cssCoverage.stop();
}
}
/**
* @public
*/
export class JSCoverage {
#client: CDPSession;
#enabled = false;
#scriptURLs = new Map<string, string>();
#scriptSources = new Map<string, string>();
#subscriptions?: DisposableStack;
#resetOnNavigation = false;
#reportAnonymousScripts = false;
#includeRawScriptCoverage = false;
/**
* @internal
*/
constructor(client: CDPSession) {
this.#client = client;
}
/**
* @internal
*/
updateClient(client: CDPSession): void {
this.#client = client;
}
async start(
options: {
resetOnNavigation?: boolean;
reportAnonymousScripts?: boolean;
includeRawScriptCoverage?: boolean;
useBlockCoverage?: boolean;
} = {},
): Promise<void> {
assert(!this.#enabled, 'JSCoverage is already enabled');
const {
resetOnNavigation = true,
reportAnonymousScripts = false,
includeRawScriptCoverage = false,
useBlockCoverage = true,
} = options;
this.#resetOnNavigation = resetOnNavigation;
this.#reportAnonymousScripts = reportAnonymousScripts;
this.#includeRawScriptCoverage = includeRawScriptCoverage;
this.#enabled = true;
this.#scriptURLs.clear();
this.#scriptSources.clear();
this.#subscriptions = new DisposableStack();
const clientEmitter = this.#subscriptions.use(
new EventEmitter(this.#client),
);
clientEmitter.on('Debugger.scriptParsed', this.#onScriptParsed.bind(this));
clientEmitter.on(
'Runtime.executionContextsCleared',
this.#onExecutionContextsCleared.bind(this),
);
await Promise.all([
this.#client.send('Profiler.enable'),
this.#client.send('Profiler.startPreciseCoverage', {
callCount: this.#includeRawScriptCoverage,
detailed: useBlockCoverage,
}),
this.#client.send('Debugger.enable'),
this.#client.send('Debugger.setSkipAllPauses', {skip: true}),
]);
}
#onExecutionContextsCleared(): void {
if (!this.#resetOnNavigation) {
return;
}
this.#scriptURLs.clear();
this.#scriptSources.clear();
}
async #onScriptParsed(
event: Protocol.Debugger.ScriptParsedEvent,
): Promise<void> {
// Ignore puppeteer-injected scripts
if (PuppeteerURL.isPuppeteerURL(event.url)) {
return;
}
// Ignore other anonymous scripts unless the reportAnonymousScripts option is true.
if (!event.url && !this.#reportAnonymousScripts) {
return;
}
try {
const response = await this.#client.send('Debugger.getScriptSource', {
scriptId: event.scriptId,
});
this.#scriptURLs.set(event.scriptId, event.url);
this.#scriptSources.set(event.scriptId, response.scriptSource);
} catch (error) {
// This might happen if the page has already navigated away.
debugError(error);
}
}
async stop(): Promise<JSCoverageEntry[]> {
assert(this.#enabled, 'JSCoverage is not enabled');
this.#enabled = false;
const result = await Promise.all([
this.#client.send('Profiler.takePreciseCoverage'),
this.#client.send('Profiler.stopPreciseCoverage'),
this.#client.send('Profiler.disable'),
this.#client.send('Debugger.disable'),
]);
this.#subscriptions?.dispose();
const coverage = [];
const profileResponse = result[0];
for (const entry of profileResponse.result) {
let url = this.#scriptURLs.get(entry.scriptId);
if (!url && this.#reportAnonymousScripts) {
url = 'debugger://VM' + entry.scriptId;
}
const text = this.#scriptSources.get(entry.scriptId);
if (text === undefined || url === undefined) {
continue;
}
const flattenRanges = [];
for (const func of entry.functions) {
flattenRanges.push(...func.ranges);
}
const ranges = convertToDisjointRanges(flattenRanges);
if (!this.#includeRawScriptCoverage) {
coverage.push({url, ranges, text});
} else {
coverage.push({url, ranges, text, rawScriptCoverage: entry});
}
}
return coverage;
}
}
/**
* @public
*/
export class CSSCoverage {
#client: CDPSession;
#enabled = false;
#stylesheetURLs = new Map<string, string>();
#stylesheetSources = new Map<string, string>();
#eventListeners?: DisposableStack;
#resetOnNavigation = false;
constructor(client: CDPSession) {
this.#client = client;
}
/**
* @internal
*/
updateClient(client: CDPSession): void {
this.#client = client;
}
async start(options: {resetOnNavigation?: boolean} = {}): Promise<void> {
assert(!this.#enabled, 'CSSCoverage is already enabled');
const {resetOnNavigation = true} = options;
this.#resetOnNavigation = resetOnNavigation;
this.#enabled = true;
this.#stylesheetURLs.clear();
this.#stylesheetSources.clear();
this.#eventListeners = new DisposableStack();
const clientEmitter = this.#eventListeners.use(
new EventEmitter(this.#client),
);
clientEmitter.on('CSS.styleSheetAdded', this.#onStyleSheet.bind(this));
clientEmitter.on(
'Runtime.executionContextsCleared',
this.#onExecutionContextsCleared.bind(this),
);
await Promise.all([
this.#client.send('DOM.enable'),
this.#client.send('CSS.enable'),
this.#client.send('CSS.startRuleUsageTracking'),
]);
}
#onExecutionContextsCleared(): void {
if (!this.#resetOnNavigation) {
return;
}
this.#stylesheetURLs.clear();
this.#stylesheetSources.clear();
}
async #onStyleSheet(event: Protocol.CSS.StyleSheetAddedEvent): Promise<void> {
const header = event.header;
// Ignore anonymous scripts
if (!header.sourceURL) {
return;
}
try {
const response = await this.#client.send('CSS.getStyleSheetText', {
styleSheetId: header.styleSheetId,
});
this.#stylesheetURLs.set(header.styleSheetId, header.sourceURL);
this.#stylesheetSources.set(header.styleSheetId, response.text);
} catch (error) {
// This might happen if the page has already navigated away.
debugError(error);
}
}
async stop(): Promise<CoverageEntry[]> {
assert(this.#enabled, 'CSSCoverage is not enabled');
this.#enabled = false;
const ruleTrackingResponse = await this.#client.send(
'CSS.stopRuleUsageTracking',
);
await Promise.all([
this.#client.send('CSS.disable'),
this.#client.send('DOM.disable'),
]);
this.#eventListeners?.dispose();
// aggregate by styleSheetId
const styleSheetIdToCoverage = new Map();
for (const entry of ruleTrackingResponse.ruleUsage) {
let ranges = styleSheetIdToCoverage.get(entry.styleSheetId);
if (!ranges) {
ranges = [];
styleSheetIdToCoverage.set(entry.styleSheetId, ranges);
}
ranges.push({
startOffset: entry.startOffset,
endOffset: entry.endOffset,
count: entry.used ? 1 : 0,
});
}
const coverage: CoverageEntry[] = [];
for (const styleSheetId of this.#stylesheetURLs.keys()) {
const url = this.#stylesheetURLs.get(styleSheetId);
assert(
typeof url !== 'undefined',
`Stylesheet URL is undefined (styleSheetId=${styleSheetId})`,
);
const text = this.#stylesheetSources.get(styleSheetId);
assert(
typeof text !== 'undefined',
`Stylesheet text is undefined (styleSheetId=${styleSheetId})`,
);
const ranges = convertToDisjointRanges(
styleSheetIdToCoverage.get(styleSheetId) || [],
);
coverage.push({url, ranges, text});
}
return coverage;
}
}
function convertToDisjointRanges(
nestedRanges: Array<{startOffset: number; endOffset: number; count: number}>,
): Array<{start: number; end: number}> {
const points = [];
for (const range of nestedRanges) {
points.push({offset: range.startOffset, type: 0, range});
points.push({offset: range.endOffset, type: 1, range});
}
// Sort points to form a valid parenthesis sequence.
points.sort((a, b) => {
// Sort with increasing offsets.
if (a.offset !== b.offset) {
return a.offset - b.offset;
}
// All "end" points should go before "start" points.
if (a.type !== b.type) {
return b.type - a.type;
}
const aLength = a.range.endOffset - a.range.startOffset;
const bLength = b.range.endOffset - b.range.startOffset;
// For two "start" points, the one with longer range goes first.
if (a.type === 0) {
return bLength - aLength;
}
// For two "end" points, the one with shorter range goes first.
return aLength - bLength;
});
const hitCountStack = [];
const results: Array<{
start: number;
end: number;
}> = [];
let lastOffset = 0;
// Run scanning line to intersect all ranges.
for (const point of points) {
if (
hitCountStack.length &&
lastOffset < point.offset &&
hitCountStack[hitCountStack.length - 1]! > 0
) {
const lastResult = results[results.length - 1];
if (lastResult && lastResult.end === lastOffset) {
lastResult.end = point.offset;
} else {
results.push({start: lastOffset, end: point.offset});
}
}
lastOffset = point.offset;
if (point.type === 0) {
hitCountStack.push(point.range.count);
} else {
hitCountStack.pop();
}
}
// Filter out empty ranges.
return results.filter(range => {
return range.end - range.start > 0;
});
}

View File

@@ -0,0 +1,230 @@
/**
* @license
* Copyright 2022 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type Protocol from 'devtools-protocol';
import type {CDPSession} from '../api/CDPSession.js';
import {DeviceRequestPrompt} from '../api/DeviceRequestPrompt.js';
import type {DeviceRequestPromptDevice} from '../api/DeviceRequestPrompt.js';
import type {WaitTimeoutOptions} from '../api/Page.js';
import type {TimeoutSettings} from '../common/TimeoutSettings.js';
import {assert} from '../util/assert.js';
import {Deferred} from '../util/Deferred.js';
/**
* @internal
*/
export class CdpDeviceRequestPrompt extends DeviceRequestPrompt {
#client: CDPSession | null;
#timeoutSettings: TimeoutSettings;
#id: string;
#handled = false;
#updateDevicesHandle = this.#updateDevices.bind(this);
#waitForDevicePromises = new Set<{
filter: (device: DeviceRequestPromptDevice) => boolean;
promise: Deferred<DeviceRequestPromptDevice>;
}>();
constructor(
client: CDPSession,
timeoutSettings: TimeoutSettings,
firstEvent: Protocol.DeviceAccess.DeviceRequestPromptedEvent,
) {
super();
this.#client = client;
this.#timeoutSettings = timeoutSettings;
this.#id = firstEvent.id;
this.#client.on(
'DeviceAccess.deviceRequestPrompted',
this.#updateDevicesHandle,
);
this.#client.on('Target.detachedFromTarget', () => {
this.#client = null;
});
this.#updateDevices(firstEvent);
}
#updateDevices(event: Protocol.DeviceAccess.DeviceRequestPromptedEvent) {
if (event.id !== this.#id) {
return;
}
for (const rawDevice of event.devices) {
if (
this.devices.some(device => {
return device.id === rawDevice.id;
})
) {
continue;
}
const newDevice = {id: rawDevice.id, name: rawDevice.name};
this.devices.push(newDevice);
for (const waitForDevicePromise of this.#waitForDevicePromises) {
if (waitForDevicePromise.filter(newDevice)) {
waitForDevicePromise.promise.resolve(newDevice);
}
}
}
}
async waitForDevice(
filter: (device: DeviceRequestPromptDevice) => boolean,
options: WaitTimeoutOptions = {},
): Promise<DeviceRequestPromptDevice> {
for (const device of this.devices) {
if (filter(device)) {
return device;
}
}
const {timeout = this.#timeoutSettings.timeout()} = options;
const deferred = Deferred.create<DeviceRequestPromptDevice>({
message: `Waiting for \`DeviceRequestPromptDevice\` failed: ${timeout}ms exceeded`,
timeout,
});
if (options.signal) {
options.signal.addEventListener(
'abort',
() => {
deferred.reject(options.signal?.reason);
},
{once: true},
);
}
const handle = {filter, promise: deferred};
this.#waitForDevicePromises.add(handle);
try {
return await deferred.valueOrThrow();
} finally {
this.#waitForDevicePromises.delete(handle);
}
}
async select(device: DeviceRequestPromptDevice): Promise<void> {
assert(
this.#client !== null,
'Cannot select device through detached session!',
);
assert(this.devices.includes(device), 'Cannot select unknown device!');
assert(
!this.#handled,
'Cannot select DeviceRequestPrompt which is already handled!',
);
this.#client.off(
'DeviceAccess.deviceRequestPrompted',
this.#updateDevicesHandle,
);
this.#handled = true;
return await this.#client.send('DeviceAccess.selectPrompt', {
id: this.#id,
deviceId: device.id,
});
}
async cancel(): Promise<void> {
assert(
this.#client !== null,
'Cannot cancel prompt through detached session!',
);
assert(
!this.#handled,
'Cannot cancel DeviceRequestPrompt which is already handled!',
);
this.#client.off(
'DeviceAccess.deviceRequestPrompted',
this.#updateDevicesHandle,
);
this.#handled = true;
return await this.#client.send('DeviceAccess.cancelPrompt', {id: this.#id});
}
}
/**
* @internal
*/
export class CdpDeviceRequestPromptManager {
#client: CDPSession | null;
#timeoutSettings: TimeoutSettings;
#deviceRequestPromptDeferreds = new Set<Deferred<DeviceRequestPrompt>>();
constructor(client: CDPSession, timeoutSettings: TimeoutSettings) {
this.#client = client;
this.#timeoutSettings = timeoutSettings;
this.#client.on('DeviceAccess.deviceRequestPrompted', event => {
this.#onDeviceRequestPrompted(event);
});
this.#client.on('Target.detachedFromTarget', () => {
this.#client = null;
});
}
async waitForDevicePrompt(
options: WaitTimeoutOptions = {},
): Promise<DeviceRequestPrompt> {
assert(
this.#client !== null,
'Cannot wait for device prompt through detached session!',
);
const needsEnable = this.#deviceRequestPromptDeferreds.size === 0;
let enablePromise: Promise<void> | undefined;
if (needsEnable) {
enablePromise = this.#client.send('DeviceAccess.enable');
}
const {timeout = this.#timeoutSettings.timeout()} = options;
const deferred = Deferred.create<DeviceRequestPrompt>({
message: `Waiting for \`DeviceRequestPrompt\` failed: ${timeout}ms exceeded`,
timeout,
});
if (options.signal) {
options.signal.addEventListener(
'abort',
() => {
deferred.reject(options.signal?.reason);
},
{once: true},
);
}
this.#deviceRequestPromptDeferreds.add(deferred);
try {
const [result] = await Promise.all([
deferred.valueOrThrow(),
enablePromise,
]);
return result;
} finally {
this.#deviceRequestPromptDeferreds.delete(deferred);
}
}
#onDeviceRequestPrompted(
event: Protocol.DeviceAccess.DeviceRequestPromptedEvent,
) {
if (!this.#deviceRequestPromptDeferreds.size) {
return;
}
assert(this.#client !== null);
const devicePrompt = new CdpDeviceRequestPrompt(
this.#client,
this.#timeoutSettings,
event,
);
for (const promise of this.#deviceRequestPromptDeferreds) {
promise.resolve(devicePrompt);
}
this.#deviceRequestPromptDeferreds.clear();
}
}

37
node_modules/puppeteer-core/src/cdp/Dialog.ts generated vendored Normal file
View File

@@ -0,0 +1,37 @@
/**
* @license
* Copyright 2017 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {Protocol} from 'devtools-protocol';
import type {CDPSession} from '../api/CDPSession.js';
import {Dialog} from '../api/Dialog.js';
/**
* @internal
*/
export class CdpDialog extends Dialog {
#client: CDPSession;
constructor(
client: CDPSession,
type: Protocol.Page.DialogType,
message: string,
defaultValue = '',
) {
super(type, message, defaultValue);
this.#client = client;
}
override async handle(options: {
accept: boolean;
text?: string;
}): Promise<void> {
await this.#client.send('Page.handleJavaScriptDialog', {
accept: options.accept,
promptText: options.text,
});
}
}

217
node_modules/puppeteer-core/src/cdp/ElementHandle.ts generated vendored Normal file
View File

@@ -0,0 +1,217 @@
/**
* @license
* Copyright 2019 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {Protocol} from 'devtools-protocol';
import type {CDPSession} from '../api/CDPSession.js';
import {
bindIsolatedHandle,
ElementHandle,
type AutofillData,
} from '../api/ElementHandle.js';
import type {AwaitableIterable} from '../common/types.js';
import {debugError} from '../common/util.js';
import {environment} from '../environment.js';
import {assert} from '../util/assert.js';
import {AsyncIterableUtil} from '../util/AsyncIterableUtil.js';
import {throwIfDisposed} from '../util/decorators.js';
import type {CdpFrame} from './Frame.js';
import type {FrameManager} from './FrameManager.js';
import type {IsolatedWorld} from './IsolatedWorld.js';
import {CdpJSHandle} from './JSHandle.js';
const NON_ELEMENT_NODE_ROLES = new Set(['StaticText', 'InlineTextBox']);
/**
* The CdpElementHandle extends ElementHandle now to keep compatibility
* with `instanceof` because of that we need to have methods for
* CdpJSHandle to in this implementation as well.
*
* @internal
*/
export class CdpElementHandle<
ElementType extends Node = Element,
> extends ElementHandle<ElementType> {
declare protected readonly handle: CdpJSHandle<ElementType>;
#backendNodeId?: number;
constructor(
world: IsolatedWorld,
remoteObject: Protocol.Runtime.RemoteObject,
) {
super(new CdpJSHandle(world, remoteObject));
}
override get realm(): IsolatedWorld {
return this.handle.realm;
}
get client(): CDPSession {
return this.handle.client;
}
override remoteObject(): Protocol.Runtime.RemoteObject {
return this.handle.remoteObject();
}
get #frameManager(): FrameManager {
return this.frame._frameManager;
}
override get frame(): CdpFrame {
return this.realm.environment as CdpFrame;
}
override async contentFrame(
this: ElementHandle<HTMLIFrameElement>,
): Promise<CdpFrame>;
@throwIfDisposed()
override async contentFrame(): Promise<CdpFrame | null> {
const nodeInfo = await this.client.send('DOM.describeNode', {
objectId: this.id,
});
if (typeof nodeInfo.node.frameId !== 'string') {
return null;
}
return this.#frameManager.frame(nodeInfo.node.frameId);
}
@throwIfDisposed()
@bindIsolatedHandle
override async scrollIntoView(
this: CdpElementHandle<Element>,
): Promise<void> {
await this.assertConnectedElement();
try {
await this.client.send('DOM.scrollIntoViewIfNeeded', {
objectId: this.id,
});
} catch (error) {
debugError(error);
// Fallback to Element.scrollIntoView if DOM.scrollIntoViewIfNeeded is not supported
await super.scrollIntoView();
}
}
@throwIfDisposed()
@bindIsolatedHandle
override async uploadFile(
this: CdpElementHandle<HTMLInputElement>,
...files: string[]
): Promise<void> {
const isMultiple = await this.evaluate(element => {
return element.multiple;
});
assert(
files.length <= 1 || isMultiple,
'Multiple file uploads only work with <input type=file multiple>',
);
// Locate all files and confirm that they exist.
const path = environment.value.path;
if (path) {
files = files.map(filePath => {
if (
path.win32.isAbsolute(filePath) ||
path.posix.isAbsolute(filePath)
) {
return filePath;
} else {
return path.resolve(filePath);
}
});
}
/**
* The zero-length array is a special case, it seems that
* DOM.setFileInputFiles does not actually update the files in that case, so
* the solution is to eval the element value to a new FileList directly.
*/
if (files.length === 0) {
// XXX: These events should converted to trusted events. Perhaps do this
// in `DOM.setFileInputFiles`?
await this.evaluate(element => {
element.files = new DataTransfer().files;
// Dispatch events for this case because it should behave akin to a user action.
element.dispatchEvent(
new Event('input', {bubbles: true, composed: true}),
);
element.dispatchEvent(new Event('change', {bubbles: true}));
});
return;
}
const {
node: {backendNodeId},
} = await this.client.send('DOM.describeNode', {
objectId: this.id,
});
await this.client.send('DOM.setFileInputFiles', {
objectId: this.id,
files,
backendNodeId,
});
}
@throwIfDisposed()
override async autofill(data: AutofillData): Promise<void> {
const nodeInfo = await this.client.send('DOM.describeNode', {
objectId: this.handle.id,
});
const fieldId = nodeInfo.node.backendNodeId;
const frameId = this.frame._id;
await this.client.send('Autofill.trigger', {
fieldId,
frameId,
card: data.creditCard,
address: data.address,
});
}
override async *queryAXTree(
name?: string | undefined,
role?: string | undefined,
): AwaitableIterable<ElementHandle<Node>> {
const {nodes} = await this.client.send('Accessibility.queryAXTree', {
objectId: this.id,
accessibleName: name,
role,
});
const results = nodes.filter(node => {
if (node.ignored) {
return false;
}
if (!node.role) {
return false;
}
if (NON_ELEMENT_NODE_ROLES.has(node.role.value)) {
return false;
}
return true;
});
return yield* AsyncIterableUtil.map(results, node => {
return this.realm.adoptBackendNode(node.backendDOMNodeId) as Promise<
ElementHandle<Node>
>;
});
}
override async backendNodeId(): Promise<number> {
if (this.#backendNodeId) {
return this.#backendNodeId;
}
const {node} = await this.client.send('DOM.describeNode', {
objectId: this.handle.id,
});
this.#backendNodeId = node.backendNodeId;
return this.#backendNodeId;
}
}

611
node_modules/puppeteer-core/src/cdp/EmulationManager.ts generated vendored Normal file
View File

@@ -0,0 +1,611 @@
/**
* @license
* Copyright 2017 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {Protocol} from 'devtools-protocol';
import {type CDPSession, CDPSessionEvent} from '../api/CDPSession.js';
import type {GeolocationOptions, MediaFeature} from '../api/Page.js';
import {debugError} from '../common/util.js';
import type {Viewport} from '../common/Viewport.js';
import {assert} from '../util/assert.js';
import {invokeAtMostOnceForArguments} from '../util/decorators.js';
import {isErrorLike} from '../util/ErrorLike.js';
interface ViewportState {
viewport?: Viewport;
active: boolean;
}
interface IdleOverridesState {
overrides?: {
isUserActive: boolean;
isScreenUnlocked: boolean;
};
active: boolean;
}
interface TimezoneState {
timezoneId?: string;
active: boolean;
}
interface VisionDeficiencyState {
visionDeficiency?: Protocol.Emulation.SetEmulatedVisionDeficiencyRequest['type'];
active: boolean;
}
interface CpuThrottlingState {
factor?: number;
active: boolean;
}
interface MediaFeaturesState {
mediaFeatures?: MediaFeature[];
active: boolean;
}
interface MediaTypeState {
type?: string;
active: boolean;
}
interface GeoLocationState {
geoLocation?: GeolocationOptions;
active: boolean;
}
interface DefaultBackgroundColorState {
color?: Protocol.DOM.RGBA;
active: boolean;
}
interface JavascriptEnabledState {
javaScriptEnabled: boolean;
active: boolean;
}
interface FocusState {
enabled: boolean;
active: boolean;
}
/**
* @internal
*/
export interface ClientProvider {
clients(): CDPSession[];
registerState(state: EmulatedState<any>): void;
}
/**
* @internal
*/
export class EmulatedState<T extends {active: boolean}> {
#state: T;
#clientProvider: ClientProvider;
#updater: (client: CDPSession, state: T) => Promise<void>;
constructor(
initialState: T,
clientProvider: ClientProvider,
updater: (client: CDPSession, state: T) => Promise<void>,
) {
this.#state = initialState;
this.#clientProvider = clientProvider;
this.#updater = updater;
this.#clientProvider.registerState(this);
}
async setState(state: T): Promise<void> {
this.#state = state;
await this.sync();
}
get state(): T {
return this.#state;
}
async sync(): Promise<void> {
await Promise.all(
this.#clientProvider.clients().map(client => {
return this.#updater(client, this.#state);
}),
);
}
}
/**
* @internal
*/
export class EmulationManager implements ClientProvider {
#client: CDPSession;
#emulatingMobile = false;
#hasTouch = false;
#states: Array<EmulatedState<any>> = [];
#viewportState = new EmulatedState<ViewportState>(
{
active: false,
},
this,
this.#applyViewport,
);
#idleOverridesState = new EmulatedState<IdleOverridesState>(
{
active: false,
},
this,
this.#emulateIdleState,
);
#timezoneState = new EmulatedState<TimezoneState>(
{
active: false,
},
this,
this.#emulateTimezone,
);
#visionDeficiencyState = new EmulatedState<VisionDeficiencyState>(
{
active: false,
},
this,
this.#emulateVisionDeficiency,
);
#cpuThrottlingState = new EmulatedState<CpuThrottlingState>(
{
active: false,
},
this,
this.#emulateCpuThrottling,
);
#mediaFeaturesState = new EmulatedState<MediaFeaturesState>(
{
active: false,
},
this,
this.#emulateMediaFeatures,
);
#mediaTypeState = new EmulatedState<MediaTypeState>(
{
active: false,
},
this,
this.#emulateMediaType,
);
#geoLocationState = new EmulatedState<GeoLocationState>(
{
active: false,
},
this,
this.#setGeolocation,
);
#defaultBackgroundColorState = new EmulatedState<DefaultBackgroundColorState>(
{
active: false,
},
this,
this.#setDefaultBackgroundColor,
);
#javascriptEnabledState = new EmulatedState<JavascriptEnabledState>(
{
javaScriptEnabled: true,
active: false,
},
this,
this.#setJavaScriptEnabled,
);
#focusState = new EmulatedState<FocusState>(
{
enabled: true,
active: false,
},
this,
this.#emulateFocus,
);
#secondaryClients = new Set<CDPSession>();
constructor(client: CDPSession) {
this.#client = client;
}
updateClient(client: CDPSession): void {
this.#client = client;
this.#secondaryClients.delete(client);
}
registerState(state: EmulatedState<any>): void {
this.#states.push(state);
}
clients(): CDPSession[] {
return [this.#client, ...Array.from(this.#secondaryClients)];
}
async registerSpeculativeSession(client: CDPSession): Promise<void> {
this.#secondaryClients.add(client);
client.once(CDPSessionEvent.Disconnected, () => {
this.#secondaryClients.delete(client);
});
// We don't await here because we want to register all state changes before
// the target is unpaused.
void Promise.all(
this.#states.map(s => {
return s.sync().catch(debugError);
}),
);
}
get javascriptEnabled(): boolean {
return this.#javascriptEnabledState.state.javaScriptEnabled;
}
async emulateViewport(viewport: Viewport | null): Promise<boolean> {
const currentState = this.#viewportState.state;
if (!viewport && !currentState.active) {
return false;
}
await this.#viewportState.setState(
viewport
? {
viewport,
active: true,
}
: {
active: false,
},
);
const mobile = viewport?.isMobile || false;
const hasTouch = viewport?.hasTouch || false;
const reloadNeeded =
this.#emulatingMobile !== mobile || this.#hasTouch !== hasTouch;
this.#emulatingMobile = mobile;
this.#hasTouch = hasTouch;
return reloadNeeded;
}
@invokeAtMostOnceForArguments
async #applyViewport(
client: CDPSession,
viewportState: ViewportState,
): Promise<void> {
if (!viewportState.viewport) {
await Promise.all([
client.send('Emulation.clearDeviceMetricsOverride'),
client.send('Emulation.setTouchEmulationEnabled', {
enabled: false,
}),
]).catch(debugError);
return;
}
const {viewport} = viewportState;
const mobile = viewport.isMobile || false;
const width = viewport.width;
const height = viewport.height;
const deviceScaleFactor = viewport.deviceScaleFactor ?? 1;
const screenOrientation: Protocol.Emulation.ScreenOrientation =
viewport.isLandscape
? {angle: 90, type: 'landscapePrimary'}
: {angle: 0, type: 'portraitPrimary'};
const hasTouch = viewport.hasTouch || false;
await Promise.all([
client
.send('Emulation.setDeviceMetricsOverride', {
mobile,
width,
height,
deviceScaleFactor,
screenOrientation,
})
.catch(err => {
if (
err.message.includes('Target does not support metrics override')
) {
debugError(err);
return;
}
throw err;
}),
client.send('Emulation.setTouchEmulationEnabled', {
enabled: hasTouch,
}),
]);
}
async emulateIdleState(overrides?: {
isUserActive: boolean;
isScreenUnlocked: boolean;
}): Promise<void> {
await this.#idleOverridesState.setState({
active: true,
overrides,
});
}
@invokeAtMostOnceForArguments
async #emulateIdleState(
client: CDPSession,
idleStateState: IdleOverridesState,
): Promise<void> {
if (!idleStateState.active) {
return;
}
if (idleStateState.overrides) {
await client.send('Emulation.setIdleOverride', {
isUserActive: idleStateState.overrides.isUserActive,
isScreenUnlocked: idleStateState.overrides.isScreenUnlocked,
});
} else {
await client.send('Emulation.clearIdleOverride');
}
}
@invokeAtMostOnceForArguments
async #emulateTimezone(
client: CDPSession,
timezoneState: TimezoneState,
): Promise<void> {
if (!timezoneState.active) {
return;
}
try {
await client.send('Emulation.setTimezoneOverride', {
timezoneId: timezoneState.timezoneId || '',
});
} catch (error) {
if (isErrorLike(error) && error.message.includes('Invalid timezone')) {
throw new Error(`Invalid timezone ID: ${timezoneState.timezoneId}`);
}
throw error;
}
}
async emulateTimezone(timezoneId?: string): Promise<void> {
await this.#timezoneState.setState({
timezoneId,
active: true,
});
}
@invokeAtMostOnceForArguments
async #emulateVisionDeficiency(
client: CDPSession,
visionDeficiency: VisionDeficiencyState,
): Promise<void> {
if (!visionDeficiency.active) {
return;
}
await client.send('Emulation.setEmulatedVisionDeficiency', {
type: visionDeficiency.visionDeficiency || 'none',
});
}
async emulateVisionDeficiency(
type?: Protocol.Emulation.SetEmulatedVisionDeficiencyRequest['type'],
): Promise<void> {
const visionDeficiencies = new Set<
Protocol.Emulation.SetEmulatedVisionDeficiencyRequest['type']
>([
'none',
'achromatopsia',
'blurredVision',
'deuteranopia',
'protanopia',
'reducedContrast',
'tritanopia',
]);
assert(
!type || visionDeficiencies.has(type),
`Unsupported vision deficiency: ${type}`,
);
await this.#visionDeficiencyState.setState({
active: true,
visionDeficiency: type,
});
}
@invokeAtMostOnceForArguments
async #emulateCpuThrottling(
client: CDPSession,
state: CpuThrottlingState,
): Promise<void> {
if (!state.active) {
return;
}
await client.send('Emulation.setCPUThrottlingRate', {
rate: state.factor ?? 1,
});
}
async emulateCPUThrottling(factor: number | null): Promise<void> {
assert(
factor === null || factor >= 1,
'Throttling rate should be greater or equal to 1',
);
await this.#cpuThrottlingState.setState({
active: true,
factor: factor ?? undefined,
});
}
@invokeAtMostOnceForArguments
async #emulateMediaFeatures(
client: CDPSession,
state: MediaFeaturesState,
): Promise<void> {
if (!state.active) {
return;
}
await client.send('Emulation.setEmulatedMedia', {
features: state.mediaFeatures,
});
}
async emulateMediaFeatures(features?: MediaFeature[]): Promise<void> {
if (Array.isArray(features)) {
for (const mediaFeature of features) {
const name = mediaFeature.name;
assert(
/^(?:prefers-(?:color-scheme|reduced-motion)|color-gamut)$/.test(
name,
),
'Unsupported media feature: ' + name,
);
}
}
await this.#mediaFeaturesState.setState({
active: true,
mediaFeatures: features,
});
}
@invokeAtMostOnceForArguments
async #emulateMediaType(
client: CDPSession,
state: MediaTypeState,
): Promise<void> {
if (!state.active) {
return;
}
await client.send('Emulation.setEmulatedMedia', {
media: state.type || '',
});
}
async emulateMediaType(type?: string): Promise<void> {
assert(
type === 'screen' ||
type === 'print' ||
(type ?? undefined) === undefined,
'Unsupported media type: ' + type,
);
await this.#mediaTypeState.setState({
type,
active: true,
});
}
@invokeAtMostOnceForArguments
async #setGeolocation(
client: CDPSession,
state: GeoLocationState,
): Promise<void> {
if (!state.active) {
return;
}
await client.send(
'Emulation.setGeolocationOverride',
state.geoLocation
? {
longitude: state.geoLocation.longitude,
latitude: state.geoLocation.latitude,
accuracy: state.geoLocation.accuracy,
}
: undefined,
);
}
async setGeolocation(options: GeolocationOptions): Promise<void> {
const {longitude, latitude, accuracy = 0} = options;
if (longitude < -180 || longitude > 180) {
throw new Error(
`Invalid longitude "${longitude}": precondition -180 <= LONGITUDE <= 180 failed.`,
);
}
if (latitude < -90 || latitude > 90) {
throw new Error(
`Invalid latitude "${latitude}": precondition -90 <= LATITUDE <= 90 failed.`,
);
}
if (accuracy < 0) {
throw new Error(
`Invalid accuracy "${accuracy}": precondition 0 <= ACCURACY failed.`,
);
}
await this.#geoLocationState.setState({
active: true,
geoLocation: {
longitude,
latitude,
accuracy,
},
});
}
@invokeAtMostOnceForArguments
async #setDefaultBackgroundColor(
client: CDPSession,
state: DefaultBackgroundColorState,
): Promise<void> {
if (!state.active) {
return;
}
await client.send('Emulation.setDefaultBackgroundColorOverride', {
color: state.color,
});
}
/**
* Resets default white background
*/
async resetDefaultBackgroundColor(): Promise<void> {
await this.#defaultBackgroundColorState.setState({
active: true,
color: undefined,
});
}
/**
* Hides default white background
*/
async setTransparentBackgroundColor(): Promise<void> {
await this.#defaultBackgroundColorState.setState({
active: true,
color: {r: 0, g: 0, b: 0, a: 0},
});
}
@invokeAtMostOnceForArguments
async #setJavaScriptEnabled(
client: CDPSession,
state: JavascriptEnabledState,
): Promise<void> {
if (!state.active) {
return;
}
await client.send('Emulation.setScriptExecutionDisabled', {
value: !state.javaScriptEnabled,
});
}
async setJavaScriptEnabled(enabled: boolean): Promise<void> {
await this.#javascriptEnabledState.setState({
active: true,
javaScriptEnabled: enabled,
});
}
@invokeAtMostOnceForArguments
async #emulateFocus(client: CDPSession, state: FocusState): Promise<void> {
if (!state.active) {
return;
}
await client.send('Emulation.setFocusEmulationEnabled', {
enabled: state.enabled,
});
}
async emulateFocus(enabled: boolean): Promise<void> {
await this.#focusState.setState({
active: true,
enabled,
});
}
}

549
node_modules/puppeteer-core/src/cdp/ExecutionContext.ts generated vendored Normal file
View File

@@ -0,0 +1,549 @@
/**
* @license
* Copyright 2017 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {Protocol} from 'devtools-protocol';
import {CDPSessionEvent, type CDPSession} from '../api/CDPSession.js';
import type {ElementHandle} from '../api/ElementHandle.js';
import type {JSHandle} from '../api/JSHandle.js';
import {ARIAQueryHandler} from '../common/AriaQueryHandler.js';
import {EventEmitter} from '../common/EventEmitter.js';
import {LazyArg} from '../common/LazyArg.js';
import {scriptInjector} from '../common/ScriptInjector.js';
import type {BindingPayload, EvaluateFunc, HandleFor} from '../common/types.js';
import {
PuppeteerURL,
SOURCE_URL_REGEX,
debugError,
getSourcePuppeteerURLIfAvailable,
getSourceUrlComment,
isString,
} from '../common/util.js';
import type {PuppeteerInjectedUtil} from '../injected/injected.js';
import {AsyncIterableUtil} from '../util/AsyncIterableUtil.js';
import {DisposableStack, disposeSymbol} from '../util/disposable.js';
import {stringifyFunction} from '../util/Function.js';
import {Mutex} from '../util/Mutex.js';
import {Binding} from './Binding.js';
import {CdpElementHandle} from './ElementHandle.js';
import type {IsolatedWorld} from './IsolatedWorld.js';
import {CdpJSHandle} from './JSHandle.js';
import {
addPageBinding,
CDP_BINDING_PREFIX,
createEvaluationError,
valueFromPrimitiveRemoteObject,
} from './utils.js';
const ariaQuerySelectorBinding = new Binding(
'__ariaQuerySelector',
ARIAQueryHandler.queryOne as (...args: unknown[]) => unknown,
'', // custom init
);
const ariaQuerySelectorAllBinding = new Binding(
'__ariaQuerySelectorAll',
(async (
element: ElementHandle<Node>,
selector: string,
): Promise<JSHandle<Node[]>> => {
const results = ARIAQueryHandler.queryAll(element, selector);
return await element.realm.evaluateHandle(
(...elements) => {
return elements;
},
...(await AsyncIterableUtil.collect(results)),
);
}) as (...args: unknown[]) => unknown,
'', // custom init
);
/**
* @internal
*/
export class ExecutionContext
extends EventEmitter<{
/** Emitted when this execution context is disposed. */
disposed: undefined;
consoleapicalled: Protocol.Runtime.ConsoleAPICalledEvent;
/** Emitted when a binding that is not installed by the ExecutionContext is called. */
bindingcalled: Protocol.Runtime.BindingCalledEvent;
}>
implements Disposable
{
#client: CDPSession;
#world: IsolatedWorld;
#id: number;
#name?: string;
readonly #disposables = new DisposableStack();
constructor(
client: CDPSession,
contextPayload: Protocol.Runtime.ExecutionContextDescription,
world: IsolatedWorld,
) {
super();
this.#client = client;
this.#world = world;
this.#id = contextPayload.id;
if (contextPayload.name) {
this.#name = contextPayload.name;
}
const clientEmitter = this.#disposables.use(new EventEmitter(this.#client));
clientEmitter.on('Runtime.bindingCalled', this.#onBindingCalled.bind(this));
clientEmitter.on('Runtime.executionContextDestroyed', async event => {
if (event.executionContextId === this.#id) {
this[disposeSymbol]();
}
});
clientEmitter.on('Runtime.executionContextsCleared', async () => {
this[disposeSymbol]();
});
clientEmitter.on('Runtime.consoleAPICalled', this.#onConsoleAPI.bind(this));
clientEmitter.on(CDPSessionEvent.Disconnected, () => {
this[disposeSymbol]();
});
}
// Contains mapping from functions that should be bound to Puppeteer functions.
#bindings = new Map<string, Binding>();
// If multiple waitFor are set up asynchronously, we need to wait for the
// first one to set up the binding in the page before running the others.
#mutex = new Mutex();
async #addBinding(binding: Binding): Promise<void> {
if (this.#bindings.has(binding.name)) {
return;
}
using _ = await this.#mutex.acquire();
try {
await this.#client.send(
'Runtime.addBinding',
this.#name
? {
name: CDP_BINDING_PREFIX + binding.name,
executionContextName: this.#name,
}
: {
name: CDP_BINDING_PREFIX + binding.name,
executionContextId: this.#id,
},
);
await this.evaluate(
addPageBinding,
'internal',
binding.name,
CDP_BINDING_PREFIX,
);
this.#bindings.set(binding.name, binding);
} catch (error) {
// We could have tried to evaluate in a context which was already
// destroyed. This happens, for example, if the page is navigated while
// we are trying to add the binding
if (error instanceof Error) {
// Destroyed context.
if (error.message.includes('Execution context was destroyed')) {
return;
}
// Missing context.
if (error.message.includes('Cannot find context with specified id')) {
return;
}
}
debugError(error);
}
}
async #onBindingCalled(
event: Protocol.Runtime.BindingCalledEvent,
): Promise<void> {
if (event.executionContextId !== this.#id) {
return;
}
let payload: BindingPayload;
try {
payload = JSON.parse(event.payload);
} catch {
// The binding was either called by something in the page or it was
// called before our wrapper was initialized.
return;
}
const {type, name, seq, args, isTrivial} = payload;
if (type !== 'internal') {
this.emit('bindingcalled', event);
return;
}
if (!this.#bindings.has(name)) {
this.emit('bindingcalled', event);
return;
}
try {
const binding = this.#bindings.get(name);
await binding?.run(this, seq, args, isTrivial);
} catch (err) {
debugError(err);
}
}
get id(): number {
return this.#id;
}
#onConsoleAPI(event: Protocol.Runtime.ConsoleAPICalledEvent): void {
if (event.executionContextId !== this.#id) {
return;
}
this.emit('consoleapicalled', event);
}
#bindingsInstalled = false;
#puppeteerUtil?: Promise<JSHandle<PuppeteerInjectedUtil>>;
get puppeteerUtil(): Promise<JSHandle<PuppeteerInjectedUtil>> {
let promise = Promise.resolve() as Promise<unknown>;
if (!this.#bindingsInstalled) {
promise = Promise.all([
this.#addBindingWithoutThrowing(ariaQuerySelectorBinding),
this.#addBindingWithoutThrowing(ariaQuerySelectorAllBinding),
]);
this.#bindingsInstalled = true;
}
scriptInjector.inject(script => {
if (this.#puppeteerUtil) {
void this.#puppeteerUtil.then(handle => {
void handle.dispose();
});
}
this.#puppeteerUtil = promise.then(() => {
return this.evaluateHandle(script) as Promise<
JSHandle<PuppeteerInjectedUtil>
>;
});
}, !this.#puppeteerUtil);
return this.#puppeteerUtil as Promise<JSHandle<PuppeteerInjectedUtil>>;
}
async #addBindingWithoutThrowing(binding: Binding) {
try {
await this.#addBinding(binding);
} catch (err) {
// If the binding cannot be added, the context is broken. We cannot
// recover so we ignore the error.
debugError(err);
}
}
/**
* Evaluates the given function.
*
* @example
*
* ```ts
* const executionContext = await page.mainFrame().executionContext();
* const result = await executionContext.evaluate(() => Promise.resolve(8 * 7))* ;
* console.log(result); // prints "56"
* ```
*
* @example
* A string can also be passed in instead of a function:
*
* ```ts
* console.log(await executionContext.evaluate('1 + 2')); // prints "3"
* ```
*
* @example
* Handles can also be passed as `args`. They resolve to their referenced object:
*
* ```ts
* const oneHandle = await executionContext.evaluateHandle(() => 1);
* const twoHandle = await executionContext.evaluateHandle(() => 2);
* const result = await executionContext.evaluate(
* (a, b) => a + b,
* oneHandle,
* twoHandle,
* );
* await oneHandle.dispose();
* await twoHandle.dispose();
* console.log(result); // prints '3'.
* ```
*
* @param pageFunction - The function to evaluate.
* @param args - Additional arguments to pass into the function.
* @returns The result of evaluating the function. If the result is an object,
* a vanilla object containing the serializable properties of the result is
* returned.
*/
async evaluate<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
>(
pageFunction: Func | string,
...args: Params
): Promise<Awaited<ReturnType<Func>>> {
return await this.#evaluate(true, pageFunction, ...args);
}
/**
* Evaluates the given function.
*
* Unlike {@link ExecutionContext.evaluate | evaluate}, this method returns a
* handle to the result of the function.
*
* This method may be better suited if the object cannot be serialized (e.g.
* `Map`) and requires further manipulation.
*
* @example
*
* ```ts
* const context = await page.mainFrame().executionContext();
* const handle: JSHandle<typeof globalThis> = await context.evaluateHandle(
* () => Promise.resolve(self),
* );
* ```
*
* @example
* A string can also be passed in instead of a function.
*
* ```ts
* const handle: JSHandle<number> = await context.evaluateHandle('1 + 2');
* ```
*
* @example
* Handles can also be passed as `args`. They resolve to their referenced object:
*
* ```ts
* const bodyHandle: ElementHandle<HTMLBodyElement> =
* await context.evaluateHandle(() => {
* return document.body;
* });
* const stringHandle: JSHandle<string> = await context.evaluateHandle(
* body => body.innerHTML,
* body,
* );
* console.log(await stringHandle.jsonValue()); // prints body's innerHTML
* // Always dispose your garbage! :)
* await bodyHandle.dispose();
* await stringHandle.dispose();
* ```
*
* @param pageFunction - The function to evaluate.
* @param args - Additional arguments to pass into the function.
* @returns A {@link JSHandle | handle} to the result of evaluating the
* function. If the result is a `Node`, then this will return an
* {@link ElementHandle | element handle}.
*/
async evaluateHandle<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
>(
pageFunction: Func | string,
...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
return await this.#evaluate(false, pageFunction, ...args);
}
async #evaluate<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
>(
returnByValue: true,
pageFunction: Func | string,
...args: Params
): Promise<Awaited<ReturnType<Func>>>;
async #evaluate<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
>(
returnByValue: false,
pageFunction: Func | string,
...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>>;
async #evaluate<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
>(
returnByValue: boolean,
pageFunction: Func | string,
...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>> | Awaited<ReturnType<Func>>> {
const sourceUrlComment = getSourceUrlComment(
getSourcePuppeteerURLIfAvailable(pageFunction)?.toString() ??
PuppeteerURL.INTERNAL_URL,
);
if (isString(pageFunction)) {
const contextId = this.#id;
const expression = pageFunction;
const expressionWithSourceUrl = SOURCE_URL_REGEX.test(expression)
? expression
: `${expression}\n${sourceUrlComment}\n`;
const {exceptionDetails, result: remoteObject} = await this.#client
.send('Runtime.evaluate', {
expression: expressionWithSourceUrl,
contextId,
returnByValue,
awaitPromise: true,
userGesture: true,
})
.catch(rewriteError);
if (exceptionDetails) {
throw createEvaluationError(exceptionDetails);
}
if (returnByValue) {
return valueFromPrimitiveRemoteObject(remoteObject) as HandleFor<
Awaited<ReturnType<Func>>
>;
}
return this.#world.createCdpHandle(remoteObject) as HandleFor<
Awaited<ReturnType<Func>>
>;
}
const functionDeclaration = stringifyFunction(pageFunction);
const functionDeclarationWithSourceUrl = SOURCE_URL_REGEX.test(
functionDeclaration,
)
? functionDeclaration
: `${functionDeclaration}\n${sourceUrlComment}\n`;
let callFunctionOnPromise;
try {
callFunctionOnPromise = this.#client.send('Runtime.callFunctionOn', {
functionDeclaration: functionDeclarationWithSourceUrl,
executionContextId: this.#id,
// LazyArgs are used only internally and should not affect the order
// evaluate calls for the public APIs.
arguments: args.some(arg => {
return arg instanceof LazyArg;
})
? await Promise.all(
args.map(arg => {
return convertArgumentAsync(this, arg);
}),
)
: args.map(arg => {
return convertArgument(this, arg);
}),
returnByValue,
awaitPromise: true,
userGesture: true,
});
} catch (error) {
if (
error instanceof TypeError &&
error.message.startsWith('Converting circular structure to JSON')
) {
error.message += ' Recursive objects are not allowed.';
}
throw error;
}
const {exceptionDetails, result: remoteObject} =
await callFunctionOnPromise.catch(rewriteError);
if (exceptionDetails) {
throw createEvaluationError(exceptionDetails);
}
if (returnByValue) {
return valueFromPrimitiveRemoteObject(
remoteObject,
) as unknown as HandleFor<Awaited<ReturnType<Func>>>;
}
return this.#world.createCdpHandle(remoteObject) as HandleFor<
Awaited<ReturnType<Func>>
>;
async function convertArgumentAsync(
context: ExecutionContext,
arg: unknown,
) {
if (arg instanceof LazyArg) {
arg = await arg.get(context);
}
return convertArgument(context, arg);
}
function convertArgument(
context: ExecutionContext,
arg: unknown,
): Protocol.Runtime.CallArgument {
if (typeof arg === 'bigint') {
return {unserializableValue: `${arg.toString()}n`};
}
if (Object.is(arg, -0)) {
return {unserializableValue: '-0'};
}
if (Object.is(arg, Infinity)) {
return {unserializableValue: 'Infinity'};
}
if (Object.is(arg, -Infinity)) {
return {unserializableValue: '-Infinity'};
}
if (Object.is(arg, NaN)) {
return {unserializableValue: 'NaN'};
}
const objectHandle =
arg && (arg instanceof CdpJSHandle || arg instanceof CdpElementHandle)
? arg
: null;
if (objectHandle) {
if (objectHandle.realm !== context.#world) {
throw new Error(
'JSHandles can be evaluated only in the context they were created!',
);
}
if (objectHandle.disposed) {
throw new Error('JSHandle is disposed!');
}
if (objectHandle.remoteObject().unserializableValue) {
return {
unserializableValue:
objectHandle.remoteObject().unserializableValue,
};
}
if (!objectHandle.remoteObject().objectId) {
return {value: objectHandle.remoteObject().value};
}
return {objectId: objectHandle.remoteObject().objectId};
}
return {value: arg};
}
}
override [disposeSymbol](): void {
this.#disposables.dispose();
this.emit('disposed', undefined);
}
}
const rewriteError = (error: Error): Protocol.Runtime.EvaluateResponse => {
if (error.message.includes('Object reference chain is too long')) {
return {result: {type: 'undefined'}};
}
if (error.message.includes("Object couldn't be returned by value")) {
return {result: {type: 'undefined'}};
}
if (
error.message.endsWith('Cannot find context with specified id') ||
error.message.endsWith('Inspected target navigated or closed')
) {
throw new Error(
'Execution context was destroyed, most likely because of a navigation.',
);
}
throw error;
};

108
node_modules/puppeteer-core/src/cdp/Extension.ts generated vendored Normal file
View File

@@ -0,0 +1,108 @@
/**
* @license
* Copyright 2026 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {Page, Target, WebWorker} from '../api/api.js';
import {Extension} from '../api/api.js';
import {debugError} from '../common/util.js';
import {isErrorLike} from '../util/ErrorLike.js';
import type {CdpBrowser} from './Browser.js';
import {isTargetClosedError} from './Connection.js';
export class CdpExtension extends Extension {
// needed to access the CDPSession to trigger an extension action.
#browser: CdpBrowser;
/*
* @internal
*/
constructor(
id: string,
version: string,
name: string,
path: string,
enabled: boolean,
browser: CdpBrowser,
) {
super(id, version, name, path, enabled);
this.#browser = browser;
}
async workers(): Promise<WebWorker[]> {
const targets = this.#browser.targets();
const extensionWorkers = targets.filter((target: Target) => {
const targetUrl = target.url();
return (
target.type() === 'service_worker' &&
targetUrl.startsWith('chrome-extension://' + this.id)
);
});
const workers: WebWorker[] = [];
for (const target of extensionWorkers) {
try {
const worker = await target.worker();
if (worker) {
workers.push(worker);
}
} catch (err) {
if (this.#canIgnoreError(err)) {
debugError(err);
continue;
}
throw err;
}
}
return workers;
}
async pages(): Promise<Page[]> {
const targets = this.#browser.targets();
const extensionPages = targets.filter((target: Target) => {
const targetUrl = target.url();
return (
(target.type() === 'page' || target.type() === 'background_page') &&
targetUrl.startsWith('chrome-extension://' + this.id)
);
});
const pages = await Promise.all(
extensionPages.map(async target => {
try {
return await target.asPage();
} catch (err) {
if (this.#canIgnoreError(err)) {
debugError(err);
return null;
}
throw err;
}
}),
);
return pages.filter((page): page is Page => {
return page !== null;
});
}
async triggerAction(page: Page): Promise<void> {
await this.#browser._connection.send('Extensions.triggerAction', {
id: this.id,
targetId: page._tabId,
});
}
#canIgnoreError(error: unknown): boolean {
return (
isErrorLike(error) &&
(isTargetClosedError(error) ||
error.message.includes('No target with given id found'))
);
}
}

View File

@@ -0,0 +1,197 @@
/**
* @license
* Copyright 2024 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {ConnectionTransport} from '../common/ConnectionTransport.js';
const tabTargetInfo = {
targetId: 'tabTargetId',
type: 'tab',
title: 'tab',
url: 'about:blank',
attached: false,
canAccessOpener: false,
};
const pageTargetInfo = {
targetId: 'pageTargetId',
type: 'page',
title: 'page',
url: 'about:blank',
attached: false,
canAccessOpener: false,
};
/**
* Experimental ExtensionTransport allows establishing a connection via
* chrome.debugger API if Puppeteer runs in an extension. Since Chrome
* DevTools Protocol is restricted for extensions, the transport
* implements missing commands and events.
*
* @experimental
* @public
*/
export class ExtensionTransport implements ConnectionTransport {
static async connectTab(tabId: number): Promise<ExtensionTransport> {
await chrome.debugger.attach({tabId}, '1.3');
return new ExtensionTransport(tabId);
}
onmessage?: (message: string) => void;
onclose?: () => void;
#tabId: number;
/**
* @internal
*/
constructor(tabId: number) {
this.#tabId = tabId;
chrome.debugger.onEvent.addListener(this.#debuggerEventHandler);
}
#debuggerEventHandler = (
source: chrome.debugger.Debuggee,
method: string,
params?: object | undefined,
): void => {
if (source.tabId !== this.#tabId) {
return;
}
this.#dispatchResponse({
// @ts-expect-error sessionId is not in stable yet.
sessionId: source.sessionId ?? 'pageTargetSessionId',
method: method,
params: params,
});
};
#dispatchResponse(message: object): void {
// Dispatch in a new task like other transports.
setTimeout(() => {
this.onmessage?.(JSON.stringify(message));
}, 0);
}
send(message: string): void {
const parsed = JSON.parse(message);
switch (parsed.method) {
case 'Browser.getVersion': {
this.#dispatchResponse({
id: parsed.id,
sessionId: parsed.sessionId,
method: parsed.method,
result: {
protocolVersion: '1.3',
product: 'chrome',
revision: 'unknown',
userAgent: 'chrome',
jsVersion: 'unknown',
},
});
return;
}
case 'Target.getBrowserContexts': {
this.#dispatchResponse({
id: parsed.id,
sessionId: parsed.sessionId,
method: parsed.method,
result: {
browserContextIds: [],
},
});
return;
}
case 'Target.setDiscoverTargets': {
this.#dispatchResponse({
method: 'Target.targetCreated',
params: {
targetInfo: tabTargetInfo,
},
});
this.#dispatchResponse({
method: 'Target.targetCreated',
params: {
targetInfo: pageTargetInfo,
},
});
this.#dispatchResponse({
id: parsed.id,
sessionId: parsed.sessionId,
method: parsed.method,
result: {},
});
return;
}
case 'Target.setAutoAttach': {
if (parsed.sessionId === 'tabTargetSessionId') {
this.#dispatchResponse({
method: 'Target.attachedToTarget',
sessionId: 'tabTargetSessionId',
params: {
targetInfo: pageTargetInfo,
sessionId: 'pageTargetSessionId',
},
});
this.#dispatchResponse({
id: parsed.id,
sessionId: parsed.sessionId,
method: parsed.method,
result: {},
});
return;
} else if (!parsed.sessionId) {
this.#dispatchResponse({
method: 'Target.attachedToTarget',
params: {
targetInfo: tabTargetInfo,
sessionId: 'tabTargetSessionId',
},
});
this.#dispatchResponse({
id: parsed.id,
sessionId: parsed.sessionId,
method: parsed.method,
result: {},
});
return;
}
}
}
if (parsed.sessionId === 'pageTargetSessionId') {
delete parsed.sessionId;
}
chrome.debugger
.sendCommand(
{tabId: this.#tabId, sessionId: parsed.sessionId},
parsed.method,
parsed.params,
)
.then(response => {
this.#dispatchResponse({
id: parsed.id,
sessionId: parsed.sessionId ?? 'pageTargetSessionId',
method: parsed.method,
result: response,
});
})
.catch(err => {
this.#dispatchResponse({
id: parsed.id,
sessionId: parsed.sessionId ?? 'pageTargetSessionId',
method: parsed.method,
error: {
code: err?.code,
data: err?.data,
message: err?.message ?? 'CDP error had no message',
},
});
});
}
close(): void {
chrome.debugger.onEvent.removeListener(this.#debuggerEventHandler);
void chrome.debugger.detach({tabId: this.#tabId});
}
}

462
node_modules/puppeteer-core/src/cdp/Frame.ts generated vendored Normal file
View File

@@ -0,0 +1,462 @@
/**
* @license
* Copyright 2017 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {Protocol} from 'devtools-protocol';
import type {CDPSession} from '../api/CDPSession.js';
import type {DeviceRequestPrompt} from '../api/DeviceRequestPrompt.js';
import type {ElementHandle} from '../api/ElementHandle.js';
import type {WaitForOptions} from '../api/Frame.js';
import {Frame, FrameEvent, throwIfDetached} from '../api/Frame.js';
import type {HTTPResponse} from '../api/HTTPResponse.js';
import type {WaitTimeoutOptions} from '../api/Page.js';
import {UnsupportedOperation} from '../common/Errors.js';
import {debugError} from '../common/util.js';
import type {Realm} from '../puppeteer-core.js';
import {Deferred} from '../util/Deferred.js';
import {disposeSymbol} from '../util/disposable.js';
import {isErrorLike} from '../util/ErrorLike.js';
import {Accessibility} from './Accessibility.js';
import type {Binding} from './Binding.js';
import type {CdpPreloadScript} from './CdpPreloadScript.js';
import type {CdpDeviceRequestPromptManager} from './DeviceRequestPrompt.js';
import type {FrameManager} from './FrameManager.js';
import {FrameManagerEvent} from './FrameManagerEvents.js';
import type {IsolatedWorldChart} from './IsolatedWorld.js';
import {IsolatedWorld} from './IsolatedWorld.js';
import {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorlds.js';
import {
LifecycleWatcher,
type PuppeteerLifeCycleEvent,
} from './LifecycleWatcher.js';
import type {CdpPage} from './Page.js';
import {CDP_BINDING_PREFIX} from './utils.js';
/**
* @internal
*/
export class CdpFrame extends Frame {
#url = '';
#detached = false;
#client: CDPSession;
_frameManager: FrameManager;
_loaderId = '';
_lifecycleEvents = new Set<string>();
override _id: string;
override _parentId?: string;
override accessibility: Accessibility;
worlds: IsolatedWorldChart;
extensionWorlds: Record<string, IsolatedWorld> = {};
constructor(
frameManager: FrameManager,
frameId: string,
parentFrameId: string | undefined,
client: CDPSession,
) {
super();
this._frameManager = frameManager;
this.#url = '';
this._id = frameId;
this._parentId = parentFrameId;
this.#detached = false;
this.#client = client;
this._loaderId = '';
this.worlds = {
[MAIN_WORLD]: new IsolatedWorld(
this,
this._frameManager.timeoutSettings,
MAIN_WORLD,
),
[PUPPETEER_WORLD]: new IsolatedWorld(
this,
this._frameManager.timeoutSettings,
PUPPETEER_WORLD,
),
};
this.accessibility = new Accessibility(this.worlds[MAIN_WORLD], frameId);
this.on(FrameEvent.FrameSwappedByActivation, () => {
// Emulate loading process for swapped frames.
this._onLoadingStarted();
this._onLoadingStopped();
});
this.registerWorldListeners(this.worlds[MAIN_WORLD]);
}
/**
* @internal
*/
registerWorldListeners(world: IsolatedWorld): void {
world.emitter.on('consoleapicalled', event => {
this._frameManager.emit(FrameManagerEvent.ConsoleApiCalled, [
world,
event,
]);
});
world.emitter.on('bindingcalled', event => {
this._frameManager.emit(FrameManagerEvent.BindingCalled, [world, event]);
});
}
/**
* This is used internally in DevTools.
*
* @internal
*/
_client(): CDPSession {
return this.#client;
}
/**
* Updates the frame ID with the new ID. This happens when the main frame is
* replaced by a different frame.
*/
updateId(id: string): void {
this._id = id;
}
updateClient(client: CDPSession): void {
this.#client = client;
}
override page(): CdpPage {
return this._frameManager.page();
}
@throwIfDetached
override async goto(
url: string,
options: {
referer?: string;
referrerPolicy?: string;
timeout?: number;
waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[];
} = {},
): Promise<HTTPResponse | null> {
const {
referer = this._frameManager.networkManager.extraHTTPHeaders()['referer'],
referrerPolicy = this._frameManager.networkManager.extraHTTPHeaders()[
'referer-policy'
],
waitUntil = ['load'],
timeout = this._frameManager.timeoutSettings.navigationTimeout(),
} = options;
let ensureNewDocumentNavigation = false;
const watcher = new LifecycleWatcher(
this._frameManager.networkManager,
this,
waitUntil,
timeout,
);
let error = await Deferred.race([
navigate(
this.#client,
url,
referer,
referrerPolicy ? referrerPolicyToProtocol(referrerPolicy) : undefined,
this._id,
),
watcher.terminationPromise(),
]);
if (!error) {
error = await Deferred.race([
watcher.terminationPromise(),
ensureNewDocumentNavigation
? watcher.newDocumentNavigationPromise()
: watcher.sameDocumentNavigationPromise(),
]);
}
try {
if (error) {
throw error;
}
return await watcher.navigationResponse();
} finally {
watcher.dispose();
}
async function navigate(
client: CDPSession,
url: string,
referrer: string | undefined,
referrerPolicy: Protocol.Page.ReferrerPolicy | undefined,
frameId: string,
): Promise<Error | null> {
try {
const response = await client.send('Page.navigate', {
url,
referrer,
frameId,
referrerPolicy,
});
ensureNewDocumentNavigation = !!response.loaderId;
if (response.errorText === 'net::ERR_HTTP_RESPONSE_CODE_FAILURE') {
return null;
}
return response.errorText
? new Error(`${response.errorText} at ${url}`)
: null;
} catch (error) {
if (isErrorLike(error)) {
return error;
}
throw error;
}
}
}
@throwIfDetached
override async waitForNavigation(
options: WaitForOptions = {},
): Promise<HTTPResponse | null> {
const {
waitUntil = ['load'],
timeout = this._frameManager.timeoutSettings.navigationTimeout(),
signal,
} = options;
const watcher = new LifecycleWatcher(
this._frameManager.networkManager,
this,
waitUntil,
timeout,
signal,
);
const error = await Deferred.race([
watcher.terminationPromise(),
...(options.ignoreSameDocumentNavigation
? []
: [watcher.sameDocumentNavigationPromise()]),
watcher.newDocumentNavigationPromise(),
]);
try {
if (error) {
throw error;
}
const result = await Deferred.race<
Error | HTTPResponse | null | undefined
>([watcher.terminationPromise(), watcher.navigationResponse()]);
if (result instanceof Error) {
throw error;
}
return result || null;
} finally {
watcher.dispose();
}
}
override get client(): CDPSession {
return this.#client;
}
override mainRealm(): IsolatedWorld {
return this.worlds[MAIN_WORLD];
}
override isolatedRealm(): IsolatedWorld {
return this.worlds[PUPPETEER_WORLD];
}
@throwIfDetached
override async setContent(
html: string,
options: {
timeout?: number;
waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[];
} = {},
): Promise<void> {
const {
waitUntil = ['load'],
timeout = this._frameManager.timeoutSettings.navigationTimeout(),
} = options;
// We rely upon the fact that document.open() will reset frame lifecycle with "init"
// lifecycle event. @see https://crrev.com/608658
await this.setFrameContent(html);
const watcher = new LifecycleWatcher(
this._frameManager.networkManager,
this,
waitUntil,
timeout,
);
const error = await Deferred.race<void | Error | undefined>([
watcher.terminationPromise(),
watcher.lifecyclePromise(),
]);
watcher.dispose();
if (error) {
throw error;
}
}
override url(): string {
return this.#url;
}
override parentFrame(): CdpFrame | null {
return this._frameManager._frameTree.parentFrame(this._id) || null;
}
override childFrames(): CdpFrame[] {
return this._frameManager._frameTree.childFrames(this._id);
}
#deviceRequestPromptManager(): CdpDeviceRequestPromptManager {
return this._frameManager._deviceRequestPromptManager(this.#client);
}
@throwIfDetached
async addPreloadScript(preloadScript: CdpPreloadScript): Promise<void> {
const parentFrame = this.parentFrame();
if (parentFrame && this.#client === parentFrame.client) {
return;
}
if (preloadScript.getIdForFrame(this)) {
return;
}
const {identifier} = await this.#client.send(
'Page.addScriptToEvaluateOnNewDocument',
{
source: preloadScript.source,
},
);
preloadScript.setIdForFrame(this, identifier);
}
@throwIfDetached
async addExposedFunctionBinding(binding: Binding): Promise<void> {
// If a frame has not started loading, it might never start. Rely on
// addScriptToEvaluateOnNewDocument in that case.
if (this !== this._frameManager.mainFrame() && !this._hasStartedLoading) {
return;
}
await Promise.all([
this.#client.send('Runtime.addBinding', {
name: CDP_BINDING_PREFIX + binding.name,
}),
this.evaluate(binding.initSource).catch(debugError),
]);
}
@throwIfDetached
async removeExposedFunctionBinding(binding: Binding): Promise<void> {
// If a frame has not started loading, it might never start. Rely on
// addScriptToEvaluateOnNewDocument in that case.
if (this !== this._frameManager.mainFrame() && !this._hasStartedLoading) {
return;
}
await Promise.all([
this.#client.send('Runtime.removeBinding', {
name: CDP_BINDING_PREFIX + binding.name,
}),
this.evaluate(name => {
// Removes the dangling Puppeteer binding wrapper.
// @ts-expect-error: In a different context.
globalThis[name] = undefined;
}, binding.name).catch(debugError),
]);
}
@throwIfDetached
override async waitForDevicePrompt(
options: WaitTimeoutOptions = {},
): Promise<DeviceRequestPrompt> {
return await this.#deviceRequestPromptManager().waitForDevicePrompt(
options,
);
}
_navigated(framePayload: Protocol.Page.Frame): void {
this._name = framePayload.name;
this.#url = `${framePayload.url}${framePayload.urlFragment || ''}`;
}
_navigatedWithinDocument(url: string): void {
this.#url = url;
}
_onLifecycleEvent(loaderId: string, name: string): void {
if (name === 'init') {
this._loaderId = loaderId;
this._lifecycleEvents.clear();
}
this._lifecycleEvents.add(name);
}
_onLoadingStopped(): void {
this._lifecycleEvents.add('DOMContentLoaded');
this._lifecycleEvents.add('load');
}
_onLoadingStarted(): void {
this._hasStartedLoading = true;
}
override get detached(): boolean {
return this.#detached;
}
override [disposeSymbol](): void {
if (this.#detached) {
return;
}
this.#detached = true;
this.worlds[MAIN_WORLD][disposeSymbol]();
this.worlds[PUPPETEER_WORLD][disposeSymbol]();
for (const extensionWorld of Object.values(this.extensionWorlds)) {
extensionWorld[disposeSymbol]();
}
}
exposeFunction(): never {
throw new UnsupportedOperation();
}
override async frameElement(): Promise<ElementHandle<HTMLIFrameElement> | null> {
const parent = this.parentFrame();
if (!parent) {
return null;
}
const {backendNodeId} = await parent.client.send('DOM.getFrameOwner', {
frameId: this._id,
});
return (await parent
.mainRealm()
.adoptBackendNode(backendNodeId)) as ElementHandle<HTMLIFrameElement>;
}
/**
* @public
*/
override extensionRealms(): Realm[] {
return Object.values(this.extensionWorlds);
}
}
/**
* @internal
*/
export function referrerPolicyToProtocol(
referrerPolicy: string,
): Protocol.Page.ReferrerPolicy {
// See
// https://chromedevtools.github.io/devtools-protocol/tot/Page/#type-ReferrerPolicy
// We need to conver from Web-facing phase to CDP's camelCase.
return referrerPolicy.replaceAll(/-./g, match => {
return match[1]!.toUpperCase();
}) as Protocol.Page.ReferrerPolicy;
}

623
node_modules/puppeteer-core/src/cdp/FrameManager.ts generated vendored Normal file
View File

@@ -0,0 +1,623 @@
/**
* @license
* Copyright 2017 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {Protocol} from 'devtools-protocol';
import {type CDPSession, CDPSessionEvent} from '../api/CDPSession.js';
import {FrameEvent} from '../api/Frame.js';
import {PageEvent, type NewDocumentScriptEvaluation} from '../api/Page.js';
import {EventEmitter} from '../common/EventEmitter.js';
import type {TimeoutSettings} from '../common/TimeoutSettings.js';
import {debugError, PuppeteerURL, UTILITY_WORLD_NAME} from '../common/util.js';
import {assert} from '../util/assert.js';
import {Deferred} from '../util/Deferred.js';
import {disposeSymbol} from '../util/disposable.js';
import {isErrorLike} from '../util/ErrorLike.js';
import type {Binding} from './Binding.js';
import {CdpIssue} from './CdpIssue.js';
import {CdpPreloadScript} from './CdpPreloadScript.js';
import type {CdpCDPSession} from './CdpSession.js';
import {isTargetClosedError} from './Connection.js';
import {CdpDeviceRequestPromptManager} from './DeviceRequestPrompt.js';
import {ExecutionContext} from './ExecutionContext.js';
import {CdpFrame} from './Frame.js';
import type {FrameManagerEvents} from './FrameManagerEvents.js';
import {FrameManagerEvent} from './FrameManagerEvents.js';
import {FrameTree} from './FrameTree.js';
import {IsolatedWorld} from './IsolatedWorld.js';
import {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorlds.js';
import {NetworkManager} from './NetworkManager.js';
import type {CdpPage} from './Page.js';
import type {CdpTarget} from './Target.js';
const TIME_FOR_WAITING_FOR_SWAP = 100; // ms.
const CHROME_EXTENSION_PREFIX = 'chrome-extension://';
/**
* A frame manager manages the frames for a given {@link Page | page}.
*
* @internal
*/
export class FrameManager extends EventEmitter<FrameManagerEvents> {
#page: CdpPage;
#networkManager: NetworkManager;
#timeoutSettings: TimeoutSettings;
#isolatedWorlds = new Set<string>();
#client: CdpCDPSession;
#scriptsToEvaluateOnNewDocument = new Map<string, CdpPreloadScript>();
#bindings = new Set<Binding>();
_frameTree = new FrameTree<CdpFrame>();
/**
* Set of frame IDs stored to indicate if a frame has received a
* frameNavigated event so that frame tree responses could be ignored as the
* frameNavigated event usually contains the latest information.
*/
#frameNavigatedReceived = new Set<string>();
#deviceRequestPromptManagerMap = new WeakMap<
CDPSession,
CdpDeviceRequestPromptManager
>();
#frameTreeHandled?: Deferred<void>;
get timeoutSettings(): TimeoutSettings {
return this.#timeoutSettings;
}
get networkManager(): NetworkManager {
return this.#networkManager;
}
get client(): CdpCDPSession {
return this.#client;
}
constructor(
client: CdpCDPSession,
page: CdpPage,
timeoutSettings: TimeoutSettings,
) {
super();
this.#client = client;
this.#page = page;
this.#networkManager = new NetworkManager(
this,
page.browser().isNetworkEnabled(),
);
this.#timeoutSettings = timeoutSettings;
this.setupEventListeners(this.#client);
client.once(CDPSessionEvent.Disconnected, () => {
this.#onClientDisconnect().catch(debugError);
});
}
/**
* Called when the frame's client is disconnected. We don't know if the
* disconnect means that the frame is removed or if it will be replaced by a
* new frame. Therefore, we wait for a swap event.
*/
async #onClientDisconnect() {
const mainFrame = this._frameTree.getMainFrame();
if (!mainFrame) {
return;
}
if (!this.#page.browser().connected) {
// If the browser is not connected we know
// that activation will not happen
this.#removeFramesRecursively(mainFrame);
return;
}
for (const child of mainFrame.childFrames()) {
this.#removeFramesRecursively(child);
}
const swapped = Deferred.create<void>({
timeout: TIME_FOR_WAITING_FOR_SWAP,
message: 'Frame was not swapped',
});
mainFrame.once(FrameEvent.FrameSwappedByActivation, () => {
swapped.resolve();
});
try {
await swapped.valueOrThrow();
} catch {
this.#removeFramesRecursively(mainFrame);
}
}
/**
* When the main frame is replaced by another main frame,
* we maintain the main frame object identity while updating
* its frame tree and ID.
*/
async swapFrameTree(client: CdpCDPSession): Promise<void> {
this.#client = client;
const frame = this._frameTree.getMainFrame();
if (frame) {
this.#frameNavigatedReceived.add(this.#client.target()._targetId);
this._frameTree.removeFrame(frame);
frame.updateId(this.#client.target()._targetId);
this._frameTree.addFrame(frame);
frame.updateClient(client);
}
this.setupEventListeners(client);
client.once(CDPSessionEvent.Disconnected, () => {
this.#onClientDisconnect().catch(debugError);
});
await this.initialize(client, frame);
await this.#networkManager.addClient(client);
if (frame) {
frame.emit(FrameEvent.FrameSwappedByActivation, undefined);
}
}
async registerSpeculativeSession(client: CdpCDPSession): Promise<void> {
await this.#networkManager.addClient(client);
}
private setupEventListeners(session: CDPSession) {
session.on('Page.frameAttached', async event => {
await this.#frameTreeHandled?.valueOrThrow();
this.#onFrameAttached(session, event.frameId, event.parentFrameId);
});
session.on('Page.frameNavigated', async event => {
this.#frameNavigatedReceived.add(event.frame.id);
await this.#frameTreeHandled?.valueOrThrow();
void this.#onFrameNavigated(event.frame, event.type);
});
session.on('Page.navigatedWithinDocument', async event => {
await this.#frameTreeHandled?.valueOrThrow();
this.#onFrameNavigatedWithinDocument(event.frameId, event.url);
});
session.on(
'Page.frameDetached',
async (event: Protocol.Page.FrameDetachedEvent) => {
await this.#frameTreeHandled?.valueOrThrow();
this.#onFrameDetached(
event.frameId,
event.reason as Protocol.Page.FrameDetachedEventReason,
);
},
);
session.on('Page.frameStartedLoading', async event => {
await this.#frameTreeHandled?.valueOrThrow();
this.#onFrameStartedLoading(event.frameId);
});
session.on('Page.frameStoppedLoading', async event => {
await this.#frameTreeHandled?.valueOrThrow();
this.#onFrameStoppedLoading(event.frameId);
});
session.on('Runtime.executionContextCreated', async event => {
await this.#frameTreeHandled?.valueOrThrow();
this.#onExecutionContextCreated(event.context, session);
});
session.on('Page.lifecycleEvent', async event => {
await this.#frameTreeHandled?.valueOrThrow();
this.#onLifecycleEvent(event);
});
session.on('Audits.issueAdded', event => {
this.#page.emit(PageEvent.Issue, new CdpIssue(event.issue));
});
}
async initialize(client: CDPSession, frame?: CdpFrame | null): Promise<void> {
try {
this.#frameTreeHandled?.resolve();
this.#frameTreeHandled = Deferred.create();
// We need to schedule all these commands while the target is paused,
// therefore, it needs to happen synchronously. At the same time we
// should not start processing execution context and frame events before
// we received the initial information about the frame tree.
await Promise.all([
this.#networkManager.addClient(client),
client.send('Page.enable'),
client.send('Page.getFrameTree').then(({frameTree}) => {
this.#handleFrameTree(client, frameTree);
this.#frameTreeHandled?.resolve();
}),
client.send('Page.setLifecycleEventsEnabled', {enabled: true}),
client.send('Runtime.enable').then(() => {
return this.#createIsolatedWorld(client, UTILITY_WORLD_NAME);
}),
...(frame
? Array.from(this.#scriptsToEvaluateOnNewDocument.values())
: []
).map(script => {
return frame?.addPreloadScript(script);
}),
...(frame ? Array.from(this.#bindings.values()) : []).map(binding => {
return frame?.addExposedFunctionBinding(binding);
}),
this.#page.browser().isIssuesEnabled() && client.send('Audits.enable'),
]);
} catch (error) {
this.#frameTreeHandled?.resolve();
// The target might have been closed before the initialization finished.
if (isErrorLike(error) && isTargetClosedError(error)) {
return;
}
throw error;
}
}
page(): CdpPage {
return this.#page;
}
mainFrame(): CdpFrame {
const mainFrame = this._frameTree.getMainFrame();
assert(mainFrame, 'Requesting main frame too early!');
return mainFrame;
}
frames(): CdpFrame[] {
return Array.from(this._frameTree.frames());
}
frame(frameId: string): CdpFrame | null {
return this._frameTree.getById(frameId) || null;
}
async addExposedFunctionBinding(binding: Binding): Promise<void> {
this.#bindings.add(binding);
await Promise.all(
this.frames().map(async frame => {
return await frame.addExposedFunctionBinding(binding);
}),
);
}
async removeExposedFunctionBinding(binding: Binding): Promise<void> {
this.#bindings.delete(binding);
await Promise.all(
this.frames().map(async frame => {
return await frame.removeExposedFunctionBinding(binding);
}),
);
}
async evaluateOnNewDocument(
source: string,
): Promise<NewDocumentScriptEvaluation> {
const {identifier} = await this.mainFrame()
._client()
.send('Page.addScriptToEvaluateOnNewDocument', {
source,
});
const preloadScript = new CdpPreloadScript(
this.mainFrame(),
identifier,
source,
);
this.#scriptsToEvaluateOnNewDocument.set(identifier, preloadScript);
await Promise.all(
this.frames().map(async frame => {
return await frame.addPreloadScript(preloadScript);
}),
);
return {identifier};
}
async removeScriptToEvaluateOnNewDocument(identifier: string): Promise<void> {
const preloadScript = this.#scriptsToEvaluateOnNewDocument.get(identifier);
if (!preloadScript) {
throw new Error(
`Script to evaluate on new document with id ${identifier} not found`,
);
}
this.#scriptsToEvaluateOnNewDocument.delete(identifier);
await Promise.all(
this.frames().map(frame => {
const identifier = preloadScript.getIdForFrame(frame);
if (!identifier) {
return;
}
return frame
._client()
.send('Page.removeScriptToEvaluateOnNewDocument', {
identifier,
})
.catch(debugError);
}),
);
}
onAttachedToTarget(target: CdpTarget): void {
if (target._getTargetInfo().type !== 'iframe') {
return;
}
const frame = this.frame(target._getTargetInfo().targetId);
if (frame) {
frame.updateClient(target._session()!);
}
this.setupEventListeners(target._session()!);
void this.initialize(target._session()!, frame).catch(debugError);
}
_deviceRequestPromptManager(
client: CDPSession,
): CdpDeviceRequestPromptManager {
let manager = this.#deviceRequestPromptManagerMap.get(client);
if (manager === undefined) {
manager = new CdpDeviceRequestPromptManager(
client,
this.#timeoutSettings,
);
this.#deviceRequestPromptManagerMap.set(client, manager);
}
return manager;
}
#onLifecycleEvent(event: Protocol.Page.LifecycleEventEvent): void {
const frame = this.frame(event.frameId);
if (!frame) {
return;
}
frame._onLifecycleEvent(event.loaderId, event.name);
this.emit(FrameManagerEvent.LifecycleEvent, frame);
frame.emit(FrameEvent.LifecycleEvent, undefined);
}
#onFrameStartedLoading(frameId: string): void {
const frame = this.frame(frameId);
if (!frame) {
return;
}
frame._onLoadingStarted();
}
#onFrameStoppedLoading(frameId: string): void {
const frame = this.frame(frameId);
if (!frame) {
return;
}
frame._onLoadingStopped();
this.emit(FrameManagerEvent.LifecycleEvent, frame);
frame.emit(FrameEvent.LifecycleEvent, undefined);
}
#handleFrameTree(
session: CDPSession,
frameTree: Protocol.Page.FrameTree,
): void {
if (frameTree.frame.parentId) {
this.#onFrameAttached(
session,
frameTree.frame.id,
frameTree.frame.parentId,
);
}
if (!this.#frameNavigatedReceived.has(frameTree.frame.id)) {
void this.#onFrameNavigated(frameTree.frame, 'Navigation');
} else {
this.#frameNavigatedReceived.delete(frameTree.frame.id);
}
if (!frameTree.childFrames) {
return;
}
for (const child of frameTree.childFrames) {
this.#handleFrameTree(session, child);
}
}
#onFrameAttached(
session: CDPSession,
frameId: string,
parentFrameId: string,
): void {
let frame = this.frame(frameId);
if (frame) {
const parentFrame = this.frame(parentFrameId);
if (session && parentFrame && frame.client !== parentFrame?.client) {
// If an OOP iframes becomes a normal iframe
// again it is first attached to the parent frame before the
// target is removed.
frame.updateClient(session);
}
return;
}
frame = new CdpFrame(this, frameId, parentFrameId, session);
this._frameTree.addFrame(frame);
this.emit(FrameManagerEvent.FrameAttached, frame);
}
async #onFrameNavigated(
framePayload: Protocol.Page.Frame,
navigationType: Protocol.Page.NavigationType,
): Promise<void> {
const frameId = framePayload.id;
const isMainFrame = !framePayload.parentId;
let frame = this._frameTree.getById(frameId);
// Detach all child frames first.
if (frame) {
for (const child of frame.childFrames()) {
this.#removeFramesRecursively(child);
}
}
// Update or create main frame.
if (isMainFrame) {
if (frame) {
// Update frame id to retain frame identity on cross-process navigation.
this._frameTree.removeFrame(frame);
frame._id = frameId;
} else {
// Initial main frame navigation.
frame = new CdpFrame(this, frameId, undefined, this.#client);
}
this._frameTree.addFrame(frame);
}
frame = await this._frameTree.waitForFrame(frameId);
frame._navigated(framePayload);
this.emit(FrameManagerEvent.FrameNavigated, frame);
frame.emit(FrameEvent.FrameNavigated, navigationType);
}
async #createIsolatedWorld(session: CDPSession, name: string): Promise<void> {
const key = `${session.id()}:${name}`;
if (this.#isolatedWorlds.has(key)) {
return;
}
await session.send('Page.addScriptToEvaluateOnNewDocument', {
source: `//# sourceURL=${PuppeteerURL.INTERNAL_URL}`,
worldName: name,
});
await Promise.all(
this.frames()
.filter(frame => {
return frame.client === session;
})
.map(frame => {
// Frames might be removed before we send this, so we don't want to
// throw an error.
return session
.send('Page.createIsolatedWorld', {
frameId: frame._id,
worldName: name,
grantUniveralAccess: true,
})
.catch(debugError);
}),
);
this.#isolatedWorlds.add(key);
}
#onFrameNavigatedWithinDocument(frameId: string, url: string): void {
const frame = this.frame(frameId);
if (!frame) {
return;
}
frame._navigatedWithinDocument(url);
this.emit(FrameManagerEvent.FrameNavigatedWithinDocument, frame);
frame.emit(FrameEvent.FrameNavigatedWithinDocument, undefined);
this.emit(FrameManagerEvent.FrameNavigated, frame);
frame.emit(FrameEvent.FrameNavigated, 'Navigation');
}
#onFrameDetached(
frameId: string,
reason: Protocol.Page.FrameDetachedEventReason,
): void {
const frame = this.frame(frameId);
if (!frame) {
return;
}
switch (reason) {
case 'remove':
// Only remove the frame if the reason for the detached event is
// an actual removement of the frame.
// For frames that become OOP iframes, the reason would be 'swap'.
this.#removeFramesRecursively(frame);
break;
case 'swap':
this.emit(FrameManagerEvent.FrameSwapped, frame);
frame.emit(FrameEvent.FrameSwapped, undefined);
break;
}
}
#isExtensionOrigin(origin: string) {
return origin.startsWith(CHROME_EXTENSION_PREFIX);
}
#extractExtensionId(origin: string): string | null {
if (!origin || !this.#isExtensionOrigin(origin)) {
return null;
}
const pathPart = origin.substring(CHROME_EXTENSION_PREFIX.length);
const slashIndex = pathPart.indexOf('/');
// if there's no / it means that pathPart is now the extensionId, otherwise
// we take everything until the first /
return slashIndex === -1 ? pathPart : pathPart.substring(0, slashIndex);
}
#onExecutionContextCreated(
contextPayload: Protocol.Runtime.ExecutionContextDescription,
session: CDPSession,
): void {
const auxData = contextPayload.auxData as {frameId?: string} | undefined;
const origin = contextPayload.origin;
const frameId = auxData && auxData.frameId;
const frame = typeof frameId === 'string' ? this.frame(frameId) : undefined;
let world: IsolatedWorld | undefined;
if (frame) {
// Only care about execution contexts created for the current session.
if (frame.client !== session) {
return;
}
if (contextPayload.auxData && contextPayload.auxData['isDefault']) {
world = frame.worlds[MAIN_WORLD];
} else if (contextPayload.name === UTILITY_WORLD_NAME) {
// In case of multiple sessions to the same target, there's a race between
// connections so we might end up creating multiple isolated worlds.
// We can use either.
world = frame.worlds[PUPPETEER_WORLD];
} else if (this.#isExtensionOrigin(origin)) {
const extId = this.#extractExtensionId(origin);
if (!extId) {
debugError('Error while parsing extension id');
return;
}
if (frame.extensionWorlds[extId]) {
world = frame.extensionWorlds[extId];
} else {
world = new IsolatedWorld(frame, this.timeoutSettings, extId);
frame.extensionWorlds[extId] = world;
frame.registerWorldListeners(world);
world.origin = origin;
world.setWorldId(extId);
}
}
}
// If there is no world, the context is not meant to be handled by us.
if (!world) {
return;
}
const context = new ExecutionContext(
frame?.client || this.#client,
contextPayload,
world,
);
world.setContext(context);
}
#removeFramesRecursively(frame: CdpFrame): void {
for (const child of frame.childFrames()) {
this.#removeFramesRecursively(child);
}
frame[disposeSymbol]();
this._frameTree.removeFrame(frame);
this.emit(FrameManagerEvent.FrameDetached, frame);
frame.emit(FrameEvent.FrameDetached, frame);
}
}

View File

@@ -0,0 +1,53 @@
/**
* @license
* Copyright 2023 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type Protocol from 'devtools-protocol';
import type {EventType} from '../common/EventEmitter.js';
import type {CdpFrame} from './Frame.js';
import type {IsolatedWorld} from './IsolatedWorld.js';
/**
* We use symbols to prevent external parties listening to these events.
* They are internal to Puppeteer.
*
* @internal
*/
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace FrameManagerEvent {
export const FrameAttached = Symbol('FrameManager.FrameAttached');
export const FrameNavigated = Symbol('FrameManager.FrameNavigated');
export const FrameDetached = Symbol('FrameManager.FrameDetached');
export const FrameSwapped = Symbol('FrameManager.FrameSwapped');
export const LifecycleEvent = Symbol('FrameManager.LifecycleEvent');
export const FrameNavigatedWithinDocument = Symbol(
'FrameManager.FrameNavigatedWithinDocument',
);
export const ConsoleApiCalled = Symbol('FrameManager.ConsoleApiCalled');
export const BindingCalled = Symbol('FrameManager.BindingCalled');
}
/**
* @internal
*/
export interface FrameManagerEvents extends Record<EventType, unknown> {
[FrameManagerEvent.FrameAttached]: CdpFrame;
[FrameManagerEvent.FrameNavigated]: CdpFrame;
[FrameManagerEvent.FrameDetached]: CdpFrame;
[FrameManagerEvent.FrameSwapped]: CdpFrame;
[FrameManagerEvent.LifecycleEvent]: CdpFrame;
[FrameManagerEvent.FrameNavigatedWithinDocument]: CdpFrame;
// Emitted when a new console message is logged.
[FrameManagerEvent.ConsoleApiCalled]: [
IsolatedWorld,
Protocol.Runtime.ConsoleAPICalledEvent,
];
[FrameManagerEvent.BindingCalled]: [
IsolatedWorld,
Protocol.Runtime.BindingCalledEvent,
];
}

100
node_modules/puppeteer-core/src/cdp/FrameTree.ts generated vendored Normal file
View File

@@ -0,0 +1,100 @@
/**
* @license
* Copyright 2022 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {Frame} from '../api/Frame.js';
import {Deferred} from '../util/Deferred.js';
/**
* Keeps track of the page frame tree and it's is managed by
* {@link FrameManager}. FrameTree uses frame IDs to reference frame and it
* means that referenced frames might not be in the tree anymore. Thus, the tree
* structure is eventually consistent.
* @internal
*/
export class FrameTree<FrameType extends Frame> {
#frames = new Map<string, FrameType>();
// frameID -> parentFrameID
#parentIds = new Map<string, string>();
// frameID -> childFrameIDs
#childIds = new Map<string, Set<string>>();
#mainFrame?: FrameType;
#isMainFrameStale = false;
#waitRequests = new Map<string, Set<Deferred<FrameType>>>();
getMainFrame(): FrameType | undefined {
return this.#mainFrame;
}
getById(frameId: string): FrameType | undefined {
return this.#frames.get(frameId);
}
/**
* Returns a promise that is resolved once the frame with
* the given ID is added to the tree.
*/
waitForFrame(frameId: string): Promise<FrameType> {
const frame = this.getById(frameId);
if (frame) {
return Promise.resolve(frame);
}
const deferred = Deferred.create<FrameType>();
const callbacks =
this.#waitRequests.get(frameId) || new Set<Deferred<FrameType>>();
callbacks.add(deferred);
return deferred.valueOrThrow();
}
frames(): FrameType[] {
return Array.from(this.#frames.values());
}
addFrame(frame: FrameType): void {
this.#frames.set(frame._id, frame);
if (frame._parentId) {
this.#parentIds.set(frame._id, frame._parentId);
if (!this.#childIds.has(frame._parentId)) {
this.#childIds.set(frame._parentId, new Set());
}
this.#childIds.get(frame._parentId)!.add(frame._id);
} else if (!this.#mainFrame || this.#isMainFrameStale) {
this.#mainFrame = frame;
this.#isMainFrameStale = false;
}
this.#waitRequests.get(frame._id)?.forEach(request => {
return request.resolve(frame);
});
}
removeFrame(frame: FrameType): void {
this.#frames.delete(frame._id);
this.#parentIds.delete(frame._id);
if (frame._parentId) {
this.#childIds.get(frame._parentId)?.delete(frame._id);
} else {
this.#isMainFrameStale = true;
}
}
childFrames(frameId: string): FrameType[] {
const childIds = this.#childIds.get(frameId);
if (!childIds) {
return [];
}
return Array.from(childIds)
.map(id => {
return this.getById(id);
})
.filter((frame): frame is FrameType => {
return frame !== undefined;
});
}
parentFrame(frameId: string): FrameType | undefined {
const parentId = this.#parentIds.get(frameId);
return parentId ? this.getById(parentId) : undefined;
}
}

301
node_modules/puppeteer-core/src/cdp/HTTPRequest.ts generated vendored Normal file
View File

@@ -0,0 +1,301 @@
/**
* @license
* Copyright 2020 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {Protocol} from 'devtools-protocol';
import type {CDPSession} from '../api/CDPSession.js';
import type {Frame} from '../api/Frame.js';
import {
type ContinueRequestOverrides,
headersArray,
HTTPRequest,
type ResourceType,
type ResponseForRequest,
STATUS_TEXTS,
handleError,
} from '../api/HTTPRequest.js';
import {debugError} from '../common/util.js';
import {
mergeUint8Arrays,
stringToBase64,
stringToTypedArray,
} from '../util/encoding.js';
import type {CdpHTTPResponse} from './HTTPResponse.js';
/**
* @internal
*/
export class CdpHTTPRequest extends HTTPRequest {
override id: string;
declare _redirectChain: CdpHTTPRequest[];
declare _response: CdpHTTPResponse | null;
#client: CDPSession;
#isNavigationRequest: boolean;
#url: string;
#resourceType: ResourceType;
#method: string;
#hasPostData = false;
#postData?: string;
#headers: Record<string, string> = {};
#frame: Frame | null;
#initiator?: Protocol.Network.Initiator;
override get client(): CDPSession {
return this.#client;
}
override set client(newClient: CDPSession) {
this.#client = newClient;
}
constructor(
client: CDPSession,
frame: Frame | null,
interceptionId: string | undefined,
allowInterception: boolean,
data: {
/**
* Request identifier.
*/
requestId: Protocol.Network.RequestId;
/**
* Loader identifier. Empty string if the request is fetched from worker.
*/
loaderId?: Protocol.Network.LoaderId;
/**
* URL of the document this request is loaded for.
*/
documentURL?: string;
/**
* Request data.
*/
request: Protocol.Network.Request;
/**
* Request initiator.
*/
initiator?: Protocol.Network.Initiator;
/**
* Type of this resource.
*/
type?: Protocol.Network.ResourceType;
},
redirectChain: CdpHTTPRequest[],
) {
super();
this.#client = client;
this.id = data.requestId;
this.#isNavigationRequest =
data.requestId === data.loaderId && data.type === 'Document';
this._interceptionId = interceptionId;
this.#url = data.request.url + (data.request.urlFragment ?? '');
this.#resourceType = (data.type || 'other').toLowerCase() as ResourceType;
this.#method = data.request.method;
if (
data.request.postDataEntries &&
data.request.postDataEntries.length > 0
) {
this.#postData = new TextDecoder().decode(
mergeUint8Arrays(
data.request.postDataEntries
.map(entry => {
return entry.bytes ? stringToTypedArray(entry.bytes, true) : null;
})
.filter((entry): entry is Uint8Array => {
return entry !== null;
}),
),
);
} else {
this.#postData = data.request.postData;
}
this.#hasPostData = data.request.hasPostData ?? false;
this.#frame = frame;
this._redirectChain = redirectChain;
this.#initiator = data.initiator;
this.interception.enabled = allowInterception;
this.updateHeaders(data.request.headers);
}
updateHeaders(headers: Protocol.Network.Headers): void {
for (const [key, value] of Object.entries(headers)) {
this.#headers[key.toLowerCase()] = value;
}
}
override url(): string {
return this.#url;
}
override resourceType(): ResourceType {
return this.#resourceType;
}
override method(): string {
return this.#method;
}
override postData(): string | undefined {
return this.#postData;
}
override hasPostData(): boolean {
return this.#hasPostData;
}
override async fetchPostData(): Promise<string | undefined> {
try {
const result = await this.#client.send('Network.getRequestPostData', {
requestId: this.id,
});
return result.postData;
} catch (err) {
debugError(err);
return;
}
}
override headers(): Record<string, string> {
// Callers should not be allowed to mutate internal structure.
return structuredClone(this.#headers);
}
override response(): CdpHTTPResponse | null {
return this._response;
}
override frame(): Frame | null {
return this.#frame;
}
override isNavigationRequest(): boolean {
return this.#isNavigationRequest;
}
override initiator(): Protocol.Network.Initiator | undefined {
return this.#initiator;
}
override redirectChain(): CdpHTTPRequest[] {
return this._redirectChain.slice();
}
override failure(): {errorText: string} | null {
if (!this._failureText) {
return null;
}
return {
errorText: this._failureText,
};
}
protected canBeIntercepted(): boolean {
return !this.url().startsWith('data:') && !this._fromMemoryCache;
}
/**
* @internal
*/
async _continue(overrides: ContinueRequestOverrides = {}): Promise<void> {
const {url, method, postData, headers} = overrides;
this.interception.handled = true;
const postDataBinaryBase64 = postData
? stringToBase64(postData)
: undefined;
if (this._interceptionId === undefined) {
throw new Error(
'HTTPRequest is missing _interceptionId needed for Fetch.continueRequest',
);
}
await this.#client
.send('Fetch.continueRequest', {
requestId: this._interceptionId,
url,
method,
postData: postDataBinaryBase64,
headers: headers ? headersArray(headers) : undefined,
})
.catch(error => {
this.interception.handled = false;
return handleError(error);
});
}
async _respond(response: Partial<ResponseForRequest>): Promise<void> {
this.interception.handled = true;
let parsedBody:
| {
contentLength: number;
base64: string;
}
| undefined;
if (response.body) {
parsedBody = HTTPRequest.getResponse(response.body);
}
const responseHeaders: Record<string, string | string[]> = {};
if (response.headers) {
for (const header of Object.keys(response.headers)) {
const value = response.headers[header];
responseHeaders[header.toLowerCase()] = Array.isArray(value)
? value.map(item => {
return String(item);
})
: String(value);
}
}
if (response.contentType) {
responseHeaders['content-type'] = response.contentType;
}
if (parsedBody?.contentLength && !('content-length' in responseHeaders)) {
responseHeaders['content-length'] = String(parsedBody.contentLength);
}
const status = response.status || 200;
if (this._interceptionId === undefined) {
throw new Error(
'HTTPRequest is missing _interceptionId needed for Fetch.fulfillRequest',
);
}
await this.#client
.send('Fetch.fulfillRequest', {
requestId: this._interceptionId,
responseCode: status,
responsePhrase: STATUS_TEXTS[status],
responseHeaders: headersArray(responseHeaders),
body: parsedBody?.base64,
})
.catch(error => {
this.interception.handled = false;
return handleError(error);
});
}
async _abort(
errorReason: Protocol.Network.ErrorReason | null,
): Promise<void> {
this.interception.handled = true;
if (this._interceptionId === undefined) {
throw new Error(
'HTTPRequest is missing _interceptionId needed for Fetch.failRequest',
);
}
await this.#client
.send('Fetch.failRequest', {
requestId: this._interceptionId,
errorReason: errorReason || 'Failed',
})
.catch(handleError);
}
}

168
node_modules/puppeteer-core/src/cdp/HTTPResponse.ts generated vendored Normal file
View File

@@ -0,0 +1,168 @@
/**
* @license
* Copyright 2020 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {Protocol} from 'devtools-protocol';
import type {Frame} from '../api/Frame.js';
import {HTTPResponse, type RemoteAddress} from '../api/HTTPResponse.js';
import {ProtocolError} from '../common/Errors.js';
import {SecurityDetails} from '../common/SecurityDetails.js';
import {Deferred} from '../util/Deferred.js';
import {stringToTypedArray} from '../util/encoding.js';
import type {CdpHTTPRequest} from './HTTPRequest.js';
/**
* @internal
*/
export class CdpHTTPResponse extends HTTPResponse {
#request: CdpHTTPRequest;
#contentPromise: Promise<Uint8Array> | null = null;
#bodyLoadedDeferred = Deferred.create<void, Error>();
#remoteAddress: RemoteAddress;
#status: number;
#statusText: string;
#fromDiskCache: boolean;
#fromServiceWorker: boolean;
#headers: Record<string, string> = {};
#securityDetails: SecurityDetails | null;
#timing: Protocol.Network.ResourceTiming | null;
constructor(
request: CdpHTTPRequest,
responsePayload: Protocol.Network.Response,
extraInfo: Protocol.Network.ResponseReceivedExtraInfoEvent | null,
) {
super();
this.#request = request;
this.#remoteAddress = {
ip: responsePayload.remoteIPAddress,
port: responsePayload.remotePort,
};
this.#statusText =
this.#parseStatusTextFromExtraInfo(extraInfo) ||
responsePayload.statusText;
this.#fromDiskCache = !!responsePayload.fromDiskCache;
this.#fromServiceWorker = !!responsePayload.fromServiceWorker;
this.#status = extraInfo ? extraInfo.statusCode : responsePayload.status;
const headers = extraInfo ? extraInfo.headers : responsePayload.headers;
for (const [key, value] of Object.entries(headers)) {
this.#headers[key.toLowerCase()] = value;
}
this.#securityDetails = responsePayload.securityDetails
? new SecurityDetails(responsePayload.securityDetails)
: null;
this.#timing = responsePayload.timing || null;
}
#parseStatusTextFromExtraInfo(
extraInfo: Protocol.Network.ResponseReceivedExtraInfoEvent | null,
): string | undefined {
if (!extraInfo || !extraInfo.headersText) {
return;
}
const firstLine = extraInfo.headersText.split('\r', 1)[0];
if (!firstLine || firstLine.length > 1_000) {
return;
}
const match = firstLine.match(/[^ ]* [^ ]* (.*)/);
if (!match) {
return;
}
const statusText = match[1];
if (!statusText) {
return;
}
return statusText;
}
_resolveBody(err?: Error): void {
if (err) {
return this.#bodyLoadedDeferred.reject(err);
}
return this.#bodyLoadedDeferred.resolve();
}
override remoteAddress(): RemoteAddress {
return this.#remoteAddress;
}
override url(): string {
return this.#request.url();
}
override status(): number {
return this.#status;
}
override statusText(): string {
return this.#statusText;
}
override headers(): Record<string, string> {
return this.#headers;
}
override securityDetails(): SecurityDetails | null {
return this.#securityDetails;
}
override timing(): Protocol.Network.ResourceTiming | null {
return this.#timing;
}
override content(): Promise<Uint8Array> {
if (!this.#contentPromise) {
this.#contentPromise = this.#bodyLoadedDeferred
.valueOrThrow()
.then(async () => {
try {
// Use CDPSession from corresponding request to retrieve body, as it's client
// might have been updated (e.g. for an adopted OOPIF).
const response = await this.#request.client.send(
'Network.getResponseBody',
{
requestId: this.#request.id,
},
);
return stringToTypedArray(response.body, response.base64Encoded);
} catch (error) {
if (
error instanceof ProtocolError &&
error.originalMessage ===
'No resource with given identifier found'
) {
throw new ProtocolError(
'Could not load response body for this request. This might happen if the request is a preflight request.',
);
}
throw error;
}
});
}
return this.#contentPromise;
}
override request(): CdpHTTPRequest {
return this.#request;
}
override fromCache(): boolean {
return this.#fromDiskCache || this.#request._fromMemoryCache;
}
override fromServiceWorker(): boolean {
return this.#fromServiceWorker;
}
override frame(): Frame | null {
return this.#request.frame();
}
}

653
node_modules/puppeteer-core/src/cdp/Input.ts generated vendored Normal file
View File

@@ -0,0 +1,653 @@
/**
* @license
* Copyright 2017 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {Protocol} from 'devtools-protocol';
import type {CDPSession} from '../api/CDPSession.js';
import type {Point} from '../api/ElementHandle.js';
import {
Keyboard,
Mouse,
MouseButton,
Touchscreen,
type TouchHandle,
type KeyDownOptions,
type KeyPressOptions,
type KeyboardTypeOptions,
type MouseClickOptions,
type MouseMoveOptions,
type MouseOptions,
type MouseWheelOptions,
} from '../api/Input.js';
import {TouchError} from '../common/Errors.js';
import {
_keyDefinitions,
type KeyDefinition,
type KeyInput,
} from '../common/USKeyboardLayout.js';
import {assert} from '../util/assert.js';
type KeyDescription = Required<
Pick<KeyDefinition, 'keyCode' | 'key' | 'text' | 'code' | 'location'>
>;
/**
* @internal
*/
export class CdpKeyboard extends Keyboard {
#client: CDPSession;
#pressedKeys = new Set<string>();
_modifiers = 0;
constructor(client: CDPSession) {
super();
this.#client = client;
}
updateClient(client: CDPSession): void {
this.#client = client;
}
override async down(
key: KeyInput,
options: Readonly<KeyDownOptions> = {
text: undefined,
commands: [],
},
): Promise<void> {
const description = this.#keyDescriptionForString(key);
const autoRepeat = this.#pressedKeys.has(description.code);
this.#pressedKeys.add(description.code);
this._modifiers |= this.#modifierBit(description.key);
const text = options.text === undefined ? description.text : options.text;
await this.#client.send('Input.dispatchKeyEvent', {
type: text ? 'keyDown' : 'rawKeyDown',
modifiers: this._modifiers,
windowsVirtualKeyCode: description.keyCode,
code: description.code,
key: description.key,
text: text,
unmodifiedText: text,
autoRepeat,
location: description.location,
isKeypad: description.location === 3,
commands: options.commands,
});
}
#modifierBit(key: string): number {
if (key === 'Alt') {
return 1;
}
if (key === 'Control') {
return 2;
}
if (key === 'Meta') {
return 4;
}
if (key === 'Shift') {
return 8;
}
return 0;
}
#keyDescriptionForString(keyString: KeyInput): KeyDescription {
const shift = this._modifiers & 8;
const description = {
key: '',
keyCode: 0,
code: '',
text: '',
location: 0,
};
const definition = _keyDefinitions[keyString];
assert(definition, `Unknown key: "${keyString}"`);
if (definition.key) {
description.key = definition.key;
}
if (shift && definition.shiftKey) {
description.key = definition.shiftKey;
}
if (definition.keyCode) {
description.keyCode = definition.keyCode;
}
if (shift && definition.shiftKeyCode) {
description.keyCode = definition.shiftKeyCode;
}
if (definition.code) {
description.code = definition.code;
}
if (definition.location) {
description.location = definition.location;
}
if (description.key.length === 1) {
description.text = description.key;
}
if (definition.text) {
description.text = definition.text;
}
if (shift && definition.shiftText) {
description.text = definition.shiftText;
}
// if any modifiers besides shift are pressed, no text should be sent
if (this._modifiers & ~8) {
description.text = '';
}
return description;
}
override async up(key: KeyInput): Promise<void> {
const description = this.#keyDescriptionForString(key);
this._modifiers &= ~this.#modifierBit(description.key);
this.#pressedKeys.delete(description.code);
await this.#client.send('Input.dispatchKeyEvent', {
type: 'keyUp',
modifiers: this._modifiers,
key: description.key,
windowsVirtualKeyCode: description.keyCode,
code: description.code,
location: description.location,
});
}
override async sendCharacter(char: string): Promise<void> {
await this.#client.send('Input.insertText', {text: char});
}
private charIsKey(char: string): char is KeyInput {
return !!_keyDefinitions[char as KeyInput];
}
override async type(
text: string,
options: Readonly<KeyboardTypeOptions> = {},
): Promise<void> {
const delay = options.delay || undefined;
for (const char of text) {
if (this.charIsKey(char)) {
await this.press(char, {delay});
} else {
if (delay) {
await new Promise(f => {
return setTimeout(f, delay);
});
}
await this.sendCharacter(char);
}
}
}
override async press(
key: KeyInput,
options: Readonly<KeyPressOptions> = {},
): Promise<void> {
const {delay = null} = options;
await this.down(key, options);
if (delay) {
await new Promise(f => {
return setTimeout(f, options.delay);
});
}
await this.up(key);
}
}
/**
* This must follow {@link Protocol.Input.DispatchMouseEventRequest.buttons}.
*/
const enum MouseButtonFlag {
None = 0,
Left = 1,
Right = 1 << 1,
Middle = 1 << 2,
Back = 1 << 3,
Forward = 1 << 4,
}
const getFlag = (button: MouseButton): MouseButtonFlag => {
switch (button) {
case MouseButton.Left:
return MouseButtonFlag.Left;
case MouseButton.Right:
return MouseButtonFlag.Right;
case MouseButton.Middle:
return MouseButtonFlag.Middle;
case MouseButton.Back:
return MouseButtonFlag.Back;
case MouseButton.Forward:
return MouseButtonFlag.Forward;
}
};
/**
* This should match
* https://source.chromium.org/chromium/chromium/src/+/refs/heads/main:content/browser/renderer_host/input/web_input_event_builders_mac.mm;drc=a61b95c63b0b75c1cfe872d9c8cdf927c226046e;bpv=1;bpt=1;l=221.
*/
const getButtonFromPressedButtons = (
buttons: number,
): Protocol.Input.MouseButton => {
if (buttons & MouseButtonFlag.Left) {
return MouseButton.Left;
} else if (buttons & MouseButtonFlag.Right) {
return MouseButton.Right;
} else if (buttons & MouseButtonFlag.Middle) {
return MouseButton.Middle;
} else if (buttons & MouseButtonFlag.Back) {
return MouseButton.Back;
} else if (buttons & MouseButtonFlag.Forward) {
return MouseButton.Forward;
}
return 'none';
};
interface MouseState {
/**
* The current position of the mouse.
*/
position: Point;
/**
* The buttons that are currently being pressed.
*/
buttons: number;
}
/**
* @internal
*/
export class CdpMouse extends Mouse {
#client: CDPSession;
#keyboard: CdpKeyboard;
constructor(client: CDPSession, keyboard: CdpKeyboard) {
super();
this.#client = client;
this.#keyboard = keyboard;
}
updateClient(client: CDPSession): void {
this.#client = client;
}
#_state: Readonly<MouseState> = {
position: {x: 0, y: 0},
buttons: MouseButtonFlag.None,
};
get #state(): MouseState {
return Object.assign({...this.#_state}, ...this.#transactions);
}
// Transactions can run in parallel, so we store each of thme in this array.
#transactions: Array<Partial<MouseState>> = [];
#createTransaction(): {
update: (updates: Partial<MouseState>) => void;
commit: () => void;
rollback: () => void;
} {
const transaction: Partial<MouseState> = {};
this.#transactions.push(transaction);
const popTransaction = () => {
this.#transactions.splice(this.#transactions.indexOf(transaction), 1);
};
return {
update: (updates: Partial<MouseState>) => {
Object.assign(transaction, updates);
},
commit: () => {
this.#_state = {...this.#_state, ...transaction};
popTransaction();
},
rollback: popTransaction,
};
}
/**
* This is a shortcut for a typical update, commit/rollback lifecycle based on
* the error of the action.
*/
async #withTransaction(
action: (
update: (updates: Partial<MouseState>) => void,
) => Promise<unknown>,
): Promise<void> {
const {update, commit, rollback} = this.#createTransaction();
try {
await action(update);
commit();
} catch (error) {
rollback();
throw error;
}
}
override async reset(): Promise<void> {
const actions = [];
for (const [flag, button] of [
[MouseButtonFlag.Left, MouseButton.Left],
[MouseButtonFlag.Middle, MouseButton.Middle],
[MouseButtonFlag.Right, MouseButton.Right],
[MouseButtonFlag.Forward, MouseButton.Forward],
[MouseButtonFlag.Back, MouseButton.Back],
] as const) {
if (this.#state.buttons & flag) {
actions.push(this.up({button: button}));
}
}
if (this.#state.position.x !== 0 || this.#state.position.y !== 0) {
actions.push(this.move(0, 0));
}
await Promise.all(actions);
}
override async move(
x: number,
y: number,
options: Readonly<MouseMoveOptions> = {},
): Promise<void> {
const {steps = 1} = options;
const from = this.#state.position;
const to = {x, y};
for (let i = 1; i <= steps; i++) {
await this.#withTransaction(updateState => {
updateState({
position: {
x: from.x + (to.x - from.x) * (i / steps),
y: from.y + (to.y - from.y) * (i / steps),
},
});
const {buttons, position} = this.#state;
return this.#client.send('Input.dispatchMouseEvent', {
type: 'mouseMoved',
modifiers: this.#keyboard._modifiers,
buttons,
button: getButtonFromPressedButtons(buttons),
...position,
});
});
}
}
override async down(options: Readonly<MouseOptions> = {}): Promise<void> {
const {button = MouseButton.Left, clickCount = 1} = options;
const flag = getFlag(button);
if (!flag) {
throw new Error(`Unsupported mouse button: ${button}`);
}
if (this.#state.buttons & flag) {
throw new Error(`'${button}' is already pressed.`);
}
await this.#withTransaction(updateState => {
updateState({
buttons: this.#state.buttons | flag,
});
const {buttons, position} = this.#state;
return this.#client.send('Input.dispatchMouseEvent', {
type: 'mousePressed',
modifiers: this.#keyboard._modifiers,
clickCount,
buttons,
button,
...position,
});
});
}
override async up(options: Readonly<MouseOptions> = {}): Promise<void> {
const {button = MouseButton.Left, clickCount = 1} = options;
const flag = getFlag(button);
if (!flag) {
throw new Error(`Unsupported mouse button: ${button}`);
}
if (!(this.#state.buttons & flag)) {
throw new Error(`'${button}' is not pressed.`);
}
await this.#withTransaction(updateState => {
updateState({
buttons: this.#state.buttons & ~flag,
});
const {buttons, position} = this.#state;
return this.#client.send('Input.dispatchMouseEvent', {
type: 'mouseReleased',
modifiers: this.#keyboard._modifiers,
clickCount,
buttons,
button,
...position,
});
});
}
override async click(
x: number,
y: number,
options: Readonly<MouseClickOptions> = {},
): Promise<void> {
const {delay, count = 1, clickCount = count} = options;
if (count < 1) {
throw new Error('Click must occur a positive number of times.');
}
const actions: Array<Promise<void>> = [this.move(x, y)];
if (clickCount === count) {
for (let i = 1; i < count; ++i) {
actions.push(
this.down({...options, clickCount: i}),
this.up({...options, clickCount: i}),
);
}
}
actions.push(this.down({...options, clickCount}));
if (typeof delay === 'number') {
await Promise.all(actions);
actions.length = 0;
await new Promise(resolve => {
setTimeout(resolve, delay);
});
}
actions.push(this.up({...options, clickCount}));
await Promise.all(actions);
}
override async wheel(
options: Readonly<MouseWheelOptions> = {},
): Promise<void> {
const {deltaX = 0, deltaY = 0} = options;
const {position, buttons} = this.#state;
await this.#client.send('Input.dispatchMouseEvent', {
type: 'mouseWheel',
pointerType: 'mouse',
modifiers: this.#keyboard._modifiers,
deltaY,
deltaX,
buttons,
...position,
});
}
override async drag(
start: Point,
target: Point,
): Promise<Protocol.Input.DragData> {
const promise = new Promise<Protocol.Input.DragData>(resolve => {
this.#client.once('Input.dragIntercepted', event => {
return resolve(event.data);
});
});
await this.move(start.x, start.y);
await this.down();
await this.move(target.x, target.y);
return await promise;
}
override async dragEnter(
target: Point,
data: Protocol.Input.DragData,
): Promise<void> {
await this.#client.send('Input.dispatchDragEvent', {
type: 'dragEnter',
x: target.x,
y: target.y,
modifiers: this.#keyboard._modifiers,
data,
});
}
override async dragOver(
target: Point,
data: Protocol.Input.DragData,
): Promise<void> {
await this.#client.send('Input.dispatchDragEvent', {
type: 'dragOver',
x: target.x,
y: target.y,
modifiers: this.#keyboard._modifiers,
data,
});
}
override async drop(
target: Point,
data: Protocol.Input.DragData,
): Promise<void> {
await this.#client.send('Input.dispatchDragEvent', {
type: 'drop',
x: target.x,
y: target.y,
modifiers: this.#keyboard._modifiers,
data,
});
}
override async dragAndDrop(
start: Point,
target: Point,
options: {delay?: number} = {},
): Promise<void> {
const {delay = null} = options;
const data = await this.drag(start, target);
await this.dragEnter(target, data);
await this.dragOver(target, data);
if (delay) {
await new Promise(resolve => {
return setTimeout(resolve, delay);
});
}
await this.drop(target, data);
await this.up();
}
}
/**
* @internal
*/
export class CdpTouchHandle implements TouchHandle {
#started = false;
#touchScreen: CdpTouchscreen;
#touchPoint: Protocol.Input.TouchPoint;
#client: CDPSession;
#keyboard: CdpKeyboard;
constructor(
client: CDPSession,
touchScreen: CdpTouchscreen,
keyboard: CdpKeyboard,
touchPoint: Protocol.Input.TouchPoint,
) {
this.#client = client;
this.#touchScreen = touchScreen;
this.#keyboard = keyboard;
this.#touchPoint = touchPoint;
}
updateClient(client: CDPSession): void {
this.#client = client;
}
async start(): Promise<void> {
if (this.#started) {
throw new TouchError('Touch has already started');
}
await this.#client.send('Input.dispatchTouchEvent', {
type: 'touchStart',
touchPoints: [this.#touchPoint],
modifiers: this.#keyboard._modifiers,
});
this.#started = true;
}
move(x: number, y: number): Promise<void> {
this.#touchPoint.x = Math.round(x);
this.#touchPoint.y = Math.round(y);
return this.#client.send('Input.dispatchTouchEvent', {
type: 'touchMove',
touchPoints: [this.#touchPoint],
modifiers: this.#keyboard._modifiers,
});
}
async end(): Promise<void> {
await this.#client.send('Input.dispatchTouchEvent', {
type: 'touchEnd',
touchPoints: [this.#touchPoint],
modifiers: this.#keyboard._modifiers,
});
this.#touchScreen.removeHandle(this);
}
}
/**
* @internal
*/
export class CdpTouchscreen extends Touchscreen {
#client: CDPSession;
#keyboard: CdpKeyboard;
declare touches: CdpTouchHandle[];
constructor(client: CDPSession, keyboard: CdpKeyboard) {
super();
this.#client = client;
this.#keyboard = keyboard;
}
updateClient(client: CDPSession): void {
this.#client = client;
this.touches.forEach(t => {
t.updateClient(client);
});
}
override async touchStart(x: number, y: number): Promise<TouchHandle> {
const id = this.idGenerator();
const touchPoint: Protocol.Input.TouchPoint = {
x: Math.round(x),
y: Math.round(y),
radiusX: 0.5,
radiusY: 0.5,
force: 0.5,
id,
};
const touch = new CdpTouchHandle(
this.#client,
this,
this.#keyboard,
touchPoint,
);
await touch.start();
this.touches.push(touch);
return touch;
}
}

304
node_modules/puppeteer-core/src/cdp/IsolatedWorld.ts generated vendored Normal file
View File

@@ -0,0 +1,304 @@
/**
* @license
* Copyright 2019 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {Protocol} from 'devtools-protocol';
import {firstValueFrom, map, raceWith} from '../../third_party/rxjs/rxjs.js';
import type {CDPSession} from '../api/CDPSession.js';
import type {ElementHandle} from '../api/ElementHandle.js';
import type {Extension} from '../api/Extension.js';
import type {JSHandle} from '../api/JSHandle.js';
import {Realm} from '../api/Realm.js';
import {EventEmitter} from '../common/EventEmitter.js';
import type {TimeoutSettings} from '../common/TimeoutSettings.js';
import type {EvaluateFunc, HandleFor} from '../common/types.js';
import {
fromEmitterEvent,
timeout,
withSourcePuppeteerURLIfNone,
} from '../common/util.js';
import {disposeSymbol} from '../util/disposable.js';
import {CdpElementHandle} from './ElementHandle.js';
import type {ExecutionContext} from './ExecutionContext.js';
import type {CdpFrame} from './Frame.js';
import type {PUPPETEER_WORLD} from './IsolatedWorlds.js';
import {MAIN_WORLD} from './IsolatedWorlds.js';
import {CdpJSHandle} from './JSHandle.js';
import {CdpWebWorker} from './WebWorker.js';
/**
* @internal
*/
export interface PageBinding {
name: string;
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
pptrFunction: Function;
}
/**
* @internal
*/
export interface IsolatedWorldChart {
[key: string]: IsolatedWorld;
[MAIN_WORLD]: IsolatedWorld;
[PUPPETEER_WORLD]: IsolatedWorld;
}
/**
* @internal
*/
export type IsolatedWorldEmitter = EventEmitter<{
// Emitted when the isolated world gets a new execution context.
context: ExecutionContext;
// Emitted when the isolated world is disposed.
disposed: undefined;
// Emitted when a new console message is logged.
consoleapicalled: Protocol.Runtime.ConsoleAPICalledEvent;
/** Emitted when a binding that is not installed by the ExecutionContext is called. */
bindingcalled: Protocol.Runtime.BindingCalledEvent;
}>;
/**
* @internal
*/
export class IsolatedWorld extends Realm {
#context?: ExecutionContext;
#emitter: IsolatedWorldEmitter = new EventEmitter();
#worldId: string | symbol;
#origin?: string;
readonly #frameOrWorker: CdpFrame | CdpWebWorker;
constructor(
frameOrWorker: CdpFrame | CdpWebWorker,
timeoutSettings: TimeoutSettings,
worldId: string | symbol,
) {
super(timeoutSettings);
this.#frameOrWorker = frameOrWorker;
this.#worldId = worldId;
}
get environment(): CdpFrame | CdpWebWorker {
return this.#frameOrWorker;
}
get client(): CDPSession {
return this.#frameOrWorker.client;
}
get emitter(): IsolatedWorldEmitter {
return this.#emitter;
}
setContext(context: ExecutionContext): void {
this.#context?.[disposeSymbol]();
context.once('disposed', this.#onContextDisposed.bind(this));
context.on('consoleapicalled', this.#onContextConsoleApiCalled.bind(this));
context.on('bindingcalled', this.#onContextBindingCalled.bind(this));
this.#context = context;
this.#emitter.emit('context', context);
void this.taskManager.rerunAll();
}
#onContextDisposed(): void {
this.#context = undefined;
if ('clearDocumentHandle' in this.#frameOrWorker) {
this.#frameOrWorker.clearDocumentHandle();
}
}
#onContextConsoleApiCalled(
event: Protocol.Runtime.ConsoleAPICalledEvent,
): void {
this.#emitter.emit('consoleapicalled', event);
}
#onContextBindingCalled(event: Protocol.Runtime.BindingCalledEvent): void {
this.#emitter.emit('bindingcalled', event);
}
hasContext(): boolean {
return !!this.#context;
}
get context(): ExecutionContext | undefined {
return this.#context;
}
#executionContext(): ExecutionContext | undefined {
if (this.disposed) {
throw new Error(
`Execution context is not available in detached frame or worker "${this.environment.url()}" (are you trying to evaluate?)`,
);
}
return this.#context;
}
/**
* Waits for the next context to be set on the isolated world.
*/
async #waitForExecutionContext(): Promise<ExecutionContext> {
const error = new Error('Execution context was destroyed');
const result = await firstValueFrom(
fromEmitterEvent(this.#emitter, 'context').pipe(
raceWith(
fromEmitterEvent(this.#emitter, 'disposed').pipe(
map(() => {
// The message has to match the CDP message expected by the WaitTask class.
throw error;
}),
),
timeout(this.timeoutSettings.timeout()),
),
),
);
return result;
}
async evaluateHandle<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
>(
pageFunction: Func | string,
...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
pageFunction = withSourcePuppeteerURLIfNone(
this.evaluateHandle.name,
pageFunction,
);
// This code needs to schedule evaluateHandle call synchronously (at
// least when the context is there) so we cannot unconditionally
// await.
let context = this.#executionContext();
if (!context) {
context = await this.#waitForExecutionContext();
}
return await context.evaluateHandle(pageFunction, ...args);
}
async evaluate<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
>(
pageFunction: Func | string,
...args: Params
): Promise<Awaited<ReturnType<Func>>> {
pageFunction = withSourcePuppeteerURLIfNone(
this.evaluate.name,
pageFunction,
);
// This code needs to schedule evaluate call synchronously (at
// least when the context is there) so we cannot unconditionally
// await.
let context = this.#executionContext();
if (!context) {
context = await this.#waitForExecutionContext();
}
return await context.evaluate(pageFunction, ...args);
}
override async adoptBackendNode(
backendNodeId?: Protocol.DOM.BackendNodeId,
): Promise<JSHandle<Node>> {
// This code needs to schedule resolveNode call synchronously (at
// least when the context is there) so we cannot unconditionally
// await.
let context = this.#executionContext();
if (!context) {
context = await this.#waitForExecutionContext();
}
const {object} = await this.client.send('DOM.resolveNode', {
backendNodeId: backendNodeId,
executionContextId: context.id,
});
return this.createCdpHandle(object) as JSHandle<Node>;
}
async adoptHandle<T extends JSHandle<Node>>(handle: T): Promise<T> {
if (handle.realm === this) {
// If the context has already adopted this handle, clone it so downstream
// disposal doesn't become an issue.
return (await handle.evaluateHandle(value => {
return value;
})) as unknown as T;
}
const nodeInfo = await this.client.send('DOM.describeNode', {
objectId: handle.id,
});
return (await this.adoptBackendNode(nodeInfo.node.backendNodeId)) as T;
}
async transferHandle<T extends JSHandle<Node>>(handle: T): Promise<T> {
if (handle.realm === this) {
return handle;
}
// Implies it's a primitive value, probably.
if (handle.remoteObject().objectId === undefined) {
return handle;
}
const info = await this.client.send('DOM.describeNode', {
objectId: handle.remoteObject().objectId,
});
const newHandle = (await this.adoptBackendNode(
info.node.backendNodeId,
)) as T;
await handle.dispose();
return newHandle;
}
/**
* @internal
*/
createCdpHandle(
remoteObject: Protocol.Runtime.RemoteObject,
): JSHandle | ElementHandle<Node> {
if (remoteObject.subtype === 'node') {
return new CdpElementHandle(this, remoteObject);
}
return new CdpJSHandle(this, remoteObject);
}
override [disposeSymbol](): void {
this.#context?.[disposeSymbol]();
this.#emitter.emit('disposed', undefined);
super[disposeSymbol]();
this.#emitter.removeAllListeners();
}
override get origin(): string | undefined {
return this.#origin;
}
set origin(origin: string) {
this.#origin = origin;
}
setWorldId(worldId: string | symbol): void {
this.#worldId = worldId;
}
async extension(): Promise<Extension | null> {
if (this.#frameOrWorker instanceof CdpWebWorker) {
throw new Error('Unable to get extension from Realm');
}
if (this.#worldId === MAIN_WORLD) {
return null;
}
if (typeof this.#worldId === 'string') {
const extensions = await this.#frameOrWorker._frameManager
.page()
.browser()
.extensions();
return extensions.get(this.#worldId) ?? null;
}
return null;
}
}

20
node_modules/puppeteer-core/src/cdp/IsolatedWorlds.ts generated vendored Normal file
View File

@@ -0,0 +1,20 @@
/**
* @license
* Copyright 2022 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
/**
* A unique key for {@link IsolatedWorldChart} to denote the default world.
* Execution contexts are automatically created in the default world.
*
* @internal
*/
export const MAIN_WORLD = Symbol('mainWorld');
/**
* A unique key for {@link IsolatedWorldChart} to denote the puppeteer world.
* This world contains all puppeteer-internal bindings/code.
*
* @internal
*/
export const PUPPETEER_WORLD = Symbol('puppeteerWorld');

126
node_modules/puppeteer-core/src/cdp/JSHandle.ts generated vendored Normal file
View File

@@ -0,0 +1,126 @@
/**
* @license
* Copyright 2019 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {Protocol} from 'devtools-protocol';
import type {CDPSession} from '../api/CDPSession.js';
import {JSHandle} from '../api/JSHandle.js';
import {debugError} from '../common/util.js';
import type {CdpElementHandle} from './ElementHandle.js';
import type {IsolatedWorld} from './IsolatedWorld.js';
import {valueFromPrimitiveRemoteObject} from './utils.js';
/**
* @internal
*/
export class CdpJSHandle<T = unknown> extends JSHandle<T> {
#disposed = false;
readonly #remoteObject: Protocol.Runtime.RemoteObject;
readonly #world: IsolatedWorld;
constructor(
world: IsolatedWorld,
remoteObject: Protocol.Runtime.RemoteObject,
) {
super();
this.#world = world;
this.#remoteObject = remoteObject;
}
override get disposed(): boolean {
return this.#disposed;
}
override get realm(): IsolatedWorld {
return this.#world;
}
get client(): CDPSession {
return this.realm.environment.client;
}
override async jsonValue(): Promise<T> {
if (!this.#remoteObject.objectId) {
return valueFromPrimitiveRemoteObject(this.#remoteObject) as T;
}
const value = await this.evaluate(object => {
return object;
});
if (value === undefined) {
throw new Error('Could not serialize referenced object');
}
return value;
}
/**
* Either `null` or the handle itself if the handle is an
* instance of {@link ElementHandle}.
*/
override asElement(): CdpElementHandle<Node> | null {
return null;
}
override async dispose(): Promise<void> {
if (this.#disposed) {
return;
}
this.#disposed = true;
await releaseObject(this.client, this.#remoteObject);
}
override toString(): string {
if (!this.#remoteObject.objectId) {
return 'JSHandle:' + valueFromPrimitiveRemoteObject(this.#remoteObject);
}
const type = this.#remoteObject.subtype || this.#remoteObject.type;
return 'JSHandle@' + type;
}
override get id(): string | undefined {
return this.#remoteObject.objectId;
}
override remoteObject(): Protocol.Runtime.RemoteObject {
return this.#remoteObject;
}
override async getProperties(): Promise<Map<string, JSHandle<unknown>>> {
// We use Runtime.getProperties rather than iterative version for
// improved performance as it allows getting everything at once.
const response = await this.client.send('Runtime.getProperties', {
objectId: this.#remoteObject.objectId!,
ownProperties: true,
});
const result = new Map<string, JSHandle>();
for (const property of response.result) {
if (!property.enumerable || !property.value) {
continue;
}
result.set(property.name, this.#world.createCdpHandle(property.value));
}
return result;
}
}
/**
* @internal
*/
export async function releaseObject(
client: CDPSession,
remoteObject: Protocol.Runtime.RemoteObject,
): Promise<void> {
if (!remoteObject.objectId) {
return;
}
await client
.send('Runtime.releaseObject', {objectId: remoteObject.objectId})
.catch(error => {
// Exceptions might happen in case of a page been navigated or closed.
// Swallow these since they are harmless and we don't leak anything in this case.
debugError(error);
});
}

276
node_modules/puppeteer-core/src/cdp/LifecycleWatcher.ts generated vendored Normal file
View File

@@ -0,0 +1,276 @@
/**
* @license
* Copyright 2019 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type Protocol from 'devtools-protocol';
import {type Frame, FrameEvent} from '../api/Frame.js';
import type {HTTPRequest} from '../api/HTTPRequest.js';
import type {HTTPResponse} from '../api/HTTPResponse.js';
import type {TimeoutError} from '../common/Errors.js';
import {EventEmitter} from '../common/EventEmitter.js';
import {NetworkManagerEvent} from '../common/NetworkManagerEvents.js';
import {assert} from '../util/assert.js';
import {Deferred} from '../util/Deferred.js';
import {DisposableStack} from '../util/disposable.js';
import type {CdpFrame} from './Frame.js';
import {FrameManagerEvent} from './FrameManagerEvents.js';
import type {NetworkManager} from './NetworkManager.js';
/**
* @public
*/
export type PuppeteerLifeCycleEvent =
/**
* Waits for the 'load' event.
*/
| 'load'
/**
* Waits for the 'DOMContentLoaded' event.
*/
| 'domcontentloaded'
/**
* Waits till there are no more than 0 network connections for at least `500`
* ms.
*/
| 'networkidle0'
/**
* Waits till there are no more than 2 network connections for at least `500`
* ms.
*/
| 'networkidle2';
/**
* @public
*/
export type ProtocolLifeCycleEvent =
| 'load'
| 'DOMContentLoaded'
| 'networkIdle'
| 'networkAlmostIdle';
const puppeteerToProtocolLifecycle = new Map<
PuppeteerLifeCycleEvent,
ProtocolLifeCycleEvent
>([
['load', 'load'],
['domcontentloaded', 'DOMContentLoaded'],
['networkidle0', 'networkIdle'],
['networkidle2', 'networkAlmostIdle'],
]);
/**
* @internal
*/
export class LifecycleWatcher {
#expectedLifecycle: ProtocolLifeCycleEvent[];
#frame: CdpFrame;
#timeout: number;
#navigationRequest: HTTPRequest | null = null;
#subscriptions = new DisposableStack();
#initialLoaderId: string;
#terminationDeferred: Deferred<Error>;
#sameDocumentNavigationDeferred = Deferred.create<undefined>();
#lifecycleDeferred = Deferred.create<void>();
#newDocumentNavigationDeferred = Deferred.create<undefined>();
#error = new Error('LifecycleWatcher terminated');
#hasSameDocumentNavigation?: boolean;
#swapped?: boolean;
#navigationResponseReceived?: Deferred<void>;
constructor(
networkManager: NetworkManager,
frame: CdpFrame,
waitUntil: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[],
timeout: number,
signal?: AbortSignal,
) {
if (Array.isArray(waitUntil)) {
waitUntil = waitUntil.slice();
} else if (typeof waitUntil === 'string') {
waitUntil = [waitUntil];
}
this.#initialLoaderId = frame._loaderId;
this.#expectedLifecycle = waitUntil.map(value => {
const protocolEvent = puppeteerToProtocolLifecycle.get(value);
assert(protocolEvent, 'Unknown value for options.waitUntil: ' + value);
return protocolEvent;
});
signal?.addEventListener('abort', () => {
if (signal.reason instanceof Error) {
signal.reason.cause = this.#error;
}
this.#terminationDeferred.reject(signal.reason);
});
this.#frame = frame;
this.#timeout = timeout;
const frameManagerEmitter = this.#subscriptions.use(
new EventEmitter(frame._frameManager),
);
frameManagerEmitter.on(
FrameManagerEvent.LifecycleEvent,
this.#checkLifecycleComplete.bind(this),
);
const frameEmitter = this.#subscriptions.use(new EventEmitter(frame));
frameEmitter.on(
FrameEvent.FrameNavigatedWithinDocument,
this.#navigatedWithinDocument.bind(this),
);
frameEmitter.on(FrameEvent.FrameNavigated, this.#navigated.bind(this));
frameEmitter.on(FrameEvent.FrameSwapped, this.#frameSwapped.bind(this));
frameEmitter.on(
FrameEvent.FrameSwappedByActivation,
this.#frameSwapped.bind(this),
);
frameEmitter.on(FrameEvent.FrameDetached, this.#onFrameDetached.bind(this));
const networkManagerEmitter = this.#subscriptions.use(
new EventEmitter(networkManager),
);
networkManagerEmitter.on(
NetworkManagerEvent.Request,
this.#onRequest.bind(this),
);
networkManagerEmitter.on(
NetworkManagerEvent.Response,
this.#onResponse.bind(this),
);
networkManagerEmitter.on(
NetworkManagerEvent.RequestFailed,
this.#onRequestFailed.bind(this),
);
this.#terminationDeferred = Deferred.create<Error>({
timeout: this.#timeout,
message: `Navigation timeout of ${this.#timeout} ms exceeded`,
});
this.#checkLifecycleComplete();
}
#onRequest(request: HTTPRequest): void {
if (request.frame() !== this.#frame || !request.isNavigationRequest()) {
return;
}
this.#navigationRequest = request;
// Resolve previous navigation response in case there are multiple
// navigation requests reported by the backend. This generally should not
// happen by it looks like it's possible.
this.#navigationResponseReceived?.resolve();
this.#navigationResponseReceived = Deferred.create();
if (request.response() !== null) {
this.#navigationResponseReceived?.resolve();
}
}
#onRequestFailed(request: HTTPRequest): void {
if (this.#navigationRequest?.id !== request.id) {
return;
}
this.#navigationResponseReceived?.resolve();
}
#onResponse(response: HTTPResponse): void {
if (this.#navigationRequest?.id !== response.request().id) {
return;
}
this.#navigationResponseReceived?.resolve();
}
#onFrameDetached(frame: Frame): void {
if (this.#frame === frame) {
this.#error.message = 'Navigating frame was detached';
this.#terminationDeferred.resolve(this.#error);
return;
}
this.#checkLifecycleComplete();
}
async navigationResponse(): Promise<HTTPResponse | null> {
// Continue with a possibly null response.
await this.#navigationResponseReceived?.valueOrThrow();
return this.#navigationRequest ? this.#navigationRequest.response() : null;
}
sameDocumentNavigationPromise(): Promise<Error | undefined> {
return this.#sameDocumentNavigationDeferred.valueOrThrow();
}
newDocumentNavigationPromise(): Promise<Error | undefined> {
return this.#newDocumentNavigationDeferred.valueOrThrow();
}
lifecyclePromise(): Promise<void> {
return this.#lifecycleDeferred.valueOrThrow();
}
terminationPromise(): Promise<Error | TimeoutError | undefined> {
return this.#terminationDeferred.valueOrThrow();
}
#navigatedWithinDocument(): void {
this.#hasSameDocumentNavigation = true;
this.#checkLifecycleComplete();
}
#navigated(navigationType: Protocol.Page.NavigationType): void {
if (navigationType === 'BackForwardCacheRestore') {
return this.#frameSwapped();
}
this.#checkLifecycleComplete();
}
#frameSwapped(): void {
this.#swapped = true;
this.#checkLifecycleComplete();
}
#checkLifecycleComplete(): void {
// We expect navigation to commit.
if (!checkLifecycle(this.#frame, this.#expectedLifecycle)) {
return;
}
this.#lifecycleDeferred.resolve();
if (this.#hasSameDocumentNavigation) {
this.#sameDocumentNavigationDeferred.resolve(undefined);
}
if (this.#swapped || this.#frame._loaderId !== this.#initialLoaderId) {
this.#newDocumentNavigationDeferred.resolve(undefined);
}
function checkLifecycle(
frame: CdpFrame,
expectedLifecycle: ProtocolLifeCycleEvent[],
): boolean {
for (const event of expectedLifecycle) {
if (!frame._lifecycleEvents.has(event)) {
return false;
}
}
for (const child of frame.childFrames()) {
if (
child._hasStartedLoading &&
!checkLifecycle(child, expectedLifecycle)
) {
return false;
}
}
return true;
}
}
dispose(): void {
this.#subscriptions.dispose();
this.#error.cause = new Error('LifecycleWatcher disposed');
this.#terminationDeferred.resolve(this.#error);
}
}

View File

@@ -0,0 +1,267 @@
/**
* @license
* Copyright 2022 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {Protocol} from 'devtools-protocol';
import {CdpHTTPRequest} from './HTTPRequest.js';
/**
* @internal
*/
export interface QueuedEventGroup {
responseReceivedEvent: Protocol.Network.ResponseReceivedEvent;
loadingFinishedEvent?: Protocol.Network.LoadingFinishedEvent;
loadingFailedEvent?: Protocol.Network.LoadingFailedEvent;
}
/**
* @internal
*/
export type FetchRequestId = string;
/**
* @internal
*/
export interface RedirectInfo {
event: Protocol.Network.RequestWillBeSentEvent;
fetchRequestId?: FetchRequestId;
}
type RedirectInfoList = RedirectInfo[];
/**
* @internal
*/
export type NetworkRequestId = string;
/**
* Helper class to track network events by request ID
*
* @internal
*/
export class NetworkEventManager {
/**
* There are four possible orders of events:
* A. `_onRequestWillBeSent`
* B. `_onRequestWillBeSent`, `_onRequestPaused`
* C. `_onRequestPaused`, `_onRequestWillBeSent`
* D. `_onRequestPaused`, `_onRequestWillBeSent`, `_onRequestPaused`,
* `_onRequestWillBeSent`, `_onRequestPaused`, `_onRequestPaused`
* (see crbug.com/1196004)
*
* For `_onRequest` we need the event from `_onRequestWillBeSent` and
* optionally the `interceptionId` from `_onRequestPaused`.
*
* If request interception is disabled, call `_onRequest` once per call to
* `_onRequestWillBeSent`.
* If request interception is enabled, call `_onRequest` once per call to
* `_onRequestPaused` (once per `interceptionId`).
*
* Events are stored to allow for subsequent events to call `_onRequest`.
*
* Note that (chains of) redirect requests have the same `requestId` (!) as
* the original request. We have to anticipate series of events like these:
* A. `_onRequestWillBeSent`,
* `_onRequestWillBeSent`, ...
* B. `_onRequestWillBeSent`, `_onRequestPaused`,
* `_onRequestWillBeSent`, `_onRequestPaused`, ...
* C. `_onRequestWillBeSent`, `_onRequestPaused`,
* `_onRequestPaused`, `_onRequestWillBeSent`, ...
* D. `_onRequestPaused`, `_onRequestWillBeSent`,
* `_onRequestPaused`, `_onRequestWillBeSent`, `_onRequestPaused`,
* `_onRequestWillBeSent`, `_onRequestPaused`, `_onRequestPaused`, ...
* (see crbug.com/1196004)
*/
#requestWillBeSentMap = new Map<
NetworkRequestId,
Protocol.Network.RequestWillBeSentEvent
>();
#requestPausedMap = new Map<
NetworkRequestId,
Protocol.Fetch.RequestPausedEvent
>();
#httpRequestsMap = new Map<NetworkRequestId, CdpHTTPRequest>();
#requestWillBeSentExtraInfoMap = new Map<
NetworkRequestId,
Protocol.Network.RequestWillBeSentExtraInfoEvent[]
>();
/*
* The below maps are used to reconcile Network.responseReceivedExtraInfo
* events with their corresponding request. Each response and redirect
* response gets an ExtraInfo event, and we don't know which will come first.
* This means that we have to store a Response or an ExtraInfo for each
* response, and emit the event when we get both of them. In addition, to
* handle redirects, we have to make them Arrays to represent the chain of
* events.
*/
#responseReceivedExtraInfoMap = new Map<
NetworkRequestId,
Protocol.Network.ResponseReceivedExtraInfoEvent[]
>();
#queuedRedirectInfoMap = new Map<NetworkRequestId, RedirectInfoList>();
#queuedEventGroupMap = new Map<NetworkRequestId, QueuedEventGroup>();
forget(networkRequestId: NetworkRequestId): void {
this.#requestWillBeSentMap.delete(networkRequestId);
this.#requestPausedMap.delete(networkRequestId);
this.#requestWillBeSentExtraInfoMap.delete(networkRequestId);
this.#queuedEventGroupMap.delete(networkRequestId);
this.#queuedRedirectInfoMap.delete(networkRequestId);
this.#responseReceivedExtraInfoMap.delete(networkRequestId);
}
requestExtraInfo(
networkRequestId: NetworkRequestId,
): Protocol.Network.RequestWillBeSentExtraInfoEvent[] {
if (!this.#requestWillBeSentExtraInfoMap.has(networkRequestId)) {
this.#requestWillBeSentExtraInfoMap.set(networkRequestId, []);
}
return this.#requestWillBeSentExtraInfoMap.get(
networkRequestId,
) as Protocol.Network.RequestWillBeSentExtraInfoEvent[];
}
responseExtraInfo(
networkRequestId: NetworkRequestId,
): Protocol.Network.ResponseReceivedExtraInfoEvent[] {
if (!this.#responseReceivedExtraInfoMap.has(networkRequestId)) {
this.#responseReceivedExtraInfoMap.set(networkRequestId, []);
}
return this.#responseReceivedExtraInfoMap.get(
networkRequestId,
) as Protocol.Network.ResponseReceivedExtraInfoEvent[];
}
private queuedRedirectInfo(fetchRequestId: FetchRequestId): RedirectInfoList {
if (!this.#queuedRedirectInfoMap.has(fetchRequestId)) {
this.#queuedRedirectInfoMap.set(fetchRequestId, []);
}
return this.#queuedRedirectInfoMap.get(fetchRequestId) as RedirectInfoList;
}
queueRedirectInfo(
fetchRequestId: FetchRequestId,
redirectInfo: RedirectInfo,
): void {
this.queuedRedirectInfo(fetchRequestId).push(redirectInfo);
}
takeQueuedRedirectInfo(
fetchRequestId: FetchRequestId,
): RedirectInfo | undefined {
return this.queuedRedirectInfo(fetchRequestId).shift();
}
inFlightRequestsCount(): number {
let inFlightRequestCounter = 0;
for (const request of this.#httpRequestsMap.values()) {
if (!request.response()) {
inFlightRequestCounter++;
}
}
return inFlightRequestCounter;
}
storeRequestWillBeSent(
networkRequestId: NetworkRequestId,
event: Protocol.Network.RequestWillBeSentEvent,
): void {
this.#requestWillBeSentMap.set(networkRequestId, event);
}
getRequestWillBeSent(
networkRequestId: NetworkRequestId,
): Protocol.Network.RequestWillBeSentEvent | undefined {
return this.#requestWillBeSentMap.get(networkRequestId);
}
forgetRequestWillBeSent(networkRequestId: NetworkRequestId): void {
this.#requestWillBeSentMap.delete(networkRequestId);
}
getRequestPaused(
networkRequestId: NetworkRequestId,
): Protocol.Fetch.RequestPausedEvent | undefined {
return this.#requestPausedMap.get(networkRequestId);
}
forgetRequestPaused(networkRequestId: NetworkRequestId): void {
this.#requestPausedMap.delete(networkRequestId);
}
storeRequestPaused(
networkRequestId: NetworkRequestId,
event: Protocol.Fetch.RequestPausedEvent,
): void {
this.#requestPausedMap.set(networkRequestId, event);
}
getRequest(networkRequestId: NetworkRequestId): CdpHTTPRequest | undefined {
return this.#httpRequestsMap.get(networkRequestId);
}
storeRequest(
networkRequestId: NetworkRequestId,
request: CdpHTTPRequest,
): void {
this.#httpRequestsMap.set(networkRequestId, request);
}
forgetRequest(networkRequestId: NetworkRequestId): void {
this.#httpRequestsMap.delete(networkRequestId);
}
getQueuedEventGroup(
networkRequestId: NetworkRequestId,
): QueuedEventGroup | undefined {
return this.#queuedEventGroupMap.get(networkRequestId);
}
queueEventGroup(
networkRequestId: NetworkRequestId,
event: QueuedEventGroup,
): void {
this.#queuedEventGroupMap.set(networkRequestId, event);
}
forgetQueuedEventGroup(networkRequestId: NetworkRequestId): void {
this.#queuedEventGroupMap.delete(networkRequestId);
}
printState(): void {
function replacer(_key: unknown, value: unknown) {
if (value instanceof Map) {
return {
dataType: 'Map',
value: Array.from(value.entries()), // or with spread: value: [...value]
};
} else if (value instanceof CdpHTTPRequest) {
return {
dataType: 'CdpHTTPRequest',
value: `${value.id}: ${value.url()}`,
};
}
{
return value;
}
}
console.log(
'httpRequestsMap',
JSON.stringify(this.#httpRequestsMap, replacer, 2),
);
console.log(
'requestWillBeSentMap',
JSON.stringify(this.#requestWillBeSentMap, replacer, 2),
);
console.log(
'requestWillBeSentMap',
JSON.stringify(this.#responseReceivedExtraInfoMap, replacer, 2),
);
console.log(
'requestWillBeSentMap',
JSON.stringify(this.#requestPausedMap, replacer, 2),
);
}
}

834
node_modules/puppeteer-core/src/cdp/NetworkManager.ts generated vendored Normal file
View File

@@ -0,0 +1,834 @@
/**
* @license
* Copyright 2017 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {Protocol} from 'devtools-protocol';
import {CDPSessionEvent, type CDPSession} from '../api/CDPSession.js';
import type {Frame} from '../api/Frame.js';
import type {Credentials} from '../api/Page.js';
import {EventEmitter} from '../common/EventEmitter.js';
import {
NetworkManagerEvent,
type NetworkManagerEvents,
} from '../common/NetworkManagerEvents.js';
import {debugError, isString} from '../common/util.js';
import {assert} from '../util/assert.js';
import {DisposableStack} from '../util/disposable.js';
import {isErrorLike} from '../util/ErrorLike.js';
import {isTargetClosedError} from './Connection.js';
import {CdpHTTPRequest} from './HTTPRequest.js';
import {CdpHTTPResponse} from './HTTPResponse.js';
import {
NetworkEventManager,
type FetchRequestId,
} from './NetworkEventManager.js';
/**
* @public
*/
export interface NetworkConditions {
/**
* Emulates the offline mode.
*
* @remarks
*
* Shortcut for {@link Page.setOfflineMode}.
*/
offline?: boolean;
/**
* Download speed (bytes/s)
*/
download: number;
/**
* Upload speed (bytes/s)
*/
upload: number;
/**
* Latency (ms)
*/
latency: number;
}
/**
* @public
*/
export interface InternalNetworkConditions extends NetworkConditions {
offline: boolean;
}
/**
* @internal
*/
export interface FrameProvider {
frame(id: string): Frame | null;
}
/**
* @internal
*/
export class NetworkManager extends EventEmitter<NetworkManagerEvents> {
#frameManager: FrameProvider;
#networkEventManager = new NetworkEventManager();
#extraHTTPHeaders?: Record<string, string>;
#credentials: Credentials | null = null;
#attemptedAuthentications = new Set<string>();
#userRequestInterceptionEnabled = false;
#protocolRequestInterceptionEnabled?: boolean;
#userCacheDisabled?: boolean;
#emulatedNetworkConditions?: InternalNetworkConditions;
#userAgent?: string;
#userAgentMetadata?: Protocol.Emulation.UserAgentMetadata;
#platform?: string;
readonly #handlers = [
['Fetch.requestPaused', this.#onRequestPaused],
['Fetch.authRequired', this.#onAuthRequired],
['Network.requestWillBeSent', this.#onRequestWillBeSent],
['Network.requestWillBeSentExtraInfo', this.#onRequestWillBeSentExtraInfo],
['Network.requestServedFromCache', this.#onRequestServedFromCache],
['Network.responseReceived', this.#onResponseReceived],
['Network.loadingFinished', this.#onLoadingFinished],
['Network.loadingFailed', this.#onLoadingFailed],
['Network.responseReceivedExtraInfo', this.#onResponseReceivedExtraInfo],
[CDPSessionEvent.Disconnected, this.#removeClient],
] as const;
#clients = new Map<CDPSession, DisposableStack>();
#networkEnabled = true;
constructor(frameManager: FrameProvider, networkEnabled?: boolean) {
super();
this.#frameManager = frameManager;
this.#networkEnabled = networkEnabled ?? true;
}
#canIgnoreError(error: unknown) {
return (
isErrorLike(error) &&
(isTargetClosedError(error) ||
error.message.includes('Not supported') ||
error.message.includes("wasn't found"))
);
}
async addClient(client: CDPSession): Promise<void> {
if (!this.#networkEnabled || this.#clients.has(client)) {
return;
}
const subscriptions = new DisposableStack();
this.#clients.set(client, subscriptions);
const clientEmitter = subscriptions.use(new EventEmitter(client));
for (const [event, handler] of this.#handlers) {
clientEmitter.on(event, (arg: any) => {
return handler.bind(this)(client, arg);
});
}
try {
await Promise.all([
client.send('Network.enable'),
this.#applyExtraHTTPHeaders(client),
this.#applyNetworkConditions(client),
this.#applyProtocolCacheDisabled(client),
this.#applyProtocolRequestInterception(client),
this.#applyUserAgent(client),
]);
} catch (error) {
if (this.#canIgnoreError(error)) {
return;
}
throw error;
}
}
async #removeClient(client: CDPSession) {
this.#clients.get(client)?.dispose();
this.#clients.delete(client);
}
async authenticate(credentials: Credentials | null): Promise<void> {
this.#credentials = credentials;
const enabled = this.#userRequestInterceptionEnabled || !!this.#credentials;
if (enabled === this.#protocolRequestInterceptionEnabled) {
return;
}
this.#protocolRequestInterceptionEnabled = enabled;
await this.#applyToAllClients(
this.#applyProtocolRequestInterception.bind(this),
);
}
async setExtraHTTPHeaders(headers: Record<string, string>): Promise<void> {
const extraHTTPHeaders: Record<string, string> = {};
for (const [key, value] of Object.entries(headers)) {
assert(
isString(value),
`Expected value of header "${key}" to be String, but "${typeof value}" is found.`,
);
extraHTTPHeaders[key.toLowerCase()] = value;
}
this.#extraHTTPHeaders = extraHTTPHeaders;
await this.#applyToAllClients(this.#applyExtraHTTPHeaders.bind(this));
}
async #applyExtraHTTPHeaders(client: CDPSession) {
if (this.#extraHTTPHeaders === undefined) {
return;
}
try {
await client.send('Network.setExtraHTTPHeaders', {
headers: this.#extraHTTPHeaders,
});
} catch (error) {
if (this.#canIgnoreError(error)) {
return;
}
throw error;
}
}
extraHTTPHeaders(): Record<string, string> {
return Object.assign({}, this.#extraHTTPHeaders);
}
inFlightRequestsCount(): number {
return this.#networkEventManager.inFlightRequestsCount();
}
async setOfflineMode(value: boolean): Promise<void> {
if (!this.#emulatedNetworkConditions) {
this.#emulatedNetworkConditions = {
offline: false,
upload: -1,
download: -1,
latency: 0,
};
}
this.#emulatedNetworkConditions.offline = value;
await this.#applyToAllClients(this.#applyNetworkConditions.bind(this));
}
async emulateNetworkConditions(
networkConditions: NetworkConditions | null,
): Promise<void> {
if (!this.#emulatedNetworkConditions) {
this.#emulatedNetworkConditions = {
offline: networkConditions?.offline ?? false,
upload: -1,
download: -1,
latency: 0,
};
}
this.#emulatedNetworkConditions.upload = networkConditions
? networkConditions.upload
: -1;
this.#emulatedNetworkConditions.download = networkConditions
? networkConditions.download
: -1;
this.#emulatedNetworkConditions.latency = networkConditions
? networkConditions.latency
: 0;
this.#emulatedNetworkConditions.offline =
networkConditions?.offline ?? false;
await this.#applyToAllClients(this.#applyNetworkConditions.bind(this));
}
async #applyToAllClients(fn: (client: CDPSession) => Promise<unknown>) {
await Promise.all(
Array.from(this.#clients.keys()).map(client => {
return fn(client);
}),
);
}
async #applyNetworkConditions(client: CDPSession): Promise<void> {
if (this.#emulatedNetworkConditions === undefined) {
return;
}
try {
await client.send('Network.emulateNetworkConditions', {
offline: this.#emulatedNetworkConditions.offline,
latency: this.#emulatedNetworkConditions.latency,
uploadThroughput: this.#emulatedNetworkConditions.upload,
downloadThroughput: this.#emulatedNetworkConditions.download,
});
} catch (error) {
if (this.#canIgnoreError(error)) {
return;
}
throw error;
}
}
async setUserAgent(
userAgent: string,
userAgentMetadata?: Protocol.Emulation.UserAgentMetadata,
platform?: string,
): Promise<void> {
this.#userAgent = userAgent;
this.#userAgentMetadata = userAgentMetadata;
this.#platform = platform;
await this.#applyToAllClients(this.#applyUserAgent.bind(this));
}
async #applyUserAgent(client: CDPSession) {
if (this.#userAgent === undefined) {
return;
}
try {
await client.send('Network.setUserAgentOverride', {
userAgent: this.#userAgent,
userAgentMetadata: this.#userAgentMetadata,
platform: this.#platform,
});
} catch (error) {
if (this.#canIgnoreError(error)) {
return;
}
throw error;
}
}
async setCacheEnabled(enabled: boolean): Promise<void> {
this.#userCacheDisabled = !enabled;
await this.#applyToAllClients(this.#applyProtocolCacheDisabled.bind(this));
}
async setRequestInterception(value: boolean): Promise<void> {
this.#userRequestInterceptionEnabled = value;
const enabled = this.#userRequestInterceptionEnabled || !!this.#credentials;
if (enabled === this.#protocolRequestInterceptionEnabled) {
return;
}
this.#protocolRequestInterceptionEnabled = enabled;
await this.#applyToAllClients(
this.#applyProtocolRequestInterception.bind(this),
);
}
async #applyProtocolRequestInterception(client: CDPSession): Promise<void> {
if (this.#protocolRequestInterceptionEnabled === undefined) {
return;
}
if (this.#userCacheDisabled === undefined) {
this.#userCacheDisabled = false;
}
try {
if (this.#protocolRequestInterceptionEnabled) {
await Promise.all([
this.#applyProtocolCacheDisabled(client),
client.send('Fetch.enable', {
handleAuthRequests: true,
patterns: [{urlPattern: '*'}],
}),
]);
} else {
await Promise.all([
this.#applyProtocolCacheDisabled(client),
client.send('Fetch.disable'),
]);
}
} catch (error) {
if (this.#canIgnoreError(error)) {
return;
}
throw error;
}
}
async #applyProtocolCacheDisabled(client: CDPSession): Promise<void> {
if (this.#userCacheDisabled === undefined) {
return;
}
try {
await client.send('Network.setCacheDisabled', {
cacheDisabled: this.#userCacheDisabled,
});
} catch (error) {
if (this.#canIgnoreError(error)) {
return;
}
throw error;
}
}
#onRequestWillBeSent(
client: CDPSession,
event: Protocol.Network.RequestWillBeSentEvent,
): void {
// Request interception doesn't happen for data URLs with Network Service.
if (
this.#userRequestInterceptionEnabled &&
!event.request.url.startsWith('data:')
) {
const {requestId: networkRequestId} = event;
this.#networkEventManager.storeRequestWillBeSent(networkRequestId, event);
/**
* CDP may have sent a Fetch.requestPaused event already. Check for it.
*/
const requestPausedEvent =
this.#networkEventManager.getRequestPaused(networkRequestId);
if (requestPausedEvent) {
const {requestId: fetchRequestId} = requestPausedEvent;
this.#patchRequestEventHeaders(event, requestPausedEvent);
this.#onRequest(client, event, fetchRequestId);
this.#networkEventManager.forgetRequestPaused(networkRequestId);
}
return;
}
this.#onRequest(client, event, undefined);
}
#onAuthRequired(
client: CDPSession,
event: Protocol.Fetch.AuthRequiredEvent,
): void {
let response: Protocol.Fetch.AuthChallengeResponse['response'] = 'Default';
if (this.#attemptedAuthentications.has(event.requestId)) {
response = 'CancelAuth';
} else if (this.#credentials) {
response = 'ProvideCredentials';
this.#attemptedAuthentications.add(event.requestId);
}
const {username, password} = this.#credentials || {
username: undefined,
password: undefined,
};
client
.send('Fetch.continueWithAuth', {
requestId: event.requestId,
authChallengeResponse: {response, username, password},
})
.catch(debugError);
}
/**
* CDP may send a Fetch.requestPaused without or before a
* Network.requestWillBeSent
*
* CDP may send multiple Fetch.requestPaused
* for the same Network.requestWillBeSent.
*/
#onRequestPaused(
client: CDPSession,
event: Protocol.Fetch.RequestPausedEvent,
): void {
if (
!this.#userRequestInterceptionEnabled &&
this.#protocolRequestInterceptionEnabled
) {
client
.send('Fetch.continueRequest', {
requestId: event.requestId,
})
.catch(debugError);
}
const {networkId: networkRequestId, requestId: fetchRequestId} = event;
if (!networkRequestId) {
this.#onRequestWithoutNetworkInstrumentation(client, event);
return;
}
const requestWillBeSentEvent = (() => {
const requestWillBeSentEvent =
this.#networkEventManager.getRequestWillBeSent(networkRequestId);
// redirect requests have the same `requestId`,
if (
requestWillBeSentEvent &&
(requestWillBeSentEvent.request.url !== event.request.url ||
requestWillBeSentEvent.request.method !== event.request.method)
) {
this.#networkEventManager.forgetRequestWillBeSent(networkRequestId);
return;
}
return requestWillBeSentEvent;
})();
if (requestWillBeSentEvent) {
this.#patchRequestEventHeaders(requestWillBeSentEvent, event);
this.#onRequest(client, requestWillBeSentEvent, fetchRequestId);
} else {
this.#networkEventManager.storeRequestPaused(networkRequestId, event);
}
}
#patchRequestEventHeaders(
requestWillBeSentEvent: Protocol.Network.RequestWillBeSentEvent,
requestPausedEvent: Protocol.Fetch.RequestPausedEvent,
): void {
requestWillBeSentEvent.request.headers = {
...requestWillBeSentEvent.request.headers,
// includes extra headers, like: Accept, Origin
...requestPausedEvent.request.headers,
};
}
#onRequestWithoutNetworkInstrumentation(
client: CDPSession,
event: Protocol.Fetch.RequestPausedEvent,
): void {
// If an event has no networkId it should not have any network events. We
// still want to dispatch it for the interception by the user.
const frame = event.frameId
? this.#frameManager.frame(event.frameId)
: null;
const request = new CdpHTTPRequest(
client,
frame,
event.requestId,
this.#userRequestInterceptionEnabled,
event,
[],
);
this.emit(NetworkManagerEvent.Request, request);
void request.finalizeInterceptions();
}
#onRequest(
client: CDPSession,
event: Protocol.Network.RequestWillBeSentEvent,
fetchRequestId?: FetchRequestId,
fromMemoryCache = false,
): void {
let redirectChain: CdpHTTPRequest[] = [];
if (event.redirectResponse) {
// We want to emit a response and requestfinished for the
// redirectResponse, but we can't do so unless we have a
// responseExtraInfo ready to pair it up with. If we don't have any
// responseExtraInfos saved in our queue, they we have to wait until
// the next one to emit response and requestfinished, *and* we should
// also wait to emit this Request too because it should come after the
// response/requestfinished.
let redirectResponseExtraInfo = null;
if (event.redirectHasExtraInfo) {
redirectResponseExtraInfo = this.#networkEventManager
.responseExtraInfo(event.requestId)
.shift();
if (!redirectResponseExtraInfo) {
this.#networkEventManager.queueRedirectInfo(event.requestId, {
event,
fetchRequestId,
});
return;
}
}
const request = this.#networkEventManager.getRequest(event.requestId);
// If we connect late to the target, we could have missed the
// requestWillBeSent event.
if (request) {
this.#handleRequestRedirect(
client,
request,
event.redirectResponse,
redirectResponseExtraInfo,
);
redirectChain = request._redirectChain;
const extraInfo = this.#networkEventManager
.requestExtraInfo(event.requestId)
.shift();
if (extraInfo) {
request.updateHeaders(extraInfo.headers);
}
}
}
const frame = event.frameId
? this.#frameManager.frame(event.frameId)
: null;
const request = new CdpHTTPRequest(
client,
frame,
fetchRequestId,
this.#userRequestInterceptionEnabled,
event,
redirectChain,
);
const extraInfo = this.#networkEventManager
.requestExtraInfo(event.requestId)
.shift();
if (extraInfo) {
request.updateHeaders(extraInfo.headers);
}
request._fromMemoryCache = fromMemoryCache;
this.#networkEventManager.storeRequest(event.requestId, request);
this.emit(NetworkManagerEvent.Request, request);
void request.finalizeInterceptions();
}
#onRequestWillBeSentExtraInfo(
_client: CDPSession,
event: Protocol.Network.RequestWillBeSentExtraInfoEvent,
): void {
const request = this.#networkEventManager.getRequest(event.requestId);
if (request) {
request.updateHeaders(event.headers);
} else {
this.#networkEventManager.requestExtraInfo(event.requestId).push(event);
}
}
#onRequestServedFromCache(
client: CDPSession,
event: Protocol.Network.RequestServedFromCacheEvent,
): void {
const requestWillBeSentEvent =
this.#networkEventManager.getRequestWillBeSent(event.requestId);
let request = this.#networkEventManager.getRequest(event.requestId);
// Requests served from memory cannot be intercepted.
if (request) {
request._fromMemoryCache = true;
}
// If request ended up being served from cache, we need to convert
// requestWillBeSentEvent to a HTTP request.
if (!request && requestWillBeSentEvent) {
this.#onRequest(client, requestWillBeSentEvent, undefined, true);
request = this.#networkEventManager.getRequest(event.requestId);
}
if (!request) {
debugError(
new Error(
`Request ${event.requestId} was served from cache but we could not find the corresponding request object`,
),
);
return;
}
this.emit(NetworkManagerEvent.RequestServedFromCache, request);
}
#handleRequestRedirect(
_client: CDPSession,
request: CdpHTTPRequest,
responsePayload: Protocol.Network.Response,
extraInfo: Protocol.Network.ResponseReceivedExtraInfoEvent | null,
): void {
const response = new CdpHTTPResponse(request, responsePayload, extraInfo);
request._response = response;
request._redirectChain.push(request);
response._resolveBody(
new Error('Response body is unavailable for redirect responses'),
);
this.#forgetRequest(request, false);
this.emit(NetworkManagerEvent.Response, response);
this.emit(NetworkManagerEvent.RequestFinished, request);
}
#emitResponseEvent(
_client: CDPSession,
responseReceived: Protocol.Network.ResponseReceivedEvent,
extraInfo: Protocol.Network.ResponseReceivedExtraInfoEvent | null,
): void {
const request = this.#networkEventManager.getRequest(
responseReceived.requestId,
);
// FileUpload sends a response without a matching request.
if (!request) {
return;
}
const extraInfos = this.#networkEventManager.responseExtraInfo(
responseReceived.requestId,
);
if (extraInfos.length) {
debugError(
new Error(
'Unexpected extraInfo events for request ' +
responseReceived.requestId,
),
);
}
// Chromium sends wrong extraInfo events for responses served from cache.
// See https://github.com/puppeteer/puppeteer/issues/9965 and
// https://crbug.com/1340398.
if (responseReceived.response.fromDiskCache) {
extraInfo = null;
}
const response = new CdpHTTPResponse(
request,
responseReceived.response,
extraInfo,
);
request._response = response;
this.emit(NetworkManagerEvent.Response, response);
}
#onResponseReceived(
client: CDPSession,
event: Protocol.Network.ResponseReceivedEvent,
): void {
const request = this.#networkEventManager.getRequest(event.requestId);
let extraInfo = null;
if (request && !request._fromMemoryCache && event.hasExtraInfo) {
extraInfo = this.#networkEventManager
.responseExtraInfo(event.requestId)
.shift();
if (!extraInfo) {
// Wait until we get the corresponding ExtraInfo event.
this.#networkEventManager.queueEventGroup(event.requestId, {
responseReceivedEvent: event,
});
return;
}
}
this.#emitResponseEvent(client, event, extraInfo);
}
#onResponseReceivedExtraInfo(
client: CDPSession,
event: Protocol.Network.ResponseReceivedExtraInfoEvent,
): void {
// We may have skipped a redirect response/request pair due to waiting for
// this ExtraInfo event. If so, continue that work now that we have the
// request.
const redirectInfo = this.#networkEventManager.takeQueuedRedirectInfo(
event.requestId,
);
if (redirectInfo) {
this.#networkEventManager.responseExtraInfo(event.requestId).push(event);
this.#onRequest(client, redirectInfo.event, redirectInfo.fetchRequestId);
return;
}
// We may have skipped response and loading events because we didn't have
// this ExtraInfo event yet. If so, emit those events now.
const queuedEvents = this.#networkEventManager.getQueuedEventGroup(
event.requestId,
);
if (queuedEvents) {
this.#networkEventManager.forgetQueuedEventGroup(event.requestId);
this.#emitResponseEvent(
client,
queuedEvents.responseReceivedEvent,
event,
);
if (queuedEvents.loadingFinishedEvent) {
this.#emitLoadingFinished(client, queuedEvents.loadingFinishedEvent);
}
if (queuedEvents.loadingFailedEvent) {
this.#emitLoadingFailed(client, queuedEvents.loadingFailedEvent);
}
return;
}
// Wait until we get another event that can use this ExtraInfo event.
this.#networkEventManager.responseExtraInfo(event.requestId).push(event);
}
#forgetRequest(request: CdpHTTPRequest, events: boolean): void {
const requestId = request.id;
const interceptionId = request._interceptionId;
this.#networkEventManager.forgetRequest(requestId);
if (interceptionId !== undefined) {
this.#attemptedAuthentications.delete(interceptionId);
}
if (events) {
this.#networkEventManager.forget(requestId);
}
}
#onLoadingFinished(
client: CDPSession,
event: Protocol.Network.LoadingFinishedEvent,
): void {
// If the response event for this request is still waiting on a
// corresponding ExtraInfo event, then wait to emit this event too.
const queuedEvents = this.#networkEventManager.getQueuedEventGroup(
event.requestId,
);
if (queuedEvents) {
queuedEvents.loadingFinishedEvent = event;
} else {
this.#emitLoadingFinished(client, event);
}
}
#emitLoadingFinished(
client: CDPSession,
event: Protocol.Network.LoadingFinishedEvent,
): void {
const request = this.#networkEventManager.getRequest(event.requestId);
// For certain requestIds we never receive requestWillBeSent event.
// @see https://crbug.com/750469
if (!request) {
return;
}
this.#adoptCdpSessionIfNeeded(client, request);
// Under certain conditions we never get the Network.responseReceived
// event from protocol. @see https://crbug.com/883475
if (request.response()) {
request.response()?._resolveBody();
}
this.#forgetRequest(request, true);
this.emit(NetworkManagerEvent.RequestFinished, request);
}
#onLoadingFailed(
client: CDPSession,
event: Protocol.Network.LoadingFailedEvent,
): void {
// If the response event for this request is still waiting on a
// corresponding ExtraInfo event, then wait to emit this event too.
const queuedEvents = this.#networkEventManager.getQueuedEventGroup(
event.requestId,
);
if (queuedEvents) {
queuedEvents.loadingFailedEvent = event;
} else {
this.#emitLoadingFailed(client, event);
}
}
#emitLoadingFailed(
client: CDPSession,
event: Protocol.Network.LoadingFailedEvent,
): void {
const request = this.#networkEventManager.getRequest(event.requestId);
// For certain requestIds we never receive requestWillBeSent event.
// @see https://crbug.com/750469
if (!request) {
return;
}
this.#adoptCdpSessionIfNeeded(client, request);
request._failureText = event.errorText;
const response = request.response();
if (response) {
response._resolveBody();
}
this.#forgetRequest(request, true);
this.emit(NetworkManagerEvent.RequestFailed, request);
}
#adoptCdpSessionIfNeeded(client: CDPSession, request: CdpHTTPRequest): void {
// Document requests for OOPIFs start in the parent frame but are
// adopted by their child frame, meaning their loadingFinished and
// loadingFailed events are fired on the child session. In this case
// we reassign the request CDPSession to ensure all subsequent
// actions use the correct session (e.g. retrieving response body in
// HTTPResponse). The same applies to main worker script requests.
if (client !== request.client) {
request.client = client;
}
}
}

1388
node_modules/puppeteer-core/src/cdp/Page.ts generated vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,71 @@
/**
* @license
* Copyright 2021 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {NetworkConditions} from './NetworkManager.js';
/**
* A list of pre-defined network conditions to be used with
* {@link Page.emulateNetworkConditions}.
*
* @example
*
* ```ts
* import {PredefinedNetworkConditions} from 'puppeteer';
* const browser = await puppeteer.launch();
* const page = await browser.newPage();
* await page.emulateNetworkConditions(PredefinedNetworkConditions['Slow 3G']);
* await page.goto('https://www.google.com');
* await page.emulateNetworkConditions(PredefinedNetworkConditions['Fast 3G']);
* await page.goto('https://www.google.com');
* // alias to Fast 3G.
* await page.emulateNetworkConditions(PredefinedNetworkConditions['Slow 4G']);
* await page.goto('https://www.google.com');
* await page.emulateNetworkConditions(PredefinedNetworkConditions['Fast 4G']);
* await page.goto('https://www.google.com');
* // other actions...
* await browser.close();
* ```
*
* @public
*/
export const PredefinedNetworkConditions = Object.freeze({
// Generally aligned with DevTools
// https://source.chromium.org/chromium/chromium/src/+/main:third_party/devtools-frontend/src/front_end/core/sdk/NetworkManager.ts;l=398;drc=225e1240f522ca684473f541ae6dae6cd766dd33.
'Slow 3G': {
// ~500Kbps down
download: ((500 * 1000) / 8) * 0.8,
// ~500Kbps up
upload: ((500 * 1000) / 8) * 0.8,
// 400ms RTT
latency: 400 * 5,
} as NetworkConditions,
'Fast 3G': {
// ~1.6 Mbps down
download: ((1.6 * 1000 * 1000) / 8) * 0.9,
// ~0.75 Mbps up
upload: ((750 * 1000) / 8) * 0.9,
// 150ms RTT
latency: 150 * 3.75,
} as NetworkConditions,
// alias to Fast 3G to align with Lighthouse (crbug.com/342406608)
// and DevTools (crbug.com/342406608),
'Slow 4G': {
// ~1.6 Mbps down
download: ((1.6 * 1000 * 1000) / 8) * 0.9,
// ~0.75 Mbps up
upload: ((750 * 1000) / 8) * 0.9,
// 150ms RTT
latency: 150 * 3.75,
} as NetworkConditions,
'Fast 4G': {
// 9 Mbps down
download: ((9 * 1000 * 1000) / 8) * 0.9,
// 1.5 Mbps up
upload: ((1.5 * 1000 * 1000) / 8) * 0.9,
// 60ms RTT
latency: 60 * 2.75,
} as NetworkConditions,
});

321
node_modules/puppeteer-core/src/cdp/Target.ts generated vendored Normal file
View File

@@ -0,0 +1,321 @@
/**
* @license
* Copyright 2019 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {Protocol} from 'devtools-protocol';
import type {Browser} from '../api/Browser.js';
import type {BrowserContext} from '../api/BrowserContext.js';
import {PageEvent, type Page} from '../api/Page.js';
import {Target, TargetType} from '../api/Target.js';
import {debugError} from '../common/util.js';
import type {Viewport} from '../common/Viewport.js';
import {Deferred} from '../util/Deferred.js';
import type {CdpCDPSession} from './CdpSession.js';
import {CdpPage} from './Page.js';
import type {TargetManager} from './TargetManager.js';
import {CdpWebWorker} from './WebWorker.js';
/**
* @internal
*/
export enum InitializationStatus {
SUCCESS = 'success',
ABORTED = 'aborted',
}
/**
* @internal
*/
export class CdpTarget extends Target {
#browserContext?: BrowserContext;
#session?: CdpCDPSession;
#targetInfo: Protocol.Target.TargetInfo;
#targetManager?: TargetManager;
#sessionFactory:
| ((isAutoAttachEmulated: boolean) => Promise<CdpCDPSession>)
| undefined;
#childTargets = new Set<CdpTarget>();
_initializedDeferred = Deferred.create<InitializationStatus>();
_isClosedDeferred = Deferred.create<void>();
_targetId: string;
_asPagePromise?: Promise<Page>;
/** @internal */
pagePromise?: Promise<Page>;
/**
* To initialize the target for use, call initialize.
*
* @internal
*/
constructor(
targetInfo: Protocol.Target.TargetInfo,
session: CdpCDPSession | undefined,
browserContext: BrowserContext | undefined,
targetManager: TargetManager | undefined,
sessionFactory:
| ((isAutoAttachEmulated: boolean) => Promise<CdpCDPSession>)
| undefined,
) {
super();
this.#session = session;
this.#targetManager = targetManager;
this.#targetInfo = targetInfo;
this.#browserContext = browserContext;
this._targetId = targetInfo.targetId;
this.#sessionFactory = sessionFactory;
if (this.#session) {
this.#session.setTarget(this);
}
}
override async asPage(): Promise<Page> {
if (this.pagePromise) {
const page = await this.pagePromise;
if (page) {
return page;
}
}
if (!this._asPagePromise) {
const session = this._session();
this._asPagePromise = (
session
? Promise.resolve(session)
: this._sessionFactory()(/* isAutoAttachEmulated=*/ false)
).then(client => {
return CdpPage._create(client, this, null);
});
}
return (await this._asPagePromise) ?? null;
}
_subtype(): string | undefined {
return this.#targetInfo.subtype;
}
_session(): CdpCDPSession | undefined {
return this.#session;
}
_addChildTarget(target: CdpTarget): void {
this.#childTargets.add(target);
}
_removeChildTarget(target: CdpTarget): void {
this.#childTargets.delete(target);
}
_childTargets(): ReadonlySet<CdpTarget> {
return this.#childTargets;
}
protected _sessionFactory(): (
isAutoAttachEmulated: boolean,
) => Promise<CdpCDPSession> {
if (!this.#sessionFactory) {
throw new Error('sessionFactory is not initialized');
}
return this.#sessionFactory;
}
override createCDPSession(): Promise<CdpCDPSession> {
if (!this.#sessionFactory) {
throw new Error('sessionFactory is not initialized');
}
return this.#sessionFactory(false).then(session => {
session.setTarget(this);
return session;
});
}
override url(): string {
return this.#targetInfo.url;
}
override type(): TargetType {
const type = this.#targetInfo.type;
switch (type) {
case 'page':
return TargetType.PAGE;
case 'background_page':
return TargetType.BACKGROUND_PAGE;
case 'service_worker':
return TargetType.SERVICE_WORKER;
case 'shared_worker':
return TargetType.SHARED_WORKER;
case 'browser':
return TargetType.BROWSER;
case 'webview':
return TargetType.WEBVIEW;
case 'tab':
return TargetType.TAB;
default:
return TargetType.OTHER;
}
}
_targetManager(): TargetManager {
if (!this.#targetManager) {
throw new Error('targetManager is not initialized');
}
return this.#targetManager;
}
_getTargetInfo(): Protocol.Target.TargetInfo {
return this.#targetInfo;
}
override browser(): Browser {
if (!this.#browserContext) {
throw new Error('browserContext is not initialized');
}
return this.#browserContext.browser();
}
override browserContext(): BrowserContext {
if (!this.#browserContext) {
throw new Error('browserContext is not initialized');
}
return this.#browserContext;
}
override opener(): Target | undefined {
const {openerId} = this.#targetInfo;
if (!openerId) {
return;
}
return this.browser()
.targets()
.find(target => {
return (target as CdpTarget)._targetId === openerId;
});
}
_targetInfoChanged(targetInfo: Protocol.Target.TargetInfo): void {
this.#targetInfo = targetInfo;
this._checkIfInitialized();
}
_initialize(): void {
this._initializedDeferred.resolve(InitializationStatus.SUCCESS);
}
_isTargetExposed(): boolean {
return this.type() !== TargetType.TAB && !this._subtype();
}
protected _checkIfInitialized(): void {
if (!this._initializedDeferred.resolved()) {
this._initializedDeferred.resolve(InitializationStatus.SUCCESS);
}
}
}
/**
* @internal
*/
export class PageTarget extends CdpTarget {
#defaultViewport?: Viewport;
constructor(
targetInfo: Protocol.Target.TargetInfo,
session: CdpCDPSession | undefined,
browserContext: BrowserContext,
targetManager: TargetManager,
sessionFactory: (isAutoAttachEmulated: boolean) => Promise<CdpCDPSession>,
defaultViewport: Viewport | null,
) {
super(targetInfo, session, browserContext, targetManager, sessionFactory);
this.#defaultViewport = defaultViewport ?? undefined;
}
override _initialize(): void {
this._initializedDeferred
.valueOrThrow()
.then(async result => {
if (result === InitializationStatus.ABORTED) {
return;
}
const opener = this.opener();
if (!(opener instanceof PageTarget)) {
return;
}
if (!opener || !opener.pagePromise || this.type() !== 'page') {
return true;
}
const openerPage = await opener.pagePromise;
if (!openerPage.listenerCount(PageEvent.Popup)) {
return true;
}
const popupPage = await this.page();
openerPage.emit(PageEvent.Popup, popupPage);
return true;
})
.catch(debugError);
this._checkIfInitialized();
}
override async page(): Promise<Page | null> {
if (!this.pagePromise) {
const session = this._session();
this.pagePromise = (
session
? Promise.resolve(session)
: this._sessionFactory()(/* isAutoAttachEmulated=*/ false)
).then(client => {
return CdpPage._create(client, this, this.#defaultViewport ?? null);
});
}
return (await this.pagePromise) ?? null;
}
override _checkIfInitialized(): void {
if (this._initializedDeferred.resolved()) {
return;
}
if (this._getTargetInfo().url !== '') {
this._initializedDeferred.resolve(InitializationStatus.SUCCESS);
}
}
}
/**
* @internal
*/
export class DevToolsTarget extends PageTarget {}
/**
* @internal
*/
export class WorkerTarget extends CdpTarget {
#workerPromise?: Promise<CdpWebWorker>;
override async worker(): Promise<CdpWebWorker | null> {
if (!this.#workerPromise) {
const session = this._session();
this.#workerPromise = (
session
? Promise.resolve(session)
: this._sessionFactory()(/* isAutoAttachEmulated=*/ false)
).then(client => {
return new CdpWebWorker(
client,
this._getTargetInfo().url,
this._targetId,
this.type(),
() => {} /* exceptionThrown */,
undefined /* networkManager */,
);
});
}
return await this.#workerPromise;
}
}
/**
* @internal
*/
export class OtherTarget extends CdpTarget {}

View File

@@ -0,0 +1,38 @@
/**
* @license
* Copyright 2024 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {Protocol} from 'devtools-protocol';
import type {EventType} from '../common/EventEmitter.js';
import type {CdpTarget} from './Target.js';
/**
* @internal
*/
export const enum TargetManagerEvent {
TargetDiscovered = 'targetDiscovered',
TargetAvailable = 'targetAvailable',
TargetGone = 'targetGone',
/**
* Emitted after a target has been initialized and whenever its URL changes.
*/
TargetChanged = 'targetChanged',
}
/**
* @internal
*/
export interface TargetManagerEvents extends Record<EventType, unknown> {
[TargetManagerEvent.TargetAvailable]: CdpTarget;
[TargetManagerEvent.TargetDiscovered]: Protocol.Target.TargetInfo;
[TargetManagerEvent.TargetGone]: CdpTarget;
[TargetManagerEvent.TargetChanged]: {
target: CdpTarget;
wasInitialized: true;
previousURL: string;
};
}

514
node_modules/puppeteer-core/src/cdp/TargetManager.ts generated vendored Normal file
View File

@@ -0,0 +1,514 @@
/**
* @license
* Copyright 2022 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {Protocol} from 'devtools-protocol';
import {URLPattern} from '../../third_party/urlpattern-polyfill/urlpattern-polyfill.js';
import type {TargetFilterCallback} from '../api/Browser.js';
import type {CDPSession} from '../api/CDPSession.js';
import {CDPSessionEvent} from '../api/CDPSession.js';
import {EventEmitter} from '../common/EventEmitter.js';
import {debugError} from '../common/util.js';
import {assert} from '../util/assert.js';
import {Deferred} from '../util/Deferred.js';
import {CdpCDPSession} from './CdpSession.js';
import type {Connection} from './Connection.js';
import type {CdpTarget} from './Target.js';
import {InitializationStatus} from './Target.js';
import type {TargetManagerEvents} from './TargetManageEvents.js';
import {TargetManagerEvent} from './TargetManageEvents.js';
/**
* @internal
*/
export type TargetFactory = (
targetInfo: Protocol.Target.TargetInfo,
session?: CdpCDPSession,
parentSession?: CdpCDPSession,
) => CdpTarget;
function isPageTargetBecomingPrimary(
target: CdpTarget,
newTargetInfo: Protocol.Target.TargetInfo,
): boolean {
return Boolean(target._subtype()) && !newTargetInfo.subtype;
}
/**
* TargetManager encapsulates all interactions with CDP targets and is
* responsible for coordinating the configuration of targets with the rest of
* Puppeteer. Code outside of this class should not subscribe `Target.*` events
* and only use the TargetManager events.
*
* TargetManager uses the CDP's auto-attach mechanism to intercept
* new targets and allow the rest of Puppeteer to configure listeners while
* the target is paused.
*
* @internal
*/
export class TargetManager
extends EventEmitter<TargetManagerEvents>
implements TargetManager
{
#connection: Connection;
/**
* Keeps track of the following events: 'Target.targetCreated',
* 'Target.targetDestroyed', 'Target.targetInfoChanged'.
*
* A target becomes discovered when 'Target.targetCreated' is received.
* A target is removed from this map once 'Target.targetDestroyed' is
* received.
*
* `targetFilterCallback` has no effect on this map.
*/
#discoveredTargetsByTargetId = new Map<string, Protocol.Target.TargetInfo>();
/**
* A target is added to this map once TargetManager has created
* a Target and attached at least once to it.
*/
#attachedTargetsByTargetId = new Map<string, CdpTarget>();
/**
* Tracks which sessions attach to which target.
*/
#attachedTargetsBySessionId = new Map<string, CdpTarget>();
/**
* If a target was filtered out by `targetFilterCallback`, we still receive
* events about it from CDP, but we don't forward them to the rest of Puppeteer.
*/
#ignoredTargets = new Set<string>();
#targetFilterCallback: TargetFilterCallback | undefined;
#targetFactory: TargetFactory;
#attachedToTargetListenersBySession = new WeakMap<
CDPSession | Connection,
(event: Protocol.Target.AttachedToTargetEvent) => void
>();
#detachedFromTargetListenersBySession = new WeakMap<
CDPSession | Connection,
(event: Protocol.Target.DetachedFromTargetEvent) => void
>();
#initializeDeferred = Deferred.create<void>();
#waitForInitiallyDiscoveredTargets = true;
#discoveryFilter: Protocol.Target.FilterEntry[] = [{}];
// IDs of tab targets detected while running the initial Target.setAutoAttach
// request. These are the targets whose initialization we want to await for
// before resolving puppeteer.connect() or launch() to avoid flakiness.
// Whenever a sub-target whose parent is a tab target is attached, we remove
// the tab target from this list. Once the list is empty, we resolve the
// initializeDeferred.
#targetsIdsForInit = new Set<string>();
// This is false until the connection-level Target.setAutoAttach request is
// done. It indicates whethere we are running the initial auto-attach step or
// if we are handling targets after that.
#initialAttachDone = false;
#blockList?: string[];
constructor(
connection: Connection,
targetFactory: TargetFactory,
targetFilterCallback?: TargetFilterCallback,
waitForInitiallyDiscoveredTargets = true,
networkConditions?: string[],
) {
super();
this.#connection = connection;
this.#targetFilterCallback = targetFilterCallback;
this.#targetFactory = targetFactory;
this.#waitForInitiallyDiscoveredTargets = waitForInitiallyDiscoveredTargets;
this.#blockList = networkConditions;
this.#connection.on('Target.targetCreated', this.#onTargetCreated);
this.#connection.on('Target.targetDestroyed', this.#onTargetDestroyed);
this.#connection.on('Target.targetInfoChanged', this.#onTargetInfoChanged);
this.#connection.on(
CDPSessionEvent.SessionDetached,
this.#onSessionDetached,
);
this.#setupAttachmentListeners(this.#connection);
}
async initialize(): Promise<void> {
await this.#connection.send('Target.setDiscoverTargets', {
discover: true,
filter: this.#discoveryFilter,
});
await this.#connection.send('Target.setAutoAttach', {
waitForDebuggerOnStart: true,
flatten: true,
autoAttach: true,
filter: [
{
type: 'page',
exclude: true,
},
...this.#discoveryFilter,
],
});
this.#initialAttachDone = true;
this.#finishInitializationIfReady();
await this.#initializeDeferred.valueOrThrow();
}
addToIgnoreTarget(targetId: string): void {
this.#ignoredTargets.add(targetId);
}
getChildTargets(target: CdpTarget): ReadonlySet<CdpTarget> {
return target._childTargets();
}
dispose(): void {
this.#connection.off('Target.targetCreated', this.#onTargetCreated);
this.#connection.off('Target.targetDestroyed', this.#onTargetDestroyed);
this.#connection.off('Target.targetInfoChanged', this.#onTargetInfoChanged);
this.#connection.off(
CDPSessionEvent.SessionDetached,
this.#onSessionDetached,
);
this.#removeAttachmentListeners(this.#connection);
}
getAvailableTargets(): ReadonlyMap<string, CdpTarget> {
return this.#attachedTargetsByTargetId;
}
getDiscoveredTargetInfos(): ReadonlyMap<string, Protocol.Target.TargetInfo> {
return this.#discoveredTargetsByTargetId;
}
#setupAttachmentListeners(session: CDPSession | Connection): void {
const listener = (event: Protocol.Target.AttachedToTargetEvent) => {
void this.#onAttachedToTarget(session, event);
};
assert(!this.#attachedToTargetListenersBySession.has(session));
this.#attachedToTargetListenersBySession.set(session, listener);
session.on('Target.attachedToTarget', listener);
const detachedListener = (
event: Protocol.Target.DetachedFromTargetEvent,
) => {
return this.#onDetachedFromTarget(session, event);
};
assert(!this.#detachedFromTargetListenersBySession.has(session));
this.#detachedFromTargetListenersBySession.set(session, detachedListener);
session.on('Target.detachedFromTarget', detachedListener);
}
#removeAttachmentListeners(session: CDPSession | Connection): void {
const listener = this.#attachedToTargetListenersBySession.get(session);
if (listener) {
session.off('Target.attachedToTarget', listener);
this.#attachedToTargetListenersBySession.delete(session);
}
const detachedListener =
this.#detachedFromTargetListenersBySession.get(session);
if (detachedListener) {
session.off('Target.detachedFromTarget', detachedListener);
this.#detachedFromTargetListenersBySession.delete(session);
}
}
#silentDetach = async (
session: CdpCDPSession,
parentSession: Connection | CDPSession,
): Promise<void> => {
await session.send('Runtime.runIfWaitingForDebugger').catch(debugError);
// We don't use `session.detach()` because that dispatches all commands on
// the connection instead of the parent session.
await parentSession
.send('Target.detachFromTarget', {
sessionId: session.id(),
})
.catch(debugError);
};
#getParentTarget = (
parentSession: Connection | CDPSession,
): CdpTarget | null => {
return parentSession instanceof CdpCDPSession
? parentSession.target()
: null;
};
#onSessionDetached = (session: CDPSession) => {
this.#removeAttachmentListeners(session);
};
#onTargetCreated = async (event: Protocol.Target.TargetCreatedEvent) => {
this.#discoveredTargetsByTargetId.set(
event.targetInfo.targetId,
event.targetInfo,
);
this.emit(TargetManagerEvent.TargetDiscovered, event.targetInfo);
// The connection is already attached to the browser target implicitly,
// therefore, no new CDPSession is created and we have special handling
// here.
if (event.targetInfo.type === 'browser' && event.targetInfo.attached) {
if (this.#attachedTargetsByTargetId.has(event.targetInfo.targetId)) {
return;
}
const target = this.#targetFactory(event.targetInfo, undefined);
target._initialize();
this.#attachedTargetsByTargetId.set(event.targetInfo.targetId, target);
}
};
#onTargetDestroyed = (event: Protocol.Target.TargetDestroyedEvent) => {
const targetInfo = this.#discoveredTargetsByTargetId.get(event.targetId);
this.#discoveredTargetsByTargetId.delete(event.targetId);
this.#finishInitializationIfReady(event.targetId);
if (targetInfo?.type === 'service_worker') {
// Special case for service workers: report TargetGone event when
// the worker is destroyed.
const target = this.#attachedTargetsByTargetId.get(event.targetId);
if (target) {
this.emit(TargetManagerEvent.TargetGone, target);
this.#attachedTargetsByTargetId.delete(event.targetId);
}
}
};
#onTargetInfoChanged = (event: Protocol.Target.TargetInfoChangedEvent) => {
this.#discoveredTargetsByTargetId.set(
event.targetInfo.targetId,
event.targetInfo,
);
if (
this.#ignoredTargets.has(event.targetInfo.targetId) ||
!event.targetInfo.attached
) {
return;
}
const target = this.#attachedTargetsByTargetId.get(
event.targetInfo.targetId,
);
if (!target) {
return;
}
const previousURL = target.url();
const wasInitialized =
target._initializedDeferred.value() === InitializationStatus.SUCCESS;
if (isPageTargetBecomingPrimary(target, event.targetInfo)) {
const session = target._session();
assert(
session,
'Target that is being activated is missing a CDPSession.',
);
session.parentSession()?.emit(CDPSessionEvent.Swapped, session);
}
target._targetInfoChanged(event.targetInfo);
if (wasInitialized && previousURL !== target.url()) {
this.emit(TargetManagerEvent.TargetChanged, {
target,
wasInitialized,
previousURL,
});
}
};
#onAttachedToTarget = async (
parentSession: Connection | CDPSession,
event: Protocol.Target.AttachedToTargetEvent,
) => {
const targetInfo = event.targetInfo;
const session = this.#connection._session(event.sessionId);
if (!session) {
throw new Error(`Session ${event.sessionId} was not created.`);
}
if (!this.#connection.isAutoAttached(targetInfo.targetId)) {
return;
}
// If we connect to a browser that is already open,
// immediately detach from any tab that is on the blocklist.
if (!this.#initialAttachDone && !this.#isUrlAllowed(targetInfo.url)) {
await this.#silentDetach(session, parentSession);
return;
}
// Special case for service workers: being attached to service workers will
// prevent them from ever being destroyed. Therefore, we silently detach
// from service workers unless the connection was manually created via
// `page.worker()`. To determine this, we use
// `this.#connection.isAutoAttached(targetInfo.targetId)`. In the future, we
// should determine if a target is auto-attached or not with the help of
// CDP.
if (targetInfo.type === 'service_worker') {
await this.#silentDetach(session, parentSession);
if (
this.#attachedTargetsByTargetId.has(targetInfo.targetId) ||
this.#ignoredTargets.has(targetInfo.targetId) ||
!this.#discoveredTargetsByTargetId.has(targetInfo.targetId)
) {
return;
}
const target = this.#targetFactory(targetInfo);
target._initialize();
this.#attachedTargetsByTargetId.set(targetInfo.targetId, target);
this.emit(TargetManagerEvent.TargetAvailable, target);
return;
}
let target = this.#attachedTargetsByTargetId.get(targetInfo.targetId);
const isExistingTarget = target !== undefined;
if (!target) {
target = this.#targetFactory(
targetInfo,
session,
parentSession instanceof CdpCDPSession ? parentSession : undefined,
);
}
const parentTarget = this.#getParentTarget(parentSession);
if (this.#targetFilterCallback && !this.#targetFilterCallback(target)) {
this.#ignoredTargets.add(targetInfo.targetId);
if (parentTarget?.type() === 'tab') {
this.#finishInitializationIfReady(parentTarget._targetId);
}
await this.#silentDetach(session, parentSession);
return;
}
if (
this.#waitForInitiallyDiscoveredTargets &&
event.targetInfo.type === 'tab' &&
!this.#initialAttachDone
) {
this.#targetsIdsForInit.add(event.targetInfo.targetId);
}
this.#setupAttachmentListeners(session);
if (isExistingTarget) {
session.setTarget(target);
this.#attachedTargetsBySessionId.set(session.id(), target);
} else {
target._initialize();
this.#attachedTargetsByTargetId.set(targetInfo.targetId, target);
this.#attachedTargetsBySessionId.set(session.id(), target);
}
parentTarget?._addChildTarget(target);
parentSession.emit(CDPSessionEvent.Ready, session);
if (!isExistingTarget) {
this.emit(TargetManagerEvent.TargetAvailable, target);
}
if (parentTarget?.type() === 'tab') {
this.#finishInitializationIfReady(parentTarget._targetId);
}
// TODO: the browser might be shutting down here. What do we do with the
// error?
await Promise.all([
session.send('Target.setAutoAttach', {
waitForDebuggerOnStart: true,
flatten: true,
autoAttach: true,
filter: this.#discoveryFilter,
}),
this.#maybeSetupNetworkConditions(session),
session.send('Runtime.runIfWaitingForDebugger'),
]).catch(debugError);
};
#finishInitializationIfReady(targetId?: string): void {
if (targetId !== undefined) {
this.#targetsIdsForInit.delete(targetId);
}
// If we are still initializing it might be that we have not learned about
// some targets yet.
if (!this.#initialAttachDone) {
return;
}
if (this.#targetsIdsForInit.size === 0) {
this.#initializeDeferred.resolve();
}
}
#onDetachedFromTarget = (
parentSession: Connection | CDPSession,
event: Protocol.Target.DetachedFromTargetEvent,
) => {
const target = this.#attachedTargetsBySessionId.get(event.sessionId);
this.#attachedTargetsBySessionId.delete(event.sessionId);
if (!target) {
return;
}
if (parentSession instanceof CdpCDPSession) {
parentSession.target()._removeChildTarget(target);
}
this.#attachedTargetsByTargetId.delete(target._targetId);
this.emit(TargetManagerEvent.TargetGone, target);
};
/**
* Helper to validate URL against blocklist patterns
*/
#isUrlAllowed = (url: string): boolean => {
if (!this.#blockList) {
return true;
}
// Always allow internal or setup pages
if (!url || url === 'about:blank') {
return true;
}
for (const rule of this.#blockList) {
try {
const pattern = new URLPattern(rule);
if (pattern.test(url)) {
return false; // return false as url matches pattern from blockList
}
} catch {
debugError(`Invalid URL pattern: ${rule}`);
}
}
return true;
};
#maybeSetupNetworkConditions = async (session: CDPSession): Promise<void> => {
if (!this.#blockList?.length) {
return;
}
const matchedNetworkConditions = this.#blockList.map(pattern => {
return {
urlPattern: pattern,
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1,
};
});
await session.send('Network.emulateNetworkConditionsByRule', {
matchedNetworkConditions,
offline: true,
});
};
}

140
node_modules/puppeteer-core/src/cdp/Tracing.ts generated vendored Normal file
View File

@@ -0,0 +1,140 @@
/**
* @license
* Copyright 2017 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {CDPSession} from '../api/CDPSession.js';
import {
getReadableAsTypedArray,
getReadableFromProtocolStream,
} from '../common/util.js';
import {assert} from '../util/assert.js';
import {Deferred} from '../util/Deferred.js';
import {isErrorLike} from '../util/ErrorLike.js';
/**
* @public
*/
export interface TracingOptions {
path?: string;
screenshots?: boolean;
categories?: string[];
}
/**
* The Tracing class exposes the tracing audit interface.
* @remarks
* You can use `tracing.start` and `tracing.stop` to create a trace file
* which can be opened in Chrome DevTools or {@link https://chromedevtools.github.io/timeline-viewer/ | timeline viewer}.
*
* @example
*
* ```ts
* await page.tracing.start({path: 'trace.json'});
* await page.goto('https://www.google.com');
* await page.tracing.stop();
* ```
*
* @public
*/
export class Tracing {
#client: CDPSession;
#recording = false;
#path?: string;
/**
* @internal
*/
constructor(client: CDPSession) {
this.#client = client;
}
/**
* @internal
*/
updateClient(client: CDPSession): void {
this.#client = client;
}
/**
* Starts a trace for the current page.
* @remarks
* Only one trace can be active at a time per browser.
*
* @param options - Optional `TracingOptions`.
*/
async start(options: TracingOptions = {}): Promise<void> {
assert(
!this.#recording,
'Cannot start recording trace while already recording trace.',
);
const defaultCategories = [
'-*',
'devtools.timeline',
'v8.execute',
'disabled-by-default-devtools.timeline',
'disabled-by-default-devtools.timeline.frame',
'toplevel',
'blink.console',
'blink.user_timing',
'latencyInfo',
'disabled-by-default-devtools.timeline.stack',
'disabled-by-default-v8.cpu_profiler',
];
const {path, screenshots = false, categories = defaultCategories} = options;
if (screenshots) {
categories.push('disabled-by-default-devtools.screenshot');
}
const excludedCategories = categories
.filter(cat => {
return cat.startsWith('-');
})
.map(cat => {
return cat.slice(1);
});
const includedCategories = categories.filter(cat => {
return !cat.startsWith('-');
});
this.#path = path;
this.#recording = true;
await this.#client.send('Tracing.start', {
transferMode: 'ReturnAsStream',
traceConfig: {
excludedCategories,
includedCategories,
},
});
}
/**
* Stops a trace started with the `start` method.
* @returns Promise which resolves to buffer with trace data.
*/
async stop(): Promise<Uint8Array | undefined> {
const contentDeferred = Deferred.create<Uint8Array | undefined>();
this.#client.once('Tracing.tracingComplete', async event => {
try {
assert(event.stream, 'Missing "stream"');
const readable = await getReadableFromProtocolStream(
this.#client,
event.stream,
);
const typedArray = await getReadableAsTypedArray(readable, this.#path);
contentDeferred.resolve(typedArray ?? undefined);
} catch (error) {
if (isErrorLike(error)) {
contentDeferred.reject(error);
} else {
contentDeferred.reject(new Error(`Unknown error: ${error}`));
}
}
});
await this.#client.send('Tracing.end');
this.#recording = false;
return await contentDeferred.valueOrThrow();
}
}

446
node_modules/puppeteer-core/src/cdp/WebMCP.ts generated vendored Normal file
View File

@@ -0,0 +1,446 @@
/**
* @license
* Copyright 2026 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {Protocol} from 'devtools-protocol';
import type {CDPSession} from '../api/CDPSession.js';
import type {ElementHandle} from '../api/ElementHandle.js';
import type {Frame} from '../api/Frame.js';
import type {ConsoleMessageLocation} from '../common/ConsoleMessage.js';
import {EventEmitter} from '../common/EventEmitter.js';
import {debugError} from '../common/util.js';
import type {CdpFrame} from './Frame.js';
import type {FrameManager} from './FrameManager.js';
import {FrameManagerEvent} from './FrameManagerEvents.js';
import {MAIN_WORLD} from './IsolatedWorlds.js';
/**
* Tool annotations
*
* @public
*/
export interface WebMCPAnnotation {
/**
* A hint indicating that the tool does not modify any state.
*/
readOnly?: boolean;
/**
* If the declarative tool was declared with the autosubmit attribute.
*/
autosubmit?: boolean;
}
/**
* Represents the status of a tool invocation.
*
* @public
*/
export type WebMCPInvocationStatus = 'Completed' | 'Canceled' | 'Error';
interface ProtocolWebMCPTool {
name: string;
description: string;
inputSchema?: object;
annotations?: WebMCPAnnotation;
frameId: string;
backendNodeId?: number;
stackTrace?: Protocol.Runtime.StackTrace;
}
interface ProtocolWebMCPToolsAddedEvent {
tools: ProtocolWebMCPTool[];
}
interface ProtocolWebMCPRemovedTool {
name: string;
frameId: string;
}
interface ProtocolWebMCPToolsRemovedEvent {
tools: ProtocolWebMCPRemovedTool[];
}
interface ProtocolWebMCPToolInvokedEvent {
toolName: string;
frameId: string;
invocationId: string;
input: string;
}
interface ProtocolWebMCPToolRespondedEvent {
invocationId: string;
status: WebMCPInvocationStatus;
output?: any;
errorText?: string;
exception?: Protocol.Runtime.RemoteObject;
}
/**
* Represents a registered WebMCP tool available on the page.
*
* @public
*/
export class WebMCPTool extends EventEmitter<{
/** Emitted when invocation starts. */
toolinvoked: WebMCPToolCall;
}> {
#webmcp: WebMCP;
#backendNodeId?: number;
#formElement?: ElementHandle<HTMLFormElement>;
/**
* Tool name.
*/
name: string;
/**
* Tool description.
*/
description: string;
/**
* Schema for the tool's input parameters.
*/
inputSchema?: object;
/**
* Optional annotations for the tool.
*/
annotations?: WebMCPAnnotation;
/**
* Frame the tool was defined for.
*/
frame: Frame;
/**
* Source location that defined the tool (if available).
*/
location?: ConsoleMessageLocation;
/**
* @internal
*/
rawStackTrace?: Protocol.Runtime.StackTrace;
/**
* @internal
*/
constructor(webmcp: WebMCP, tool: ProtocolWebMCPTool, frame: Frame) {
super();
this.#webmcp = webmcp;
this.name = tool.name;
this.description = tool.description;
this.inputSchema = tool.inputSchema;
this.annotations = tool.annotations;
this.frame = frame;
this.#backendNodeId = tool.backendNodeId;
if (tool.stackTrace?.callFrames.length) {
this.location = {
url: tool.stackTrace.callFrames[0]!.url,
lineNumber: tool.stackTrace.callFrames[0]!.lineNumber,
columnNumber: tool.stackTrace.callFrames[0]!.columnNumber,
};
}
this.rawStackTrace = tool.stackTrace;
}
/**
* The corresponding ElementHandle when tool was registered via a form.
*/
get formElement(): Promise<ElementHandle<HTMLFormElement> | undefined> {
return (async () => {
if (this.#formElement && !this.#formElement.disposed) {
return this.#formElement;
}
if (!this.#backendNodeId) {
return undefined;
}
this.#formElement = (await (this.frame as CdpFrame).worlds[
MAIN_WORLD
].adoptBackendNode(
this.#backendNodeId,
)) as ElementHandle<HTMLFormElement>;
return this.#formElement;
})();
}
/**
* Executes tool with input parameters, matching tool's `inputSchema`.
*/
async execute(input: object = {}): Promise<WebMCPToolCallResult> {
const {invocationId} = await this.#webmcp.invokeTool(this, input);
return await new Promise<WebMCPToolCallResult>(resolve => {
const handler = (event: WebMCPToolCallResult) => {
if (event.id === invocationId) {
this.#webmcp.off('toolresponded', handler);
resolve(event);
}
};
this.#webmcp.on('toolresponded', handler);
});
}
}
/**
* @public
*/
export interface WebMCPToolsAddedEvent {
/**
* Array of tools that were added.
*/
tools: WebMCPTool[];
}
/**
* @public
*/
export interface WebMCPToolsRemovedEvent {
/**
* Array of tools that were removed.
*/
tools: WebMCPTool[];
}
/**
* @public
*/
export class WebMCPToolCall {
/**
* Tool invocation identifier.
*/
id: string;
/**
* Tool that was called.
*/
tool: WebMCPTool;
/**
* The input parameters used for the call.
*/
input: object;
/**
* @internal
*/
constructor(invocationId: string, tool: WebMCPTool, input: string) {
this.id = invocationId;
this.tool = tool;
try {
this.input = JSON.parse(input);
} catch (error) {
this.input = {};
debugError(error);
}
}
}
/**
* @public
*/
export interface WebMCPToolCallResult {
/**
* Tool invocation identifier.
*/
id: string;
/**
* The corresponding tool call if available.
*/
call?: WebMCPToolCall;
/**
* Status of the invocation.
*/
status: WebMCPInvocationStatus;
/**
* Output or error delivered as delivered to the agent. Missing if `status` is anything
* other than Completed.
*/
output?: any;
/**
* Error text.
*/
errorText?: string;
/**
* The exception object, if the javascript tool threw an error.
*/
exception?: Protocol.Runtime.RemoteObject;
}
/**
* The experimental WebMCP class provides an API for the WebMCP API.
*
* See the
* {@link https://pptr.dev/guides/webmcp|WebMCP guide}
* for more details.
*
* @example
*
* ```ts
* await page.goto('https://www.example.com');
* const tools = page.webmcp.tools();
* for (const tool of tools) {
* console.log(`Tool found: ${tool.name} - ${tool.description}`);
* }
* ```
*
* @experimental
* @public
*/
export class WebMCP extends EventEmitter<{
/** Emitted when tools are added. */
toolsadded: WebMCPToolsAddedEvent;
/** Emitted when tools are removed. */
toolsremoved: WebMCPToolsRemovedEvent;
/** Emitted when a tool invocation starts. */
toolinvoked: WebMCPToolCall;
/** Emitted when a tool invocation completes or fails. */
toolresponded: WebMCPToolCallResult;
}> {
#client: CDPSession;
#frameManager: FrameManager;
#tools = new Map<string, Map<string, WebMCPTool>>();
#pendingCalls = new Map<string, WebMCPToolCall>();
#onToolsAdded = (event: ProtocolWebMCPToolsAddedEvent) => {
const tools: WebMCPTool[] = [];
for (const tool of event.tools) {
const frame = this.#frameManager.frame(tool.frameId);
if (!frame) {
continue;
}
const frameTools = this.#tools.get(tool.frameId) ?? new Map();
if (!this.#tools.has(tool.frameId)) {
this.#tools.set(tool.frameId, frameTools);
}
const addedTool = new WebMCPTool(this, tool, frame);
frameTools.set(tool.name, addedTool);
tools.push(addedTool);
}
this.emit('toolsadded', {tools});
};
#onToolsRemoved = (event: ProtocolWebMCPToolsRemovedEvent) => {
const tools: WebMCPTool[] = [];
event.tools.forEach(tool => {
const removedTool = this.#tools.get(tool.frameId)?.get(tool.name);
if (removedTool) {
tools.push(removedTool);
}
this.#tools.get(tool.frameId)?.delete(tool.name);
});
this.emit('toolsremoved', {tools});
};
#onToolInvoked = (event: ProtocolWebMCPToolInvokedEvent) => {
const tool = this.#tools.get(event.frameId)?.get(event.toolName);
if (!tool) {
return;
}
const call = new WebMCPToolCall(event.invocationId, tool, event.input);
this.#pendingCalls.set(call.id, call);
tool.emit('toolinvoked', call);
this.emit('toolinvoked', call);
};
#onToolResponded = (event: ProtocolWebMCPToolRespondedEvent) => {
const call = this.#pendingCalls.get(event.invocationId);
if (call) {
this.#pendingCalls.delete(event.invocationId);
}
const response: WebMCPToolCallResult = {
id: event.invocationId,
call: call,
status: event.status,
output: event.output,
errorText: event.errorText,
exception: event.exception,
};
this.emit('toolresponded', response);
};
#onFrameNavigated = (frame: Frame) => {
this.#pendingCalls.clear();
const frameTools = this.#tools.get(frame._id);
if (!frameTools) {
return;
}
const tools = Array.from(frameTools.values());
this.#tools.delete(frame._id);
if (tools.length) {
this.emit('toolsremoved', {tools});
}
};
/**
* @internal
*/
constructor(client: CDPSession, frameManager: FrameManager) {
super();
this.#client = client;
this.#frameManager = frameManager;
this.#frameManager.on(
FrameManagerEvent.FrameNavigated,
this.#onFrameNavigated,
);
this.#bindListeners();
}
/**
* @internal
*/
async initialize(): Promise<void> {
// @ts-expect-error WebMCP is not yet in the Protocol types.
return await this.#client.send('WebMCP.enable').catch(debugError);
}
/**
* @internal
*/
async invokeTool(
tool: WebMCPTool,
input: object,
): Promise<{invocationId: string}> {
// @ts-expect-error WebMCP is not yet in the Protocol types.
return await this.#client.send('WebMCP.invokeTool', {
frameId: tool.frame._id,
toolName: tool.name,
input,
});
}
/**
* Gets all WebMCP tools defined by the page.
*/
tools(): WebMCPTool[] {
return Array.from(this.#tools.values()).flatMap(toolMap => {
return Array.from(toolMap.values());
});
}
#bindListeners(): void {
// @ts-expect-error WebMCP is not yet in the Protocol types.
this.#client.on('WebMCP.toolsAdded', this.#onToolsAdded);
// @ts-expect-error WebMCP is not yet in the Protocol types.
this.#client.on('WebMCP.toolsRemoved', this.#onToolsRemoved);
// @ts-expect-error WebMCP is not yet in the Protocol types.
this.#client.on('WebMCP.toolInvoked', this.#onToolInvoked);
// @ts-expect-error WebMCP is not yet in the Protocol types.
this.#client.on('WebMCP.toolResponded', this.#onToolResponded);
}
/**
* @internal
*/
updateClient(client: CDPSession): void {
// @ts-expect-error WebMCP is not yet in the Protocol types.
this.#client.off('WebMCP.toolsAdded', this.#onToolsAdded);
// @ts-expect-error WebMCP is not yet in the Protocol types.
this.#client.off('WebMCP.toolsRemoved', this.#onToolsRemoved);
// @ts-expect-error WebMCP is not yet in the Protocol types.
this.#client.off('WebMCP.toolInvoked', this.#onToolInvoked);
// @ts-expect-error WebMCP is not yet in the Protocol types.
this.#client.off('WebMCP.toolResponded', this.#onToolResponded);
this.#client = client;
this.#bindListeners();
}
}

139
node_modules/puppeteer-core/src/cdp/WebWorker.ts generated vendored Normal file
View File

@@ -0,0 +1,139 @@
/**
* @license
* Copyright 2018 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {Protocol} from 'devtools-protocol';
import {CDPSessionEvent, type CDPSession} from '../api/CDPSession.js';
import type {Realm} from '../api/Realm.js';
import {TargetType} from '../api/Target.js';
import {
WebWorker,
WebWorkerEvent,
type WebWorkerEvents,
} from '../api/WebWorker.js';
import {EventEmitter} from '../common/EventEmitter.js';
import {TimeoutSettings} from '../common/TimeoutSettings.js';
import {debugError} from '../common/util.js';
import {ExecutionContext} from './ExecutionContext.js';
import {IsolatedWorld} from './IsolatedWorld.js';
import {MAIN_WORLD} from './IsolatedWorlds.js';
import type {NetworkManager} from './NetworkManager.js';
import {createConsoleMessage} from './utils.js';
/**
* @internal
*/
export type ExceptionThrownCallback = (
event: Protocol.Runtime.ExceptionThrownEvent,
) => void;
/**
* @internal
*/
export class CdpWebWorker extends WebWorker {
#world: IsolatedWorld;
#client: CDPSession;
readonly #id: string;
readonly #targetType: TargetType;
readonly #emitter: EventEmitter<WebWorkerEvents>;
get internalEmitter(): EventEmitter<WebWorkerEvents> {
return this.#emitter;
}
constructor(
client: CDPSession,
url: string,
targetId: string,
targetType: TargetType,
exceptionThrown: ExceptionThrownCallback,
networkManager?: NetworkManager,
) {
super(url);
this.#id = targetId;
this.#client = client;
this.#targetType = targetType;
this.#world = new IsolatedWorld(this, new TimeoutSettings(), MAIN_WORLD);
this.#emitter = new EventEmitter<WebWorkerEvents>();
this.#client.once('Runtime.executionContextCreated', async event => {
this.#world.setContext(
new ExecutionContext(client, event.context, this.#world),
);
});
this.#world.emitter.on('consoleapicalled', async event => {
try {
const values = event.args.map(arg => {
return this.#world.createCdpHandle(arg);
});
const noInternalListeners =
this.#emitter.listenerCount(WebWorkerEvent.Console) === 0;
const noWorkerListeners =
this.listenerCount(WebWorkerEvent.Console) === 0;
if (noInternalListeners && noWorkerListeners) {
// eslint-disable-next-line max-len -- The comment is long.
// eslint-disable-next-line @puppeteer/use-using -- These are not owned by this function.
for (const value of values) {
void value.dispose().catch(debugError);
}
return;
}
const consoleMessages = createConsoleMessage(event, values, this.#id);
this.#emitter.emit(WebWorkerEvent.Console, consoleMessages);
if (!noWorkerListeners) {
this.emit(WebWorkerEvent.Console, consoleMessages);
}
} catch (err) {
debugError(err);
}
});
this.#client.on('Runtime.exceptionThrown', exceptionThrown);
this.#client.once(CDPSessionEvent.Disconnected, () => {
this.#world.dispose();
});
// This might fail if the target is closed before we receive all execution contexts.
networkManager?.addClient(this.#client).catch(debugError);
this.#client.send('Runtime.enable').catch(debugError);
}
mainRealm(): Realm {
return this.#world;
}
get client(): CDPSession {
return this.#client;
}
override async close(): Promise<void> {
switch (this.#targetType) {
case TargetType.SERVICE_WORKER: {
// For service workers we need to close the target and detach to allow
// the worker to stop.
await this.client.connection()?.send('Target.closeTarget', {
targetId: this.#id,
});
await this.client.connection()?.send('Target.detachFromTarget', {
sessionId: this.client.id(),
});
break;
}
case TargetType.SHARED_WORKER: {
await this.client.connection()?.send('Target.closeTarget', {
targetId: this.#id,
});
break;
}
default:
await this.evaluate(() => {
self.close();
});
}
}
}

44
node_modules/puppeteer-core/src/cdp/cdp.ts generated vendored Normal file
View File

@@ -0,0 +1,44 @@
/**
* @license
* Copyright 2023 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
export * from './Accessibility.js';
export * from './Binding.js';
export * from './BluetoothEmulation.js';
export * from './Browser.js';
export * from './BrowserContext.js';
export * from './BrowserConnector.js';
export * from './CdpSession.js';
export * from './Connection.js';
export * from './Coverage.js';
export * from './CdpPreloadScript.js';
export * from './DeviceRequestPrompt.js';
export * from './Dialog.js';
export * from './ElementHandle.js';
export * from './EmulationManager.js';
export * from './ExecutionContext.js';
export * from './ExtensionTransport.js';
export * from './Frame.js';
export * from './FrameManager.js';
export * from './FrameManagerEvents.js';
export * from './FrameTree.js';
export * from './HTTPRequest.js';
export * from './HTTPResponse.js';
export * from './Input.js';
export * from './IsolatedWorld.js';
export * from './IsolatedWorlds.js';
export * from './JSHandle.js';
export * from './LifecycleWatcher.js';
export * from './NetworkEventManager.js';
export * from './NetworkManager.js';
export * from './Page.js';
export * from './PredefinedNetworkConditions.js';
export * from './Target.js';
export * from './TargetManager.js';
export * from './TargetManageEvents.js';
export * from './Tracing.js';
export * from './WebMCP.js';
export * from './utils.js';
export * from './WebWorker.js';

323
node_modules/puppeteer-core/src/cdp/utils.ts generated vendored Normal file
View File

@@ -0,0 +1,323 @@
/**
* @license
* Copyright 2017 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {Protocol} from 'devtools-protocol';
import type {JSHandle} from '../api/JSHandle.js';
import {
ConsoleMessage,
type ConsoleMessageType,
} from '../common/ConsoleMessage.js';
import {PuppeteerURL, evaluationString} from '../common/util.js';
import {assert} from '../util/assert.js';
/**
* @internal
*/
export function createConsoleMessage(
event: Protocol.Runtime.ConsoleAPICalledEvent,
values: JSHandle[],
targetId?: string,
): ConsoleMessage {
const textTokens = [];
// eslint-disable-next-line max-len -- The comment is long.
// eslint-disable-next-line @puppeteer/use-using -- These are not owned by this function.
for (const arg of values) {
textTokens.push(valueFromJSHandle(arg));
}
const stackTraceLocations = [];
if (event.stackTrace) {
for (const callFrame of event.stackTrace.callFrames) {
stackTraceLocations.push({
url: callFrame.url,
lineNumber: callFrame.lineNumber,
columnNumber: callFrame.columnNumber,
});
}
}
return new ConsoleMessage(
convertConsoleMessageLevel(event.type),
textTokens.join(' '),
values,
stackTraceLocations,
undefined,
event.stackTrace,
targetId,
);
}
/**
* @internal
*/
export function createEvaluationError(
details: Protocol.Runtime.ExceptionDetails,
): unknown {
let name: string;
let message: string;
if (!details.exception) {
name = 'Error';
message = details.text;
} else if (
(details.exception.type !== 'object' ||
details.exception.subtype !== 'error') &&
!details.exception.objectId
) {
return valueFromPrimitiveRemoteObject(details.exception);
} else {
const detail = getErrorDetails(details);
name = detail.name;
message = detail.message;
}
const messageHeight = message.split('\n').length;
const error = new Error(message);
error.name = name;
const stackLines = error.stack!.split('\n');
const messageLines = stackLines.splice(0, messageHeight);
// The first line is this function which we ignore.
stackLines.shift();
if (details.stackTrace && stackLines.length < Error.stackTraceLimit) {
for (const frame of details.stackTrace.callFrames.reverse()) {
if (
PuppeteerURL.isPuppeteerURL(frame.url) &&
frame.url !== PuppeteerURL.INTERNAL_URL
) {
const url = PuppeteerURL.parse(frame.url);
stackLines.unshift(
` at ${frame.functionName || url.functionName} (${
url.functionName
} at ${url.siteString}, <anonymous>:${frame.lineNumber}:${
frame.columnNumber
})`,
);
} else {
stackLines.push(
` at ${frame.functionName || '<anonymous>'} (${frame.url}:${
frame.lineNumber
}:${frame.columnNumber})`,
);
}
if (stackLines.length >= Error.stackTraceLimit) {
break;
}
}
}
error.stack = [...messageLines, ...stackLines].join('\n');
return error;
}
const getErrorDetails = (details: Protocol.Runtime.ExceptionDetails) => {
let name = '';
let message: string;
const lines = details.exception?.description?.split('\n at ') ?? [];
const size = Math.min(
details.stackTrace?.callFrames.length ?? 0,
lines.length - 1,
);
lines.splice(-size, size);
if (details.exception?.className) {
name = details.exception.className;
}
message = lines.join('\n');
if (name && message.startsWith(`${name}: `)) {
message = message.slice(name.length + 2);
}
return {message, name};
};
/**
* @internal
*/
export function createClientError(
details: Protocol.Runtime.ExceptionDetails,
): Error | unknown {
let name: string;
let message: string;
if (!details.exception) {
name = 'Error';
message = details.text;
} else if (
(details.exception.type !== 'object' ||
details.exception.subtype !== 'error') &&
!details.exception.objectId
) {
return valueFromPrimitiveRemoteObject(details.exception);
} else {
const detail = getErrorDetails(details);
name = detail.name;
message = detail.message;
}
const error = new Error(message);
error.name = name;
const messageHeight = error.message.split('\n').length;
const messageLines = error.stack!.split('\n').splice(0, messageHeight);
const stackLines = [];
if (details.stackTrace) {
for (const frame of details.stackTrace.callFrames) {
// Note we need to add `1` because the values are 0-indexed.
stackLines.push(
` at ${frame.functionName || '<anonymous>'} (${frame.url}:${
frame.lineNumber + 1
}:${frame.columnNumber + 1})`,
);
if (stackLines.length >= Error.stackTraceLimit) {
break;
}
}
}
error.stack = [...messageLines, ...stackLines].join('\n');
return error;
}
/**
* @internal
*/
export function valueFromJSHandle(handle: JSHandle): unknown {
const remoteObject = handle.remoteObject();
if (remoteObject.objectId) {
return valueFromRemoteObjectReference(handle);
} else {
return valueFromPrimitiveRemoteObject(remoteObject);
}
}
/**
* @internal
*/
export function valueFromRemoteObjectReference(handle: JSHandle): string {
const remoteObject = handle.remoteObject();
assert(
remoteObject.objectId,
'Cannot extract value when no objectId is given',
);
const description = remoteObject.description ?? '';
if (remoteObject.subtype === 'error' && description) {
const newlineIdx = description.indexOf('\n');
if (newlineIdx === -1) {
return description;
}
return description.slice(0, newlineIdx);
}
return `[${remoteObject.subtype || remoteObject.type} ${remoteObject.className}]`;
}
/**
* @internal
*/
export function valueFromPrimitiveRemoteObject(
remoteObject: Protocol.Runtime.RemoteObject,
): unknown {
assert(!remoteObject.objectId, 'Cannot extract value when objectId is given');
if (remoteObject.unserializableValue) {
if (remoteObject.type === 'bigint') {
return BigInt(remoteObject.unserializableValue.replace('n', ''));
}
switch (remoteObject.unserializableValue) {
case '-0':
return -0;
case 'NaN':
return NaN;
case 'Infinity':
return Infinity;
case '-Infinity':
return -Infinity;
default:
throw new Error(
'Unsupported unserializable value: ' +
remoteObject.unserializableValue,
);
}
}
return remoteObject.value;
}
/**
* @internal
*/
export function addPageBinding(
type: string,
name: string,
prefix: string,
): void {
// Depending on the frame loading state either Runtime.evaluate or
// Page.addScriptToEvaluateOnNewDocument might succeed. Let's check that we
// don't re-wrap Puppeteer's binding.
// @ts-expect-error: In a different context.
if (globalThis[name]) {
return;
}
// We replace the CDP binding with a Puppeteer binding.
Object.assign(globalThis, {
[name](...args: unknown[]): Promise<unknown> {
// This is the Puppeteer binding.
// @ts-expect-error: In a different context.
const callPuppeteer = globalThis[name];
callPuppeteer.args ??= new Map();
callPuppeteer.callbacks ??= new Map();
const seq = (callPuppeteer.lastSeq ?? 0) + 1;
callPuppeteer.lastSeq = seq;
callPuppeteer.args.set(seq, args);
// @ts-expect-error: In a different context.
// Needs to be the same as CDP_BINDING_PREFIX.
globalThis[prefix + name](
JSON.stringify({
type,
name,
seq,
args,
isTrivial: !args.some(value => {
return value instanceof Node;
}),
}),
);
return new Promise((resolve, reject) => {
callPuppeteer.callbacks.set(seq, {
resolve(value: unknown) {
callPuppeteer.args.delete(seq);
resolve(value);
},
reject(value?: unknown) {
callPuppeteer.args.delete(seq);
reject(value);
},
});
});
},
});
}
/**
* @internal
*/
export const CDP_BINDING_PREFIX = 'puppeteer_';
/**
* @internal
*/
export function pageBindingInitString(type: string, name: string): string {
return evaluationString(addPageBinding, type, name, CDP_BINDING_PREFIX);
}
/**
* @internal
*/
export function convertConsoleMessageLevel(method: string): ConsoleMessageType {
switch (method) {
case 'warning':
return 'warn';
default:
return method as ConsoleMessageType;
}
}

View File

@@ -0,0 +1,89 @@
/**
* @license
* Copyright 2020 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {ElementHandle} from '../api/ElementHandle.js';
import {assert} from '../util/assert.js';
import {AsyncIterableUtil} from '../util/AsyncIterableUtil.js';
import {QueryHandler, type QuerySelector} from './QueryHandler.js';
import type {AwaitableIterable} from './types.js';
interface ARIASelector {
name?: string;
role?: string;
}
const isKnownAttribute = (
attribute: string,
): attribute is keyof ARIASelector => {
return ['name', 'role'].includes(attribute);
};
/**
* The selectors consist of an accessible name to query for and optionally
* further aria attributes on the form `[<attribute>=<value>]`.
* Currently, we only support the `name` and `role` attribute.
* The following examples showcase how the syntax works wrt. querying:
*
* - 'title[role="heading"]' queries for elements with name 'title' and role 'heading'.
* - '[role="image"]' queries for elements with role 'image' and any name.
* - 'label' queries for elements with name 'label' and any role.
* - '[name=""][role="button"]' queries for elements with no name and role 'button'.
*/
const ATTRIBUTE_REGEXP =
/\[\s*(?<attribute>\w+)\s*=\s*(?<quote>"|')(?<value>\\.|.*?(?=\k<quote>))\k<quote>\s*\]/g;
const parseARIASelector = (selector: string): ARIASelector => {
if (selector.length > 10_000) {
throw new Error(`Selector ${selector} is too long`);
}
const queryOptions: ARIASelector = {};
const defaultName = selector.replace(
ATTRIBUTE_REGEXP,
(_, attribute, __, value) => {
assert(
isKnownAttribute(attribute),
`Unknown aria attribute "${attribute}" in selector`,
);
queryOptions[attribute] = value;
return '';
},
);
if (defaultName && !queryOptions.name) {
queryOptions.name = defaultName;
}
return queryOptions;
};
/**
* @internal
*/
export class ARIAQueryHandler extends QueryHandler {
static override querySelector: QuerySelector = async (
node,
selector,
{ariaQuerySelector},
) => {
return await ariaQuerySelector(node, selector);
};
static override async *queryAll(
element: ElementHandle<Node>,
selector: string,
): AwaitableIterable<ElementHandle<Node>> {
const {name, role} = parseARIASelector(selector);
yield* element.queryAXTree(name, role);
}
static override queryOne = async (
element: ElementHandle<Node>,
selector: string,
): Promise<ElementHandle<Node> | null> => {
return (
(await AsyncIterableUtil.first(this.queryAll(element, selector))) ?? null
);
};
}

View File

@@ -0,0 +1,174 @@
/**
* @license
* Copyright 2023 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {Browser} from '../api/Browser.js';
import {_connectToBiDiBrowser} from '../bidi/BrowserConnector.js';
import {_connectToCdpBrowser} from '../cdp/BrowserConnector.js';
import {environment, isNode} from '../environment.js';
import {assert} from '../util/assert.js';
import {isErrorLike} from '../util/ErrorLike.js';
import type {ConnectionTransport} from './ConnectionTransport.js';
import type {ConnectOptions} from './ConnectOptions.js';
const getWebSocketTransportClass = async () => {
return isNode
? (await import('../node/NodeWebSocketTransport.js')).NodeWebSocketTransport
: (await import('../common/BrowserWebSocketTransport.js'))
.BrowserWebSocketTransport;
};
/**
* Users should never call this directly; it's called when calling
* `puppeteer.connect`. This method attaches Puppeteer to an existing browser instance.
*
* @internal
*/
export async function _connectToBrowser(
options: ConnectOptions,
): Promise<Browser> {
const {connectionTransport, endpointUrl} =
await getConnectionTransport(options);
if (options.protocol === 'webDriverBiDi') {
const bidiBrowser = await _connectToBiDiBrowser(
connectionTransport,
endpointUrl,
options,
);
return bidiBrowser;
} else {
const cdpBrowser = await _connectToCdpBrowser(
connectionTransport,
endpointUrl,
options,
);
return cdpBrowser;
}
}
/**
* Establishes a websocket connection by given options and returns both transport and
* endpoint url the transport is connected to.
*/
async function getConnectionTransport(
options: ConnectOptions,
): Promise<{connectionTransport: ConnectionTransport; endpointUrl: string}> {
const {
browserWSEndpoint,
browserURL,
channel,
transport,
headers = {},
} = options;
assert(
Number(!!browserWSEndpoint) +
Number(!!browserURL) +
Number(!!transport) +
Number(!!channel) ===
1,
'Exactly one of browserWSEndpoint, browserURL, transport or channel must be passed to puppeteer.connect',
);
if (transport) {
return {connectionTransport: transport, endpointUrl: ''};
} else if (browserWSEndpoint) {
const WebSocketClass = await getWebSocketTransportClass();
const connectionTransport: ConnectionTransport =
await WebSocketClass.create(browserWSEndpoint, headers);
return {
connectionTransport: connectionTransport,
endpointUrl: browserWSEndpoint,
};
} else if (browserURL) {
const connectionURL = await getWSEndpoint(browserURL);
const WebSocketClass = await getWebSocketTransportClass();
const connectionTransport: ConnectionTransport =
await WebSocketClass.create(connectionURL);
return {
connectionTransport: connectionTransport,
endpointUrl: connectionURL,
};
} else if (options.channel && isNode) {
const {detectBrowserPlatform, resolveDefaultUserDataDir, Browser} =
await import('@puppeteer/browsers');
const platform = detectBrowserPlatform();
if (!platform) {
throw new Error('Could not detect required browser platform');
}
const {convertPuppeteerChannelToBrowsersChannel} =
await import('../node/LaunchOptions.js');
const {join} = await import('node:path');
const userDataDir = resolveDefaultUserDataDir(
Browser.CHROME,
platform,
convertPuppeteerChannelToBrowsersChannel(options.channel),
);
const portPath = join(userDataDir, 'DevToolsActivePort');
try {
const fileContent = await environment.value.fs.promises.readFile(
portPath,
'ascii',
);
const [rawPort, rawPath] = fileContent
.split('\n')
.map(line => {
return line.trim();
})
.filter(line => {
return !!line;
});
if (!rawPort || !rawPath) {
throw new Error(`Invalid DevToolsActivePort '${fileContent}' found`);
}
const port = parseInt(rawPort, 10);
if (isNaN(port) || port <= 0 || port > 65535) {
throw new Error(`Invalid port '${rawPort}' found`);
}
const browserWSEndpoint = `ws://localhost:${port}${rawPath}`;
const WebSocketClass = await getWebSocketTransportClass();
const connectionTransport = await WebSocketClass.create(
browserWSEndpoint,
headers,
);
return {
connectionTransport: connectionTransport,
endpointUrl: browserWSEndpoint,
};
} catch (error) {
throw new Error(
`Could not find DevToolsActivePort for ${options.channel} at ${portPath}`,
{
cause: error,
},
);
}
}
throw new Error('Invalid connection options');
}
async function getWSEndpoint(browserURL: string): Promise<string> {
const endpointURL = new URL('/json/version', browserURL);
try {
const result = await globalThis.fetch(endpointURL.toString(), {
method: 'GET',
});
if (!result.ok) {
throw new Error(`HTTP ${result.statusText}`);
}
const data = await result.json();
return data.webSocketDebuggerUrl;
} catch (error) {
if (isErrorLike(error)) {
error.message =
`Failed to fetch browser webSocket URL from ${endpointURL}: ` +
error.message;
}
throw error;
}
}

Some files were not shown because too many files have changed in this diff Show More