FRE-709: Document duplicate recovery wake - FRE-635 already recovered via FRE-708
This commit is contained in:
103
node_modules/puppeteer-core/src/api/BluetoothEmulation.ts
generated
vendored
Normal file
103
node_modules/puppeteer-core/src/api/BluetoothEmulation.ts
generated
vendored
Normal 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
743
node_modules/puppeteer-core/src/api/Browser.ts
generated
vendored
Normal 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
384
node_modules/puppeteer-core/src/api/BrowserContext.ts
generated
vendored
Normal 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
137
node_modules/puppeteer-core/src/api/CDPSession.ts
generated
vendored
Normal 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;
|
||||
}
|
||||
71
node_modules/puppeteer-core/src/api/DeviceRequestPrompt.ts
generated
vendored
Normal file
71
node_modules/puppeteer-core/src/api/DeviceRequestPrompt.ts
generated
vendored
Normal 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
111
node_modules/puppeteer-core/src/api/Dialog.ts
generated
vendored
Normal 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
1708
node_modules/puppeteer-core/src/api/ElementHandle.ts
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
10
node_modules/puppeteer-core/src/api/ElementHandleSymbol.ts
generated
vendored
Normal file
10
node_modules/puppeteer-core/src/api/ElementHandleSymbol.ts
generated
vendored
Normal 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
16
node_modules/puppeteer-core/src/api/Environment.ts
generated
vendored
Normal 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
126
node_modules/puppeteer-core/src/api/Extension.ts
generated
vendored
Normal 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
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
750
node_modules/puppeteer-core/src/api/HTTPRequest.ts
generated
vendored
Normal 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
148
node_modules/puppeteer-core/src/api/HTTPResponse.ts
generated
vendored
Normal 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
567
node_modules/puppeteer-core/src/api/Input.ts
generated
vendored
Normal 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
24
node_modules/puppeteer-core/src/api/Issue.ts
generated
vendored
Normal 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
212
node_modules/puppeteer-core/src/api/JSHandle.ts
generated
vendored
Normal 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
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
207
node_modules/puppeteer-core/src/api/Realm.ts
generated
vendored
Normal 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
95
node_modules/puppeteer-core/src/api/Target.ts
generated
vendored
Normal 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
161
node_modules/puppeteer-core/src/api/WebWorker.ts
generated
vendored
Normal 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
26
node_modules/puppeteer-core/src/api/api.ts
generated
vendored
Normal 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
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
208
node_modules/puppeteer-core/src/bidi/BidiOverCdp.ts
generated
vendored
Normal 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;
|
||||
};
|
||||
}
|
||||
}
|
||||
52
node_modules/puppeteer-core/src/bidi/BluetoothEmulation.ts
generated
vendored
Normal file
52
node_modules/puppeteer-core/src/bidi/BluetoothEmulation.ts
generated
vendored
Normal 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
390
node_modules/puppeteer-core/src/bidi/Browser.ts
generated
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
132
node_modules/puppeteer-core/src/bidi/BrowserConnector.ts
generated
vendored
Normal file
132
node_modules/puppeteer-core/src/bidi/BrowserConnector.ts
generated
vendored
Normal 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
401
node_modules/puppeteer-core/src/bidi/BrowserContext.ts
generated
vendored
Normal 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
124
node_modules/puppeteer-core/src/bidi/CDPSession.ts
generated
vendored
Normal 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
232
node_modules/puppeteer-core/src/bidi/Connection.ts
generated
vendored
Normal 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
92
node_modules/puppeteer-core/src/bidi/Deserializer.ts
generated
vendored
Normal 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};
|
||||
}
|
||||
}
|
||||
138
node_modules/puppeteer-core/src/bidi/DeviceRequestPrompt.ts
generated
vendored
Normal file
138
node_modules/puppeteer-core/src/bidi/DeviceRequestPrompt.ts
generated
vendored
Normal 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
32
node_modules/puppeteer-core/src/bidi/Dialog.ts
generated
vendored
Normal 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
153
node_modules/puppeteer-core/src/bidi/ElementHandle.ts
generated
vendored
Normal 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
256
node_modules/puppeteer-core/src/bidi/ExposedFunction.ts
generated
vendored
Normal 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
615
node_modules/puppeteer-core/src/bidi/Frame.ts
generated
vendored
Normal 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
354
node_modules/puppeteer-core/src/bidi/HTTPRequest.ts
generated
vendored
Normal 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
162
node_modules/puppeteer-core/src/bidi/HTTPResponse.ts
generated
vendored
Normal 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
741
node_modules/puppeteer-core/src/bidi/Input.ts
generated
vendored
Normal 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
95
node_modules/puppeteer-core/src/bidi/JSHandle.ts
generated
vendored
Normal 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
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
448
node_modules/puppeteer-core/src/bidi/Realm.ts
generated
vendored
Normal 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
126
node_modules/puppeteer-core/src/bidi/Serializer.ts
generated
vendored
Normal 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
167
node_modules/puppeteer-core/src/bidi/Target.ts
generated
vendored
Normal 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
48
node_modules/puppeteer-core/src/bidi/WebWorker.ts
generated
vendored
Normal 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
18
node_modules/puppeteer-core/src/bidi/bidi.ts
generated
vendored
Normal 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
330
node_modules/puppeteer-core/src/bidi/core/Browser.ts
generated
vendored
Normal 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]();
|
||||
}
|
||||
}
|
||||
845
node_modules/puppeteer-core/src/bidi/core/BrowsingContext.ts
generated
vendored
Normal file
845
node_modules/puppeteer-core/src/bidi/core/BrowsingContext.ts
generated
vendored
Normal 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],
|
||||
});
|
||||
}
|
||||
}
|
||||
31
node_modules/puppeteer-core/src/bidi/core/Connection.ts
generated
vendored
Normal file
31
node_modules/puppeteer-core/src/bidi/core/Connection.ts
generated
vendored
Normal 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
174
node_modules/puppeteer-core/src/bidi/core/Navigation.ts
generated
vendored
Normal 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
52
node_modules/puppeteer-core/src/bidi/core/README.md
generated
vendored
Normal 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
361
node_modules/puppeteer-core/src/bidi/core/Realm.ts
generated
vendored
Normal 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
349
node_modules/puppeteer-core/src/bidi/core/Request.ts
generated
vendored
Normal 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
162
node_modules/puppeteer-core/src/bidi/core/Session.ts
generated
vendored
Normal 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]();
|
||||
}
|
||||
}
|
||||
244
node_modules/puppeteer-core/src/bidi/core/UserContext.ts
generated
vendored
Normal file
244
node_modules/puppeteer-core/src/bidi/core/UserContext.ts
generated
vendored
Normal 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
138
node_modules/puppeteer-core/src/bidi/core/UserPrompt.ts
generated
vendored
Normal 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
15
node_modules/puppeteer-core/src/bidi/core/core.ts
generated
vendored
Normal 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
195
node_modules/puppeteer-core/src/bidi/util.ts
generated
vendored
Normal 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
784
node_modules/puppeteer-core/src/cdp/Accessibility.ts
generated
vendored
Normal 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
133
node_modules/puppeteer-core/src/cdp/Binding.ts
generated
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
47
node_modules/puppeteer-core/src/cdp/BluetoothEmulation.ts
generated
vendored
Normal file
47
node_modules/puppeteer-core/src/cdp/BluetoothEmulation.ts
generated
vendored
Normal 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
627
node_modules/puppeteer-core/src/cdp/Browser.ts
generated
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
72
node_modules/puppeteer-core/src/cdp/BrowserConnector.ts
generated
vendored
Normal file
72
node_modules/puppeteer-core/src/cdp/BrowserConnector.ts
generated
vendored
Normal 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
183
node_modules/puppeteer-core/src/cdp/BrowserContext.ts
generated
vendored
Normal 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
30
node_modules/puppeteer-core/src/cdp/CdpIssue.ts
generated
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
46
node_modules/puppeteer-core/src/cdp/CdpPreloadScript.ts
generated
vendored
Normal file
46
node_modules/puppeteer-core/src/cdp/CdpPreloadScript.ts
generated
vendored
Normal 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
181
node_modules/puppeteer-core/src/cdp/CdpSession.ts
generated
vendored
Normal 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
308
node_modules/puppeteer-core/src/cdp/Connection.ts
generated
vendored
Normal 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
508
node_modules/puppeteer-core/src/cdp/Coverage.ts
generated
vendored
Normal 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;
|
||||
});
|
||||
}
|
||||
230
node_modules/puppeteer-core/src/cdp/DeviceRequestPrompt.ts
generated
vendored
Normal file
230
node_modules/puppeteer-core/src/cdp/DeviceRequestPrompt.ts
generated
vendored
Normal 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
37
node_modules/puppeteer-core/src/cdp/Dialog.ts
generated
vendored
Normal 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
217
node_modules/puppeteer-core/src/cdp/ElementHandle.ts
generated
vendored
Normal 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
611
node_modules/puppeteer-core/src/cdp/EmulationManager.ts
generated
vendored
Normal 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
549
node_modules/puppeteer-core/src/cdp/ExecutionContext.ts
generated
vendored
Normal 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
108
node_modules/puppeteer-core/src/cdp/Extension.ts
generated
vendored
Normal 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'))
|
||||
);
|
||||
}
|
||||
}
|
||||
197
node_modules/puppeteer-core/src/cdp/ExtensionTransport.ts
generated
vendored
Normal file
197
node_modules/puppeteer-core/src/cdp/ExtensionTransport.ts
generated
vendored
Normal 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
462
node_modules/puppeteer-core/src/cdp/Frame.ts
generated
vendored
Normal 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
623
node_modules/puppeteer-core/src/cdp/FrameManager.ts
generated
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
53
node_modules/puppeteer-core/src/cdp/FrameManagerEvents.ts
generated
vendored
Normal file
53
node_modules/puppeteer-core/src/cdp/FrameManagerEvents.ts
generated
vendored
Normal 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
100
node_modules/puppeteer-core/src/cdp/FrameTree.ts
generated
vendored
Normal 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
301
node_modules/puppeteer-core/src/cdp/HTTPRequest.ts
generated
vendored
Normal 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
168
node_modules/puppeteer-core/src/cdp/HTTPResponse.ts
generated
vendored
Normal 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
653
node_modules/puppeteer-core/src/cdp/Input.ts
generated
vendored
Normal 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
304
node_modules/puppeteer-core/src/cdp/IsolatedWorld.ts
generated
vendored
Normal 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
20
node_modules/puppeteer-core/src/cdp/IsolatedWorlds.ts
generated
vendored
Normal 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
126
node_modules/puppeteer-core/src/cdp/JSHandle.ts
generated
vendored
Normal 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
276
node_modules/puppeteer-core/src/cdp/LifecycleWatcher.ts
generated
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
267
node_modules/puppeteer-core/src/cdp/NetworkEventManager.ts
generated
vendored
Normal file
267
node_modules/puppeteer-core/src/cdp/NetworkEventManager.ts
generated
vendored
Normal 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
834
node_modules/puppeteer-core/src/cdp/NetworkManager.ts
generated
vendored
Normal 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
1388
node_modules/puppeteer-core/src/cdp/Page.ts
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
71
node_modules/puppeteer-core/src/cdp/PredefinedNetworkConditions.ts
generated
vendored
Normal file
71
node_modules/puppeteer-core/src/cdp/PredefinedNetworkConditions.ts
generated
vendored
Normal 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
321
node_modules/puppeteer-core/src/cdp/Target.ts
generated
vendored
Normal 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 {}
|
||||
38
node_modules/puppeteer-core/src/cdp/TargetManageEvents.ts
generated
vendored
Normal file
38
node_modules/puppeteer-core/src/cdp/TargetManageEvents.ts
generated
vendored
Normal 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
514
node_modules/puppeteer-core/src/cdp/TargetManager.ts
generated
vendored
Normal 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
140
node_modules/puppeteer-core/src/cdp/Tracing.ts
generated
vendored
Normal 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
446
node_modules/puppeteer-core/src/cdp/WebMCP.ts
generated
vendored
Normal 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
139
node_modules/puppeteer-core/src/cdp/WebWorker.ts
generated
vendored
Normal 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
44
node_modules/puppeteer-core/src/cdp/cdp.ts
generated
vendored
Normal 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
323
node_modules/puppeteer-core/src/cdp/utils.ts
generated
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
89
node_modules/puppeteer-core/src/common/AriaQueryHandler.ts
generated
vendored
Normal file
89
node_modules/puppeteer-core/src/common/AriaQueryHandler.ts
generated
vendored
Normal 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
|
||||
);
|
||||
};
|
||||
}
|
||||
174
node_modules/puppeteer-core/src/common/BrowserConnector.ts
generated
vendored
Normal file
174
node_modules/puppeteer-core/src/common/BrowserConnector.ts
generated
vendored
Normal 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
Reference in New Issue
Block a user