Update dashboard, kb, memory +4 more (+28 ~18 -1)

This commit is contained in:
Echo
2026-02-06 14:25:10 +00:00
parent 7f64d5054a
commit 19d178268a
6767 changed files with 1346472 additions and 1282 deletions

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

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

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

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

View File

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

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

@@ -0,0 +1,541 @@
/**
* @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 {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 {
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,
handleDevToolsAsPage = false,
): Promise<CdpBrowser> {
const browser = new CdpBrowser(
connection,
contextIds,
defaultViewport,
process,
closeCallback,
targetFilterCallback,
isPageTargetCallback,
waitForInitiallyDiscoveredTargets,
networkEnabled,
handleDevToolsAsPage,
);
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;
#targetManager: TargetManager;
#handleDevToolsAsPage = false;
constructor(
connection: Connection,
contextIds: string[],
defaultViewport?: Viewport | null,
process?: ChildProcess,
closeCallback?: BrowserCloseCallback,
targetFilterCallback?: TargetFilterCallback,
isPageTargetCallback?: IsPageTargetCallback,
waitForInitiallyDiscoveredTargets = true,
networkEnabled = true,
handleDevToolsAsPage = false,
) {
super();
this.#networkEnabled = networkEnabled;
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,
);
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;
}
override async installExtension(path: string): Promise<string> {
const {id} = await this.#connection.send('Extensions.loadUnpacked', {path});
return id;
}
override uninstallExtension(id: string): Promise<void> {
return this.#connection.send('Extensions.uninstall', {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();
}
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;
}
}

View File

@@ -0,0 +1,68 @@
/**
* @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,
defaultViewport = DEFAULT_VIEWPORT,
downloadBehavior,
targetFilter,
_isPageTarget: isPageTarget,
slowMo = 0,
protocolTimeout,
handleDevToolsAsPage,
idGenerator = createIncrementalIdGenerator(),
} = 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,
handleDevToolsAsPage,
);
return browser;
}

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

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

View File

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

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

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

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

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

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

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

View File

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

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

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

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

@@ -0,0 +1,216 @@
/**
* @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,
});
}
override async *queryAXTree(
name?: string | undefined,
role?: string | undefined,
): AwaitableIterable<ElementHandle<Node>> {
const {nodes} = await this.client.send('Accessibility.queryAXTree', {
objectId: this.id,
accessibleName: name,
role,
});
const results = nodes.filter(node => {
if (node.ignored) {
return false;
}
if (!node.role) {
return false;
}
if (NON_ELEMENT_NODE_ROLES.has(node.role.value)) {
return false;
}
return true;
});
return yield* AsyncIterableUtil.map(results, node => {
return this.realm.adoptBackendNode(node.backendDOMNodeId) as Promise<
ElementHandle<Node>
>;
});
}
override async backendNodeId(): Promise<number> {
if (this.#backendNodeId) {
return this.#backendNodeId;
}
const {node} = await this.client.send('DOM.describeNode', {
objectId: this.handle.id,
});
this.#backendNodeId = node.backendNodeId;
return this.#backendNodeId;
}
}

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

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

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

@@ -0,0 +1,549 @@
/**
* @license
* Copyright 2017 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {Protocol} from 'devtools-protocol';
import {CDPSessionEvent, type CDPSession} from '../api/CDPSession.js';
import type {ElementHandle} from '../api/ElementHandle.js';
import type {JSHandle} from '../api/JSHandle.js';
import {ARIAQueryHandler} from '../common/AriaQueryHandler.js';
import {EventEmitter} from '../common/EventEmitter.js';
import {LazyArg} from '../common/LazyArg.js';
import {scriptInjector} from '../common/ScriptInjector.js';
import type {BindingPayload, EvaluateFunc, HandleFor} from '../common/types.js';
import {
PuppeteerURL,
SOURCE_URL_REGEX,
debugError,
getSourcePuppeteerURLIfAvailable,
getSourceUrlComment,
isString,
} from '../common/util.js';
import type {PuppeteerInjectedUtil} from '../injected/injected.js';
import {AsyncIterableUtil} from '../util/AsyncIterableUtil.js';
import {DisposableStack, disposeSymbol} from '../util/disposable.js';
import {stringifyFunction} from '../util/Function.js';
import {Mutex} from '../util/Mutex.js';
import {Binding} from './Binding.js';
import {CdpElementHandle} from './ElementHandle.js';
import type {IsolatedWorld} from './IsolatedWorld.js';
import {CdpJSHandle} from './JSHandle.js';
import {
addPageBinding,
CDP_BINDING_PREFIX,
createEvaluationError,
valueFromRemoteObject,
} 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 valueFromRemoteObject(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 valueFromRemoteObject(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;
};

View File

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

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

@@ -0,0 +1,452 @@
/**
* @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 {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;
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),
[PUPPETEER_WORLD]: new IsolatedWorld(
this,
this._frameManager.timeoutSettings,
),
};
this.accessibility = new Accessibility(this.worlds[MAIN_WORLD], frameId);
this.on(FrameEvent.FrameSwappedByActivation, () => {
// Emulate loading process for swapped frames.
this._onLoadingStarted();
this._onLoadingStopped();
});
this.worlds[MAIN_WORLD].emitter.on(
'consoleapicalled',
this.#onMainWorldConsoleApiCalled.bind(this),
);
this.worlds[MAIN_WORLD].emitter.on(
'bindingcalled',
this.#onMainWorldBindingCalled.bind(this),
);
}
#onMainWorldConsoleApiCalled(
event: Protocol.Runtime.ConsoleAPICalledEvent,
): void {
this._frameManager.emit(FrameManagerEvent.ConsoleApiCalled, [
this.worlds[MAIN_WORLD],
event,
]);
}
#onMainWorldBindingCalled(event: Protocol.Runtime.BindingCalledEvent) {
this._frameManager.emit(FrameManagerEvent.BindingCalled, [
this.worlds[MAIN_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]();
}
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>;
}
}
/**
* @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;
}

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

@@ -0,0 +1,582 @@
/**
* @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 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 {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 type {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.
/**
* 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);
});
}
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);
}),
]);
} 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);
}
_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;
}
}
#onExecutionContextCreated(
contextPayload: Protocol.Runtime.ExecutionContextDescription,
session: CDPSession,
): void {
const auxData = contextPayload.auxData as {frameId?: string} | undefined;
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];
}
}
// If there is no world, the context is not meant to be handled by us.
if (!world) {
return;
}
const context = new ExecutionContext(
frame?.client || this.#client,
contextPayload,
world,
);
world.setContext(context);
}
#removeFramesRecursively(frame: CdpFrame): void {
for (const child of frame.childFrames()) {
this.#removeFramesRecursively(child);
}
frame[disposeSymbol]();
this._frameTree.removeFrame(frame);
this.emit(FrameManagerEvent.FrameDetached, frame);
frame.emit(FrameEvent.FrameDetached, frame);
}
}

View File

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

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

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

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

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

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

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

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

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

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

@@ -0,0 +1,266 @@
/**
* @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 {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 {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorlds.js';
import {CdpJSHandle} from './JSHandle.js';
import type {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();
readonly #frameOrWorker: CdpFrame | CdpWebWorker;
constructor(
frameOrWorker: CdpFrame | CdpWebWorker,
timeoutSettings: TimeoutSettings,
) {
super(timeoutSettings);
this.#frameOrWorker = frameOrWorker;
}
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();
}
}

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

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

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

@@ -0,0 +1,126 @@
/**
* @license
* Copyright 2019 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {Protocol} from 'devtools-protocol';
import type {CDPSession} from '../api/CDPSession.js';
import {JSHandle} from '../api/JSHandle.js';
import {debugError} from '../common/util.js';
import type {CdpElementHandle} from './ElementHandle.js';
import type {IsolatedWorld} from './IsolatedWorld.js';
import {valueFromRemoteObject} 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 valueFromRemoteObject(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:' + valueFromRemoteObject(this.#remoteObject);
}
const type = this.#remoteObject.subtype || this.#remoteObject.type;
return 'JSHandle@' + type;
}
override get id(): string | undefined {
return this.#remoteObject.objectId;
}
override remoteObject(): Protocol.Runtime.RemoteObject {
return this.#remoteObject;
}
override async getProperties(): Promise<Map<string, JSHandle<unknown>>> {
// We use Runtime.getProperties rather than iterative version for
// improved performance as it allows getting everything at once.
const response = await this.client.send('Runtime.getProperties', {
objectId: this.#remoteObject.objectId!,
ownProperties: true,
});
const result = new Map<string, JSHandle>();
for (const property of response.result) {
if (!property.enumerable || !property.value) {
continue;
}
result.set(property.name, this.#world.createCdpHandle(property.value));
}
return result;
}
}
/**
* @internal
*/
export async function releaseObject(
client: CDPSession,
remoteObject: Protocol.Runtime.RemoteObject,
): Promise<void> {
if (!remoteObject.objectId) {
return;
}
await client
.send('Runtime.releaseObject', {objectId: remoteObject.objectId})
.catch(error => {
// Exceptions might happen in case of a page been navigated or closed.
// Swallow these since they are harmless and we don't leak anything in this case.
debugError(error);
});
}

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

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

View File

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

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

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

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

File diff suppressed because it is too large Load Diff

View File

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

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

@@ -0,0 +1,311 @@
/**
* @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;
/**
* 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> {
const session = this._session();
if (!session) {
return await this.createCDPSession().then(client => {
return CdpPage._create(client, this, null);
});
}
return await CdpPage._create(session, this, 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;
protected pagePromise?: Promise<Page>;
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();
// TODO(einbinder): Make workers send their console logs.
this.#workerPromise = (
session
? Promise.resolve(session)
: this._sessionFactory()(/* isAutoAttachEmulated=*/ false)
).then(client => {
return new CdpWebWorker(
client,
this._getTargetInfo().url,
this._targetId,
this.type(),
() => {} /* consoleAPICalled */,
() => {} /* exceptionThrown */,
undefined /* networkManager */,
);
});
}
return await this.#workerPromise;
}
}
/**
* @internal
*/
export class OtherTarget extends CdpTarget {}

View File

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

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

@@ -0,0 +1,442 @@
/**
* @license
* Copyright 2022 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {Protocol} from 'devtools-protocol';
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;
constructor(
connection: Connection,
targetFactory: TargetFactory,
targetFilterCallback?: TargetFilterCallback,
waitForInitiallyDiscoveredTargets = true,
) {
super();
this.#connection = connection;
this.#targetFilterCallback = targetFilterCallback;
this.#targetFactory = targetFactory;
this.#waitForInitiallyDiscoveredTargets = waitForInitiallyDiscoveredTargets;
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();
}
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;
}
#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;
}
// 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)) {
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,
}),
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);
};
}

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

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

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

@@ -0,0 +1,113 @@
/**
* @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} from '../api/WebWorker.js';
import {TimeoutSettings} from '../common/TimeoutSettings.js';
import {debugError} from '../common/util.js';
import {ExecutionContext} from './ExecutionContext.js';
import {IsolatedWorld} from './IsolatedWorld.js';
import type {NetworkManager} from './NetworkManager.js';
/**
* @internal
*/
export type ConsoleAPICalledCallback = (
world: IsolatedWorld,
event: Protocol.Runtime.ConsoleAPICalledEvent,
) => void;
/**
* @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;
constructor(
client: CDPSession,
url: string,
targetId: string,
targetType: TargetType,
consoleAPICalled: ConsoleAPICalledCallback,
exceptionThrown: ExceptionThrownCallback,
networkManager?: NetworkManager,
) {
super(url);
this.#id = targetId;
this.#client = client;
this.#targetType = targetType;
this.#world = new IsolatedWorld(this, new TimeoutSettings());
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 {
return consoleAPICalled(this.#world, event);
} 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();
});
}
}
}

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

@@ -0,0 +1,43 @@
/**
* @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 './utils.js';
export * from './WebWorker.js';

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

@@ -0,0 +1,238 @@
/**
* @license
* Copyright 2017 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {Protocol} from 'devtools-protocol';
import {PuppeteerURL, evaluationString} from '../common/util.js';
import {assert} from '../util/assert.js';
/**
* @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 valueFromRemoteObject(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 valueFromRemoteObject(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 valueFromRemoteObject(
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);
}