Update dashboard, kb, memory +4 more (+28 ~18 -1)
This commit is contained in:
784
node_modules/puppeteer-core/src/cdp/Accessibility.ts
generated
vendored
Normal file
784
node_modules/puppeteer-core/src/cdp/Accessibility.ts
generated
vendored
Normal file
@@ -0,0 +1,784 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2018 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type {Protocol} from 'devtools-protocol';
|
||||
|
||||
import type {ElementHandle} from '../api/ElementHandle.js';
|
||||
import type {Realm} from '../api/Realm.js';
|
||||
import type {CdpFrame} from '../cdp/Frame.js';
|
||||
import {debugError} from '../common/util.js';
|
||||
|
||||
/**
|
||||
* Represents a Node and the properties of it that are relevant to Accessibility.
|
||||
* @public
|
||||
*/
|
||||
export interface SerializedAXNode {
|
||||
/**
|
||||
* The {@link https://www.w3.org/TR/wai-aria/#usage_intro | role} of the node.
|
||||
*/
|
||||
role: string;
|
||||
/**
|
||||
* A human readable name for the node.
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* The current value of the node.
|
||||
*/
|
||||
value?: string | number;
|
||||
/**
|
||||
* An additional human readable description of the node.
|
||||
*/
|
||||
description?: string;
|
||||
/**
|
||||
* Any keyboard shortcuts associated with this node.
|
||||
*/
|
||||
keyshortcuts?: string;
|
||||
/**
|
||||
* A human readable alternative to the role.
|
||||
*/
|
||||
roledescription?: string;
|
||||
/**
|
||||
* A description of the current value.
|
||||
*/
|
||||
valuetext?: string;
|
||||
disabled?: boolean;
|
||||
expanded?: boolean;
|
||||
focused?: boolean;
|
||||
modal?: boolean;
|
||||
multiline?: boolean;
|
||||
/**
|
||||
* Whether more than one child can be selected.
|
||||
*/
|
||||
multiselectable?: boolean;
|
||||
readonly?: boolean;
|
||||
required?: boolean;
|
||||
selected?: boolean;
|
||||
/**
|
||||
* Whether the checkbox is checked, or in a
|
||||
* {@link https://www.w3.org/TR/wai-aria-practices/examples/checkbox/checkbox-2/checkbox-2.html | mixed state}.
|
||||
*/
|
||||
checked?: boolean | 'mixed';
|
||||
/**
|
||||
* Whether the node is checked or in a mixed state.
|
||||
*/
|
||||
pressed?: boolean | 'mixed';
|
||||
/**
|
||||
* The level of a heading.
|
||||
*/
|
||||
level?: number;
|
||||
valuemin?: number;
|
||||
valuemax?: number;
|
||||
autocomplete?: string;
|
||||
haspopup?: string;
|
||||
/**
|
||||
* Whether and in what way this node's value is invalid.
|
||||
*/
|
||||
invalid?: string;
|
||||
orientation?: string;
|
||||
/**
|
||||
* Whether the node is {@link https://www.w3.org/TR/wai-aria/#aria-busy | busy}.
|
||||
*/
|
||||
busy?: boolean;
|
||||
/**
|
||||
* The {@link https://www.w3.org/TR/wai-aria/#aria-live | live} status of the
|
||||
* node.
|
||||
*/
|
||||
live?: string;
|
||||
/**
|
||||
* Whether the live region is
|
||||
* {@link https://www.w3.org/TR/wai-aria/#aria-atomic | atomic}.
|
||||
*/
|
||||
atomic?: boolean;
|
||||
/**
|
||||
* The {@link https://www.w3.org/TR/wai-aria/#aria-relevant | relevant}
|
||||
* changes for the live region.
|
||||
*/
|
||||
relevant?: string;
|
||||
/**
|
||||
* The {@link https://www.w3.org/TR/wai-aria/#aria-errormessage | error message}
|
||||
* for the node.
|
||||
*/
|
||||
errormessage?: string;
|
||||
/**
|
||||
* The {@link https://www.w3.org/TR/wai-aria/#aria-details | details} for the
|
||||
* node.
|
||||
*/
|
||||
details?: string;
|
||||
|
||||
/**
|
||||
* Url for link elements.
|
||||
*/
|
||||
url?: string;
|
||||
/**
|
||||
* Children of this node, if there are any.
|
||||
*/
|
||||
children?: SerializedAXNode[];
|
||||
|
||||
/**
|
||||
* CDP-specific ID to reference the DOM node.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
backendNodeId?: number;
|
||||
|
||||
/**
|
||||
* CDP-specific documentId.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
loaderId: string;
|
||||
|
||||
/**
|
||||
* Get an ElementHandle for this AXNode if available.
|
||||
*
|
||||
* If the underlying DOM element has been disposed, the method might return an
|
||||
* error.
|
||||
*/
|
||||
elementHandle(): Promise<ElementHandle | null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface SnapshotOptions {
|
||||
/**
|
||||
* Prune uninteresting nodes from the tree.
|
||||
* @defaultValue `true`
|
||||
*/
|
||||
interestingOnly?: boolean;
|
||||
/**
|
||||
* If true, gets accessibility trees for each of the iframes in the frame
|
||||
* subtree.
|
||||
*
|
||||
* @defaultValue `false`
|
||||
*/
|
||||
includeIframes?: boolean;
|
||||
/**
|
||||
* Root node to get the accessibility tree for
|
||||
* @defaultValue The root node of the entire page.
|
||||
*/
|
||||
root?: ElementHandle<Node>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Accessibility class provides methods for inspecting the browser's
|
||||
* accessibility tree. The accessibility tree is used by assistive technology
|
||||
* such as {@link https://en.wikipedia.org/wiki/Screen_reader | screen readers} or
|
||||
* {@link https://en.wikipedia.org/wiki/Switch_access | switches}.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* Accessibility is a very platform-specific thing. On different platforms,
|
||||
* there are different screen readers that might have wildly different output.
|
||||
*
|
||||
* Blink - Chrome's rendering engine - has a concept of "accessibility tree",
|
||||
* which is then translated into different platform-specific APIs. Accessibility
|
||||
* namespace gives users access to the Blink Accessibility Tree.
|
||||
*
|
||||
* Most of the accessibility tree gets filtered out when converting from Blink
|
||||
* AX Tree to Platform-specific AX-Tree or by assistive technologies themselves.
|
||||
* By default, Puppeteer tries to approximate this filtering, exposing only
|
||||
* the "interesting" nodes of the tree.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class Accessibility {
|
||||
#realm: Realm;
|
||||
#frameId: string;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
constructor(realm: Realm, frameId = '') {
|
||||
this.#realm = realm;
|
||||
this.#frameId = frameId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Captures the current state of the accessibility tree.
|
||||
* The returned object represents the root accessible node of the page.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* **NOTE** The Chrome accessibility tree contains nodes that go unused on
|
||||
* most platforms and by most screen readers. Puppeteer will discard them as
|
||||
* well for an easier to process tree, unless `interestingOnly` is set to
|
||||
* `false`.
|
||||
*
|
||||
* @example
|
||||
* An example of dumping the entire accessibility tree:
|
||||
*
|
||||
* ```ts
|
||||
* const snapshot = await page.accessibility.snapshot();
|
||||
* console.log(snapshot);
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* An example of logging the focused node's name:
|
||||
*
|
||||
* ```ts
|
||||
* const snapshot = await page.accessibility.snapshot();
|
||||
* const node = findFocusedNode(snapshot);
|
||||
* console.log(node && node.name);
|
||||
*
|
||||
* function findFocusedNode(node) {
|
||||
* if (node.focused) return node;
|
||||
* for (const child of node.children || []) {
|
||||
* const foundNode = findFocusedNode(child);
|
||||
* return foundNode;
|
||||
* }
|
||||
* return null;
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @returns An AXNode object representing the snapshot.
|
||||
*/
|
||||
public async snapshot(
|
||||
options: SnapshotOptions = {},
|
||||
): Promise<SerializedAXNode | null> {
|
||||
const {
|
||||
interestingOnly = true,
|
||||
root = null,
|
||||
includeIframes = false,
|
||||
} = options;
|
||||
const {nodes} = await this.#realm.environment.client.send(
|
||||
'Accessibility.getFullAXTree',
|
||||
{
|
||||
frameId: this.#frameId,
|
||||
},
|
||||
);
|
||||
let backendNodeId: number | undefined;
|
||||
if (root) {
|
||||
const {node} = await this.#realm.environment.client.send(
|
||||
'DOM.describeNode',
|
||||
{
|
||||
objectId: root.id,
|
||||
},
|
||||
);
|
||||
backendNodeId = node.backendNodeId;
|
||||
}
|
||||
const defaultRoot = AXNode.createTree(this.#realm, nodes);
|
||||
const populateIframes = async (root: AXNode): Promise<void> => {
|
||||
if (root.payload.role?.value === 'Iframe') {
|
||||
if (!root.payload.backendDOMNodeId) {
|
||||
return;
|
||||
}
|
||||
using handle = (await this.#realm.adoptBackendNode(
|
||||
root.payload.backendDOMNodeId,
|
||||
)) as ElementHandle<Element>;
|
||||
if (!handle || !('contentFrame' in handle)) {
|
||||
return;
|
||||
}
|
||||
const frame = await handle.contentFrame();
|
||||
if (!frame) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const iframeSnapshot = await frame.accessibility.snapshot(options);
|
||||
root.iframeSnapshot = iframeSnapshot ?? undefined;
|
||||
} catch (error) {
|
||||
// Frames can get detached at any time resulting in errors.
|
||||
debugError(error);
|
||||
}
|
||||
}
|
||||
for (const child of root.children) {
|
||||
await populateIframes(child);
|
||||
}
|
||||
};
|
||||
|
||||
let needle: AXNode | null = defaultRoot;
|
||||
if (!defaultRoot) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (includeIframes) {
|
||||
await populateIframes(defaultRoot);
|
||||
}
|
||||
|
||||
if (backendNodeId) {
|
||||
needle = defaultRoot.find(node => {
|
||||
return node.payload.backendDOMNodeId === backendNodeId;
|
||||
});
|
||||
}
|
||||
|
||||
if (!needle) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!interestingOnly) {
|
||||
return this.serializeTree(needle)[0] ?? null;
|
||||
}
|
||||
|
||||
const interestingNodes = new Set<AXNode>();
|
||||
this.collectInterestingNodes(interestingNodes, defaultRoot, false);
|
||||
|
||||
return this.serializeTree(needle, interestingNodes)[0] ?? null;
|
||||
}
|
||||
|
||||
private serializeTree(
|
||||
node: AXNode,
|
||||
interestingNodes?: Set<AXNode>,
|
||||
): SerializedAXNode[] {
|
||||
const children: SerializedAXNode[] = [];
|
||||
for (const child of node.children) {
|
||||
children.push(...this.serializeTree(child, interestingNodes));
|
||||
}
|
||||
|
||||
if (interestingNodes && !interestingNodes.has(node)) {
|
||||
return children;
|
||||
}
|
||||
|
||||
const serializedNode = node.serialize();
|
||||
if (children.length) {
|
||||
serializedNode.children = children;
|
||||
}
|
||||
if (node.iframeSnapshot) {
|
||||
if (!serializedNode.children) {
|
||||
serializedNode.children = [];
|
||||
}
|
||||
serializedNode.children.push(node.iframeSnapshot);
|
||||
}
|
||||
return [serializedNode];
|
||||
}
|
||||
|
||||
private collectInterestingNodes(
|
||||
collection: Set<AXNode>,
|
||||
node: AXNode,
|
||||
insideControl: boolean,
|
||||
): void {
|
||||
if (node.isInteresting(insideControl) || node.iframeSnapshot) {
|
||||
collection.add(node);
|
||||
}
|
||||
if (node.isLeafNode()) {
|
||||
return;
|
||||
}
|
||||
insideControl = insideControl || node.isControl();
|
||||
for (const child of node.children) {
|
||||
this.collectInterestingNodes(collection, child, insideControl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AXNode {
|
||||
public payload: Protocol.Accessibility.AXNode;
|
||||
public children: AXNode[] = [];
|
||||
public iframeSnapshot?: SerializedAXNode;
|
||||
|
||||
#richlyEditable = false;
|
||||
#editable = false;
|
||||
#focusable = false;
|
||||
#hidden = false;
|
||||
#busy = false;
|
||||
#modal = false;
|
||||
#hasErrormessage = false;
|
||||
#hasDetails = false;
|
||||
#name: string;
|
||||
#role: string;
|
||||
#description?: string;
|
||||
#roledescription?: string;
|
||||
#live?: string;
|
||||
#ignored: boolean;
|
||||
#cachedHasFocusableChild?: boolean;
|
||||
#realm: Realm;
|
||||
|
||||
constructor(realm: Realm, payload: Protocol.Accessibility.AXNode) {
|
||||
this.payload = payload;
|
||||
this.#role = this.payload.role ? this.payload.role.value : 'Unknown';
|
||||
this.#ignored = this.payload.ignored;
|
||||
this.#name = this.payload.name ? this.payload.name.value : '';
|
||||
this.#description = this.payload.description
|
||||
? this.payload.description.value
|
||||
: undefined;
|
||||
this.#realm = realm;
|
||||
for (const property of this.payload.properties || []) {
|
||||
if (property.name === 'editable') {
|
||||
this.#richlyEditable = property.value.value === 'richtext';
|
||||
this.#editable = true;
|
||||
}
|
||||
if (property.name === 'focusable') {
|
||||
this.#focusable = property.value.value;
|
||||
}
|
||||
if (property.name === 'hidden') {
|
||||
this.#hidden = property.value.value;
|
||||
}
|
||||
if (property.name === 'busy') {
|
||||
this.#busy = property.value.value;
|
||||
}
|
||||
if (property.name === 'live') {
|
||||
this.#live = property.value.value;
|
||||
}
|
||||
if (property.name === 'modal') {
|
||||
this.#modal = property.value.value;
|
||||
}
|
||||
if (property.name === 'roledescription') {
|
||||
this.#roledescription = property.value.value;
|
||||
}
|
||||
if (property.name === 'errormessage') {
|
||||
this.#hasErrormessage = true;
|
||||
}
|
||||
if (property.name === 'details') {
|
||||
this.#hasDetails = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#isPlainTextField(): boolean {
|
||||
if (this.#richlyEditable) {
|
||||
return false;
|
||||
}
|
||||
if (this.#editable) {
|
||||
return true;
|
||||
}
|
||||
return this.#role === 'textbox' || this.#role === 'searchbox';
|
||||
}
|
||||
|
||||
#isTextOnlyObject(): boolean {
|
||||
const role = this.#role;
|
||||
return (
|
||||
role === 'LineBreak' ||
|
||||
role === 'text' ||
|
||||
role === 'InlineTextBox' ||
|
||||
role === 'StaticText'
|
||||
);
|
||||
}
|
||||
|
||||
#hasFocusableChild(): boolean {
|
||||
if (this.#cachedHasFocusableChild === undefined) {
|
||||
this.#cachedHasFocusableChild = false;
|
||||
for (const child of this.children) {
|
||||
if (child.#focusable || child.#hasFocusableChild()) {
|
||||
this.#cachedHasFocusableChild = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.#cachedHasFocusableChild;
|
||||
}
|
||||
|
||||
public find(predicate: (x: AXNode) => boolean): AXNode | null {
|
||||
if (predicate(this)) {
|
||||
return this;
|
||||
}
|
||||
for (const child of this.children) {
|
||||
const result = child.find(predicate);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public isLeafNode(): boolean {
|
||||
if (!this.children.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// These types of objects may have children that we use as internal
|
||||
// implementation details, but we want to expose them as leaves to platform
|
||||
// accessibility APIs because screen readers might be confused if they find
|
||||
// any children.
|
||||
if (this.#isPlainTextField() || this.#isTextOnlyObject()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Roles whose children are only presentational according to the ARIA and
|
||||
// HTML5 Specs should be hidden from screen readers.
|
||||
// (Note that whilst ARIA buttons can have only presentational children, HTML5
|
||||
// buttons are allowed to have content.)
|
||||
switch (this.#role) {
|
||||
case 'doc-cover':
|
||||
case 'graphics-symbol':
|
||||
case 'img':
|
||||
case 'image':
|
||||
case 'Meter':
|
||||
case 'scrollbar':
|
||||
case 'slider':
|
||||
case 'separator':
|
||||
case 'progressbar':
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (this.#hasFocusableChild()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.#role === 'heading' && this.#name) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public isControl(): boolean {
|
||||
switch (this.#role) {
|
||||
case 'button':
|
||||
case 'checkbox':
|
||||
case 'ColorWell':
|
||||
case 'combobox':
|
||||
case 'DisclosureTriangle':
|
||||
case 'listbox':
|
||||
case 'menu':
|
||||
case 'menubar':
|
||||
case 'menuitem':
|
||||
case 'menuitemcheckbox':
|
||||
case 'menuitemradio':
|
||||
case 'radio':
|
||||
case 'scrollbar':
|
||||
case 'searchbox':
|
||||
case 'slider':
|
||||
case 'spinbutton':
|
||||
case 'switch':
|
||||
case 'tab':
|
||||
case 'textbox':
|
||||
case 'tree':
|
||||
case 'treeitem':
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public isLandmark(): boolean {
|
||||
switch (this.#role) {
|
||||
case 'banner':
|
||||
case 'complementary':
|
||||
case 'contentinfo':
|
||||
case 'form':
|
||||
case 'main':
|
||||
case 'navigation':
|
||||
case 'region':
|
||||
case 'search':
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public isInteresting(insideControl: boolean): boolean {
|
||||
const role = this.#role;
|
||||
if (role === 'Ignored' || this.#hidden || this.#ignored) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.isLandmark()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
this.#focusable ||
|
||||
this.#richlyEditable ||
|
||||
this.#busy ||
|
||||
(this.#live && this.#live !== 'off') ||
|
||||
this.#modal ||
|
||||
this.#hasErrormessage ||
|
||||
this.#hasDetails ||
|
||||
this.#roledescription
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If it's not focusable but has a control role, then it's interesting.
|
||||
if (this.isControl()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// A non focusable child of a control is not interesting
|
||||
if (insideControl) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.isLeafNode() && (!!this.#name || !!this.#description);
|
||||
}
|
||||
|
||||
public serialize(): SerializedAXNode {
|
||||
const properties = new Map<string, number | string | boolean>();
|
||||
for (const property of this.payload.properties || []) {
|
||||
properties.set(property.name.toLowerCase(), property.value.value);
|
||||
}
|
||||
if (this.payload.name) {
|
||||
properties.set('name', this.payload.name.value);
|
||||
}
|
||||
if (this.payload.value) {
|
||||
properties.set('value', this.payload.value.value);
|
||||
}
|
||||
if (this.payload.description) {
|
||||
properties.set('description', this.payload.description.value);
|
||||
}
|
||||
|
||||
const node: SerializedAXNode = {
|
||||
role: this.#role,
|
||||
elementHandle: async (): Promise<ElementHandle | null> => {
|
||||
if (!this.payload.backendDOMNodeId) {
|
||||
return null;
|
||||
}
|
||||
using handle = await this.#realm.adoptBackendNode(
|
||||
this.payload.backendDOMNodeId,
|
||||
);
|
||||
|
||||
// Since Text nodes are not elements, we want to
|
||||
// return a handle to the parent element for them.
|
||||
return (await handle.evaluateHandle(node => {
|
||||
return node.nodeType === Node.TEXT_NODE ? node.parentElement : node;
|
||||
})) as ElementHandle<Element>;
|
||||
},
|
||||
backendNodeId: this.payload.backendDOMNodeId,
|
||||
// LoaderId is an experimental mechanism to establish unique IDs across
|
||||
// navigations.
|
||||
loaderId: (this.#realm.environment as CdpFrame)._loaderId,
|
||||
};
|
||||
|
||||
type UserStringProperty =
|
||||
| 'name'
|
||||
| 'value'
|
||||
| 'description'
|
||||
| 'keyshortcuts'
|
||||
| 'roledescription'
|
||||
| 'valuetext'
|
||||
| 'url';
|
||||
|
||||
const userStringProperties: UserStringProperty[] = [
|
||||
'name',
|
||||
'value',
|
||||
'description',
|
||||
'keyshortcuts',
|
||||
'roledescription',
|
||||
'valuetext',
|
||||
'url',
|
||||
];
|
||||
const getUserStringPropertyValue = (key: UserStringProperty): string => {
|
||||
return properties.get(key) as string;
|
||||
};
|
||||
|
||||
for (const userStringProperty of userStringProperties) {
|
||||
if (!properties.has(userStringProperty)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
node[userStringProperty] = getUserStringPropertyValue(userStringProperty);
|
||||
}
|
||||
|
||||
type BooleanProperty =
|
||||
| 'disabled'
|
||||
| 'expanded'
|
||||
| 'focused'
|
||||
| 'modal'
|
||||
| 'multiline'
|
||||
| 'multiselectable'
|
||||
| 'readonly'
|
||||
| 'required'
|
||||
| 'selected'
|
||||
| 'busy'
|
||||
| 'atomic';
|
||||
const booleanProperties: BooleanProperty[] = [
|
||||
'disabled',
|
||||
'expanded',
|
||||
'focused',
|
||||
'modal',
|
||||
'multiline',
|
||||
'multiselectable',
|
||||
'readonly',
|
||||
'required',
|
||||
'selected',
|
||||
'busy',
|
||||
'atomic',
|
||||
];
|
||||
const getBooleanPropertyValue = (key: BooleanProperty): boolean => {
|
||||
return !!properties.get(key);
|
||||
};
|
||||
|
||||
for (const booleanProperty of booleanProperties) {
|
||||
// RootWebArea's treat focus differently than other nodes. They report whether
|
||||
// their frame has focus, not whether focus is specifically on the root
|
||||
// node.
|
||||
if (booleanProperty === 'focused' && this.#role === 'RootWebArea') {
|
||||
continue;
|
||||
}
|
||||
if (!properties.has(booleanProperty)) {
|
||||
continue;
|
||||
}
|
||||
node[booleanProperty] = getBooleanPropertyValue(booleanProperty);
|
||||
}
|
||||
|
||||
type TristateProperty = 'checked' | 'pressed';
|
||||
const tristateProperties: TristateProperty[] = ['checked', 'pressed'];
|
||||
for (const tristateProperty of tristateProperties) {
|
||||
if (!properties.has(tristateProperty)) {
|
||||
continue;
|
||||
}
|
||||
const value = properties.get(tristateProperty);
|
||||
node[tristateProperty] =
|
||||
value === 'mixed' ? 'mixed' : value === 'true' ? true : false;
|
||||
}
|
||||
|
||||
type NumbericalProperty = 'level' | 'valuemax' | 'valuemin';
|
||||
const numericalProperties: NumbericalProperty[] = [
|
||||
'level',
|
||||
'valuemax',
|
||||
'valuemin',
|
||||
];
|
||||
const getNumericalPropertyValue = (key: NumbericalProperty): number => {
|
||||
return properties.get(key) as number;
|
||||
};
|
||||
for (const numericalProperty of numericalProperties) {
|
||||
if (!properties.has(numericalProperty)) {
|
||||
continue;
|
||||
}
|
||||
node[numericalProperty] = getNumericalPropertyValue(numericalProperty);
|
||||
}
|
||||
|
||||
type TokenProperty =
|
||||
| 'autocomplete'
|
||||
| 'haspopup'
|
||||
| 'invalid'
|
||||
| 'orientation'
|
||||
| 'live'
|
||||
| 'relevant'
|
||||
| 'errormessage'
|
||||
| 'details';
|
||||
const tokenProperties: TokenProperty[] = [
|
||||
'autocomplete',
|
||||
'haspopup',
|
||||
'invalid',
|
||||
'orientation',
|
||||
'live',
|
||||
'relevant',
|
||||
'errormessage',
|
||||
'details',
|
||||
];
|
||||
const getTokenPropertyValue = (key: TokenProperty): string => {
|
||||
return properties.get(key) as string;
|
||||
};
|
||||
for (const tokenProperty of tokenProperties) {
|
||||
const value = getTokenPropertyValue(tokenProperty);
|
||||
if (!value || value === 'false') {
|
||||
continue;
|
||||
}
|
||||
node[tokenProperty] = getTokenPropertyValue(tokenProperty);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
public static createTree(
|
||||
realm: Realm,
|
||||
payloads: Protocol.Accessibility.AXNode[],
|
||||
): AXNode | null {
|
||||
const nodeById = new Map<string, AXNode>();
|
||||
for (const payload of payloads) {
|
||||
nodeById.set(payload.nodeId, new AXNode(realm, payload));
|
||||
}
|
||||
for (const node of nodeById.values()) {
|
||||
for (const childId of node.payload.childIds || []) {
|
||||
const child = nodeById.get(childId);
|
||||
if (child) {
|
||||
node.children.push(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
return nodeById.values().next().value ?? null;
|
||||
}
|
||||
}
|
||||
133
node_modules/puppeteer-core/src/cdp/Binding.ts
generated
vendored
Normal file
133
node_modules/puppeteer-core/src/cdp/Binding.ts
generated
vendored
Normal file
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2024 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
import {JSHandle} from '../api/JSHandle.js';
|
||||
import {debugError} from '../common/util.js';
|
||||
import {DisposableStack} from '../util/disposable.js';
|
||||
import {isErrorLike} from '../util/ErrorLike.js';
|
||||
|
||||
import type {ExecutionContext} from './ExecutionContext.js';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export class Binding {
|
||||
#name: string;
|
||||
#fn: (...args: unknown[]) => unknown;
|
||||
#initSource: string;
|
||||
constructor(
|
||||
name: string,
|
||||
fn: (...args: unknown[]) => unknown,
|
||||
initSource: string,
|
||||
) {
|
||||
this.#name = name;
|
||||
this.#fn = fn;
|
||||
this.#initSource = initSource;
|
||||
}
|
||||
|
||||
get name(): string {
|
||||
return this.#name;
|
||||
}
|
||||
|
||||
get initSource(): string {
|
||||
return this.#initSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param context - Context to run the binding in; the context should have
|
||||
* the binding added to it beforehand.
|
||||
* @param id - ID of the call. This should come from the CDP
|
||||
* `onBindingCalled` response.
|
||||
* @param args - Plain arguments from CDP.
|
||||
*/
|
||||
async run(
|
||||
context: ExecutionContext,
|
||||
id: number,
|
||||
args: unknown[],
|
||||
isTrivial: boolean,
|
||||
): Promise<void> {
|
||||
const stack = new DisposableStack();
|
||||
try {
|
||||
if (!isTrivial) {
|
||||
// Getting non-trivial arguments.
|
||||
using handles = await context.evaluateHandle(
|
||||
(name, seq) => {
|
||||
// @ts-expect-error Code is evaluated in a different context.
|
||||
return globalThis[name].args.get(seq);
|
||||
},
|
||||
this.#name,
|
||||
id,
|
||||
);
|
||||
const properties = await handles.getProperties();
|
||||
for (const [index, handle] of properties) {
|
||||
// This is not straight-forward since some arguments can stringify, but
|
||||
// aren't plain objects so add subtypes when the use-case arises.
|
||||
if (index in args) {
|
||||
switch (handle.remoteObject().subtype) {
|
||||
case 'node':
|
||||
args[+index] = handle;
|
||||
break;
|
||||
default:
|
||||
stack.use(handle);
|
||||
}
|
||||
} else {
|
||||
stack.use(handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await context.evaluate(
|
||||
(name, seq, result) => {
|
||||
// @ts-expect-error Code is evaluated in a different context.
|
||||
const callbacks = globalThis[name].callbacks;
|
||||
callbacks.get(seq).resolve(result);
|
||||
callbacks.delete(seq);
|
||||
},
|
||||
this.#name,
|
||||
id,
|
||||
await this.#fn(...args),
|
||||
);
|
||||
|
||||
for (const arg of args) {
|
||||
if (arg instanceof JSHandle) {
|
||||
stack.use(arg);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (isErrorLike(error)) {
|
||||
await context
|
||||
.evaluate(
|
||||
(name, seq, message, stack) => {
|
||||
const error = new Error(message);
|
||||
error.stack = stack;
|
||||
// @ts-expect-error Code is evaluated in a different context.
|
||||
const callbacks = globalThis[name].callbacks;
|
||||
callbacks.get(seq).reject(error);
|
||||
callbacks.delete(seq);
|
||||
},
|
||||
this.#name,
|
||||
id,
|
||||
error.message,
|
||||
error.stack,
|
||||
)
|
||||
.catch(debugError);
|
||||
} else {
|
||||
await context
|
||||
.evaluate(
|
||||
(name, seq, error) => {
|
||||
// @ts-expect-error Code is evaluated in a different context.
|
||||
const callbacks = globalThis[name].callbacks;
|
||||
callbacks.get(seq).reject(error);
|
||||
callbacks.delete(seq);
|
||||
},
|
||||
this.#name,
|
||||
id,
|
||||
error,
|
||||
)
|
||||
.catch(debugError);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
47
node_modules/puppeteer-core/src/cdp/BluetoothEmulation.ts
generated
vendored
Normal file
47
node_modules/puppeteer-core/src/cdp/BluetoothEmulation.ts
generated
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
import type {
|
||||
AdapterState,
|
||||
BluetoothEmulation,
|
||||
PreconnectedPeripheral,
|
||||
} from '../api/BluetoothEmulation.js';
|
||||
|
||||
import type {Connection} from './Connection.js';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export class CdpBluetoothEmulation implements BluetoothEmulation {
|
||||
#connection: Connection;
|
||||
|
||||
constructor(connection: Connection) {
|
||||
this.#connection = connection;
|
||||
}
|
||||
|
||||
async emulateAdapter(state: AdapterState, leSupported = true): Promise<void> {
|
||||
// Bluetooth spec requires overriding the existing adapter (step 6). From the CDP
|
||||
// perspective, it means disabling the emulation first.
|
||||
// https://webbluetoothcg.github.io/web-bluetooth/#bluetooth-simulateAdapter-command
|
||||
await this.#connection.send('BluetoothEmulation.disable');
|
||||
await this.#connection.send('BluetoothEmulation.enable', {
|
||||
state,
|
||||
leSupported,
|
||||
});
|
||||
}
|
||||
|
||||
async disableEmulation(): Promise<void> {
|
||||
await this.#connection.send('BluetoothEmulation.disable');
|
||||
}
|
||||
|
||||
async simulatePreconnectedPeripheral(
|
||||
preconnectedPeripheral: PreconnectedPeripheral,
|
||||
): Promise<void> {
|
||||
await this.#connection.send(
|
||||
'BluetoothEmulation.simulatePreconnectedPeripheral',
|
||||
preconnectedPeripheral,
|
||||
);
|
||||
}
|
||||
}
|
||||
541
node_modules/puppeteer-core/src/cdp/Browser.ts
generated
vendored
Normal file
541
node_modules/puppeteer-core/src/cdp/Browser.ts
generated
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
68
node_modules/puppeteer-core/src/cdp/BrowserConnector.ts
generated
vendored
Normal file
68
node_modules/puppeteer-core/src/cdp/BrowserConnector.ts
generated
vendored
Normal 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
183
node_modules/puppeteer-core/src/cdp/BrowserContext.ts
generated
vendored
Normal file
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2024 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type {Protocol} from 'devtools-protocol';
|
||||
|
||||
import type {CreatePageOptions} from '../api/Browser.js';
|
||||
import {
|
||||
WEB_PERMISSION_TO_PROTOCOL_PERMISSION,
|
||||
type Permission,
|
||||
type PermissionDescriptor,
|
||||
type PermissionState,
|
||||
} from '../api/Browser.js';
|
||||
import {BrowserContext} from '../api/BrowserContext.js';
|
||||
import type {Page} from '../api/Page.js';
|
||||
import type {Cookie, CookieData} from '../common/Cookie.js';
|
||||
import type {DownloadBehavior} from '../common/DownloadBehavior.js';
|
||||
import {assert} from '../util/assert.js';
|
||||
|
||||
import type {CdpBrowser} from './Browser.js';
|
||||
import type {Connection} from './Connection.js';
|
||||
import {
|
||||
convertCookiesPartitionKeyFromPuppeteerToCdp,
|
||||
convertSameSiteFromPuppeteerToCdp,
|
||||
} from './Page.js';
|
||||
import type {CdpTarget} from './Target.js';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export class CdpBrowserContext extends BrowserContext {
|
||||
#connection: Connection;
|
||||
#browser: CdpBrowser;
|
||||
#id?: string;
|
||||
|
||||
constructor(connection: Connection, browser: CdpBrowser, contextId?: string) {
|
||||
super();
|
||||
this.#connection = connection;
|
||||
this.#browser = browser;
|
||||
this.#id = contextId;
|
||||
}
|
||||
|
||||
override get id(): string | undefined {
|
||||
return this.#id;
|
||||
}
|
||||
|
||||
override targets(): CdpTarget[] {
|
||||
return this.#browser.targets().filter(target => {
|
||||
return target.browserContext() === this;
|
||||
});
|
||||
}
|
||||
|
||||
override async pages(includeAll = false): Promise<Page[]> {
|
||||
const pages = await Promise.all(
|
||||
this.targets()
|
||||
.filter(target => {
|
||||
return (
|
||||
target.type() === 'page' ||
|
||||
((target.type() === 'other' || includeAll) &&
|
||||
this.#browser._getIsPageTargetCallback()?.(target))
|
||||
);
|
||||
})
|
||||
.map(target => {
|
||||
return target.page();
|
||||
}),
|
||||
);
|
||||
return pages.filter((page): page is Page => {
|
||||
return !!page;
|
||||
});
|
||||
}
|
||||
|
||||
override async overridePermissions(
|
||||
origin: string,
|
||||
permissions: Permission[],
|
||||
): Promise<void> {
|
||||
const protocolPermissions = permissions.map(permission => {
|
||||
const protocolPermission =
|
||||
WEB_PERMISSION_TO_PROTOCOL_PERMISSION.get(permission);
|
||||
if (!protocolPermission) {
|
||||
throw new Error('Unknown permission: ' + permission);
|
||||
}
|
||||
return protocolPermission;
|
||||
});
|
||||
await this.#connection.send('Browser.grantPermissions', {
|
||||
origin,
|
||||
browserContextId: this.#id || undefined,
|
||||
permissions: protocolPermissions,
|
||||
});
|
||||
}
|
||||
|
||||
override async setPermission(
|
||||
origin: string | '*',
|
||||
...permissions: Array<{
|
||||
permission: PermissionDescriptor;
|
||||
state: PermissionState;
|
||||
}>
|
||||
): Promise<void> {
|
||||
await Promise.all(
|
||||
permissions.map(async permission => {
|
||||
const protocolPermission: Protocol.Browser.PermissionDescriptor = {
|
||||
name: permission.permission.name,
|
||||
userVisibleOnly: permission.permission.userVisibleOnly,
|
||||
sysex: permission.permission.sysex,
|
||||
allowWithoutSanitization:
|
||||
permission.permission.allowWithoutSanitization,
|
||||
panTiltZoom: permission.permission.panTiltZoom,
|
||||
};
|
||||
await this.#connection.send('Browser.setPermission', {
|
||||
origin: origin === '*' ? undefined : origin,
|
||||
browserContextId: this.#id || undefined,
|
||||
permission: protocolPermission,
|
||||
setting: permission.state as Protocol.Browser.PermissionSetting,
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
override async clearPermissionOverrides(): Promise<void> {
|
||||
await this.#connection.send('Browser.resetPermissions', {
|
||||
browserContextId: this.#id || undefined,
|
||||
});
|
||||
}
|
||||
|
||||
override async newPage(options?: CreatePageOptions): Promise<Page> {
|
||||
using _guard = await this.waitForScreenshotOperations();
|
||||
return await this.#browser._createPageInContext(this.#id, options);
|
||||
}
|
||||
|
||||
override browser(): CdpBrowser {
|
||||
return this.#browser;
|
||||
}
|
||||
|
||||
override async close(): Promise<void> {
|
||||
assert(this.#id, 'Default BrowserContext cannot be closed!');
|
||||
await this.#browser._disposeContext(this.#id);
|
||||
}
|
||||
|
||||
override async cookies(): Promise<Cookie[]> {
|
||||
const {cookies} = await this.#connection.send('Storage.getCookies', {
|
||||
browserContextId: this.#id,
|
||||
});
|
||||
return cookies.map(cookie => {
|
||||
return {
|
||||
...cookie,
|
||||
partitionKey: cookie.partitionKey
|
||||
? {
|
||||
sourceOrigin: cookie.partitionKey.topLevelSite,
|
||||
hasCrossSiteAncestor: cookie.partitionKey.hasCrossSiteAncestor,
|
||||
}
|
||||
: undefined,
|
||||
// TODO: remove sameParty as it is removed from Chrome.
|
||||
sameParty: 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
46
node_modules/puppeteer-core/src/cdp/CdpPreloadScript.ts
generated
vendored
Normal file
46
node_modules/puppeteer-core/src/cdp/CdpPreloadScript.ts
generated
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2024 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type {CdpFrame} from './Frame.js';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export class CdpPreloadScript {
|
||||
/**
|
||||
* This is the ID of the preload script returned by
|
||||
* Page.addScriptToEvaluateOnNewDocument in the main frame.
|
||||
*
|
||||
* Sub-frames would get a different CDP ID because
|
||||
* addScriptToEvaluateOnNewDocument is called for each subframe. But
|
||||
* users only see this ID and subframe IDs are internal to Puppeteer.
|
||||
*/
|
||||
#id: string;
|
||||
#source: string;
|
||||
#frameToId = new WeakMap<CdpFrame, string>();
|
||||
|
||||
constructor(mainFrame: CdpFrame, id: string, source: string) {
|
||||
this.#id = id;
|
||||
this.#source = source;
|
||||
this.#frameToId.set(mainFrame, id);
|
||||
}
|
||||
|
||||
get id(): string {
|
||||
return this.#id;
|
||||
}
|
||||
|
||||
get source(): string {
|
||||
return this.#source;
|
||||
}
|
||||
|
||||
getIdForFrame(frame: CdpFrame): string | undefined {
|
||||
return this.#frameToId.get(frame);
|
||||
}
|
||||
|
||||
setIdForFrame(frame: CdpFrame, identifier: string): void {
|
||||
this.#frameToId.set(frame, identifier);
|
||||
}
|
||||
}
|
||||
181
node_modules/puppeteer-core/src/cdp/CdpSession.ts
generated
vendored
Normal file
181
node_modules/puppeteer-core/src/cdp/CdpSession.ts
generated
vendored
Normal file
@@ -0,0 +1,181 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2017 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type {ProtocolMapping} from 'devtools-protocol/types/protocol-mapping.js';
|
||||
|
||||
import {
|
||||
type CDPEvents,
|
||||
CDPSession,
|
||||
CDPSessionEvent,
|
||||
type CommandOptions,
|
||||
} from '../api/CDPSession.js';
|
||||
import {CallbackRegistry} from '../common/CallbackRegistry.js';
|
||||
import {TargetCloseError} from '../common/Errors.js';
|
||||
import {assert} from '../util/assert.js';
|
||||
import {createProtocolErrorMessage} from '../util/ErrorLike.js';
|
||||
|
||||
import type {Connection} from './Connection.js';
|
||||
import type {CdpTarget} from './Target.js';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
|
||||
export class CdpCDPSession extends CDPSession {
|
||||
#sessionId: string;
|
||||
#targetType: string;
|
||||
#callbacks: CallbackRegistry;
|
||||
#connection: Connection;
|
||||
#parentSessionId?: string;
|
||||
#target?: CdpTarget;
|
||||
#rawErrors = false;
|
||||
#detached = false;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
constructor(
|
||||
connection: Connection,
|
||||
targetType: string,
|
||||
sessionId: string,
|
||||
parentSessionId: string | undefined,
|
||||
rawErrors: boolean,
|
||||
) {
|
||||
super();
|
||||
this.#connection = connection;
|
||||
this.#targetType = targetType;
|
||||
this.#callbacks = new CallbackRegistry(connection._idGenerator);
|
||||
this.#sessionId = sessionId;
|
||||
this.#parentSessionId = parentSessionId;
|
||||
this.#rawErrors = rawErrors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link CdpTarget} associated with the session instance.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
setTarget(target: CdpTarget): void {
|
||||
this.#target = target;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link CdpTarget} associated with the session instance.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
target(): CdpTarget {
|
||||
assert(this.#target, 'Target must exist');
|
||||
return this.#target;
|
||||
}
|
||||
|
||||
override connection(): Connection {
|
||||
return this.#connection;
|
||||
}
|
||||
|
||||
override get detached(): boolean {
|
||||
return this.#connection._closed || this.#detached;
|
||||
}
|
||||
|
||||
override parentSession(): CDPSession | undefined {
|
||||
if (!this.#parentSessionId) {
|
||||
// In some cases, e.g., DevTools pages there is no parent session. In this
|
||||
// case, we treat the current session as the parent session.
|
||||
return this;
|
||||
}
|
||||
const parent = this.#connection?.session(this.#parentSessionId);
|
||||
return parent ?? undefined;
|
||||
}
|
||||
|
||||
override send<T extends keyof ProtocolMapping.Commands>(
|
||||
method: T,
|
||||
params?: ProtocolMapping.Commands[T]['paramsType'][0],
|
||||
options?: CommandOptions,
|
||||
): Promise<ProtocolMapping.Commands[T]['returnType']> {
|
||||
if (this.detached) {
|
||||
return Promise.reject(
|
||||
new TargetCloseError(
|
||||
`Protocol error (${method}): Session closed. Most likely the ${this.#targetType} has been closed.`,
|
||||
),
|
||||
);
|
||||
}
|
||||
return this.#connection._rawSend(
|
||||
this.#callbacks,
|
||||
method,
|
||||
params,
|
||||
this.#sessionId,
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
onMessage(object: {
|
||||
id?: number;
|
||||
method: keyof CDPEvents;
|
||||
params: CDPEvents[keyof CDPEvents];
|
||||
error: {message: string; data: any; code: number};
|
||||
result?: any;
|
||||
}): void {
|
||||
if (object.id) {
|
||||
if (object.error) {
|
||||
if (this.#rawErrors) {
|
||||
this.#callbacks.rejectRaw(object.id, object.error);
|
||||
} else {
|
||||
this.#callbacks.reject(
|
||||
object.id,
|
||||
createProtocolErrorMessage(object),
|
||||
object.error.message,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.#callbacks.resolve(object.id, object.result);
|
||||
}
|
||||
} else {
|
||||
assert(!object.id);
|
||||
this.emit(object.method, object.params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detaches the cdpSession from the target. Once detached, the cdpSession object
|
||||
* won't emit any events and can't be used to send messages.
|
||||
*/
|
||||
override async detach(): Promise<void> {
|
||||
if (this.detached) {
|
||||
throw new Error(
|
||||
`Session already detached. Most likely the ${this.#targetType} has been closed.`,
|
||||
);
|
||||
}
|
||||
await this.#connection.send('Target.detachFromTarget', {
|
||||
sessionId: this.#sessionId,
|
||||
});
|
||||
this.#detached = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
onClosed(): void {
|
||||
this.#callbacks.clear();
|
||||
this.#detached = true;
|
||||
this.emit(CDPSessionEvent.Disconnected, undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the session's id.
|
||||
*/
|
||||
override id(): string {
|
||||
return this.#sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
getPendingProtocolErrors(): Error[] {
|
||||
return this.#callbacks.getPendingProtocolErrors();
|
||||
}
|
||||
}
|
||||
308
node_modules/puppeteer-core/src/cdp/Connection.ts
generated
vendored
Normal file
308
node_modules/puppeteer-core/src/cdp/Connection.ts
generated
vendored
Normal file
@@ -0,0 +1,308 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2017 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type {Protocol} from 'devtools-protocol';
|
||||
import type {ProtocolMapping} from 'devtools-protocol/types/protocol-mapping.js';
|
||||
|
||||
import type {CommandOptions} from '../api/CDPSession.js';
|
||||
import {
|
||||
CDPSessionEvent,
|
||||
type CDPSession,
|
||||
type CDPSessionEvents,
|
||||
} from '../api/CDPSession.js';
|
||||
import {CallbackRegistry} from '../common/CallbackRegistry.js';
|
||||
import type {ConnectionTransport} from '../common/ConnectionTransport.js';
|
||||
import {debug} from '../common/Debug.js';
|
||||
import {ConnectionClosedError, TargetCloseError} from '../common/Errors.js';
|
||||
import {EventEmitter} from '../common/EventEmitter.js';
|
||||
import {createProtocolErrorMessage} from '../util/ErrorLike.js';
|
||||
import {
|
||||
createIncrementalIdGenerator,
|
||||
type GetIdFn,
|
||||
} from '../util/incremental-id-generator.js';
|
||||
|
||||
import {CdpCDPSession} from './CdpSession.js';
|
||||
|
||||
const debugProtocolSend = debug('puppeteer:protocol:SEND ►');
|
||||
const debugProtocolReceive = debug('puppeteer:protocol:RECV ◀');
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export class Connection extends EventEmitter<CDPSessionEvents> {
|
||||
#url: string;
|
||||
#transport: ConnectionTransport;
|
||||
#delay: number;
|
||||
#timeout: number;
|
||||
#sessions = new Map<string, CdpCDPSession>();
|
||||
#closed = false;
|
||||
#manuallyAttached = new Set<string>();
|
||||
#callbacks: CallbackRegistry;
|
||||
#rawErrors = false;
|
||||
#idGenerator: GetIdFn;
|
||||
|
||||
constructor(
|
||||
url: string,
|
||||
transport: ConnectionTransport,
|
||||
delay = 0,
|
||||
timeout?: number,
|
||||
rawErrors = false,
|
||||
idGenerator: () => number = createIncrementalIdGenerator(),
|
||||
) {
|
||||
super();
|
||||
this.#rawErrors = rawErrors;
|
||||
this.#idGenerator = idGenerator;
|
||||
this.#callbacks = new CallbackRegistry(idGenerator);
|
||||
this.#url = url;
|
||||
this.#delay = delay;
|
||||
this.#timeout = timeout ?? 180_000;
|
||||
|
||||
this.#transport = transport;
|
||||
this.#transport.onmessage = this.onMessage.bind(this);
|
||||
this.#transport.onclose = this.#onClose.bind(this);
|
||||
}
|
||||
|
||||
static fromSession(session: CDPSession): Connection | undefined {
|
||||
return session.connection();
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
get delay(): number {
|
||||
return this.#delay;
|
||||
}
|
||||
|
||||
get timeout(): number {
|
||||
return this.#timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
get _closed(): boolean {
|
||||
return this.#closed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
get _idGenerator(): GetIdFn {
|
||||
return this.#idGenerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
get _sessions(): Map<string, CdpCDPSession> {
|
||||
return this.#sessions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_session(sessionId: string): CdpCDPSession | null {
|
||||
return this.#sessions.get(sessionId) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param sessionId - The session id
|
||||
* @returns The current CDP session if it exists
|
||||
*/
|
||||
session(sessionId: string): CDPSession | null {
|
||||
return this._session(sessionId);
|
||||
}
|
||||
|
||||
url(): string {
|
||||
return this.#url;
|
||||
}
|
||||
|
||||
send<T extends keyof ProtocolMapping.Commands>(
|
||||
method: T,
|
||||
params?: ProtocolMapping.Commands[T]['paramsType'][0],
|
||||
options?: CommandOptions,
|
||||
): Promise<ProtocolMapping.Commands[T]['returnType']> {
|
||||
// There is only ever 1 param arg passed, but the Protocol defines it as an
|
||||
// array of 0 or 1 items See this comment:
|
||||
// https://github.com/ChromeDevTools/devtools-protocol/pull/113#issuecomment-412603285
|
||||
// which explains why the protocol defines the params this way for better
|
||||
// type-inference.
|
||||
// So now we check if there are any params or not and deal with them accordingly.
|
||||
return this._rawSend(this.#callbacks, method, params, undefined, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_rawSend<T extends keyof ProtocolMapping.Commands>(
|
||||
callbacks: CallbackRegistry,
|
||||
method: T,
|
||||
params: ProtocolMapping.Commands[T]['paramsType'][0],
|
||||
sessionId?: string,
|
||||
options?: CommandOptions,
|
||||
): Promise<ProtocolMapping.Commands[T]['returnType']> {
|
||||
if (this.#closed) {
|
||||
return Promise.reject(new ConnectionClosedError('Connection closed.'));
|
||||
}
|
||||
return callbacks.create(method, options?.timeout ?? this.#timeout, id => {
|
||||
const stringifiedMessage = JSON.stringify({
|
||||
method,
|
||||
params,
|
||||
id,
|
||||
sessionId,
|
||||
});
|
||||
debugProtocolSend(stringifiedMessage);
|
||||
this.#transport.send(stringifiedMessage);
|
||||
}) as Promise<ProtocolMapping.Commands[T]['returnType']>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async closeBrowser(): Promise<void> {
|
||||
await this.send('Browser.close');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected async onMessage(message: string): Promise<void> {
|
||||
if (this.#delay) {
|
||||
await new Promise(r => {
|
||||
return setTimeout(r, this.#delay);
|
||||
});
|
||||
}
|
||||
debugProtocolReceive(message);
|
||||
const object = JSON.parse(message);
|
||||
if (object.method === 'Target.attachedToTarget') {
|
||||
const sessionId = object.params.sessionId;
|
||||
const session = new CdpCDPSession(
|
||||
this,
|
||||
object.params.targetInfo.type,
|
||||
sessionId,
|
||||
object.sessionId,
|
||||
this.#rawErrors,
|
||||
);
|
||||
this.#sessions.set(sessionId, session);
|
||||
this.emit(CDPSessionEvent.SessionAttached, session);
|
||||
const parentSession = this.#sessions.get(object.sessionId);
|
||||
if (parentSession) {
|
||||
parentSession.emit(CDPSessionEvent.SessionAttached, session);
|
||||
}
|
||||
} else if (object.method === 'Target.detachedFromTarget') {
|
||||
const session = this.#sessions.get(object.params.sessionId);
|
||||
if (session) {
|
||||
session.onClosed();
|
||||
this.#sessions.delete(object.params.sessionId);
|
||||
this.emit(CDPSessionEvent.SessionDetached, session);
|
||||
const parentSession = this.#sessions.get(object.sessionId);
|
||||
if (parentSession) {
|
||||
parentSession.emit(CDPSessionEvent.SessionDetached, session);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (object.sessionId) {
|
||||
const session = this.#sessions.get(object.sessionId);
|
||||
if (session) {
|
||||
session.onMessage(object);
|
||||
}
|
||||
} else if (object.id) {
|
||||
if (object.error) {
|
||||
if (this.#rawErrors) {
|
||||
this.#callbacks.rejectRaw(object.id, object.error);
|
||||
} else {
|
||||
this.#callbacks.reject(
|
||||
object.id,
|
||||
createProtocolErrorMessage(object),
|
||||
object.error.message,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.#callbacks.resolve(object.id, object.result);
|
||||
}
|
||||
} else {
|
||||
this.emit(object.method, object.params);
|
||||
}
|
||||
}
|
||||
|
||||
#onClose(): void {
|
||||
if (this.#closed) {
|
||||
return;
|
||||
}
|
||||
this.#closed = true;
|
||||
this.#transport.onmessage = undefined;
|
||||
this.#transport.onclose = undefined;
|
||||
this.#callbacks.clear();
|
||||
for (const session of this.#sessions.values()) {
|
||||
session.onClosed();
|
||||
}
|
||||
this.#sessions.clear();
|
||||
this.emit(CDPSessionEvent.Disconnected, undefined);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.#onClose();
|
||||
this.#transport.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
isAutoAttached(targetId: string): boolean {
|
||||
return !this.#manuallyAttached.has(targetId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async _createSession(
|
||||
targetInfo: {targetId: string},
|
||||
isAutoAttachEmulated = true,
|
||||
): Promise<CdpCDPSession> {
|
||||
if (!isAutoAttachEmulated) {
|
||||
this.#manuallyAttached.add(targetInfo.targetId);
|
||||
}
|
||||
const {sessionId} = await this.send('Target.attachToTarget', {
|
||||
targetId: targetInfo.targetId,
|
||||
flatten: true,
|
||||
});
|
||||
this.#manuallyAttached.delete(targetInfo.targetId);
|
||||
const session = this.#sessions.get(sessionId);
|
||||
if (!session) {
|
||||
throw new Error('CDPSession creation failed.');
|
||||
}
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param targetInfo - The target info
|
||||
* @returns The CDP session that is created
|
||||
*/
|
||||
async createSession(
|
||||
targetInfo: Protocol.Target.TargetInfo,
|
||||
): Promise<CDPSession> {
|
||||
return await this._createSession(targetInfo, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
getPendingProtocolErrors(): Error[] {
|
||||
const result: Error[] = [];
|
||||
result.push(...this.#callbacks.getPendingProtocolErrors());
|
||||
for (const session of this.#sessions.values()) {
|
||||
result.push(...session.getPendingProtocolErrors());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export function isTargetClosedError(error: Error): boolean {
|
||||
return error instanceof TargetCloseError;
|
||||
}
|
||||
508
node_modules/puppeteer-core/src/cdp/Coverage.ts
generated
vendored
Normal file
508
node_modules/puppeteer-core/src/cdp/Coverage.ts
generated
vendored
Normal file
@@ -0,0 +1,508 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2017 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type {Protocol} from 'devtools-protocol';
|
||||
|
||||
import type {CDPSession} from '../api/CDPSession.js';
|
||||
import {EventEmitter} from '../common/EventEmitter.js';
|
||||
import {debugError, PuppeteerURL} from '../common/util.js';
|
||||
import {assert} from '../util/assert.js';
|
||||
import {DisposableStack} from '../util/disposable.js';
|
||||
|
||||
/**
|
||||
* The CoverageEntry class represents one entry of the coverage report.
|
||||
* @public
|
||||
*/
|
||||
export interface CoverageEntry {
|
||||
/**
|
||||
* The URL of the style sheet or script.
|
||||
*/
|
||||
url: string;
|
||||
/**
|
||||
* The content of the style sheet or script.
|
||||
*/
|
||||
text: string;
|
||||
/**
|
||||
* The covered range as start and end positions.
|
||||
*/
|
||||
ranges: Array<{start: number; end: number}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The CoverageEntry class for JavaScript
|
||||
* @public
|
||||
*/
|
||||
export interface JSCoverageEntry extends CoverageEntry {
|
||||
/**
|
||||
* Raw V8 script coverage entry.
|
||||
*/
|
||||
rawScriptCoverage?: Protocol.Profiler.ScriptCoverage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set of configurable options for JS coverage.
|
||||
* @public
|
||||
*/
|
||||
export interface JSCoverageOptions {
|
||||
/**
|
||||
* Whether to reset coverage on every navigation.
|
||||
*/
|
||||
resetOnNavigation?: boolean;
|
||||
/**
|
||||
* Whether anonymous scripts generated by the page should be reported.
|
||||
*/
|
||||
reportAnonymousScripts?: boolean;
|
||||
/**
|
||||
* Whether the result includes raw V8 script coverage entries.
|
||||
*/
|
||||
includeRawScriptCoverage?: boolean;
|
||||
/**
|
||||
* Whether to collect coverage information at the block level.
|
||||
* If true, coverage will be collected at the block level (this is the default).
|
||||
* If false, coverage will be collected at the function level.
|
||||
*/
|
||||
useBlockCoverage?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set of configurable options for CSS coverage.
|
||||
* @public
|
||||
*/
|
||||
export interface CSSCoverageOptions {
|
||||
/**
|
||||
* Whether to reset coverage on every navigation.
|
||||
*/
|
||||
resetOnNavigation?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Coverage class provides methods to gather information about parts of
|
||||
* JavaScript and CSS that were used by the page.
|
||||
*
|
||||
* @remarks
|
||||
* To output coverage in a form consumable by {@link https://github.com/istanbuljs | Istanbul},
|
||||
* see {@link https://github.com/istanbuljs/puppeteer-to-istanbul | puppeteer-to-istanbul}.
|
||||
*
|
||||
* @example
|
||||
* An example of using JavaScript and CSS coverage to get percentage of initially
|
||||
* executed code:
|
||||
*
|
||||
* ```ts
|
||||
* // Enable both JavaScript and CSS coverage
|
||||
* await Promise.all([
|
||||
* page.coverage.startJSCoverage(),
|
||||
* page.coverage.startCSSCoverage(),
|
||||
* ]);
|
||||
* // Navigate to page
|
||||
* await page.goto('https://example.com');
|
||||
* // Disable both JavaScript and CSS coverage
|
||||
* const [jsCoverage, cssCoverage] = await Promise.all([
|
||||
* page.coverage.stopJSCoverage(),
|
||||
* page.coverage.stopCSSCoverage(),
|
||||
* ]);
|
||||
* let totalBytes = 0;
|
||||
* let usedBytes = 0;
|
||||
* const coverage = [...jsCoverage, ...cssCoverage];
|
||||
* for (const entry of coverage) {
|
||||
* totalBytes += entry.text.length;
|
||||
* for (const range of entry.ranges) usedBytes += range.end - range.start - 1;
|
||||
* }
|
||||
* console.log(`Bytes used: ${(usedBytes / totalBytes) * 100}%`);
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class Coverage {
|
||||
#jsCoverage: JSCoverage;
|
||||
#cssCoverage: CSSCoverage;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
constructor(client: CDPSession) {
|
||||
this.#jsCoverage = new JSCoverage(client);
|
||||
this.#cssCoverage = new CSSCoverage(client);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
updateClient(client: CDPSession): void {
|
||||
this.#jsCoverage.updateClient(client);
|
||||
this.#cssCoverage.updateClient(client);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param options - Set of configurable options for coverage defaults to
|
||||
* `resetOnNavigation : true, reportAnonymousScripts : false,`
|
||||
* `includeRawScriptCoverage : false, useBlockCoverage : true`
|
||||
* @returns Promise that resolves when coverage is started.
|
||||
*
|
||||
* @remarks
|
||||
* Anonymous scripts are ones that don't have an associated url. These are
|
||||
* scripts that are dynamically created on the page using `eval` or
|
||||
* `new Function`. If `reportAnonymousScripts` is set to `true`, anonymous
|
||||
* scripts URL will start with `debugger://VM` (unless a magic //# sourceURL
|
||||
* comment is present, in which case that will the be URL).
|
||||
*/
|
||||
async startJSCoverage(options: JSCoverageOptions = {}): Promise<void> {
|
||||
return await this.#jsCoverage.start(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Promise that resolves to the array of coverage reports for
|
||||
* all scripts.
|
||||
*
|
||||
* @remarks
|
||||
* JavaScript Coverage doesn't include anonymous scripts by default.
|
||||
* However, scripts with sourceURLs are reported.
|
||||
*/
|
||||
async stopJSCoverage(): Promise<JSCoverageEntry[]> {
|
||||
return await this.#jsCoverage.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param options - Set of configurable options for coverage, defaults to
|
||||
* `resetOnNavigation : true`
|
||||
* @returns Promise that resolves when coverage is started.
|
||||
*/
|
||||
async startCSSCoverage(options: CSSCoverageOptions = {}): Promise<void> {
|
||||
return await this.#cssCoverage.start(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Promise that resolves to the array of coverage reports
|
||||
* for all stylesheets.
|
||||
*
|
||||
* @remarks
|
||||
* CSS Coverage doesn't include dynamically injected style tags
|
||||
* without sourceURLs.
|
||||
*/
|
||||
async stopCSSCoverage(): Promise<CoverageEntry[]> {
|
||||
return await this.#cssCoverage.stop();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export class JSCoverage {
|
||||
#client: CDPSession;
|
||||
#enabled = false;
|
||||
#scriptURLs = new Map<string, string>();
|
||||
#scriptSources = new Map<string, string>();
|
||||
#subscriptions?: DisposableStack;
|
||||
#resetOnNavigation = false;
|
||||
#reportAnonymousScripts = false;
|
||||
#includeRawScriptCoverage = false;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
constructor(client: CDPSession) {
|
||||
this.#client = client;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
updateClient(client: CDPSession): void {
|
||||
this.#client = client;
|
||||
}
|
||||
|
||||
async start(
|
||||
options: {
|
||||
resetOnNavigation?: boolean;
|
||||
reportAnonymousScripts?: boolean;
|
||||
includeRawScriptCoverage?: boolean;
|
||||
useBlockCoverage?: boolean;
|
||||
} = {},
|
||||
): Promise<void> {
|
||||
assert(!this.#enabled, 'JSCoverage is already enabled');
|
||||
const {
|
||||
resetOnNavigation = true,
|
||||
reportAnonymousScripts = false,
|
||||
includeRawScriptCoverage = false,
|
||||
useBlockCoverage = true,
|
||||
} = options;
|
||||
this.#resetOnNavigation = resetOnNavigation;
|
||||
this.#reportAnonymousScripts = reportAnonymousScripts;
|
||||
this.#includeRawScriptCoverage = includeRawScriptCoverage;
|
||||
this.#enabled = true;
|
||||
this.#scriptURLs.clear();
|
||||
this.#scriptSources.clear();
|
||||
this.#subscriptions = new DisposableStack();
|
||||
const clientEmitter = this.#subscriptions.use(
|
||||
new EventEmitter(this.#client),
|
||||
);
|
||||
clientEmitter.on('Debugger.scriptParsed', this.#onScriptParsed.bind(this));
|
||||
clientEmitter.on(
|
||||
'Runtime.executionContextsCleared',
|
||||
this.#onExecutionContextsCleared.bind(this),
|
||||
);
|
||||
await Promise.all([
|
||||
this.#client.send('Profiler.enable'),
|
||||
this.#client.send('Profiler.startPreciseCoverage', {
|
||||
callCount: this.#includeRawScriptCoverage,
|
||||
detailed: useBlockCoverage,
|
||||
}),
|
||||
this.#client.send('Debugger.enable'),
|
||||
this.#client.send('Debugger.setSkipAllPauses', {skip: true}),
|
||||
]);
|
||||
}
|
||||
|
||||
#onExecutionContextsCleared(): void {
|
||||
if (!this.#resetOnNavigation) {
|
||||
return;
|
||||
}
|
||||
this.#scriptURLs.clear();
|
||||
this.#scriptSources.clear();
|
||||
}
|
||||
|
||||
async #onScriptParsed(
|
||||
event: Protocol.Debugger.ScriptParsedEvent,
|
||||
): Promise<void> {
|
||||
// Ignore puppeteer-injected scripts
|
||||
if (PuppeteerURL.isPuppeteerURL(event.url)) {
|
||||
return;
|
||||
}
|
||||
// Ignore other anonymous scripts unless the reportAnonymousScripts option is true.
|
||||
if (!event.url && !this.#reportAnonymousScripts) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await this.#client.send('Debugger.getScriptSource', {
|
||||
scriptId: event.scriptId,
|
||||
});
|
||||
this.#scriptURLs.set(event.scriptId, event.url);
|
||||
this.#scriptSources.set(event.scriptId, response.scriptSource);
|
||||
} catch (error) {
|
||||
// This might happen if the page has already navigated away.
|
||||
debugError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async stop(): Promise<JSCoverageEntry[]> {
|
||||
assert(this.#enabled, 'JSCoverage is not enabled');
|
||||
this.#enabled = false;
|
||||
|
||||
const result = await Promise.all([
|
||||
this.#client.send('Profiler.takePreciseCoverage'),
|
||||
this.#client.send('Profiler.stopPreciseCoverage'),
|
||||
this.#client.send('Profiler.disable'),
|
||||
this.#client.send('Debugger.disable'),
|
||||
]);
|
||||
|
||||
this.#subscriptions?.dispose();
|
||||
|
||||
const coverage = [];
|
||||
const profileResponse = result[0];
|
||||
|
||||
for (const entry of profileResponse.result) {
|
||||
let url = this.#scriptURLs.get(entry.scriptId);
|
||||
if (!url && this.#reportAnonymousScripts) {
|
||||
url = 'debugger://VM' + entry.scriptId;
|
||||
}
|
||||
const text = this.#scriptSources.get(entry.scriptId);
|
||||
if (text === undefined || url === undefined) {
|
||||
continue;
|
||||
}
|
||||
const flattenRanges = [];
|
||||
for (const func of entry.functions) {
|
||||
flattenRanges.push(...func.ranges);
|
||||
}
|
||||
const ranges = convertToDisjointRanges(flattenRanges);
|
||||
if (!this.#includeRawScriptCoverage) {
|
||||
coverage.push({url, ranges, text});
|
||||
} else {
|
||||
coverage.push({url, ranges, text, rawScriptCoverage: entry});
|
||||
}
|
||||
}
|
||||
return coverage;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export class CSSCoverage {
|
||||
#client: CDPSession;
|
||||
#enabled = false;
|
||||
#stylesheetURLs = new Map<string, string>();
|
||||
#stylesheetSources = new Map<string, string>();
|
||||
#eventListeners?: DisposableStack;
|
||||
#resetOnNavigation = false;
|
||||
|
||||
constructor(client: CDPSession) {
|
||||
this.#client = client;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
updateClient(client: CDPSession): void {
|
||||
this.#client = client;
|
||||
}
|
||||
|
||||
async start(options: {resetOnNavigation?: boolean} = {}): Promise<void> {
|
||||
assert(!this.#enabled, 'CSSCoverage is already enabled');
|
||||
const {resetOnNavigation = true} = options;
|
||||
this.#resetOnNavigation = resetOnNavigation;
|
||||
this.#enabled = true;
|
||||
this.#stylesheetURLs.clear();
|
||||
this.#stylesheetSources.clear();
|
||||
this.#eventListeners = new DisposableStack();
|
||||
const clientEmitter = this.#eventListeners.use(
|
||||
new EventEmitter(this.#client),
|
||||
);
|
||||
clientEmitter.on('CSS.styleSheetAdded', this.#onStyleSheet.bind(this));
|
||||
clientEmitter.on(
|
||||
'Runtime.executionContextsCleared',
|
||||
this.#onExecutionContextsCleared.bind(this),
|
||||
);
|
||||
|
||||
await Promise.all([
|
||||
this.#client.send('DOM.enable'),
|
||||
this.#client.send('CSS.enable'),
|
||||
this.#client.send('CSS.startRuleUsageTracking'),
|
||||
]);
|
||||
}
|
||||
|
||||
#onExecutionContextsCleared(): void {
|
||||
if (!this.#resetOnNavigation) {
|
||||
return;
|
||||
}
|
||||
this.#stylesheetURLs.clear();
|
||||
this.#stylesheetSources.clear();
|
||||
}
|
||||
|
||||
async #onStyleSheet(event: Protocol.CSS.StyleSheetAddedEvent): Promise<void> {
|
||||
const header = event.header;
|
||||
// Ignore anonymous scripts
|
||||
if (!header.sourceURL) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await this.#client.send('CSS.getStyleSheetText', {
|
||||
styleSheetId: header.styleSheetId,
|
||||
});
|
||||
this.#stylesheetURLs.set(header.styleSheetId, header.sourceURL);
|
||||
this.#stylesheetSources.set(header.styleSheetId, response.text);
|
||||
} catch (error) {
|
||||
// This might happen if the page has already navigated away.
|
||||
debugError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async stop(): Promise<CoverageEntry[]> {
|
||||
assert(this.#enabled, 'CSSCoverage is not enabled');
|
||||
this.#enabled = false;
|
||||
const ruleTrackingResponse = await this.#client.send(
|
||||
'CSS.stopRuleUsageTracking',
|
||||
);
|
||||
await Promise.all([
|
||||
this.#client.send('CSS.disable'),
|
||||
this.#client.send('DOM.disable'),
|
||||
]);
|
||||
this.#eventListeners?.dispose();
|
||||
|
||||
// aggregate by styleSheetId
|
||||
const styleSheetIdToCoverage = new Map();
|
||||
for (const entry of ruleTrackingResponse.ruleUsage) {
|
||||
let ranges = styleSheetIdToCoverage.get(entry.styleSheetId);
|
||||
if (!ranges) {
|
||||
ranges = [];
|
||||
styleSheetIdToCoverage.set(entry.styleSheetId, ranges);
|
||||
}
|
||||
ranges.push({
|
||||
startOffset: entry.startOffset,
|
||||
endOffset: entry.endOffset,
|
||||
count: entry.used ? 1 : 0,
|
||||
});
|
||||
}
|
||||
|
||||
const coverage: CoverageEntry[] = [];
|
||||
for (const styleSheetId of this.#stylesheetURLs.keys()) {
|
||||
const url = this.#stylesheetURLs.get(styleSheetId);
|
||||
assert(
|
||||
typeof url !== 'undefined',
|
||||
`Stylesheet URL is undefined (styleSheetId=${styleSheetId})`,
|
||||
);
|
||||
const text = this.#stylesheetSources.get(styleSheetId);
|
||||
assert(
|
||||
typeof text !== 'undefined',
|
||||
`Stylesheet text is undefined (styleSheetId=${styleSheetId})`,
|
||||
);
|
||||
const ranges = convertToDisjointRanges(
|
||||
styleSheetIdToCoverage.get(styleSheetId) || [],
|
||||
);
|
||||
coverage.push({url, ranges, text});
|
||||
}
|
||||
|
||||
return coverage;
|
||||
}
|
||||
}
|
||||
|
||||
function convertToDisjointRanges(
|
||||
nestedRanges: Array<{startOffset: number; endOffset: number; count: number}>,
|
||||
): Array<{start: number; end: number}> {
|
||||
const points = [];
|
||||
for (const range of nestedRanges) {
|
||||
points.push({offset: range.startOffset, type: 0, range});
|
||||
points.push({offset: range.endOffset, type: 1, range});
|
||||
}
|
||||
// Sort points to form a valid parenthesis sequence.
|
||||
points.sort((a, b) => {
|
||||
// Sort with increasing offsets.
|
||||
if (a.offset !== b.offset) {
|
||||
return a.offset - b.offset;
|
||||
}
|
||||
// All "end" points should go before "start" points.
|
||||
if (a.type !== b.type) {
|
||||
return b.type - a.type;
|
||||
}
|
||||
const aLength = a.range.endOffset - a.range.startOffset;
|
||||
const bLength = b.range.endOffset - b.range.startOffset;
|
||||
// For two "start" points, the one with longer range goes first.
|
||||
if (a.type === 0) {
|
||||
return bLength - aLength;
|
||||
}
|
||||
// For two "end" points, the one with shorter range goes first.
|
||||
return aLength - bLength;
|
||||
});
|
||||
|
||||
const hitCountStack = [];
|
||||
const results: Array<{
|
||||
start: number;
|
||||
end: number;
|
||||
}> = [];
|
||||
let lastOffset = 0;
|
||||
// Run scanning line to intersect all ranges.
|
||||
for (const point of points) {
|
||||
if (
|
||||
hitCountStack.length &&
|
||||
lastOffset < point.offset &&
|
||||
hitCountStack[hitCountStack.length - 1]! > 0
|
||||
) {
|
||||
const lastResult = results[results.length - 1];
|
||||
if (lastResult && lastResult.end === lastOffset) {
|
||||
lastResult.end = point.offset;
|
||||
} else {
|
||||
results.push({start: lastOffset, end: point.offset});
|
||||
}
|
||||
}
|
||||
lastOffset = point.offset;
|
||||
if (point.type === 0) {
|
||||
hitCountStack.push(point.range.count);
|
||||
} else {
|
||||
hitCountStack.pop();
|
||||
}
|
||||
}
|
||||
// Filter out empty ranges.
|
||||
return results.filter(range => {
|
||||
return range.end - range.start > 0;
|
||||
});
|
||||
}
|
||||
230
node_modules/puppeteer-core/src/cdp/DeviceRequestPrompt.ts
generated
vendored
Normal file
230
node_modules/puppeteer-core/src/cdp/DeviceRequestPrompt.ts
generated
vendored
Normal file
@@ -0,0 +1,230 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2022 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type Protocol from 'devtools-protocol';
|
||||
|
||||
import type {CDPSession} from '../api/CDPSession.js';
|
||||
import {DeviceRequestPrompt} from '../api/DeviceRequestPrompt.js';
|
||||
import type {DeviceRequestPromptDevice} from '../api/DeviceRequestPrompt.js';
|
||||
import type {WaitTimeoutOptions} from '../api/Page.js';
|
||||
import type {TimeoutSettings} from '../common/TimeoutSettings.js';
|
||||
import {assert} from '../util/assert.js';
|
||||
import {Deferred} from '../util/Deferred.js';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export class CdpDeviceRequestPrompt extends DeviceRequestPrompt {
|
||||
#client: CDPSession | null;
|
||||
#timeoutSettings: TimeoutSettings;
|
||||
#id: string;
|
||||
#handled = false;
|
||||
#updateDevicesHandle = this.#updateDevices.bind(this);
|
||||
#waitForDevicePromises = new Set<{
|
||||
filter: (device: DeviceRequestPromptDevice) => boolean;
|
||||
promise: Deferred<DeviceRequestPromptDevice>;
|
||||
}>();
|
||||
|
||||
constructor(
|
||||
client: CDPSession,
|
||||
timeoutSettings: TimeoutSettings,
|
||||
firstEvent: Protocol.DeviceAccess.DeviceRequestPromptedEvent,
|
||||
) {
|
||||
super();
|
||||
this.#client = client;
|
||||
this.#timeoutSettings = timeoutSettings;
|
||||
this.#id = firstEvent.id;
|
||||
|
||||
this.#client.on(
|
||||
'DeviceAccess.deviceRequestPrompted',
|
||||
this.#updateDevicesHandle,
|
||||
);
|
||||
this.#client.on('Target.detachedFromTarget', () => {
|
||||
this.#client = null;
|
||||
});
|
||||
|
||||
this.#updateDevices(firstEvent);
|
||||
}
|
||||
|
||||
#updateDevices(event: Protocol.DeviceAccess.DeviceRequestPromptedEvent) {
|
||||
if (event.id !== this.#id) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const rawDevice of event.devices) {
|
||||
if (
|
||||
this.devices.some(device => {
|
||||
return device.id === rawDevice.id;
|
||||
})
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const newDevice = {id: rawDevice.id, name: rawDevice.name};
|
||||
this.devices.push(newDevice);
|
||||
|
||||
for (const waitForDevicePromise of this.#waitForDevicePromises) {
|
||||
if (waitForDevicePromise.filter(newDevice)) {
|
||||
waitForDevicePromise.promise.resolve(newDevice);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async waitForDevice(
|
||||
filter: (device: DeviceRequestPromptDevice) => boolean,
|
||||
options: WaitTimeoutOptions = {},
|
||||
): Promise<DeviceRequestPromptDevice> {
|
||||
for (const device of this.devices) {
|
||||
if (filter(device)) {
|
||||
return device;
|
||||
}
|
||||
}
|
||||
|
||||
const {timeout = this.#timeoutSettings.timeout()} = options;
|
||||
const deferred = Deferred.create<DeviceRequestPromptDevice>({
|
||||
message: `Waiting for \`DeviceRequestPromptDevice\` failed: ${timeout}ms exceeded`,
|
||||
timeout,
|
||||
});
|
||||
|
||||
if (options.signal) {
|
||||
options.signal.addEventListener(
|
||||
'abort',
|
||||
() => {
|
||||
deferred.reject(options.signal?.reason);
|
||||
},
|
||||
{once: true},
|
||||
);
|
||||
}
|
||||
|
||||
const handle = {filter, promise: deferred};
|
||||
this.#waitForDevicePromises.add(handle);
|
||||
try {
|
||||
return await deferred.valueOrThrow();
|
||||
} finally {
|
||||
this.#waitForDevicePromises.delete(handle);
|
||||
}
|
||||
}
|
||||
|
||||
async select(device: DeviceRequestPromptDevice): Promise<void> {
|
||||
assert(
|
||||
this.#client !== null,
|
||||
'Cannot select device through detached session!',
|
||||
);
|
||||
assert(this.devices.includes(device), 'Cannot select unknown device!');
|
||||
assert(
|
||||
!this.#handled,
|
||||
'Cannot select DeviceRequestPrompt which is already handled!',
|
||||
);
|
||||
this.#client.off(
|
||||
'DeviceAccess.deviceRequestPrompted',
|
||||
this.#updateDevicesHandle,
|
||||
);
|
||||
this.#handled = true;
|
||||
return await this.#client.send('DeviceAccess.selectPrompt', {
|
||||
id: this.#id,
|
||||
deviceId: device.id,
|
||||
});
|
||||
}
|
||||
|
||||
async cancel(): Promise<void> {
|
||||
assert(
|
||||
this.#client !== null,
|
||||
'Cannot cancel prompt through detached session!',
|
||||
);
|
||||
assert(
|
||||
!this.#handled,
|
||||
'Cannot cancel DeviceRequestPrompt which is already handled!',
|
||||
);
|
||||
this.#client.off(
|
||||
'DeviceAccess.deviceRequestPrompted',
|
||||
this.#updateDevicesHandle,
|
||||
);
|
||||
this.#handled = true;
|
||||
return await this.#client.send('DeviceAccess.cancelPrompt', {id: this.#id});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export class CdpDeviceRequestPromptManager {
|
||||
#client: CDPSession | null;
|
||||
#timeoutSettings: TimeoutSettings;
|
||||
#deviceRequestPromptDeferreds = new Set<Deferred<DeviceRequestPrompt>>();
|
||||
|
||||
constructor(client: CDPSession, timeoutSettings: TimeoutSettings) {
|
||||
this.#client = client;
|
||||
this.#timeoutSettings = timeoutSettings;
|
||||
|
||||
this.#client.on('DeviceAccess.deviceRequestPrompted', event => {
|
||||
this.#onDeviceRequestPrompted(event);
|
||||
});
|
||||
this.#client.on('Target.detachedFromTarget', () => {
|
||||
this.#client = null;
|
||||
});
|
||||
}
|
||||
|
||||
async waitForDevicePrompt(
|
||||
options: WaitTimeoutOptions = {},
|
||||
): Promise<DeviceRequestPrompt> {
|
||||
assert(
|
||||
this.#client !== null,
|
||||
'Cannot wait for device prompt through detached session!',
|
||||
);
|
||||
const needsEnable = this.#deviceRequestPromptDeferreds.size === 0;
|
||||
let enablePromise: Promise<void> | undefined;
|
||||
if (needsEnable) {
|
||||
enablePromise = this.#client.send('DeviceAccess.enable');
|
||||
}
|
||||
|
||||
const {timeout = this.#timeoutSettings.timeout()} = options;
|
||||
const deferred = Deferred.create<DeviceRequestPrompt>({
|
||||
message: `Waiting for \`DeviceRequestPrompt\` failed: ${timeout}ms exceeded`,
|
||||
timeout,
|
||||
});
|
||||
if (options.signal) {
|
||||
options.signal.addEventListener(
|
||||
'abort',
|
||||
() => {
|
||||
deferred.reject(options.signal?.reason);
|
||||
},
|
||||
{once: true},
|
||||
);
|
||||
}
|
||||
|
||||
this.#deviceRequestPromptDeferreds.add(deferred);
|
||||
|
||||
try {
|
||||
const [result] = await Promise.all([
|
||||
deferred.valueOrThrow(),
|
||||
enablePromise,
|
||||
]);
|
||||
return result;
|
||||
} finally {
|
||||
this.#deviceRequestPromptDeferreds.delete(deferred);
|
||||
}
|
||||
}
|
||||
|
||||
#onDeviceRequestPrompted(
|
||||
event: Protocol.DeviceAccess.DeviceRequestPromptedEvent,
|
||||
) {
|
||||
if (!this.#deviceRequestPromptDeferreds.size) {
|
||||
return;
|
||||
}
|
||||
|
||||
assert(this.#client !== null);
|
||||
const devicePrompt = new CdpDeviceRequestPrompt(
|
||||
this.#client,
|
||||
this.#timeoutSettings,
|
||||
event,
|
||||
);
|
||||
for (const promise of this.#deviceRequestPromptDeferreds) {
|
||||
promise.resolve(devicePrompt);
|
||||
}
|
||||
this.#deviceRequestPromptDeferreds.clear();
|
||||
}
|
||||
}
|
||||
37
node_modules/puppeteer-core/src/cdp/Dialog.ts
generated
vendored
Normal file
37
node_modules/puppeteer-core/src/cdp/Dialog.ts
generated
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2017 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type {Protocol} from 'devtools-protocol';
|
||||
|
||||
import type {CDPSession} from '../api/CDPSession.js';
|
||||
import {Dialog} from '../api/Dialog.js';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export class CdpDialog extends Dialog {
|
||||
#client: CDPSession;
|
||||
|
||||
constructor(
|
||||
client: CDPSession,
|
||||
type: Protocol.Page.DialogType,
|
||||
message: string,
|
||||
defaultValue = '',
|
||||
) {
|
||||
super(type, message, defaultValue);
|
||||
this.#client = client;
|
||||
}
|
||||
|
||||
override async handle(options: {
|
||||
accept: boolean;
|
||||
text?: string;
|
||||
}): Promise<void> {
|
||||
await this.#client.send('Page.handleJavaScriptDialog', {
|
||||
accept: options.accept,
|
||||
promptText: options.text,
|
||||
});
|
||||
}
|
||||
}
|
||||
216
node_modules/puppeteer-core/src/cdp/ElementHandle.ts
generated
vendored
Normal file
216
node_modules/puppeteer-core/src/cdp/ElementHandle.ts
generated
vendored
Normal 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
611
node_modules/puppeteer-core/src/cdp/EmulationManager.ts
generated
vendored
Normal file
@@ -0,0 +1,611 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2017 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
import type {Protocol} from 'devtools-protocol';
|
||||
|
||||
import {type CDPSession, CDPSessionEvent} from '../api/CDPSession.js';
|
||||
import type {GeolocationOptions, MediaFeature} from '../api/Page.js';
|
||||
import {debugError} from '../common/util.js';
|
||||
import type {Viewport} from '../common/Viewport.js';
|
||||
import {assert} from '../util/assert.js';
|
||||
import {invokeAtMostOnceForArguments} from '../util/decorators.js';
|
||||
import {isErrorLike} from '../util/ErrorLike.js';
|
||||
|
||||
interface ViewportState {
|
||||
viewport?: Viewport;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
interface IdleOverridesState {
|
||||
overrides?: {
|
||||
isUserActive: boolean;
|
||||
isScreenUnlocked: boolean;
|
||||
};
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
interface TimezoneState {
|
||||
timezoneId?: string;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
interface VisionDeficiencyState {
|
||||
visionDeficiency?: Protocol.Emulation.SetEmulatedVisionDeficiencyRequest['type'];
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
interface CpuThrottlingState {
|
||||
factor?: number;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
interface MediaFeaturesState {
|
||||
mediaFeatures?: MediaFeature[];
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
interface MediaTypeState {
|
||||
type?: string;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
interface GeoLocationState {
|
||||
geoLocation?: GeolocationOptions;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
interface DefaultBackgroundColorState {
|
||||
color?: Protocol.DOM.RGBA;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
interface JavascriptEnabledState {
|
||||
javaScriptEnabled: boolean;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
interface FocusState {
|
||||
enabled: boolean;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export interface ClientProvider {
|
||||
clients(): CDPSession[];
|
||||
registerState(state: EmulatedState<any>): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export class EmulatedState<T extends {active: boolean}> {
|
||||
#state: T;
|
||||
#clientProvider: ClientProvider;
|
||||
#updater: (client: CDPSession, state: T) => Promise<void>;
|
||||
|
||||
constructor(
|
||||
initialState: T,
|
||||
clientProvider: ClientProvider,
|
||||
updater: (client: CDPSession, state: T) => Promise<void>,
|
||||
) {
|
||||
this.#state = initialState;
|
||||
this.#clientProvider = clientProvider;
|
||||
this.#updater = updater;
|
||||
this.#clientProvider.registerState(this);
|
||||
}
|
||||
|
||||
async setState(state: T): Promise<void> {
|
||||
this.#state = state;
|
||||
await this.sync();
|
||||
}
|
||||
|
||||
get state(): T {
|
||||
return this.#state;
|
||||
}
|
||||
|
||||
async sync(): Promise<void> {
|
||||
await Promise.all(
|
||||
this.#clientProvider.clients().map(client => {
|
||||
return this.#updater(client, this.#state);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export class EmulationManager implements ClientProvider {
|
||||
#client: CDPSession;
|
||||
|
||||
#emulatingMobile = false;
|
||||
#hasTouch = false;
|
||||
|
||||
#states: Array<EmulatedState<any>> = [];
|
||||
|
||||
#viewportState = new EmulatedState<ViewportState>(
|
||||
{
|
||||
active: false,
|
||||
},
|
||||
this,
|
||||
this.#applyViewport,
|
||||
);
|
||||
#idleOverridesState = new EmulatedState<IdleOverridesState>(
|
||||
{
|
||||
active: false,
|
||||
},
|
||||
this,
|
||||
this.#emulateIdleState,
|
||||
);
|
||||
#timezoneState = new EmulatedState<TimezoneState>(
|
||||
{
|
||||
active: false,
|
||||
},
|
||||
this,
|
||||
this.#emulateTimezone,
|
||||
);
|
||||
#visionDeficiencyState = new EmulatedState<VisionDeficiencyState>(
|
||||
{
|
||||
active: false,
|
||||
},
|
||||
this,
|
||||
this.#emulateVisionDeficiency,
|
||||
);
|
||||
#cpuThrottlingState = new EmulatedState<CpuThrottlingState>(
|
||||
{
|
||||
active: false,
|
||||
},
|
||||
this,
|
||||
this.#emulateCpuThrottling,
|
||||
);
|
||||
#mediaFeaturesState = new EmulatedState<MediaFeaturesState>(
|
||||
{
|
||||
active: false,
|
||||
},
|
||||
this,
|
||||
this.#emulateMediaFeatures,
|
||||
);
|
||||
#mediaTypeState = new EmulatedState<MediaTypeState>(
|
||||
{
|
||||
active: false,
|
||||
},
|
||||
this,
|
||||
this.#emulateMediaType,
|
||||
);
|
||||
#geoLocationState = new EmulatedState<GeoLocationState>(
|
||||
{
|
||||
active: false,
|
||||
},
|
||||
this,
|
||||
this.#setGeolocation,
|
||||
);
|
||||
#defaultBackgroundColorState = new EmulatedState<DefaultBackgroundColorState>(
|
||||
{
|
||||
active: false,
|
||||
},
|
||||
this,
|
||||
this.#setDefaultBackgroundColor,
|
||||
);
|
||||
#javascriptEnabledState = new EmulatedState<JavascriptEnabledState>(
|
||||
{
|
||||
javaScriptEnabled: true,
|
||||
active: false,
|
||||
},
|
||||
this,
|
||||
this.#setJavaScriptEnabled,
|
||||
);
|
||||
#focusState = new EmulatedState<FocusState>(
|
||||
{
|
||||
enabled: true,
|
||||
active: false,
|
||||
},
|
||||
this,
|
||||
this.#emulateFocus,
|
||||
);
|
||||
|
||||
#secondaryClients = new Set<CDPSession>();
|
||||
|
||||
constructor(client: CDPSession) {
|
||||
this.#client = client;
|
||||
}
|
||||
|
||||
updateClient(client: CDPSession): void {
|
||||
this.#client = client;
|
||||
this.#secondaryClients.delete(client);
|
||||
}
|
||||
|
||||
registerState(state: EmulatedState<any>): void {
|
||||
this.#states.push(state);
|
||||
}
|
||||
|
||||
clients(): CDPSession[] {
|
||||
return [this.#client, ...Array.from(this.#secondaryClients)];
|
||||
}
|
||||
|
||||
async registerSpeculativeSession(client: CDPSession): Promise<void> {
|
||||
this.#secondaryClients.add(client);
|
||||
client.once(CDPSessionEvent.Disconnected, () => {
|
||||
this.#secondaryClients.delete(client);
|
||||
});
|
||||
// We don't await here because we want to register all state changes before
|
||||
// the target is unpaused.
|
||||
void Promise.all(
|
||||
this.#states.map(s => {
|
||||
return s.sync().catch(debugError);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
get javascriptEnabled(): boolean {
|
||||
return this.#javascriptEnabledState.state.javaScriptEnabled;
|
||||
}
|
||||
|
||||
async emulateViewport(viewport: Viewport | null): Promise<boolean> {
|
||||
const currentState = this.#viewportState.state;
|
||||
if (!viewport && !currentState.active) {
|
||||
return false;
|
||||
}
|
||||
await this.#viewportState.setState(
|
||||
viewport
|
||||
? {
|
||||
viewport,
|
||||
active: true,
|
||||
}
|
||||
: {
|
||||
active: false,
|
||||
},
|
||||
);
|
||||
|
||||
const mobile = viewport?.isMobile || false;
|
||||
const hasTouch = viewport?.hasTouch || false;
|
||||
const reloadNeeded =
|
||||
this.#emulatingMobile !== mobile || this.#hasTouch !== hasTouch;
|
||||
this.#emulatingMobile = mobile;
|
||||
this.#hasTouch = hasTouch;
|
||||
|
||||
return reloadNeeded;
|
||||
}
|
||||
|
||||
@invokeAtMostOnceForArguments
|
||||
async #applyViewport(
|
||||
client: CDPSession,
|
||||
viewportState: ViewportState,
|
||||
): Promise<void> {
|
||||
if (!viewportState.viewport) {
|
||||
await Promise.all([
|
||||
client.send('Emulation.clearDeviceMetricsOverride'),
|
||||
client.send('Emulation.setTouchEmulationEnabled', {
|
||||
enabled: false,
|
||||
}),
|
||||
]).catch(debugError);
|
||||
return;
|
||||
}
|
||||
const {viewport} = viewportState;
|
||||
const mobile = viewport.isMobile || false;
|
||||
const width = viewport.width;
|
||||
const height = viewport.height;
|
||||
const deviceScaleFactor = viewport.deviceScaleFactor ?? 1;
|
||||
const screenOrientation: Protocol.Emulation.ScreenOrientation =
|
||||
viewport.isLandscape
|
||||
? {angle: 90, type: 'landscapePrimary'}
|
||||
: {angle: 0, type: 'portraitPrimary'};
|
||||
const hasTouch = viewport.hasTouch || false;
|
||||
|
||||
await Promise.all([
|
||||
client
|
||||
.send('Emulation.setDeviceMetricsOverride', {
|
||||
mobile,
|
||||
width,
|
||||
height,
|
||||
deviceScaleFactor,
|
||||
screenOrientation,
|
||||
})
|
||||
.catch(err => {
|
||||
if (
|
||||
err.message.includes('Target does not support metrics override')
|
||||
) {
|
||||
debugError(err);
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
}),
|
||||
client.send('Emulation.setTouchEmulationEnabled', {
|
||||
enabled: hasTouch,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
async emulateIdleState(overrides?: {
|
||||
isUserActive: boolean;
|
||||
isScreenUnlocked: boolean;
|
||||
}): Promise<void> {
|
||||
await this.#idleOverridesState.setState({
|
||||
active: true,
|
||||
overrides,
|
||||
});
|
||||
}
|
||||
|
||||
@invokeAtMostOnceForArguments
|
||||
async #emulateIdleState(
|
||||
client: CDPSession,
|
||||
idleStateState: IdleOverridesState,
|
||||
): Promise<void> {
|
||||
if (!idleStateState.active) {
|
||||
return;
|
||||
}
|
||||
if (idleStateState.overrides) {
|
||||
await client.send('Emulation.setIdleOverride', {
|
||||
isUserActive: idleStateState.overrides.isUserActive,
|
||||
isScreenUnlocked: idleStateState.overrides.isScreenUnlocked,
|
||||
});
|
||||
} else {
|
||||
await client.send('Emulation.clearIdleOverride');
|
||||
}
|
||||
}
|
||||
|
||||
@invokeAtMostOnceForArguments
|
||||
async #emulateTimezone(
|
||||
client: CDPSession,
|
||||
timezoneState: TimezoneState,
|
||||
): Promise<void> {
|
||||
if (!timezoneState.active) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await client.send('Emulation.setTimezoneOverride', {
|
||||
timezoneId: timezoneState.timezoneId || '',
|
||||
});
|
||||
} catch (error) {
|
||||
if (isErrorLike(error) && error.message.includes('Invalid timezone')) {
|
||||
throw new Error(`Invalid timezone ID: ${timezoneState.timezoneId}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async emulateTimezone(timezoneId?: string): Promise<void> {
|
||||
await this.#timezoneState.setState({
|
||||
timezoneId,
|
||||
active: true,
|
||||
});
|
||||
}
|
||||
|
||||
@invokeAtMostOnceForArguments
|
||||
async #emulateVisionDeficiency(
|
||||
client: CDPSession,
|
||||
visionDeficiency: VisionDeficiencyState,
|
||||
): Promise<void> {
|
||||
if (!visionDeficiency.active) {
|
||||
return;
|
||||
}
|
||||
await client.send('Emulation.setEmulatedVisionDeficiency', {
|
||||
type: visionDeficiency.visionDeficiency || 'none',
|
||||
});
|
||||
}
|
||||
|
||||
async emulateVisionDeficiency(
|
||||
type?: Protocol.Emulation.SetEmulatedVisionDeficiencyRequest['type'],
|
||||
): Promise<void> {
|
||||
const visionDeficiencies = new Set<
|
||||
Protocol.Emulation.SetEmulatedVisionDeficiencyRequest['type']
|
||||
>([
|
||||
'none',
|
||||
'achromatopsia',
|
||||
'blurredVision',
|
||||
'deuteranopia',
|
||||
'protanopia',
|
||||
'reducedContrast',
|
||||
'tritanopia',
|
||||
]);
|
||||
assert(
|
||||
!type || visionDeficiencies.has(type),
|
||||
`Unsupported vision deficiency: ${type}`,
|
||||
);
|
||||
await this.#visionDeficiencyState.setState({
|
||||
active: true,
|
||||
visionDeficiency: type,
|
||||
});
|
||||
}
|
||||
|
||||
@invokeAtMostOnceForArguments
|
||||
async #emulateCpuThrottling(
|
||||
client: CDPSession,
|
||||
state: CpuThrottlingState,
|
||||
): Promise<void> {
|
||||
if (!state.active) {
|
||||
return;
|
||||
}
|
||||
await client.send('Emulation.setCPUThrottlingRate', {
|
||||
rate: state.factor ?? 1,
|
||||
});
|
||||
}
|
||||
|
||||
async emulateCPUThrottling(factor: number | null): Promise<void> {
|
||||
assert(
|
||||
factor === null || factor >= 1,
|
||||
'Throttling rate should be greater or equal to 1',
|
||||
);
|
||||
await this.#cpuThrottlingState.setState({
|
||||
active: true,
|
||||
factor: factor ?? undefined,
|
||||
});
|
||||
}
|
||||
|
||||
@invokeAtMostOnceForArguments
|
||||
async #emulateMediaFeatures(
|
||||
client: CDPSession,
|
||||
state: MediaFeaturesState,
|
||||
): Promise<void> {
|
||||
if (!state.active) {
|
||||
return;
|
||||
}
|
||||
await client.send('Emulation.setEmulatedMedia', {
|
||||
features: state.mediaFeatures,
|
||||
});
|
||||
}
|
||||
|
||||
async emulateMediaFeatures(features?: MediaFeature[]): Promise<void> {
|
||||
if (Array.isArray(features)) {
|
||||
for (const mediaFeature of features) {
|
||||
const name = mediaFeature.name;
|
||||
assert(
|
||||
/^(?:prefers-(?:color-scheme|reduced-motion)|color-gamut)$/.test(
|
||||
name,
|
||||
),
|
||||
'Unsupported media feature: ' + name,
|
||||
);
|
||||
}
|
||||
}
|
||||
await this.#mediaFeaturesState.setState({
|
||||
active: true,
|
||||
mediaFeatures: features,
|
||||
});
|
||||
}
|
||||
|
||||
@invokeAtMostOnceForArguments
|
||||
async #emulateMediaType(
|
||||
client: CDPSession,
|
||||
state: MediaTypeState,
|
||||
): Promise<void> {
|
||||
if (!state.active) {
|
||||
return;
|
||||
}
|
||||
await client.send('Emulation.setEmulatedMedia', {
|
||||
media: state.type || '',
|
||||
});
|
||||
}
|
||||
|
||||
async emulateMediaType(type?: string): Promise<void> {
|
||||
assert(
|
||||
type === 'screen' ||
|
||||
type === 'print' ||
|
||||
(type ?? undefined) === undefined,
|
||||
'Unsupported media type: ' + type,
|
||||
);
|
||||
await this.#mediaTypeState.setState({
|
||||
type,
|
||||
active: true,
|
||||
});
|
||||
}
|
||||
|
||||
@invokeAtMostOnceForArguments
|
||||
async #setGeolocation(
|
||||
client: CDPSession,
|
||||
state: GeoLocationState,
|
||||
): Promise<void> {
|
||||
if (!state.active) {
|
||||
return;
|
||||
}
|
||||
await client.send(
|
||||
'Emulation.setGeolocationOverride',
|
||||
state.geoLocation
|
||||
? {
|
||||
longitude: state.geoLocation.longitude,
|
||||
latitude: state.geoLocation.latitude,
|
||||
accuracy: state.geoLocation.accuracy,
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
}
|
||||
|
||||
async setGeolocation(options: GeolocationOptions): Promise<void> {
|
||||
const {longitude, latitude, accuracy = 0} = options;
|
||||
if (longitude < -180 || longitude > 180) {
|
||||
throw new Error(
|
||||
`Invalid longitude "${longitude}": precondition -180 <= LONGITUDE <= 180 failed.`,
|
||||
);
|
||||
}
|
||||
if (latitude < -90 || latitude > 90) {
|
||||
throw new Error(
|
||||
`Invalid latitude "${latitude}": precondition -90 <= LATITUDE <= 90 failed.`,
|
||||
);
|
||||
}
|
||||
if (accuracy < 0) {
|
||||
throw new Error(
|
||||
`Invalid accuracy "${accuracy}": precondition 0 <= ACCURACY failed.`,
|
||||
);
|
||||
}
|
||||
await this.#geoLocationState.setState({
|
||||
active: true,
|
||||
geoLocation: {
|
||||
longitude,
|
||||
latitude,
|
||||
accuracy,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@invokeAtMostOnceForArguments
|
||||
async #setDefaultBackgroundColor(
|
||||
client: CDPSession,
|
||||
state: DefaultBackgroundColorState,
|
||||
): Promise<void> {
|
||||
if (!state.active) {
|
||||
return;
|
||||
}
|
||||
await client.send('Emulation.setDefaultBackgroundColorOverride', {
|
||||
color: state.color,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets default white background
|
||||
*/
|
||||
async resetDefaultBackgroundColor(): Promise<void> {
|
||||
await this.#defaultBackgroundColorState.setState({
|
||||
active: true,
|
||||
color: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides default white background
|
||||
*/
|
||||
async setTransparentBackgroundColor(): Promise<void> {
|
||||
await this.#defaultBackgroundColorState.setState({
|
||||
active: true,
|
||||
color: {r: 0, g: 0, b: 0, a: 0},
|
||||
});
|
||||
}
|
||||
|
||||
@invokeAtMostOnceForArguments
|
||||
async #setJavaScriptEnabled(
|
||||
client: CDPSession,
|
||||
state: JavascriptEnabledState,
|
||||
): Promise<void> {
|
||||
if (!state.active) {
|
||||
return;
|
||||
}
|
||||
await client.send('Emulation.setScriptExecutionDisabled', {
|
||||
value: !state.javaScriptEnabled,
|
||||
});
|
||||
}
|
||||
|
||||
async setJavaScriptEnabled(enabled: boolean): Promise<void> {
|
||||
await this.#javascriptEnabledState.setState({
|
||||
active: true,
|
||||
javaScriptEnabled: enabled,
|
||||
});
|
||||
}
|
||||
|
||||
@invokeAtMostOnceForArguments
|
||||
async #emulateFocus(client: CDPSession, state: FocusState): Promise<void> {
|
||||
if (!state.active) {
|
||||
return;
|
||||
}
|
||||
await client.send('Emulation.setFocusEmulationEnabled', {
|
||||
enabled: state.enabled,
|
||||
});
|
||||
}
|
||||
|
||||
async emulateFocus(enabled: boolean): Promise<void> {
|
||||
await this.#focusState.setState({
|
||||
active: true,
|
||||
enabled,
|
||||
});
|
||||
}
|
||||
}
|
||||
549
node_modules/puppeteer-core/src/cdp/ExecutionContext.ts
generated
vendored
Normal file
549
node_modules/puppeteer-core/src/cdp/ExecutionContext.ts
generated
vendored
Normal file
@@ -0,0 +1,549 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2017 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type {Protocol} from 'devtools-protocol';
|
||||
|
||||
import {CDPSessionEvent, type CDPSession} from '../api/CDPSession.js';
|
||||
import type {ElementHandle} from '../api/ElementHandle.js';
|
||||
import type {JSHandle} from '../api/JSHandle.js';
|
||||
import {ARIAQueryHandler} from '../common/AriaQueryHandler.js';
|
||||
import {EventEmitter} from '../common/EventEmitter.js';
|
||||
import {LazyArg} from '../common/LazyArg.js';
|
||||
import {scriptInjector} from '../common/ScriptInjector.js';
|
||||
import type {BindingPayload, EvaluateFunc, HandleFor} from '../common/types.js';
|
||||
import {
|
||||
PuppeteerURL,
|
||||
SOURCE_URL_REGEX,
|
||||
debugError,
|
||||
getSourcePuppeteerURLIfAvailable,
|
||||
getSourceUrlComment,
|
||||
isString,
|
||||
} from '../common/util.js';
|
||||
import type {PuppeteerInjectedUtil} from '../injected/injected.js';
|
||||
import {AsyncIterableUtil} from '../util/AsyncIterableUtil.js';
|
||||
import {DisposableStack, disposeSymbol} from '../util/disposable.js';
|
||||
import {stringifyFunction} from '../util/Function.js';
|
||||
import {Mutex} from '../util/Mutex.js';
|
||||
|
||||
import {Binding} from './Binding.js';
|
||||
import {CdpElementHandle} from './ElementHandle.js';
|
||||
import type {IsolatedWorld} from './IsolatedWorld.js';
|
||||
import {CdpJSHandle} from './JSHandle.js';
|
||||
import {
|
||||
addPageBinding,
|
||||
CDP_BINDING_PREFIX,
|
||||
createEvaluationError,
|
||||
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;
|
||||
};
|
||||
197
node_modules/puppeteer-core/src/cdp/ExtensionTransport.ts
generated
vendored
Normal file
197
node_modules/puppeteer-core/src/cdp/ExtensionTransport.ts
generated
vendored
Normal file
@@ -0,0 +1,197 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2024 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
import type {ConnectionTransport} from '../common/ConnectionTransport.js';
|
||||
|
||||
const tabTargetInfo = {
|
||||
targetId: 'tabTargetId',
|
||||
type: 'tab',
|
||||
title: 'tab',
|
||||
url: 'about:blank',
|
||||
attached: false,
|
||||
canAccessOpener: false,
|
||||
};
|
||||
|
||||
const pageTargetInfo = {
|
||||
targetId: 'pageTargetId',
|
||||
type: 'page',
|
||||
title: 'page',
|
||||
url: 'about:blank',
|
||||
attached: false,
|
||||
canAccessOpener: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* Experimental ExtensionTransport allows establishing a connection via
|
||||
* chrome.debugger API if Puppeteer runs in an extension. Since Chrome
|
||||
* DevTools Protocol is restricted for extensions, the transport
|
||||
* implements missing commands and events.
|
||||
*
|
||||
* @experimental
|
||||
* @public
|
||||
*/
|
||||
export class ExtensionTransport implements ConnectionTransport {
|
||||
static async connectTab(tabId: number): Promise<ExtensionTransport> {
|
||||
await chrome.debugger.attach({tabId}, '1.3');
|
||||
return new ExtensionTransport(tabId);
|
||||
}
|
||||
|
||||
onmessage?: (message: string) => void;
|
||||
onclose?: () => void;
|
||||
|
||||
#tabId: number;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
constructor(tabId: number) {
|
||||
this.#tabId = tabId;
|
||||
chrome.debugger.onEvent.addListener(this.#debuggerEventHandler);
|
||||
}
|
||||
|
||||
#debuggerEventHandler = (
|
||||
source: chrome.debugger.Debuggee,
|
||||
method: string,
|
||||
params?: object | undefined,
|
||||
): void => {
|
||||
if (source.tabId !== this.#tabId) {
|
||||
return;
|
||||
}
|
||||
this.#dispatchResponse({
|
||||
// @ts-expect-error sessionId is not in stable yet.
|
||||
sessionId: source.sessionId ?? 'pageTargetSessionId',
|
||||
method: method,
|
||||
params: params,
|
||||
});
|
||||
};
|
||||
|
||||
#dispatchResponse(message: object): void {
|
||||
// Dispatch in a new task like other transports.
|
||||
setTimeout(() => {
|
||||
this.onmessage?.(JSON.stringify(message));
|
||||
}, 0);
|
||||
}
|
||||
|
||||
send(message: string): void {
|
||||
const parsed = JSON.parse(message);
|
||||
switch (parsed.method) {
|
||||
case 'Browser.getVersion': {
|
||||
this.#dispatchResponse({
|
||||
id: parsed.id,
|
||||
sessionId: parsed.sessionId,
|
||||
method: parsed.method,
|
||||
result: {
|
||||
protocolVersion: '1.3',
|
||||
product: 'chrome',
|
||||
revision: 'unknown',
|
||||
userAgent: 'chrome',
|
||||
jsVersion: 'unknown',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
case 'Target.getBrowserContexts': {
|
||||
this.#dispatchResponse({
|
||||
id: parsed.id,
|
||||
sessionId: parsed.sessionId,
|
||||
method: parsed.method,
|
||||
result: {
|
||||
browserContextIds: [],
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
case 'Target.setDiscoverTargets': {
|
||||
this.#dispatchResponse({
|
||||
method: 'Target.targetCreated',
|
||||
params: {
|
||||
targetInfo: tabTargetInfo,
|
||||
},
|
||||
});
|
||||
this.#dispatchResponse({
|
||||
method: 'Target.targetCreated',
|
||||
params: {
|
||||
targetInfo: pageTargetInfo,
|
||||
},
|
||||
});
|
||||
this.#dispatchResponse({
|
||||
id: parsed.id,
|
||||
sessionId: parsed.sessionId,
|
||||
method: parsed.method,
|
||||
result: {},
|
||||
});
|
||||
return;
|
||||
}
|
||||
case 'Target.setAutoAttach': {
|
||||
if (parsed.sessionId === 'tabTargetSessionId') {
|
||||
this.#dispatchResponse({
|
||||
method: 'Target.attachedToTarget',
|
||||
sessionId: 'tabTargetSessionId',
|
||||
params: {
|
||||
targetInfo: pageTargetInfo,
|
||||
sessionId: 'pageTargetSessionId',
|
||||
},
|
||||
});
|
||||
this.#dispatchResponse({
|
||||
id: parsed.id,
|
||||
sessionId: parsed.sessionId,
|
||||
method: parsed.method,
|
||||
result: {},
|
||||
});
|
||||
return;
|
||||
} else if (!parsed.sessionId) {
|
||||
this.#dispatchResponse({
|
||||
method: 'Target.attachedToTarget',
|
||||
params: {
|
||||
targetInfo: tabTargetInfo,
|
||||
sessionId: 'tabTargetSessionId',
|
||||
},
|
||||
});
|
||||
this.#dispatchResponse({
|
||||
id: parsed.id,
|
||||
sessionId: parsed.sessionId,
|
||||
method: parsed.method,
|
||||
result: {},
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (parsed.sessionId === 'pageTargetSessionId') {
|
||||
delete parsed.sessionId;
|
||||
}
|
||||
chrome.debugger
|
||||
.sendCommand(
|
||||
{tabId: this.#tabId, sessionId: parsed.sessionId},
|
||||
parsed.method,
|
||||
parsed.params,
|
||||
)
|
||||
.then(response => {
|
||||
this.#dispatchResponse({
|
||||
id: parsed.id,
|
||||
sessionId: parsed.sessionId ?? 'pageTargetSessionId',
|
||||
method: parsed.method,
|
||||
result: response,
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
this.#dispatchResponse({
|
||||
id: parsed.id,
|
||||
sessionId: parsed.sessionId ?? 'pageTargetSessionId',
|
||||
method: parsed.method,
|
||||
error: {
|
||||
code: err?.code,
|
||||
data: err?.data,
|
||||
message: err?.message ?? 'CDP error had no message',
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
close(): void {
|
||||
chrome.debugger.onEvent.removeListener(this.#debuggerEventHandler);
|
||||
void chrome.debugger.detach({tabId: this.#tabId});
|
||||
}
|
||||
}
|
||||
452
node_modules/puppeteer-core/src/cdp/Frame.ts
generated
vendored
Normal file
452
node_modules/puppeteer-core/src/cdp/Frame.ts
generated
vendored
Normal 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
582
node_modules/puppeteer-core/src/cdp/FrameManager.ts
generated
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
53
node_modules/puppeteer-core/src/cdp/FrameManagerEvents.ts
generated
vendored
Normal file
53
node_modules/puppeteer-core/src/cdp/FrameManagerEvents.ts
generated
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2023 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type Protocol from 'devtools-protocol';
|
||||
|
||||
import type {EventType} from '../common/EventEmitter.js';
|
||||
|
||||
import type {CdpFrame} from './Frame.js';
|
||||
import type {IsolatedWorld} from './IsolatedWorld.js';
|
||||
|
||||
/**
|
||||
* We use symbols to prevent external parties listening to these events.
|
||||
* They are internal to Puppeteer.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace FrameManagerEvent {
|
||||
export const FrameAttached = Symbol('FrameManager.FrameAttached');
|
||||
export const FrameNavigated = Symbol('FrameManager.FrameNavigated');
|
||||
export const FrameDetached = Symbol('FrameManager.FrameDetached');
|
||||
export const FrameSwapped = Symbol('FrameManager.FrameSwapped');
|
||||
export const LifecycleEvent = Symbol('FrameManager.LifecycleEvent');
|
||||
export const FrameNavigatedWithinDocument = Symbol(
|
||||
'FrameManager.FrameNavigatedWithinDocument',
|
||||
);
|
||||
export const ConsoleApiCalled = Symbol('FrameManager.ConsoleApiCalled');
|
||||
export const BindingCalled = Symbol('FrameManager.BindingCalled');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export interface FrameManagerEvents extends Record<EventType, unknown> {
|
||||
[FrameManagerEvent.FrameAttached]: CdpFrame;
|
||||
[FrameManagerEvent.FrameNavigated]: CdpFrame;
|
||||
[FrameManagerEvent.FrameDetached]: CdpFrame;
|
||||
[FrameManagerEvent.FrameSwapped]: CdpFrame;
|
||||
[FrameManagerEvent.LifecycleEvent]: CdpFrame;
|
||||
[FrameManagerEvent.FrameNavigatedWithinDocument]: CdpFrame;
|
||||
// Emitted when a new console message is logged.
|
||||
[FrameManagerEvent.ConsoleApiCalled]: [
|
||||
IsolatedWorld,
|
||||
Protocol.Runtime.ConsoleAPICalledEvent,
|
||||
];
|
||||
[FrameManagerEvent.BindingCalled]: [
|
||||
IsolatedWorld,
|
||||
Protocol.Runtime.BindingCalledEvent,
|
||||
];
|
||||
}
|
||||
100
node_modules/puppeteer-core/src/cdp/FrameTree.ts
generated
vendored
Normal file
100
node_modules/puppeteer-core/src/cdp/FrameTree.ts
generated
vendored
Normal file
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2022 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type {Frame} from '../api/Frame.js';
|
||||
import {Deferred} from '../util/Deferred.js';
|
||||
|
||||
/**
|
||||
* Keeps track of the page frame tree and it's is managed by
|
||||
* {@link FrameManager}. FrameTree uses frame IDs to reference frame and it
|
||||
* means that referenced frames might not be in the tree anymore. Thus, the tree
|
||||
* structure is eventually consistent.
|
||||
* @internal
|
||||
*/
|
||||
export class FrameTree<FrameType extends Frame> {
|
||||
#frames = new Map<string, FrameType>();
|
||||
// frameID -> parentFrameID
|
||||
#parentIds = new Map<string, string>();
|
||||
// frameID -> childFrameIDs
|
||||
#childIds = new Map<string, Set<string>>();
|
||||
#mainFrame?: FrameType;
|
||||
#isMainFrameStale = false;
|
||||
#waitRequests = new Map<string, Set<Deferred<FrameType>>>();
|
||||
|
||||
getMainFrame(): FrameType | undefined {
|
||||
return this.#mainFrame;
|
||||
}
|
||||
|
||||
getById(frameId: string): FrameType | undefined {
|
||||
return this.#frames.get(frameId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise that is resolved once the frame with
|
||||
* the given ID is added to the tree.
|
||||
*/
|
||||
waitForFrame(frameId: string): Promise<FrameType> {
|
||||
const frame = this.getById(frameId);
|
||||
if (frame) {
|
||||
return Promise.resolve(frame);
|
||||
}
|
||||
const deferred = Deferred.create<FrameType>();
|
||||
const callbacks =
|
||||
this.#waitRequests.get(frameId) || new Set<Deferred<FrameType>>();
|
||||
callbacks.add(deferred);
|
||||
return deferred.valueOrThrow();
|
||||
}
|
||||
|
||||
frames(): FrameType[] {
|
||||
return Array.from(this.#frames.values());
|
||||
}
|
||||
|
||||
addFrame(frame: FrameType): void {
|
||||
this.#frames.set(frame._id, frame);
|
||||
if (frame._parentId) {
|
||||
this.#parentIds.set(frame._id, frame._parentId);
|
||||
if (!this.#childIds.has(frame._parentId)) {
|
||||
this.#childIds.set(frame._parentId, new Set());
|
||||
}
|
||||
this.#childIds.get(frame._parentId)!.add(frame._id);
|
||||
} else if (!this.#mainFrame || this.#isMainFrameStale) {
|
||||
this.#mainFrame = frame;
|
||||
this.#isMainFrameStale = false;
|
||||
}
|
||||
this.#waitRequests.get(frame._id)?.forEach(request => {
|
||||
return request.resolve(frame);
|
||||
});
|
||||
}
|
||||
|
||||
removeFrame(frame: FrameType): void {
|
||||
this.#frames.delete(frame._id);
|
||||
this.#parentIds.delete(frame._id);
|
||||
if (frame._parentId) {
|
||||
this.#childIds.get(frame._parentId)?.delete(frame._id);
|
||||
} else {
|
||||
this.#isMainFrameStale = true;
|
||||
}
|
||||
}
|
||||
|
||||
childFrames(frameId: string): FrameType[] {
|
||||
const childIds = this.#childIds.get(frameId);
|
||||
if (!childIds) {
|
||||
return [];
|
||||
}
|
||||
return Array.from(childIds)
|
||||
.map(id => {
|
||||
return this.getById(id);
|
||||
})
|
||||
.filter((frame): frame is FrameType => {
|
||||
return frame !== undefined;
|
||||
});
|
||||
}
|
||||
|
||||
parentFrame(frameId: string): FrameType | undefined {
|
||||
const parentId = this.#parentIds.get(frameId);
|
||||
return parentId ? this.getById(parentId) : undefined;
|
||||
}
|
||||
}
|
||||
301
node_modules/puppeteer-core/src/cdp/HTTPRequest.ts
generated
vendored
Normal file
301
node_modules/puppeteer-core/src/cdp/HTTPRequest.ts
generated
vendored
Normal file
@@ -0,0 +1,301 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2020 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
import type {Protocol} from 'devtools-protocol';
|
||||
|
||||
import type {CDPSession} from '../api/CDPSession.js';
|
||||
import type {Frame} from '../api/Frame.js';
|
||||
import {
|
||||
type ContinueRequestOverrides,
|
||||
headersArray,
|
||||
HTTPRequest,
|
||||
type ResourceType,
|
||||
type ResponseForRequest,
|
||||
STATUS_TEXTS,
|
||||
handleError,
|
||||
} from '../api/HTTPRequest.js';
|
||||
import {debugError} from '../common/util.js';
|
||||
import {
|
||||
mergeUint8Arrays,
|
||||
stringToBase64,
|
||||
stringToTypedArray,
|
||||
} from '../util/encoding.js';
|
||||
|
||||
import type {CdpHTTPResponse} from './HTTPResponse.js';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export class CdpHTTPRequest extends HTTPRequest {
|
||||
override id: string;
|
||||
declare _redirectChain: CdpHTTPRequest[];
|
||||
declare _response: CdpHTTPResponse | null;
|
||||
|
||||
#client: CDPSession;
|
||||
#isNavigationRequest: boolean;
|
||||
|
||||
#url: string;
|
||||
#resourceType: ResourceType;
|
||||
|
||||
#method: string;
|
||||
#hasPostData = false;
|
||||
#postData?: string;
|
||||
#headers: Record<string, string> = {};
|
||||
#frame: Frame | null;
|
||||
#initiator?: Protocol.Network.Initiator;
|
||||
|
||||
override get client(): CDPSession {
|
||||
return this.#client;
|
||||
}
|
||||
|
||||
override set client(newClient: CDPSession) {
|
||||
this.#client = newClient;
|
||||
}
|
||||
|
||||
constructor(
|
||||
client: CDPSession,
|
||||
frame: Frame | null,
|
||||
interceptionId: string | undefined,
|
||||
allowInterception: boolean,
|
||||
data: {
|
||||
/**
|
||||
* Request identifier.
|
||||
*/
|
||||
requestId: Protocol.Network.RequestId;
|
||||
/**
|
||||
* Loader identifier. Empty string if the request is fetched from worker.
|
||||
*/
|
||||
loaderId?: Protocol.Network.LoaderId;
|
||||
/**
|
||||
* URL of the document this request is loaded for.
|
||||
*/
|
||||
documentURL?: string;
|
||||
/**
|
||||
* Request data.
|
||||
*/
|
||||
request: Protocol.Network.Request;
|
||||
/**
|
||||
* Request initiator.
|
||||
*/
|
||||
initiator?: Protocol.Network.Initiator;
|
||||
/**
|
||||
* Type of this resource.
|
||||
*/
|
||||
type?: Protocol.Network.ResourceType;
|
||||
},
|
||||
redirectChain: CdpHTTPRequest[],
|
||||
) {
|
||||
super();
|
||||
this.#client = client;
|
||||
this.id = data.requestId;
|
||||
this.#isNavigationRequest =
|
||||
data.requestId === data.loaderId && data.type === 'Document';
|
||||
this._interceptionId = interceptionId;
|
||||
this.#url = data.request.url + (data.request.urlFragment ?? '');
|
||||
this.#resourceType = (data.type || 'other').toLowerCase() as ResourceType;
|
||||
this.#method = data.request.method;
|
||||
if (
|
||||
data.request.postDataEntries &&
|
||||
data.request.postDataEntries.length > 0
|
||||
) {
|
||||
this.#postData = new TextDecoder().decode(
|
||||
mergeUint8Arrays(
|
||||
data.request.postDataEntries
|
||||
.map(entry => {
|
||||
return entry.bytes ? stringToTypedArray(entry.bytes, true) : null;
|
||||
})
|
||||
.filter((entry): entry is Uint8Array => {
|
||||
return entry !== null;
|
||||
}),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
this.#postData = data.request.postData;
|
||||
}
|
||||
this.#hasPostData = data.request.hasPostData ?? false;
|
||||
this.#frame = frame;
|
||||
this._redirectChain = redirectChain;
|
||||
this.#initiator = data.initiator;
|
||||
|
||||
this.interception.enabled = allowInterception;
|
||||
|
||||
this.updateHeaders(data.request.headers);
|
||||
}
|
||||
|
||||
updateHeaders(headers: Protocol.Network.Headers): void {
|
||||
for (const [key, value] of Object.entries(headers)) {
|
||||
this.#headers[key.toLowerCase()] = value;
|
||||
}
|
||||
}
|
||||
|
||||
override url(): string {
|
||||
return this.#url;
|
||||
}
|
||||
|
||||
override resourceType(): ResourceType {
|
||||
return this.#resourceType;
|
||||
}
|
||||
|
||||
override method(): string {
|
||||
return this.#method;
|
||||
}
|
||||
|
||||
override postData(): string | undefined {
|
||||
return this.#postData;
|
||||
}
|
||||
|
||||
override hasPostData(): boolean {
|
||||
return this.#hasPostData;
|
||||
}
|
||||
|
||||
override async fetchPostData(): Promise<string | undefined> {
|
||||
try {
|
||||
const result = await this.#client.send('Network.getRequestPostData', {
|
||||
requestId: this.id,
|
||||
});
|
||||
return result.postData;
|
||||
} catch (err) {
|
||||
debugError(err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
override headers(): Record<string, string> {
|
||||
// Callers should not be allowed to mutate internal structure.
|
||||
return structuredClone(this.#headers);
|
||||
}
|
||||
|
||||
override response(): CdpHTTPResponse | null {
|
||||
return this._response;
|
||||
}
|
||||
|
||||
override frame(): Frame | null {
|
||||
return this.#frame;
|
||||
}
|
||||
|
||||
override isNavigationRequest(): boolean {
|
||||
return this.#isNavigationRequest;
|
||||
}
|
||||
|
||||
override initiator(): Protocol.Network.Initiator | undefined {
|
||||
return this.#initiator;
|
||||
}
|
||||
|
||||
override redirectChain(): CdpHTTPRequest[] {
|
||||
return this._redirectChain.slice();
|
||||
}
|
||||
|
||||
override failure(): {errorText: string} | null {
|
||||
if (!this._failureText) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
errorText: this._failureText,
|
||||
};
|
||||
}
|
||||
|
||||
protected canBeIntercepted(): boolean {
|
||||
return !this.url().startsWith('data:') && !this._fromMemoryCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async _continue(overrides: ContinueRequestOverrides = {}): Promise<void> {
|
||||
const {url, method, postData, headers} = overrides;
|
||||
this.interception.handled = true;
|
||||
|
||||
const postDataBinaryBase64 = postData
|
||||
? stringToBase64(postData)
|
||||
: undefined;
|
||||
|
||||
if (this._interceptionId === undefined) {
|
||||
throw new Error(
|
||||
'HTTPRequest is missing _interceptionId needed for Fetch.continueRequest',
|
||||
);
|
||||
}
|
||||
await this.#client
|
||||
.send('Fetch.continueRequest', {
|
||||
requestId: this._interceptionId,
|
||||
url,
|
||||
method,
|
||||
postData: postDataBinaryBase64,
|
||||
headers: headers ? headersArray(headers) : undefined,
|
||||
})
|
||||
.catch(error => {
|
||||
this.interception.handled = false;
|
||||
return handleError(error);
|
||||
});
|
||||
}
|
||||
|
||||
async _respond(response: Partial<ResponseForRequest>): Promise<void> {
|
||||
this.interception.handled = true;
|
||||
|
||||
let parsedBody:
|
||||
| {
|
||||
contentLength: number;
|
||||
base64: string;
|
||||
}
|
||||
| undefined;
|
||||
if (response.body) {
|
||||
parsedBody = HTTPRequest.getResponse(response.body);
|
||||
}
|
||||
|
||||
const responseHeaders: Record<string, string | string[]> = {};
|
||||
if (response.headers) {
|
||||
for (const header of Object.keys(response.headers)) {
|
||||
const value = response.headers[header];
|
||||
|
||||
responseHeaders[header.toLowerCase()] = Array.isArray(value)
|
||||
? value.map(item => {
|
||||
return String(item);
|
||||
})
|
||||
: String(value);
|
||||
}
|
||||
}
|
||||
if (response.contentType) {
|
||||
responseHeaders['content-type'] = response.contentType;
|
||||
}
|
||||
if (parsedBody?.contentLength && !('content-length' in responseHeaders)) {
|
||||
responseHeaders['content-length'] = String(parsedBody.contentLength);
|
||||
}
|
||||
|
||||
const status = response.status || 200;
|
||||
if (this._interceptionId === undefined) {
|
||||
throw new Error(
|
||||
'HTTPRequest is missing _interceptionId needed for Fetch.fulfillRequest',
|
||||
);
|
||||
}
|
||||
await this.#client
|
||||
.send('Fetch.fulfillRequest', {
|
||||
requestId: this._interceptionId,
|
||||
responseCode: status,
|
||||
responsePhrase: STATUS_TEXTS[status],
|
||||
responseHeaders: headersArray(responseHeaders),
|
||||
body: parsedBody?.base64,
|
||||
})
|
||||
.catch(error => {
|
||||
this.interception.handled = false;
|
||||
return handleError(error);
|
||||
});
|
||||
}
|
||||
|
||||
async _abort(
|
||||
errorReason: Protocol.Network.ErrorReason | null,
|
||||
): Promise<void> {
|
||||
this.interception.handled = true;
|
||||
if (this._interceptionId === undefined) {
|
||||
throw new Error(
|
||||
'HTTPRequest is missing _interceptionId needed for Fetch.failRequest',
|
||||
);
|
||||
}
|
||||
await this.#client
|
||||
.send('Fetch.failRequest', {
|
||||
requestId: this._interceptionId,
|
||||
errorReason: errorReason || 'Failed',
|
||||
})
|
||||
.catch(handleError);
|
||||
}
|
||||
}
|
||||
168
node_modules/puppeteer-core/src/cdp/HTTPResponse.ts
generated
vendored
Normal file
168
node_modules/puppeteer-core/src/cdp/HTTPResponse.ts
generated
vendored
Normal file
@@ -0,0 +1,168 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2020 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
import type {Protocol} from 'devtools-protocol';
|
||||
|
||||
import type {Frame} from '../api/Frame.js';
|
||||
import {HTTPResponse, type RemoteAddress} from '../api/HTTPResponse.js';
|
||||
import {ProtocolError} from '../common/Errors.js';
|
||||
import {SecurityDetails} from '../common/SecurityDetails.js';
|
||||
import {Deferred} from '../util/Deferred.js';
|
||||
import {stringToTypedArray} from '../util/encoding.js';
|
||||
|
||||
import type {CdpHTTPRequest} from './HTTPRequest.js';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export class CdpHTTPResponse extends HTTPResponse {
|
||||
#request: CdpHTTPRequest;
|
||||
#contentPromise: Promise<Uint8Array> | null = null;
|
||||
#bodyLoadedDeferred = Deferred.create<void, Error>();
|
||||
#remoteAddress: RemoteAddress;
|
||||
#status: number;
|
||||
#statusText: string;
|
||||
#fromDiskCache: boolean;
|
||||
#fromServiceWorker: boolean;
|
||||
#headers: Record<string, string> = {};
|
||||
#securityDetails: SecurityDetails | null;
|
||||
#timing: Protocol.Network.ResourceTiming | null;
|
||||
|
||||
constructor(
|
||||
request: CdpHTTPRequest,
|
||||
responsePayload: Protocol.Network.Response,
|
||||
extraInfo: Protocol.Network.ResponseReceivedExtraInfoEvent | null,
|
||||
) {
|
||||
super();
|
||||
this.#request = request;
|
||||
|
||||
this.#remoteAddress = {
|
||||
ip: responsePayload.remoteIPAddress,
|
||||
port: responsePayload.remotePort,
|
||||
};
|
||||
this.#statusText =
|
||||
this.#parseStatusTextFromExtraInfo(extraInfo) ||
|
||||
responsePayload.statusText;
|
||||
this.#fromDiskCache = !!responsePayload.fromDiskCache;
|
||||
this.#fromServiceWorker = !!responsePayload.fromServiceWorker;
|
||||
|
||||
this.#status = extraInfo ? extraInfo.statusCode : responsePayload.status;
|
||||
const headers = extraInfo ? extraInfo.headers : responsePayload.headers;
|
||||
for (const [key, value] of Object.entries(headers)) {
|
||||
this.#headers[key.toLowerCase()] = value;
|
||||
}
|
||||
|
||||
this.#securityDetails = responsePayload.securityDetails
|
||||
? new SecurityDetails(responsePayload.securityDetails)
|
||||
: null;
|
||||
this.#timing = responsePayload.timing || null;
|
||||
}
|
||||
|
||||
#parseStatusTextFromExtraInfo(
|
||||
extraInfo: Protocol.Network.ResponseReceivedExtraInfoEvent | null,
|
||||
): string | undefined {
|
||||
if (!extraInfo || !extraInfo.headersText) {
|
||||
return;
|
||||
}
|
||||
const firstLine = extraInfo.headersText.split('\r', 1)[0];
|
||||
if (!firstLine || firstLine.length > 1_000) {
|
||||
return;
|
||||
}
|
||||
const match = firstLine.match(/[^ ]* [^ ]* (.*)/);
|
||||
if (!match) {
|
||||
return;
|
||||
}
|
||||
const statusText = match[1];
|
||||
if (!statusText) {
|
||||
return;
|
||||
}
|
||||
return statusText;
|
||||
}
|
||||
|
||||
_resolveBody(err?: Error): void {
|
||||
if (err) {
|
||||
return this.#bodyLoadedDeferred.reject(err);
|
||||
}
|
||||
return this.#bodyLoadedDeferred.resolve();
|
||||
}
|
||||
|
||||
override remoteAddress(): RemoteAddress {
|
||||
return this.#remoteAddress;
|
||||
}
|
||||
|
||||
override url(): string {
|
||||
return this.#request.url();
|
||||
}
|
||||
|
||||
override status(): number {
|
||||
return this.#status;
|
||||
}
|
||||
|
||||
override statusText(): string {
|
||||
return this.#statusText;
|
||||
}
|
||||
|
||||
override headers(): Record<string, string> {
|
||||
return this.#headers;
|
||||
}
|
||||
|
||||
override securityDetails(): SecurityDetails | null {
|
||||
return this.#securityDetails;
|
||||
}
|
||||
|
||||
override timing(): Protocol.Network.ResourceTiming | null {
|
||||
return this.#timing;
|
||||
}
|
||||
|
||||
override content(): Promise<Uint8Array> {
|
||||
if (!this.#contentPromise) {
|
||||
this.#contentPromise = this.#bodyLoadedDeferred
|
||||
.valueOrThrow()
|
||||
.then(async () => {
|
||||
try {
|
||||
// Use CDPSession from corresponding request to retrieve body, as it's client
|
||||
// might have been updated (e.g. for an adopted OOPIF).
|
||||
const response = await this.#request.client.send(
|
||||
'Network.getResponseBody',
|
||||
{
|
||||
requestId: this.#request.id,
|
||||
},
|
||||
);
|
||||
|
||||
return stringToTypedArray(response.body, response.base64Encoded);
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof ProtocolError &&
|
||||
error.originalMessage ===
|
||||
'No resource with given identifier found'
|
||||
) {
|
||||
throw new ProtocolError(
|
||||
'Could not load response body for this request. This might happen if the request is a preflight request.',
|
||||
);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
return this.#contentPromise;
|
||||
}
|
||||
|
||||
override request(): CdpHTTPRequest {
|
||||
return this.#request;
|
||||
}
|
||||
|
||||
override fromCache(): boolean {
|
||||
return this.#fromDiskCache || this.#request._fromMemoryCache;
|
||||
}
|
||||
|
||||
override fromServiceWorker(): boolean {
|
||||
return this.#fromServiceWorker;
|
||||
}
|
||||
|
||||
override frame(): Frame | null {
|
||||
return this.#request.frame();
|
||||
}
|
||||
}
|
||||
653
node_modules/puppeteer-core/src/cdp/Input.ts
generated
vendored
Normal file
653
node_modules/puppeteer-core/src/cdp/Input.ts
generated
vendored
Normal file
@@ -0,0 +1,653 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2017 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type {Protocol} from 'devtools-protocol';
|
||||
|
||||
import type {CDPSession} from '../api/CDPSession.js';
|
||||
import type {Point} from '../api/ElementHandle.js';
|
||||
import {
|
||||
Keyboard,
|
||||
Mouse,
|
||||
MouseButton,
|
||||
Touchscreen,
|
||||
type TouchHandle,
|
||||
type KeyDownOptions,
|
||||
type KeyPressOptions,
|
||||
type KeyboardTypeOptions,
|
||||
type MouseClickOptions,
|
||||
type MouseMoveOptions,
|
||||
type MouseOptions,
|
||||
type MouseWheelOptions,
|
||||
} from '../api/Input.js';
|
||||
import {TouchError} from '../common/Errors.js';
|
||||
import {
|
||||
_keyDefinitions,
|
||||
type KeyDefinition,
|
||||
type KeyInput,
|
||||
} from '../common/USKeyboardLayout.js';
|
||||
import {assert} from '../util/assert.js';
|
||||
|
||||
type KeyDescription = Required<
|
||||
Pick<KeyDefinition, 'keyCode' | 'key' | 'text' | 'code' | 'location'>
|
||||
>;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export class CdpKeyboard extends Keyboard {
|
||||
#client: CDPSession;
|
||||
#pressedKeys = new Set<string>();
|
||||
|
||||
_modifiers = 0;
|
||||
|
||||
constructor(client: CDPSession) {
|
||||
super();
|
||||
this.#client = client;
|
||||
}
|
||||
|
||||
updateClient(client: CDPSession): void {
|
||||
this.#client = client;
|
||||
}
|
||||
|
||||
override async down(
|
||||
key: KeyInput,
|
||||
options: Readonly<KeyDownOptions> = {
|
||||
text: undefined,
|
||||
commands: [],
|
||||
},
|
||||
): Promise<void> {
|
||||
const description = this.#keyDescriptionForString(key);
|
||||
|
||||
const autoRepeat = this.#pressedKeys.has(description.code);
|
||||
this.#pressedKeys.add(description.code);
|
||||
this._modifiers |= this.#modifierBit(description.key);
|
||||
|
||||
const text = options.text === undefined ? description.text : options.text;
|
||||
await this.#client.send('Input.dispatchKeyEvent', {
|
||||
type: text ? 'keyDown' : 'rawKeyDown',
|
||||
modifiers: this._modifiers,
|
||||
windowsVirtualKeyCode: description.keyCode,
|
||||
code: description.code,
|
||||
key: description.key,
|
||||
text: text,
|
||||
unmodifiedText: text,
|
||||
autoRepeat,
|
||||
location: description.location,
|
||||
isKeypad: description.location === 3,
|
||||
commands: options.commands,
|
||||
});
|
||||
}
|
||||
|
||||
#modifierBit(key: string): number {
|
||||
if (key === 'Alt') {
|
||||
return 1;
|
||||
}
|
||||
if (key === 'Control') {
|
||||
return 2;
|
||||
}
|
||||
if (key === 'Meta') {
|
||||
return 4;
|
||||
}
|
||||
if (key === 'Shift') {
|
||||
return 8;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
#keyDescriptionForString(keyString: KeyInput): KeyDescription {
|
||||
const shift = this._modifiers & 8;
|
||||
const description = {
|
||||
key: '',
|
||||
keyCode: 0,
|
||||
code: '',
|
||||
text: '',
|
||||
location: 0,
|
||||
};
|
||||
|
||||
const definition = _keyDefinitions[keyString];
|
||||
assert(definition, `Unknown key: "${keyString}"`);
|
||||
|
||||
if (definition.key) {
|
||||
description.key = definition.key;
|
||||
}
|
||||
if (shift && definition.shiftKey) {
|
||||
description.key = definition.shiftKey;
|
||||
}
|
||||
|
||||
if (definition.keyCode) {
|
||||
description.keyCode = definition.keyCode;
|
||||
}
|
||||
if (shift && definition.shiftKeyCode) {
|
||||
description.keyCode = definition.shiftKeyCode;
|
||||
}
|
||||
|
||||
if (definition.code) {
|
||||
description.code = definition.code;
|
||||
}
|
||||
|
||||
if (definition.location) {
|
||||
description.location = definition.location;
|
||||
}
|
||||
|
||||
if (description.key.length === 1) {
|
||||
description.text = description.key;
|
||||
}
|
||||
|
||||
if (definition.text) {
|
||||
description.text = definition.text;
|
||||
}
|
||||
if (shift && definition.shiftText) {
|
||||
description.text = definition.shiftText;
|
||||
}
|
||||
|
||||
// if any modifiers besides shift are pressed, no text should be sent
|
||||
if (this._modifiers & ~8) {
|
||||
description.text = '';
|
||||
}
|
||||
|
||||
return description;
|
||||
}
|
||||
|
||||
override async up(key: KeyInput): Promise<void> {
|
||||
const description = this.#keyDescriptionForString(key);
|
||||
|
||||
this._modifiers &= ~this.#modifierBit(description.key);
|
||||
this.#pressedKeys.delete(description.code);
|
||||
await this.#client.send('Input.dispatchKeyEvent', {
|
||||
type: 'keyUp',
|
||||
modifiers: this._modifiers,
|
||||
key: description.key,
|
||||
windowsVirtualKeyCode: description.keyCode,
|
||||
code: description.code,
|
||||
location: description.location,
|
||||
});
|
||||
}
|
||||
|
||||
override async sendCharacter(char: string): Promise<void> {
|
||||
await this.#client.send('Input.insertText', {text: char});
|
||||
}
|
||||
|
||||
private charIsKey(char: string): char is KeyInput {
|
||||
return !!_keyDefinitions[char as KeyInput];
|
||||
}
|
||||
|
||||
override async type(
|
||||
text: string,
|
||||
options: Readonly<KeyboardTypeOptions> = {},
|
||||
): Promise<void> {
|
||||
const delay = options.delay || undefined;
|
||||
for (const char of text) {
|
||||
if (this.charIsKey(char)) {
|
||||
await this.press(char, {delay});
|
||||
} else {
|
||||
if (delay) {
|
||||
await new Promise(f => {
|
||||
return setTimeout(f, delay);
|
||||
});
|
||||
}
|
||||
await this.sendCharacter(char);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override async press(
|
||||
key: KeyInput,
|
||||
options: Readonly<KeyPressOptions> = {},
|
||||
): Promise<void> {
|
||||
const {delay = null} = options;
|
||||
await this.down(key, options);
|
||||
if (delay) {
|
||||
await new Promise(f => {
|
||||
return setTimeout(f, options.delay);
|
||||
});
|
||||
}
|
||||
await this.up(key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This must follow {@link Protocol.Input.DispatchMouseEventRequest.buttons}.
|
||||
*/
|
||||
const enum MouseButtonFlag {
|
||||
None = 0,
|
||||
Left = 1,
|
||||
Right = 1 << 1,
|
||||
Middle = 1 << 2,
|
||||
Back = 1 << 3,
|
||||
Forward = 1 << 4,
|
||||
}
|
||||
|
||||
const getFlag = (button: MouseButton): MouseButtonFlag => {
|
||||
switch (button) {
|
||||
case MouseButton.Left:
|
||||
return MouseButtonFlag.Left;
|
||||
case MouseButton.Right:
|
||||
return MouseButtonFlag.Right;
|
||||
case MouseButton.Middle:
|
||||
return MouseButtonFlag.Middle;
|
||||
case MouseButton.Back:
|
||||
return MouseButtonFlag.Back;
|
||||
case MouseButton.Forward:
|
||||
return MouseButtonFlag.Forward;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This should match
|
||||
* https://source.chromium.org/chromium/chromium/src/+/refs/heads/main:content/browser/renderer_host/input/web_input_event_builders_mac.mm;drc=a61b95c63b0b75c1cfe872d9c8cdf927c226046e;bpv=1;bpt=1;l=221.
|
||||
*/
|
||||
const getButtonFromPressedButtons = (
|
||||
buttons: number,
|
||||
): Protocol.Input.MouseButton => {
|
||||
if (buttons & MouseButtonFlag.Left) {
|
||||
return MouseButton.Left;
|
||||
} else if (buttons & MouseButtonFlag.Right) {
|
||||
return MouseButton.Right;
|
||||
} else if (buttons & MouseButtonFlag.Middle) {
|
||||
return MouseButton.Middle;
|
||||
} else if (buttons & MouseButtonFlag.Back) {
|
||||
return MouseButton.Back;
|
||||
} else if (buttons & MouseButtonFlag.Forward) {
|
||||
return MouseButton.Forward;
|
||||
}
|
||||
return 'none';
|
||||
};
|
||||
|
||||
interface MouseState {
|
||||
/**
|
||||
* The current position of the mouse.
|
||||
*/
|
||||
position: Point;
|
||||
/**
|
||||
* The buttons that are currently being pressed.
|
||||
*/
|
||||
buttons: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export class CdpMouse extends Mouse {
|
||||
#client: CDPSession;
|
||||
#keyboard: CdpKeyboard;
|
||||
|
||||
constructor(client: CDPSession, keyboard: CdpKeyboard) {
|
||||
super();
|
||||
this.#client = client;
|
||||
this.#keyboard = keyboard;
|
||||
}
|
||||
|
||||
updateClient(client: CDPSession): void {
|
||||
this.#client = client;
|
||||
}
|
||||
|
||||
#_state: Readonly<MouseState> = {
|
||||
position: {x: 0, y: 0},
|
||||
buttons: MouseButtonFlag.None,
|
||||
};
|
||||
get #state(): MouseState {
|
||||
return Object.assign({...this.#_state}, ...this.#transactions);
|
||||
}
|
||||
|
||||
// Transactions can run in parallel, so we store each of thme in this array.
|
||||
#transactions: Array<Partial<MouseState>> = [];
|
||||
#createTransaction(): {
|
||||
update: (updates: Partial<MouseState>) => void;
|
||||
commit: () => void;
|
||||
rollback: () => void;
|
||||
} {
|
||||
const transaction: Partial<MouseState> = {};
|
||||
this.#transactions.push(transaction);
|
||||
const popTransaction = () => {
|
||||
this.#transactions.splice(this.#transactions.indexOf(transaction), 1);
|
||||
};
|
||||
return {
|
||||
update: (updates: Partial<MouseState>) => {
|
||||
Object.assign(transaction, updates);
|
||||
},
|
||||
commit: () => {
|
||||
this.#_state = {...this.#_state, ...transaction};
|
||||
popTransaction();
|
||||
},
|
||||
rollback: popTransaction,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a shortcut for a typical update, commit/rollback lifecycle based on
|
||||
* the error of the action.
|
||||
*/
|
||||
async #withTransaction(
|
||||
action: (
|
||||
update: (updates: Partial<MouseState>) => void,
|
||||
) => Promise<unknown>,
|
||||
): Promise<void> {
|
||||
const {update, commit, rollback} = this.#createTransaction();
|
||||
try {
|
||||
await action(update);
|
||||
commit();
|
||||
} catch (error) {
|
||||
rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
override async reset(): Promise<void> {
|
||||
const actions = [];
|
||||
for (const [flag, button] of [
|
||||
[MouseButtonFlag.Left, MouseButton.Left],
|
||||
[MouseButtonFlag.Middle, MouseButton.Middle],
|
||||
[MouseButtonFlag.Right, MouseButton.Right],
|
||||
[MouseButtonFlag.Forward, MouseButton.Forward],
|
||||
[MouseButtonFlag.Back, MouseButton.Back],
|
||||
] as const) {
|
||||
if (this.#state.buttons & flag) {
|
||||
actions.push(this.up({button: button}));
|
||||
}
|
||||
}
|
||||
if (this.#state.position.x !== 0 || this.#state.position.y !== 0) {
|
||||
actions.push(this.move(0, 0));
|
||||
}
|
||||
await Promise.all(actions);
|
||||
}
|
||||
|
||||
override async move(
|
||||
x: number,
|
||||
y: number,
|
||||
options: Readonly<MouseMoveOptions> = {},
|
||||
): Promise<void> {
|
||||
const {steps = 1} = options;
|
||||
const from = this.#state.position;
|
||||
const to = {x, y};
|
||||
for (let i = 1; i <= steps; i++) {
|
||||
await this.#withTransaction(updateState => {
|
||||
updateState({
|
||||
position: {
|
||||
x: from.x + (to.x - from.x) * (i / steps),
|
||||
y: from.y + (to.y - from.y) * (i / steps),
|
||||
},
|
||||
});
|
||||
const {buttons, position} = this.#state;
|
||||
return this.#client.send('Input.dispatchMouseEvent', {
|
||||
type: 'mouseMoved',
|
||||
modifiers: this.#keyboard._modifiers,
|
||||
buttons,
|
||||
button: getButtonFromPressedButtons(buttons),
|
||||
...position,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
override async down(options: Readonly<MouseOptions> = {}): Promise<void> {
|
||||
const {button = MouseButton.Left, clickCount = 1} = options;
|
||||
const flag = getFlag(button);
|
||||
if (!flag) {
|
||||
throw new Error(`Unsupported mouse button: ${button}`);
|
||||
}
|
||||
if (this.#state.buttons & flag) {
|
||||
throw new Error(`'${button}' is already pressed.`);
|
||||
}
|
||||
await this.#withTransaction(updateState => {
|
||||
updateState({
|
||||
buttons: this.#state.buttons | flag,
|
||||
});
|
||||
const {buttons, position} = this.#state;
|
||||
return this.#client.send('Input.dispatchMouseEvent', {
|
||||
type: 'mousePressed',
|
||||
modifiers: this.#keyboard._modifiers,
|
||||
clickCount,
|
||||
buttons,
|
||||
button,
|
||||
...position,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
override async up(options: Readonly<MouseOptions> = {}): Promise<void> {
|
||||
const {button = MouseButton.Left, clickCount = 1} = options;
|
||||
const flag = getFlag(button);
|
||||
if (!flag) {
|
||||
throw new Error(`Unsupported mouse button: ${button}`);
|
||||
}
|
||||
if (!(this.#state.buttons & flag)) {
|
||||
throw new Error(`'${button}' is not pressed.`);
|
||||
}
|
||||
await this.#withTransaction(updateState => {
|
||||
updateState({
|
||||
buttons: this.#state.buttons & ~flag,
|
||||
});
|
||||
const {buttons, position} = this.#state;
|
||||
return this.#client.send('Input.dispatchMouseEvent', {
|
||||
type: 'mouseReleased',
|
||||
modifiers: this.#keyboard._modifiers,
|
||||
clickCount,
|
||||
buttons,
|
||||
button,
|
||||
...position,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
override async click(
|
||||
x: number,
|
||||
y: number,
|
||||
options: Readonly<MouseClickOptions> = {},
|
||||
): Promise<void> {
|
||||
const {delay, count = 1, clickCount = count} = options;
|
||||
if (count < 1) {
|
||||
throw new Error('Click must occur a positive number of times.');
|
||||
}
|
||||
const actions: Array<Promise<void>> = [this.move(x, y)];
|
||||
if (clickCount === count) {
|
||||
for (let i = 1; i < count; ++i) {
|
||||
actions.push(
|
||||
this.down({...options, clickCount: i}),
|
||||
this.up({...options, clickCount: i}),
|
||||
);
|
||||
}
|
||||
}
|
||||
actions.push(this.down({...options, clickCount}));
|
||||
if (typeof delay === 'number') {
|
||||
await Promise.all(actions);
|
||||
actions.length = 0;
|
||||
await new Promise(resolve => {
|
||||
setTimeout(resolve, delay);
|
||||
});
|
||||
}
|
||||
actions.push(this.up({...options, clickCount}));
|
||||
await Promise.all(actions);
|
||||
}
|
||||
|
||||
override async wheel(
|
||||
options: Readonly<MouseWheelOptions> = {},
|
||||
): Promise<void> {
|
||||
const {deltaX = 0, deltaY = 0} = options;
|
||||
const {position, buttons} = this.#state;
|
||||
await this.#client.send('Input.dispatchMouseEvent', {
|
||||
type: 'mouseWheel',
|
||||
pointerType: 'mouse',
|
||||
modifiers: this.#keyboard._modifiers,
|
||||
deltaY,
|
||||
deltaX,
|
||||
buttons,
|
||||
...position,
|
||||
});
|
||||
}
|
||||
|
||||
override async drag(
|
||||
start: Point,
|
||||
target: Point,
|
||||
): Promise<Protocol.Input.DragData> {
|
||||
const promise = new Promise<Protocol.Input.DragData>(resolve => {
|
||||
this.#client.once('Input.dragIntercepted', event => {
|
||||
return resolve(event.data);
|
||||
});
|
||||
});
|
||||
await this.move(start.x, start.y);
|
||||
await this.down();
|
||||
await this.move(target.x, target.y);
|
||||
return await promise;
|
||||
}
|
||||
|
||||
override async dragEnter(
|
||||
target: Point,
|
||||
data: Protocol.Input.DragData,
|
||||
): Promise<void> {
|
||||
await this.#client.send('Input.dispatchDragEvent', {
|
||||
type: 'dragEnter',
|
||||
x: target.x,
|
||||
y: target.y,
|
||||
modifiers: this.#keyboard._modifiers,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
override async dragOver(
|
||||
target: Point,
|
||||
data: Protocol.Input.DragData,
|
||||
): Promise<void> {
|
||||
await this.#client.send('Input.dispatchDragEvent', {
|
||||
type: 'dragOver',
|
||||
x: target.x,
|
||||
y: target.y,
|
||||
modifiers: this.#keyboard._modifiers,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
override async drop(
|
||||
target: Point,
|
||||
data: Protocol.Input.DragData,
|
||||
): Promise<void> {
|
||||
await this.#client.send('Input.dispatchDragEvent', {
|
||||
type: 'drop',
|
||||
x: target.x,
|
||||
y: target.y,
|
||||
modifiers: this.#keyboard._modifiers,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
override async dragAndDrop(
|
||||
start: Point,
|
||||
target: Point,
|
||||
options: {delay?: number} = {},
|
||||
): Promise<void> {
|
||||
const {delay = null} = options;
|
||||
const data = await this.drag(start, target);
|
||||
await this.dragEnter(target, data);
|
||||
await this.dragOver(target, data);
|
||||
if (delay) {
|
||||
await new Promise(resolve => {
|
||||
return setTimeout(resolve, delay);
|
||||
});
|
||||
}
|
||||
await this.drop(target, data);
|
||||
await this.up();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export class CdpTouchHandle implements TouchHandle {
|
||||
#started = false;
|
||||
#touchScreen: CdpTouchscreen;
|
||||
#touchPoint: Protocol.Input.TouchPoint;
|
||||
#client: CDPSession;
|
||||
#keyboard: CdpKeyboard;
|
||||
|
||||
constructor(
|
||||
client: CDPSession,
|
||||
touchScreen: CdpTouchscreen,
|
||||
keyboard: CdpKeyboard,
|
||||
touchPoint: Protocol.Input.TouchPoint,
|
||||
) {
|
||||
this.#client = client;
|
||||
this.#touchScreen = touchScreen;
|
||||
this.#keyboard = keyboard;
|
||||
this.#touchPoint = touchPoint;
|
||||
}
|
||||
|
||||
updateClient(client: CDPSession): void {
|
||||
this.#client = client;
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
if (this.#started) {
|
||||
throw new TouchError('Touch has already started');
|
||||
}
|
||||
await this.#client.send('Input.dispatchTouchEvent', {
|
||||
type: 'touchStart',
|
||||
touchPoints: [this.#touchPoint],
|
||||
modifiers: this.#keyboard._modifiers,
|
||||
});
|
||||
this.#started = true;
|
||||
}
|
||||
|
||||
move(x: number, y: number): Promise<void> {
|
||||
this.#touchPoint.x = Math.round(x);
|
||||
this.#touchPoint.y = Math.round(y);
|
||||
return this.#client.send('Input.dispatchTouchEvent', {
|
||||
type: 'touchMove',
|
||||
touchPoints: [this.#touchPoint],
|
||||
modifiers: this.#keyboard._modifiers,
|
||||
});
|
||||
}
|
||||
|
||||
async end(): Promise<void> {
|
||||
await this.#client.send('Input.dispatchTouchEvent', {
|
||||
type: 'touchEnd',
|
||||
touchPoints: [this.#touchPoint],
|
||||
modifiers: this.#keyboard._modifiers,
|
||||
});
|
||||
this.#touchScreen.removeHandle(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export class CdpTouchscreen extends Touchscreen {
|
||||
#client: CDPSession;
|
||||
#keyboard: CdpKeyboard;
|
||||
declare touches: CdpTouchHandle[];
|
||||
|
||||
constructor(client: CDPSession, keyboard: CdpKeyboard) {
|
||||
super();
|
||||
this.#client = client;
|
||||
this.#keyboard = keyboard;
|
||||
}
|
||||
|
||||
updateClient(client: CDPSession): void {
|
||||
this.#client = client;
|
||||
this.touches.forEach(t => {
|
||||
t.updateClient(client);
|
||||
});
|
||||
}
|
||||
|
||||
override async touchStart(x: number, y: number): Promise<TouchHandle> {
|
||||
const id = this.idGenerator();
|
||||
const touchPoint: Protocol.Input.TouchPoint = {
|
||||
x: Math.round(x),
|
||||
y: Math.round(y),
|
||||
radiusX: 0.5,
|
||||
radiusY: 0.5,
|
||||
force: 0.5,
|
||||
id,
|
||||
};
|
||||
const touch = new CdpTouchHandle(
|
||||
this.#client,
|
||||
this,
|
||||
this.#keyboard,
|
||||
touchPoint,
|
||||
);
|
||||
await touch.start();
|
||||
this.touches.push(touch);
|
||||
return touch;
|
||||
}
|
||||
}
|
||||
266
node_modules/puppeteer-core/src/cdp/IsolatedWorld.ts
generated
vendored
Normal file
266
node_modules/puppeteer-core/src/cdp/IsolatedWorld.ts
generated
vendored
Normal 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
20
node_modules/puppeteer-core/src/cdp/IsolatedWorlds.ts
generated
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2022 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* A unique key for {@link IsolatedWorldChart} to denote the default world.
|
||||
* Execution contexts are automatically created in the default world.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export const MAIN_WORLD = Symbol('mainWorld');
|
||||
/**
|
||||
* A unique key for {@link IsolatedWorldChart} to denote the puppeteer world.
|
||||
* This world contains all puppeteer-internal bindings/code.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export const PUPPETEER_WORLD = Symbol('puppeteerWorld');
|
||||
126
node_modules/puppeteer-core/src/cdp/JSHandle.ts
generated
vendored
Normal file
126
node_modules/puppeteer-core/src/cdp/JSHandle.ts
generated
vendored
Normal file
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type {Protocol} from 'devtools-protocol';
|
||||
|
||||
import type {CDPSession} from '../api/CDPSession.js';
|
||||
import {JSHandle} from '../api/JSHandle.js';
|
||||
import {debugError} from '../common/util.js';
|
||||
|
||||
import type {CdpElementHandle} from './ElementHandle.js';
|
||||
import type {IsolatedWorld} from './IsolatedWorld.js';
|
||||
import {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
276
node_modules/puppeteer-core/src/cdp/LifecycleWatcher.ts
generated
vendored
Normal file
@@ -0,0 +1,276 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type Protocol from 'devtools-protocol';
|
||||
|
||||
import {type Frame, FrameEvent} from '../api/Frame.js';
|
||||
import type {HTTPRequest} from '../api/HTTPRequest.js';
|
||||
import type {HTTPResponse} from '../api/HTTPResponse.js';
|
||||
import type {TimeoutError} from '../common/Errors.js';
|
||||
import {EventEmitter} from '../common/EventEmitter.js';
|
||||
import {NetworkManagerEvent} from '../common/NetworkManagerEvents.js';
|
||||
import {assert} from '../util/assert.js';
|
||||
import {Deferred} from '../util/Deferred.js';
|
||||
import {DisposableStack} from '../util/disposable.js';
|
||||
|
||||
import type {CdpFrame} from './Frame.js';
|
||||
import {FrameManagerEvent} from './FrameManagerEvents.js';
|
||||
import type {NetworkManager} from './NetworkManager.js';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type PuppeteerLifeCycleEvent =
|
||||
/**
|
||||
* Waits for the 'load' event.
|
||||
*/
|
||||
| 'load'
|
||||
/**
|
||||
* Waits for the 'DOMContentLoaded' event.
|
||||
*/
|
||||
| 'domcontentloaded'
|
||||
/**
|
||||
* Waits till there are no more than 0 network connections for at least `500`
|
||||
* ms.
|
||||
*/
|
||||
| 'networkidle0'
|
||||
/**
|
||||
* Waits till there are no more than 2 network connections for at least `500`
|
||||
* ms.
|
||||
*/
|
||||
| 'networkidle2';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type ProtocolLifeCycleEvent =
|
||||
| 'load'
|
||||
| 'DOMContentLoaded'
|
||||
| 'networkIdle'
|
||||
| 'networkAlmostIdle';
|
||||
|
||||
const puppeteerToProtocolLifecycle = new Map<
|
||||
PuppeteerLifeCycleEvent,
|
||||
ProtocolLifeCycleEvent
|
||||
>([
|
||||
['load', 'load'],
|
||||
['domcontentloaded', 'DOMContentLoaded'],
|
||||
['networkidle0', 'networkIdle'],
|
||||
['networkidle2', 'networkAlmostIdle'],
|
||||
]);
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export class LifecycleWatcher {
|
||||
#expectedLifecycle: ProtocolLifeCycleEvent[];
|
||||
#frame: CdpFrame;
|
||||
#timeout: number;
|
||||
#navigationRequest: HTTPRequest | null = null;
|
||||
#subscriptions = new DisposableStack();
|
||||
#initialLoaderId: string;
|
||||
|
||||
#terminationDeferred: Deferred<Error>;
|
||||
#sameDocumentNavigationDeferred = Deferred.create<undefined>();
|
||||
#lifecycleDeferred = Deferred.create<void>();
|
||||
#newDocumentNavigationDeferred = Deferred.create<undefined>();
|
||||
#error = new Error('LifecycleWatcher terminated');
|
||||
|
||||
#hasSameDocumentNavigation?: boolean;
|
||||
#swapped?: boolean;
|
||||
|
||||
#navigationResponseReceived?: Deferred<void>;
|
||||
|
||||
constructor(
|
||||
networkManager: NetworkManager,
|
||||
frame: CdpFrame,
|
||||
waitUntil: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[],
|
||||
timeout: number,
|
||||
signal?: AbortSignal,
|
||||
) {
|
||||
if (Array.isArray(waitUntil)) {
|
||||
waitUntil = waitUntil.slice();
|
||||
} else if (typeof waitUntil === 'string') {
|
||||
waitUntil = [waitUntil];
|
||||
}
|
||||
this.#initialLoaderId = frame._loaderId;
|
||||
this.#expectedLifecycle = waitUntil.map(value => {
|
||||
const protocolEvent = puppeteerToProtocolLifecycle.get(value);
|
||||
assert(protocolEvent, 'Unknown value for options.waitUntil: ' + value);
|
||||
return protocolEvent;
|
||||
});
|
||||
|
||||
signal?.addEventListener('abort', () => {
|
||||
if (signal.reason instanceof Error) {
|
||||
signal.reason.cause = this.#error;
|
||||
}
|
||||
this.#terminationDeferred.reject(signal.reason);
|
||||
});
|
||||
|
||||
this.#frame = frame;
|
||||
this.#timeout = timeout;
|
||||
const frameManagerEmitter = this.#subscriptions.use(
|
||||
new EventEmitter(frame._frameManager),
|
||||
);
|
||||
frameManagerEmitter.on(
|
||||
FrameManagerEvent.LifecycleEvent,
|
||||
this.#checkLifecycleComplete.bind(this),
|
||||
);
|
||||
|
||||
const frameEmitter = this.#subscriptions.use(new EventEmitter(frame));
|
||||
frameEmitter.on(
|
||||
FrameEvent.FrameNavigatedWithinDocument,
|
||||
this.#navigatedWithinDocument.bind(this),
|
||||
);
|
||||
frameEmitter.on(FrameEvent.FrameNavigated, this.#navigated.bind(this));
|
||||
frameEmitter.on(FrameEvent.FrameSwapped, this.#frameSwapped.bind(this));
|
||||
frameEmitter.on(
|
||||
FrameEvent.FrameSwappedByActivation,
|
||||
this.#frameSwapped.bind(this),
|
||||
);
|
||||
frameEmitter.on(FrameEvent.FrameDetached, this.#onFrameDetached.bind(this));
|
||||
|
||||
const networkManagerEmitter = this.#subscriptions.use(
|
||||
new EventEmitter(networkManager),
|
||||
);
|
||||
networkManagerEmitter.on(
|
||||
NetworkManagerEvent.Request,
|
||||
this.#onRequest.bind(this),
|
||||
);
|
||||
networkManagerEmitter.on(
|
||||
NetworkManagerEvent.Response,
|
||||
this.#onResponse.bind(this),
|
||||
);
|
||||
networkManagerEmitter.on(
|
||||
NetworkManagerEvent.RequestFailed,
|
||||
this.#onRequestFailed.bind(this),
|
||||
);
|
||||
|
||||
this.#terminationDeferred = Deferred.create<Error>({
|
||||
timeout: this.#timeout,
|
||||
message: `Navigation timeout of ${this.#timeout} ms exceeded`,
|
||||
});
|
||||
|
||||
this.#checkLifecycleComplete();
|
||||
}
|
||||
|
||||
#onRequest(request: HTTPRequest): void {
|
||||
if (request.frame() !== this.#frame || !request.isNavigationRequest()) {
|
||||
return;
|
||||
}
|
||||
this.#navigationRequest = request;
|
||||
// Resolve previous navigation response in case there are multiple
|
||||
// navigation requests reported by the backend. This generally should not
|
||||
// happen by it looks like it's possible.
|
||||
this.#navigationResponseReceived?.resolve();
|
||||
this.#navigationResponseReceived = Deferred.create();
|
||||
if (request.response() !== null) {
|
||||
this.#navigationResponseReceived?.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
#onRequestFailed(request: HTTPRequest): void {
|
||||
if (this.#navigationRequest?.id !== request.id) {
|
||||
return;
|
||||
}
|
||||
this.#navigationResponseReceived?.resolve();
|
||||
}
|
||||
|
||||
#onResponse(response: HTTPResponse): void {
|
||||
if (this.#navigationRequest?.id !== response.request().id) {
|
||||
return;
|
||||
}
|
||||
this.#navigationResponseReceived?.resolve();
|
||||
}
|
||||
|
||||
#onFrameDetached(frame: Frame): void {
|
||||
if (this.#frame === frame) {
|
||||
this.#error.message = 'Navigating frame was detached';
|
||||
this.#terminationDeferred.resolve(this.#error);
|
||||
return;
|
||||
}
|
||||
this.#checkLifecycleComplete();
|
||||
}
|
||||
|
||||
async navigationResponse(): Promise<HTTPResponse | null> {
|
||||
// Continue with a possibly null response.
|
||||
await this.#navigationResponseReceived?.valueOrThrow();
|
||||
return this.#navigationRequest ? this.#navigationRequest.response() : null;
|
||||
}
|
||||
|
||||
sameDocumentNavigationPromise(): Promise<Error | undefined> {
|
||||
return this.#sameDocumentNavigationDeferred.valueOrThrow();
|
||||
}
|
||||
|
||||
newDocumentNavigationPromise(): Promise<Error | undefined> {
|
||||
return this.#newDocumentNavigationDeferred.valueOrThrow();
|
||||
}
|
||||
|
||||
lifecyclePromise(): Promise<void> {
|
||||
return this.#lifecycleDeferred.valueOrThrow();
|
||||
}
|
||||
|
||||
terminationPromise(): Promise<Error | TimeoutError | undefined> {
|
||||
return this.#terminationDeferred.valueOrThrow();
|
||||
}
|
||||
|
||||
#navigatedWithinDocument(): void {
|
||||
this.#hasSameDocumentNavigation = true;
|
||||
this.#checkLifecycleComplete();
|
||||
}
|
||||
|
||||
#navigated(navigationType: Protocol.Page.NavigationType): void {
|
||||
if (navigationType === 'BackForwardCacheRestore') {
|
||||
return this.#frameSwapped();
|
||||
}
|
||||
this.#checkLifecycleComplete();
|
||||
}
|
||||
|
||||
#frameSwapped(): void {
|
||||
this.#swapped = true;
|
||||
this.#checkLifecycleComplete();
|
||||
}
|
||||
|
||||
#checkLifecycleComplete(): void {
|
||||
// We expect navigation to commit.
|
||||
if (!checkLifecycle(this.#frame, this.#expectedLifecycle)) {
|
||||
return;
|
||||
}
|
||||
this.#lifecycleDeferred.resolve();
|
||||
if (this.#hasSameDocumentNavigation) {
|
||||
this.#sameDocumentNavigationDeferred.resolve(undefined);
|
||||
}
|
||||
if (this.#swapped || this.#frame._loaderId !== this.#initialLoaderId) {
|
||||
this.#newDocumentNavigationDeferred.resolve(undefined);
|
||||
}
|
||||
|
||||
function checkLifecycle(
|
||||
frame: CdpFrame,
|
||||
expectedLifecycle: ProtocolLifeCycleEvent[],
|
||||
): boolean {
|
||||
for (const event of expectedLifecycle) {
|
||||
if (!frame._lifecycleEvents.has(event)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (const child of frame.childFrames()) {
|
||||
if (
|
||||
child._hasStartedLoading &&
|
||||
!checkLifecycle(child, expectedLifecycle)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.#subscriptions.dispose();
|
||||
this.#error.cause = new Error('LifecycleWatcher disposed');
|
||||
this.#terminationDeferred.resolve(this.#error);
|
||||
}
|
||||
}
|
||||
267
node_modules/puppeteer-core/src/cdp/NetworkEventManager.ts
generated
vendored
Normal file
267
node_modules/puppeteer-core/src/cdp/NetworkEventManager.ts
generated
vendored
Normal file
@@ -0,0 +1,267 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2022 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type {Protocol} from 'devtools-protocol';
|
||||
|
||||
import {CdpHTTPRequest} from './HTTPRequest.js';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export interface QueuedEventGroup {
|
||||
responseReceivedEvent: Protocol.Network.ResponseReceivedEvent;
|
||||
loadingFinishedEvent?: Protocol.Network.LoadingFinishedEvent;
|
||||
loadingFailedEvent?: Protocol.Network.LoadingFailedEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export type FetchRequestId = string;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export interface RedirectInfo {
|
||||
event: Protocol.Network.RequestWillBeSentEvent;
|
||||
fetchRequestId?: FetchRequestId;
|
||||
}
|
||||
type RedirectInfoList = RedirectInfo[];
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export type NetworkRequestId = string;
|
||||
|
||||
/**
|
||||
* Helper class to track network events by request ID
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export class NetworkEventManager {
|
||||
/**
|
||||
* There are four possible orders of events:
|
||||
* A. `_onRequestWillBeSent`
|
||||
* B. `_onRequestWillBeSent`, `_onRequestPaused`
|
||||
* C. `_onRequestPaused`, `_onRequestWillBeSent`
|
||||
* D. `_onRequestPaused`, `_onRequestWillBeSent`, `_onRequestPaused`,
|
||||
* `_onRequestWillBeSent`, `_onRequestPaused`, `_onRequestPaused`
|
||||
* (see crbug.com/1196004)
|
||||
*
|
||||
* For `_onRequest` we need the event from `_onRequestWillBeSent` and
|
||||
* optionally the `interceptionId` from `_onRequestPaused`.
|
||||
*
|
||||
* If request interception is disabled, call `_onRequest` once per call to
|
||||
* `_onRequestWillBeSent`.
|
||||
* If request interception is enabled, call `_onRequest` once per call to
|
||||
* `_onRequestPaused` (once per `interceptionId`).
|
||||
*
|
||||
* Events are stored to allow for subsequent events to call `_onRequest`.
|
||||
*
|
||||
* Note that (chains of) redirect requests have the same `requestId` (!) as
|
||||
* the original request. We have to anticipate series of events like these:
|
||||
* A. `_onRequestWillBeSent`,
|
||||
* `_onRequestWillBeSent`, ...
|
||||
* B. `_onRequestWillBeSent`, `_onRequestPaused`,
|
||||
* `_onRequestWillBeSent`, `_onRequestPaused`, ...
|
||||
* C. `_onRequestWillBeSent`, `_onRequestPaused`,
|
||||
* `_onRequestPaused`, `_onRequestWillBeSent`, ...
|
||||
* D. `_onRequestPaused`, `_onRequestWillBeSent`,
|
||||
* `_onRequestPaused`, `_onRequestWillBeSent`, `_onRequestPaused`,
|
||||
* `_onRequestWillBeSent`, `_onRequestPaused`, `_onRequestPaused`, ...
|
||||
* (see crbug.com/1196004)
|
||||
*/
|
||||
#requestWillBeSentMap = new Map<
|
||||
NetworkRequestId,
|
||||
Protocol.Network.RequestWillBeSentEvent
|
||||
>();
|
||||
#requestPausedMap = new Map<
|
||||
NetworkRequestId,
|
||||
Protocol.Fetch.RequestPausedEvent
|
||||
>();
|
||||
#httpRequestsMap = new Map<NetworkRequestId, CdpHTTPRequest>();
|
||||
#requestWillBeSentExtraInfoMap = new Map<
|
||||
NetworkRequestId,
|
||||
Protocol.Network.RequestWillBeSentExtraInfoEvent[]
|
||||
>();
|
||||
/*
|
||||
* The below maps are used to reconcile Network.responseReceivedExtraInfo
|
||||
* events with their corresponding request. Each response and redirect
|
||||
* response gets an ExtraInfo event, and we don't know which will come first.
|
||||
* This means that we have to store a Response or an ExtraInfo for each
|
||||
* response, and emit the event when we get both of them. In addition, to
|
||||
* handle redirects, we have to make them Arrays to represent the chain of
|
||||
* events.
|
||||
*/
|
||||
#responseReceivedExtraInfoMap = new Map<
|
||||
NetworkRequestId,
|
||||
Protocol.Network.ResponseReceivedExtraInfoEvent[]
|
||||
>();
|
||||
#queuedRedirectInfoMap = new Map<NetworkRequestId, RedirectInfoList>();
|
||||
#queuedEventGroupMap = new Map<NetworkRequestId, QueuedEventGroup>();
|
||||
|
||||
forget(networkRequestId: NetworkRequestId): void {
|
||||
this.#requestWillBeSentMap.delete(networkRequestId);
|
||||
this.#requestPausedMap.delete(networkRequestId);
|
||||
this.#requestWillBeSentExtraInfoMap.delete(networkRequestId);
|
||||
this.#queuedEventGroupMap.delete(networkRequestId);
|
||||
this.#queuedRedirectInfoMap.delete(networkRequestId);
|
||||
this.#responseReceivedExtraInfoMap.delete(networkRequestId);
|
||||
}
|
||||
|
||||
requestExtraInfo(
|
||||
networkRequestId: NetworkRequestId,
|
||||
): Protocol.Network.RequestWillBeSentExtraInfoEvent[] {
|
||||
if (!this.#requestWillBeSentExtraInfoMap.has(networkRequestId)) {
|
||||
this.#requestWillBeSentExtraInfoMap.set(networkRequestId, []);
|
||||
}
|
||||
return this.#requestWillBeSentExtraInfoMap.get(
|
||||
networkRequestId,
|
||||
) as Protocol.Network.RequestWillBeSentExtraInfoEvent[];
|
||||
}
|
||||
|
||||
responseExtraInfo(
|
||||
networkRequestId: NetworkRequestId,
|
||||
): Protocol.Network.ResponseReceivedExtraInfoEvent[] {
|
||||
if (!this.#responseReceivedExtraInfoMap.has(networkRequestId)) {
|
||||
this.#responseReceivedExtraInfoMap.set(networkRequestId, []);
|
||||
}
|
||||
return this.#responseReceivedExtraInfoMap.get(
|
||||
networkRequestId,
|
||||
) as Protocol.Network.ResponseReceivedExtraInfoEvent[];
|
||||
}
|
||||
|
||||
private queuedRedirectInfo(fetchRequestId: FetchRequestId): RedirectInfoList {
|
||||
if (!this.#queuedRedirectInfoMap.has(fetchRequestId)) {
|
||||
this.#queuedRedirectInfoMap.set(fetchRequestId, []);
|
||||
}
|
||||
return this.#queuedRedirectInfoMap.get(fetchRequestId) as RedirectInfoList;
|
||||
}
|
||||
|
||||
queueRedirectInfo(
|
||||
fetchRequestId: FetchRequestId,
|
||||
redirectInfo: RedirectInfo,
|
||||
): void {
|
||||
this.queuedRedirectInfo(fetchRequestId).push(redirectInfo);
|
||||
}
|
||||
|
||||
takeQueuedRedirectInfo(
|
||||
fetchRequestId: FetchRequestId,
|
||||
): RedirectInfo | undefined {
|
||||
return this.queuedRedirectInfo(fetchRequestId).shift();
|
||||
}
|
||||
|
||||
inFlightRequestsCount(): number {
|
||||
let inFlightRequestCounter = 0;
|
||||
for (const request of this.#httpRequestsMap.values()) {
|
||||
if (!request.response()) {
|
||||
inFlightRequestCounter++;
|
||||
}
|
||||
}
|
||||
return inFlightRequestCounter;
|
||||
}
|
||||
|
||||
storeRequestWillBeSent(
|
||||
networkRequestId: NetworkRequestId,
|
||||
event: Protocol.Network.RequestWillBeSentEvent,
|
||||
): void {
|
||||
this.#requestWillBeSentMap.set(networkRequestId, event);
|
||||
}
|
||||
|
||||
getRequestWillBeSent(
|
||||
networkRequestId: NetworkRequestId,
|
||||
): Protocol.Network.RequestWillBeSentEvent | undefined {
|
||||
return this.#requestWillBeSentMap.get(networkRequestId);
|
||||
}
|
||||
|
||||
forgetRequestWillBeSent(networkRequestId: NetworkRequestId): void {
|
||||
this.#requestWillBeSentMap.delete(networkRequestId);
|
||||
}
|
||||
|
||||
getRequestPaused(
|
||||
networkRequestId: NetworkRequestId,
|
||||
): Protocol.Fetch.RequestPausedEvent | undefined {
|
||||
return this.#requestPausedMap.get(networkRequestId);
|
||||
}
|
||||
|
||||
forgetRequestPaused(networkRequestId: NetworkRequestId): void {
|
||||
this.#requestPausedMap.delete(networkRequestId);
|
||||
}
|
||||
|
||||
storeRequestPaused(
|
||||
networkRequestId: NetworkRequestId,
|
||||
event: Protocol.Fetch.RequestPausedEvent,
|
||||
): void {
|
||||
this.#requestPausedMap.set(networkRequestId, event);
|
||||
}
|
||||
|
||||
getRequest(networkRequestId: NetworkRequestId): CdpHTTPRequest | undefined {
|
||||
return this.#httpRequestsMap.get(networkRequestId);
|
||||
}
|
||||
|
||||
storeRequest(
|
||||
networkRequestId: NetworkRequestId,
|
||||
request: CdpHTTPRequest,
|
||||
): void {
|
||||
this.#httpRequestsMap.set(networkRequestId, request);
|
||||
}
|
||||
|
||||
forgetRequest(networkRequestId: NetworkRequestId): void {
|
||||
this.#httpRequestsMap.delete(networkRequestId);
|
||||
}
|
||||
|
||||
getQueuedEventGroup(
|
||||
networkRequestId: NetworkRequestId,
|
||||
): QueuedEventGroup | undefined {
|
||||
return this.#queuedEventGroupMap.get(networkRequestId);
|
||||
}
|
||||
|
||||
queueEventGroup(
|
||||
networkRequestId: NetworkRequestId,
|
||||
event: QueuedEventGroup,
|
||||
): void {
|
||||
this.#queuedEventGroupMap.set(networkRequestId, event);
|
||||
}
|
||||
|
||||
forgetQueuedEventGroup(networkRequestId: NetworkRequestId): void {
|
||||
this.#queuedEventGroupMap.delete(networkRequestId);
|
||||
}
|
||||
|
||||
printState(): void {
|
||||
function replacer(_key: unknown, value: unknown) {
|
||||
if (value instanceof Map) {
|
||||
return {
|
||||
dataType: 'Map',
|
||||
value: Array.from(value.entries()), // or with spread: value: [...value]
|
||||
};
|
||||
} else if (value instanceof CdpHTTPRequest) {
|
||||
return {
|
||||
dataType: 'CdpHTTPRequest',
|
||||
value: `${value.id}: ${value.url()}`,
|
||||
};
|
||||
}
|
||||
{
|
||||
return value;
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
'httpRequestsMap',
|
||||
JSON.stringify(this.#httpRequestsMap, replacer, 2),
|
||||
);
|
||||
console.log(
|
||||
'requestWillBeSentMap',
|
||||
JSON.stringify(this.#requestWillBeSentMap, replacer, 2),
|
||||
);
|
||||
console.log(
|
||||
'requestWillBeSentMap',
|
||||
JSON.stringify(this.#responseReceivedExtraInfoMap, replacer, 2),
|
||||
);
|
||||
console.log(
|
||||
'requestWillBeSentMap',
|
||||
JSON.stringify(this.#requestPausedMap, replacer, 2),
|
||||
);
|
||||
}
|
||||
}
|
||||
834
node_modules/puppeteer-core/src/cdp/NetworkManager.ts
generated
vendored
Normal file
834
node_modules/puppeteer-core/src/cdp/NetworkManager.ts
generated
vendored
Normal file
@@ -0,0 +1,834 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2017 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type {Protocol} from 'devtools-protocol';
|
||||
|
||||
import {CDPSessionEvent, type CDPSession} from '../api/CDPSession.js';
|
||||
import type {Frame} from '../api/Frame.js';
|
||||
import type {Credentials} from '../api/Page.js';
|
||||
import {EventEmitter} from '../common/EventEmitter.js';
|
||||
import {
|
||||
NetworkManagerEvent,
|
||||
type NetworkManagerEvents,
|
||||
} from '../common/NetworkManagerEvents.js';
|
||||
import {debugError, isString} from '../common/util.js';
|
||||
import {assert} from '../util/assert.js';
|
||||
import {DisposableStack} from '../util/disposable.js';
|
||||
import {isErrorLike} from '../util/ErrorLike.js';
|
||||
|
||||
import {isTargetClosedError} from './Connection.js';
|
||||
import {CdpHTTPRequest} from './HTTPRequest.js';
|
||||
import {CdpHTTPResponse} from './HTTPResponse.js';
|
||||
import {
|
||||
NetworkEventManager,
|
||||
type FetchRequestId,
|
||||
} from './NetworkEventManager.js';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface NetworkConditions {
|
||||
/**
|
||||
* Emulates the offline mode.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* Shortcut for {@link Page.setOfflineMode}.
|
||||
*/
|
||||
offline?: boolean;
|
||||
/**
|
||||
* Download speed (bytes/s)
|
||||
*/
|
||||
download: number;
|
||||
/**
|
||||
* Upload speed (bytes/s)
|
||||
*/
|
||||
upload: number;
|
||||
/**
|
||||
* Latency (ms)
|
||||
*/
|
||||
latency: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface InternalNetworkConditions extends NetworkConditions {
|
||||
offline: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export interface FrameProvider {
|
||||
frame(id: string): Frame | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export class NetworkManager extends EventEmitter<NetworkManagerEvents> {
|
||||
#frameManager: FrameProvider;
|
||||
#networkEventManager = new NetworkEventManager();
|
||||
#extraHTTPHeaders?: Record<string, string>;
|
||||
#credentials: Credentials | null = null;
|
||||
#attemptedAuthentications = new Set<string>();
|
||||
#userRequestInterceptionEnabled = false;
|
||||
#protocolRequestInterceptionEnabled?: boolean;
|
||||
#userCacheDisabled?: boolean;
|
||||
#emulatedNetworkConditions?: InternalNetworkConditions;
|
||||
#userAgent?: string;
|
||||
#userAgentMetadata?: Protocol.Emulation.UserAgentMetadata;
|
||||
#platform?: string;
|
||||
|
||||
readonly #handlers = [
|
||||
['Fetch.requestPaused', this.#onRequestPaused],
|
||||
['Fetch.authRequired', this.#onAuthRequired],
|
||||
['Network.requestWillBeSent', this.#onRequestWillBeSent],
|
||||
['Network.requestWillBeSentExtraInfo', this.#onRequestWillBeSentExtraInfo],
|
||||
['Network.requestServedFromCache', this.#onRequestServedFromCache],
|
||||
['Network.responseReceived', this.#onResponseReceived],
|
||||
['Network.loadingFinished', this.#onLoadingFinished],
|
||||
['Network.loadingFailed', this.#onLoadingFailed],
|
||||
['Network.responseReceivedExtraInfo', this.#onResponseReceivedExtraInfo],
|
||||
[CDPSessionEvent.Disconnected, this.#removeClient],
|
||||
] as const;
|
||||
|
||||
#clients = new Map<CDPSession, DisposableStack>();
|
||||
#networkEnabled = true;
|
||||
|
||||
constructor(frameManager: FrameProvider, networkEnabled?: boolean) {
|
||||
super();
|
||||
this.#frameManager = frameManager;
|
||||
this.#networkEnabled = networkEnabled ?? true;
|
||||
}
|
||||
|
||||
#canIgnoreError(error: unknown) {
|
||||
return (
|
||||
isErrorLike(error) &&
|
||||
(isTargetClosedError(error) ||
|
||||
error.message.includes('Not supported') ||
|
||||
error.message.includes("wasn't found"))
|
||||
);
|
||||
}
|
||||
|
||||
async addClient(client: CDPSession): Promise<void> {
|
||||
if (!this.#networkEnabled || this.#clients.has(client)) {
|
||||
return;
|
||||
}
|
||||
const subscriptions = new DisposableStack();
|
||||
this.#clients.set(client, subscriptions);
|
||||
const clientEmitter = subscriptions.use(new EventEmitter(client));
|
||||
|
||||
for (const [event, handler] of this.#handlers) {
|
||||
clientEmitter.on(event, (arg: any) => {
|
||||
return handler.bind(this)(client, arg);
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
client.send('Network.enable'),
|
||||
this.#applyExtraHTTPHeaders(client),
|
||||
this.#applyNetworkConditions(client),
|
||||
this.#applyProtocolCacheDisabled(client),
|
||||
this.#applyProtocolRequestInterception(client),
|
||||
this.#applyUserAgent(client),
|
||||
]);
|
||||
} catch (error) {
|
||||
if (this.#canIgnoreError(error)) {
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async #removeClient(client: CDPSession) {
|
||||
this.#clients.get(client)?.dispose();
|
||||
this.#clients.delete(client);
|
||||
}
|
||||
|
||||
async authenticate(credentials: Credentials | null): Promise<void> {
|
||||
this.#credentials = credentials;
|
||||
const enabled = this.#userRequestInterceptionEnabled || !!this.#credentials;
|
||||
if (enabled === this.#protocolRequestInterceptionEnabled) {
|
||||
return;
|
||||
}
|
||||
this.#protocolRequestInterceptionEnabled = enabled;
|
||||
await this.#applyToAllClients(
|
||||
this.#applyProtocolRequestInterception.bind(this),
|
||||
);
|
||||
}
|
||||
|
||||
async setExtraHTTPHeaders(headers: Record<string, string>): Promise<void> {
|
||||
const extraHTTPHeaders: Record<string, string> = {};
|
||||
for (const [key, value] of Object.entries(headers)) {
|
||||
assert(
|
||||
isString(value),
|
||||
`Expected value of header "${key}" to be String, but "${typeof value}" is found.`,
|
||||
);
|
||||
extraHTTPHeaders[key.toLowerCase()] = value;
|
||||
}
|
||||
this.#extraHTTPHeaders = extraHTTPHeaders;
|
||||
|
||||
await this.#applyToAllClients(this.#applyExtraHTTPHeaders.bind(this));
|
||||
}
|
||||
|
||||
async #applyExtraHTTPHeaders(client: CDPSession) {
|
||||
if (this.#extraHTTPHeaders === undefined) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await client.send('Network.setExtraHTTPHeaders', {
|
||||
headers: this.#extraHTTPHeaders,
|
||||
});
|
||||
} catch (error) {
|
||||
if (this.#canIgnoreError(error)) {
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
extraHTTPHeaders(): Record<string, string> {
|
||||
return Object.assign({}, this.#extraHTTPHeaders);
|
||||
}
|
||||
|
||||
inFlightRequestsCount(): number {
|
||||
return this.#networkEventManager.inFlightRequestsCount();
|
||||
}
|
||||
|
||||
async setOfflineMode(value: boolean): Promise<void> {
|
||||
if (!this.#emulatedNetworkConditions) {
|
||||
this.#emulatedNetworkConditions = {
|
||||
offline: false,
|
||||
upload: -1,
|
||||
download: -1,
|
||||
latency: 0,
|
||||
};
|
||||
}
|
||||
this.#emulatedNetworkConditions.offline = value;
|
||||
await this.#applyToAllClients(this.#applyNetworkConditions.bind(this));
|
||||
}
|
||||
|
||||
async emulateNetworkConditions(
|
||||
networkConditions: NetworkConditions | null,
|
||||
): Promise<void> {
|
||||
if (!this.#emulatedNetworkConditions) {
|
||||
this.#emulatedNetworkConditions = {
|
||||
offline: networkConditions?.offline ?? false,
|
||||
upload: -1,
|
||||
download: -1,
|
||||
latency: 0,
|
||||
};
|
||||
}
|
||||
this.#emulatedNetworkConditions.upload = networkConditions
|
||||
? networkConditions.upload
|
||||
: -1;
|
||||
this.#emulatedNetworkConditions.download = networkConditions
|
||||
? networkConditions.download
|
||||
: -1;
|
||||
this.#emulatedNetworkConditions.latency = networkConditions
|
||||
? networkConditions.latency
|
||||
: 0;
|
||||
this.#emulatedNetworkConditions.offline =
|
||||
networkConditions?.offline ?? false;
|
||||
await this.#applyToAllClients(this.#applyNetworkConditions.bind(this));
|
||||
}
|
||||
|
||||
async #applyToAllClients(fn: (client: CDPSession) => Promise<unknown>) {
|
||||
await Promise.all(
|
||||
Array.from(this.#clients.keys()).map(client => {
|
||||
return fn(client);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async #applyNetworkConditions(client: CDPSession): Promise<void> {
|
||||
if (this.#emulatedNetworkConditions === undefined) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await client.send('Network.emulateNetworkConditions', {
|
||||
offline: this.#emulatedNetworkConditions.offline,
|
||||
latency: this.#emulatedNetworkConditions.latency,
|
||||
uploadThroughput: this.#emulatedNetworkConditions.upload,
|
||||
downloadThroughput: this.#emulatedNetworkConditions.download,
|
||||
});
|
||||
} catch (error) {
|
||||
if (this.#canIgnoreError(error)) {
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async setUserAgent(
|
||||
userAgent: string,
|
||||
userAgentMetadata?: Protocol.Emulation.UserAgentMetadata,
|
||||
platform?: string,
|
||||
): Promise<void> {
|
||||
this.#userAgent = userAgent;
|
||||
this.#userAgentMetadata = userAgentMetadata;
|
||||
this.#platform = platform;
|
||||
await this.#applyToAllClients(this.#applyUserAgent.bind(this));
|
||||
}
|
||||
|
||||
async #applyUserAgent(client: CDPSession) {
|
||||
if (this.#userAgent === undefined) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await client.send('Network.setUserAgentOverride', {
|
||||
userAgent: this.#userAgent,
|
||||
userAgentMetadata: this.#userAgentMetadata,
|
||||
platform: this.#platform,
|
||||
});
|
||||
} catch (error) {
|
||||
if (this.#canIgnoreError(error)) {
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async setCacheEnabled(enabled: boolean): Promise<void> {
|
||||
this.#userCacheDisabled = !enabled;
|
||||
await this.#applyToAllClients(this.#applyProtocolCacheDisabled.bind(this));
|
||||
}
|
||||
|
||||
async setRequestInterception(value: boolean): Promise<void> {
|
||||
this.#userRequestInterceptionEnabled = value;
|
||||
const enabled = this.#userRequestInterceptionEnabled || !!this.#credentials;
|
||||
if (enabled === this.#protocolRequestInterceptionEnabled) {
|
||||
return;
|
||||
}
|
||||
this.#protocolRequestInterceptionEnabled = enabled;
|
||||
await this.#applyToAllClients(
|
||||
this.#applyProtocolRequestInterception.bind(this),
|
||||
);
|
||||
}
|
||||
|
||||
async #applyProtocolRequestInterception(client: CDPSession): Promise<void> {
|
||||
if (this.#protocolRequestInterceptionEnabled === undefined) {
|
||||
return;
|
||||
}
|
||||
if (this.#userCacheDisabled === undefined) {
|
||||
this.#userCacheDisabled = false;
|
||||
}
|
||||
try {
|
||||
if (this.#protocolRequestInterceptionEnabled) {
|
||||
await Promise.all([
|
||||
this.#applyProtocolCacheDisabled(client),
|
||||
client.send('Fetch.enable', {
|
||||
handleAuthRequests: true,
|
||||
patterns: [{urlPattern: '*'}],
|
||||
}),
|
||||
]);
|
||||
} else {
|
||||
await Promise.all([
|
||||
this.#applyProtocolCacheDisabled(client),
|
||||
client.send('Fetch.disable'),
|
||||
]);
|
||||
}
|
||||
} catch (error) {
|
||||
if (this.#canIgnoreError(error)) {
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async #applyProtocolCacheDisabled(client: CDPSession): Promise<void> {
|
||||
if (this.#userCacheDisabled === undefined) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await client.send('Network.setCacheDisabled', {
|
||||
cacheDisabled: this.#userCacheDisabled,
|
||||
});
|
||||
} catch (error) {
|
||||
if (this.#canIgnoreError(error)) {
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
#onRequestWillBeSent(
|
||||
client: CDPSession,
|
||||
event: Protocol.Network.RequestWillBeSentEvent,
|
||||
): void {
|
||||
// Request interception doesn't happen for data URLs with Network Service.
|
||||
if (
|
||||
this.#userRequestInterceptionEnabled &&
|
||||
!event.request.url.startsWith('data:')
|
||||
) {
|
||||
const {requestId: networkRequestId} = event;
|
||||
|
||||
this.#networkEventManager.storeRequestWillBeSent(networkRequestId, event);
|
||||
|
||||
/**
|
||||
* CDP may have sent a Fetch.requestPaused event already. Check for it.
|
||||
*/
|
||||
const requestPausedEvent =
|
||||
this.#networkEventManager.getRequestPaused(networkRequestId);
|
||||
if (requestPausedEvent) {
|
||||
const {requestId: fetchRequestId} = requestPausedEvent;
|
||||
this.#patchRequestEventHeaders(event, requestPausedEvent);
|
||||
this.#onRequest(client, event, fetchRequestId);
|
||||
this.#networkEventManager.forgetRequestPaused(networkRequestId);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
this.#onRequest(client, event, undefined);
|
||||
}
|
||||
|
||||
#onAuthRequired(
|
||||
client: CDPSession,
|
||||
event: Protocol.Fetch.AuthRequiredEvent,
|
||||
): void {
|
||||
let response: Protocol.Fetch.AuthChallengeResponse['response'] = 'Default';
|
||||
if (this.#attemptedAuthentications.has(event.requestId)) {
|
||||
response = 'CancelAuth';
|
||||
} else if (this.#credentials) {
|
||||
response = 'ProvideCredentials';
|
||||
this.#attemptedAuthentications.add(event.requestId);
|
||||
}
|
||||
const {username, password} = this.#credentials || {
|
||||
username: undefined,
|
||||
password: undefined,
|
||||
};
|
||||
client
|
||||
.send('Fetch.continueWithAuth', {
|
||||
requestId: event.requestId,
|
||||
authChallengeResponse: {response, username, password},
|
||||
})
|
||||
.catch(debugError);
|
||||
}
|
||||
|
||||
/**
|
||||
* CDP may send a Fetch.requestPaused without or before a
|
||||
* Network.requestWillBeSent
|
||||
*
|
||||
* CDP may send multiple Fetch.requestPaused
|
||||
* for the same Network.requestWillBeSent.
|
||||
*/
|
||||
#onRequestPaused(
|
||||
client: CDPSession,
|
||||
event: Protocol.Fetch.RequestPausedEvent,
|
||||
): void {
|
||||
if (
|
||||
!this.#userRequestInterceptionEnabled &&
|
||||
this.#protocolRequestInterceptionEnabled
|
||||
) {
|
||||
client
|
||||
.send('Fetch.continueRequest', {
|
||||
requestId: event.requestId,
|
||||
})
|
||||
.catch(debugError);
|
||||
}
|
||||
|
||||
const {networkId: networkRequestId, requestId: fetchRequestId} = event;
|
||||
|
||||
if (!networkRequestId) {
|
||||
this.#onRequestWithoutNetworkInstrumentation(client, event);
|
||||
return;
|
||||
}
|
||||
|
||||
const requestWillBeSentEvent = (() => {
|
||||
const requestWillBeSentEvent =
|
||||
this.#networkEventManager.getRequestWillBeSent(networkRequestId);
|
||||
|
||||
// redirect requests have the same `requestId`,
|
||||
if (
|
||||
requestWillBeSentEvent &&
|
||||
(requestWillBeSentEvent.request.url !== event.request.url ||
|
||||
requestWillBeSentEvent.request.method !== event.request.method)
|
||||
) {
|
||||
this.#networkEventManager.forgetRequestWillBeSent(networkRequestId);
|
||||
return;
|
||||
}
|
||||
return requestWillBeSentEvent;
|
||||
})();
|
||||
|
||||
if (requestWillBeSentEvent) {
|
||||
this.#patchRequestEventHeaders(requestWillBeSentEvent, event);
|
||||
this.#onRequest(client, requestWillBeSentEvent, fetchRequestId);
|
||||
} else {
|
||||
this.#networkEventManager.storeRequestPaused(networkRequestId, event);
|
||||
}
|
||||
}
|
||||
|
||||
#patchRequestEventHeaders(
|
||||
requestWillBeSentEvent: Protocol.Network.RequestWillBeSentEvent,
|
||||
requestPausedEvent: Protocol.Fetch.RequestPausedEvent,
|
||||
): void {
|
||||
requestWillBeSentEvent.request.headers = {
|
||||
...requestWillBeSentEvent.request.headers,
|
||||
// includes extra headers, like: Accept, Origin
|
||||
...requestPausedEvent.request.headers,
|
||||
};
|
||||
}
|
||||
|
||||
#onRequestWithoutNetworkInstrumentation(
|
||||
client: CDPSession,
|
||||
event: Protocol.Fetch.RequestPausedEvent,
|
||||
): void {
|
||||
// If an event has no networkId it should not have any network events. We
|
||||
// still want to dispatch it for the interception by the user.
|
||||
const frame = event.frameId
|
||||
? this.#frameManager.frame(event.frameId)
|
||||
: null;
|
||||
|
||||
const request = new CdpHTTPRequest(
|
||||
client,
|
||||
frame,
|
||||
event.requestId,
|
||||
this.#userRequestInterceptionEnabled,
|
||||
event,
|
||||
[],
|
||||
);
|
||||
this.emit(NetworkManagerEvent.Request, request);
|
||||
void request.finalizeInterceptions();
|
||||
}
|
||||
|
||||
#onRequest(
|
||||
client: CDPSession,
|
||||
event: Protocol.Network.RequestWillBeSentEvent,
|
||||
fetchRequestId?: FetchRequestId,
|
||||
fromMemoryCache = false,
|
||||
): void {
|
||||
let redirectChain: CdpHTTPRequest[] = [];
|
||||
if (event.redirectResponse) {
|
||||
// We want to emit a response and requestfinished for the
|
||||
// redirectResponse, but we can't do so unless we have a
|
||||
// responseExtraInfo ready to pair it up with. If we don't have any
|
||||
// responseExtraInfos saved in our queue, they we have to wait until
|
||||
// the next one to emit response and requestfinished, *and* we should
|
||||
// also wait to emit this Request too because it should come after the
|
||||
// response/requestfinished.
|
||||
let redirectResponseExtraInfo = null;
|
||||
if (event.redirectHasExtraInfo) {
|
||||
redirectResponseExtraInfo = this.#networkEventManager
|
||||
.responseExtraInfo(event.requestId)
|
||||
.shift();
|
||||
if (!redirectResponseExtraInfo) {
|
||||
this.#networkEventManager.queueRedirectInfo(event.requestId, {
|
||||
event,
|
||||
fetchRequestId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const request = this.#networkEventManager.getRequest(event.requestId);
|
||||
// If we connect late to the target, we could have missed the
|
||||
// requestWillBeSent event.
|
||||
if (request) {
|
||||
this.#handleRequestRedirect(
|
||||
client,
|
||||
request,
|
||||
event.redirectResponse,
|
||||
redirectResponseExtraInfo,
|
||||
);
|
||||
redirectChain = request._redirectChain;
|
||||
|
||||
const extraInfo = this.#networkEventManager
|
||||
.requestExtraInfo(event.requestId)
|
||||
.shift();
|
||||
if (extraInfo) {
|
||||
request.updateHeaders(extraInfo.headers);
|
||||
}
|
||||
}
|
||||
}
|
||||
const frame = event.frameId
|
||||
? this.#frameManager.frame(event.frameId)
|
||||
: null;
|
||||
|
||||
const request = new CdpHTTPRequest(
|
||||
client,
|
||||
frame,
|
||||
fetchRequestId,
|
||||
this.#userRequestInterceptionEnabled,
|
||||
event,
|
||||
redirectChain,
|
||||
);
|
||||
|
||||
const extraInfo = this.#networkEventManager
|
||||
.requestExtraInfo(event.requestId)
|
||||
.shift();
|
||||
if (extraInfo) {
|
||||
request.updateHeaders(extraInfo.headers);
|
||||
}
|
||||
|
||||
request._fromMemoryCache = fromMemoryCache;
|
||||
this.#networkEventManager.storeRequest(event.requestId, request);
|
||||
this.emit(NetworkManagerEvent.Request, request);
|
||||
void request.finalizeInterceptions();
|
||||
}
|
||||
|
||||
#onRequestWillBeSentExtraInfo(
|
||||
_client: CDPSession,
|
||||
event: Protocol.Network.RequestWillBeSentExtraInfoEvent,
|
||||
): void {
|
||||
const request = this.#networkEventManager.getRequest(event.requestId);
|
||||
if (request) {
|
||||
request.updateHeaders(event.headers);
|
||||
} else {
|
||||
this.#networkEventManager.requestExtraInfo(event.requestId).push(event);
|
||||
}
|
||||
}
|
||||
|
||||
#onRequestServedFromCache(
|
||||
client: CDPSession,
|
||||
event: Protocol.Network.RequestServedFromCacheEvent,
|
||||
): void {
|
||||
const requestWillBeSentEvent =
|
||||
this.#networkEventManager.getRequestWillBeSent(event.requestId);
|
||||
let request = this.#networkEventManager.getRequest(event.requestId);
|
||||
// Requests served from memory cannot be intercepted.
|
||||
if (request) {
|
||||
request._fromMemoryCache = true;
|
||||
}
|
||||
// If request ended up being served from cache, we need to convert
|
||||
// requestWillBeSentEvent to a HTTP request.
|
||||
if (!request && requestWillBeSentEvent) {
|
||||
this.#onRequest(client, requestWillBeSentEvent, undefined, true);
|
||||
request = this.#networkEventManager.getRequest(event.requestId);
|
||||
}
|
||||
if (!request) {
|
||||
debugError(
|
||||
new Error(
|
||||
`Request ${event.requestId} was served from cache but we could not find the corresponding request object`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.emit(NetworkManagerEvent.RequestServedFromCache, request);
|
||||
}
|
||||
|
||||
#handleRequestRedirect(
|
||||
_client: CDPSession,
|
||||
request: CdpHTTPRequest,
|
||||
responsePayload: Protocol.Network.Response,
|
||||
extraInfo: Protocol.Network.ResponseReceivedExtraInfoEvent | null,
|
||||
): void {
|
||||
const response = new CdpHTTPResponse(request, responsePayload, extraInfo);
|
||||
request._response = response;
|
||||
request._redirectChain.push(request);
|
||||
response._resolveBody(
|
||||
new Error('Response body is unavailable for redirect responses'),
|
||||
);
|
||||
this.#forgetRequest(request, false);
|
||||
this.emit(NetworkManagerEvent.Response, response);
|
||||
this.emit(NetworkManagerEvent.RequestFinished, request);
|
||||
}
|
||||
|
||||
#emitResponseEvent(
|
||||
_client: CDPSession,
|
||||
responseReceived: Protocol.Network.ResponseReceivedEvent,
|
||||
extraInfo: Protocol.Network.ResponseReceivedExtraInfoEvent | null,
|
||||
): void {
|
||||
const request = this.#networkEventManager.getRequest(
|
||||
responseReceived.requestId,
|
||||
);
|
||||
// FileUpload sends a response without a matching request.
|
||||
if (!request) {
|
||||
return;
|
||||
}
|
||||
|
||||
const extraInfos = this.#networkEventManager.responseExtraInfo(
|
||||
responseReceived.requestId,
|
||||
);
|
||||
if (extraInfos.length) {
|
||||
debugError(
|
||||
new Error(
|
||||
'Unexpected extraInfo events for request ' +
|
||||
responseReceived.requestId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Chromium sends wrong extraInfo events for responses served from cache.
|
||||
// See https://github.com/puppeteer/puppeteer/issues/9965 and
|
||||
// https://crbug.com/1340398.
|
||||
if (responseReceived.response.fromDiskCache) {
|
||||
extraInfo = null;
|
||||
}
|
||||
|
||||
const response = new CdpHTTPResponse(
|
||||
request,
|
||||
responseReceived.response,
|
||||
extraInfo,
|
||||
);
|
||||
request._response = response;
|
||||
this.emit(NetworkManagerEvent.Response, response);
|
||||
}
|
||||
|
||||
#onResponseReceived(
|
||||
client: CDPSession,
|
||||
event: Protocol.Network.ResponseReceivedEvent,
|
||||
): void {
|
||||
const request = this.#networkEventManager.getRequest(event.requestId);
|
||||
let extraInfo = null;
|
||||
if (request && !request._fromMemoryCache && event.hasExtraInfo) {
|
||||
extraInfo = this.#networkEventManager
|
||||
.responseExtraInfo(event.requestId)
|
||||
.shift();
|
||||
if (!extraInfo) {
|
||||
// Wait until we get the corresponding ExtraInfo event.
|
||||
this.#networkEventManager.queueEventGroup(event.requestId, {
|
||||
responseReceivedEvent: event,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.#emitResponseEvent(client, event, extraInfo);
|
||||
}
|
||||
|
||||
#onResponseReceivedExtraInfo(
|
||||
client: CDPSession,
|
||||
event: Protocol.Network.ResponseReceivedExtraInfoEvent,
|
||||
): void {
|
||||
// We may have skipped a redirect response/request pair due to waiting for
|
||||
// this ExtraInfo event. If so, continue that work now that we have the
|
||||
// request.
|
||||
const redirectInfo = this.#networkEventManager.takeQueuedRedirectInfo(
|
||||
event.requestId,
|
||||
);
|
||||
if (redirectInfo) {
|
||||
this.#networkEventManager.responseExtraInfo(event.requestId).push(event);
|
||||
this.#onRequest(client, redirectInfo.event, redirectInfo.fetchRequestId);
|
||||
return;
|
||||
}
|
||||
|
||||
// We may have skipped response and loading events because we didn't have
|
||||
// this ExtraInfo event yet. If so, emit those events now.
|
||||
const queuedEvents = this.#networkEventManager.getQueuedEventGroup(
|
||||
event.requestId,
|
||||
);
|
||||
if (queuedEvents) {
|
||||
this.#networkEventManager.forgetQueuedEventGroup(event.requestId);
|
||||
this.#emitResponseEvent(
|
||||
client,
|
||||
queuedEvents.responseReceivedEvent,
|
||||
event,
|
||||
);
|
||||
if (queuedEvents.loadingFinishedEvent) {
|
||||
this.#emitLoadingFinished(client, queuedEvents.loadingFinishedEvent);
|
||||
}
|
||||
if (queuedEvents.loadingFailedEvent) {
|
||||
this.#emitLoadingFailed(client, queuedEvents.loadingFailedEvent);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait until we get another event that can use this ExtraInfo event.
|
||||
this.#networkEventManager.responseExtraInfo(event.requestId).push(event);
|
||||
}
|
||||
|
||||
#forgetRequest(request: CdpHTTPRequest, events: boolean): void {
|
||||
const requestId = request.id;
|
||||
const interceptionId = request._interceptionId;
|
||||
|
||||
this.#networkEventManager.forgetRequest(requestId);
|
||||
if (interceptionId !== undefined) {
|
||||
this.#attemptedAuthentications.delete(interceptionId);
|
||||
}
|
||||
|
||||
if (events) {
|
||||
this.#networkEventManager.forget(requestId);
|
||||
}
|
||||
}
|
||||
|
||||
#onLoadingFinished(
|
||||
client: CDPSession,
|
||||
event: Protocol.Network.LoadingFinishedEvent,
|
||||
): void {
|
||||
// If the response event for this request is still waiting on a
|
||||
// corresponding ExtraInfo event, then wait to emit this event too.
|
||||
const queuedEvents = this.#networkEventManager.getQueuedEventGroup(
|
||||
event.requestId,
|
||||
);
|
||||
if (queuedEvents) {
|
||||
queuedEvents.loadingFinishedEvent = event;
|
||||
} else {
|
||||
this.#emitLoadingFinished(client, event);
|
||||
}
|
||||
}
|
||||
|
||||
#emitLoadingFinished(
|
||||
client: CDPSession,
|
||||
event: Protocol.Network.LoadingFinishedEvent,
|
||||
): void {
|
||||
const request = this.#networkEventManager.getRequest(event.requestId);
|
||||
// For certain requestIds we never receive requestWillBeSent event.
|
||||
// @see https://crbug.com/750469
|
||||
if (!request) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#adoptCdpSessionIfNeeded(client, request);
|
||||
|
||||
// Under certain conditions we never get the Network.responseReceived
|
||||
// event from protocol. @see https://crbug.com/883475
|
||||
if (request.response()) {
|
||||
request.response()?._resolveBody();
|
||||
}
|
||||
this.#forgetRequest(request, true);
|
||||
this.emit(NetworkManagerEvent.RequestFinished, request);
|
||||
}
|
||||
|
||||
#onLoadingFailed(
|
||||
client: CDPSession,
|
||||
event: Protocol.Network.LoadingFailedEvent,
|
||||
): void {
|
||||
// If the response event for this request is still waiting on a
|
||||
// corresponding ExtraInfo event, then wait to emit this event too.
|
||||
const queuedEvents = this.#networkEventManager.getQueuedEventGroup(
|
||||
event.requestId,
|
||||
);
|
||||
if (queuedEvents) {
|
||||
queuedEvents.loadingFailedEvent = event;
|
||||
} else {
|
||||
this.#emitLoadingFailed(client, event);
|
||||
}
|
||||
}
|
||||
|
||||
#emitLoadingFailed(
|
||||
client: CDPSession,
|
||||
event: Protocol.Network.LoadingFailedEvent,
|
||||
): void {
|
||||
const request = this.#networkEventManager.getRequest(event.requestId);
|
||||
// For certain requestIds we never receive requestWillBeSent event.
|
||||
// @see https://crbug.com/750469
|
||||
if (!request) {
|
||||
return;
|
||||
}
|
||||
this.#adoptCdpSessionIfNeeded(client, request);
|
||||
request._failureText = event.errorText;
|
||||
const response = request.response();
|
||||
if (response) {
|
||||
response._resolveBody();
|
||||
}
|
||||
this.#forgetRequest(request, true);
|
||||
this.emit(NetworkManagerEvent.RequestFailed, request);
|
||||
}
|
||||
|
||||
#adoptCdpSessionIfNeeded(client: CDPSession, request: CdpHTTPRequest): void {
|
||||
// Document requests for OOPIFs start in the parent frame but are
|
||||
// adopted by their child frame, meaning their loadingFinished and
|
||||
// loadingFailed events are fired on the child session. In this case
|
||||
// we reassign the request CDPSession to ensure all subsequent
|
||||
// actions use the correct session (e.g. retrieving response body in
|
||||
// HTTPResponse). The same applies to main worker script requests.
|
||||
if (client !== request.client) {
|
||||
request.client = client;
|
||||
}
|
||||
}
|
||||
}
|
||||
1373
node_modules/puppeteer-core/src/cdp/Page.ts
generated
vendored
Normal file
1373
node_modules/puppeteer-core/src/cdp/Page.ts
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
71
node_modules/puppeteer-core/src/cdp/PredefinedNetworkConditions.ts
generated
vendored
Normal file
71
node_modules/puppeteer-core/src/cdp/PredefinedNetworkConditions.ts
generated
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2021 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type {NetworkConditions} from './NetworkManager.js';
|
||||
|
||||
/**
|
||||
* A list of pre-defined network conditions to be used with
|
||||
* {@link Page.emulateNetworkConditions}.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```ts
|
||||
* import {PredefinedNetworkConditions} from 'puppeteer';
|
||||
* const browser = await puppeteer.launch();
|
||||
* const page = await browser.newPage();
|
||||
* await page.emulateNetworkConditions(PredefinedNetworkConditions['Slow 3G']);
|
||||
* await page.goto('https://www.google.com');
|
||||
* await page.emulateNetworkConditions(PredefinedNetworkConditions['Fast 3G']);
|
||||
* await page.goto('https://www.google.com');
|
||||
* // alias to Fast 3G.
|
||||
* await page.emulateNetworkConditions(PredefinedNetworkConditions['Slow 4G']);
|
||||
* await page.goto('https://www.google.com');
|
||||
* await page.emulateNetworkConditions(PredefinedNetworkConditions['Fast 4G']);
|
||||
* await page.goto('https://www.google.com');
|
||||
* // other actions...
|
||||
* await browser.close();
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const PredefinedNetworkConditions = Object.freeze({
|
||||
// Generally aligned with DevTools
|
||||
// https://source.chromium.org/chromium/chromium/src/+/main:third_party/devtools-frontend/src/front_end/core/sdk/NetworkManager.ts;l=398;drc=225e1240f522ca684473f541ae6dae6cd766dd33.
|
||||
'Slow 3G': {
|
||||
// ~500Kbps down
|
||||
download: ((500 * 1000) / 8) * 0.8,
|
||||
// ~500Kbps up
|
||||
upload: ((500 * 1000) / 8) * 0.8,
|
||||
// 400ms RTT
|
||||
latency: 400 * 5,
|
||||
} as NetworkConditions,
|
||||
'Fast 3G': {
|
||||
// ~1.6 Mbps down
|
||||
download: ((1.6 * 1000 * 1000) / 8) * 0.9,
|
||||
// ~0.75 Mbps up
|
||||
upload: ((750 * 1000) / 8) * 0.9,
|
||||
// 150ms RTT
|
||||
latency: 150 * 3.75,
|
||||
} as NetworkConditions,
|
||||
// alias to Fast 3G to align with Lighthouse (crbug.com/342406608)
|
||||
// and DevTools (crbug.com/342406608),
|
||||
'Slow 4G': {
|
||||
// ~1.6 Mbps down
|
||||
download: ((1.6 * 1000 * 1000) / 8) * 0.9,
|
||||
// ~0.75 Mbps up
|
||||
upload: ((750 * 1000) / 8) * 0.9,
|
||||
// 150ms RTT
|
||||
latency: 150 * 3.75,
|
||||
} as NetworkConditions,
|
||||
'Fast 4G': {
|
||||
// 9 Mbps down
|
||||
download: ((9 * 1000 * 1000) / 8) * 0.9,
|
||||
// 1.5 Mbps up
|
||||
upload: ((1.5 * 1000 * 1000) / 8) * 0.9,
|
||||
// 60ms RTT
|
||||
latency: 60 * 2.75,
|
||||
} as NetworkConditions,
|
||||
});
|
||||
311
node_modules/puppeteer-core/src/cdp/Target.ts
generated
vendored
Normal file
311
node_modules/puppeteer-core/src/cdp/Target.ts
generated
vendored
Normal 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 {}
|
||||
38
node_modules/puppeteer-core/src/cdp/TargetManageEvents.ts
generated
vendored
Normal file
38
node_modules/puppeteer-core/src/cdp/TargetManageEvents.ts
generated
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2024 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type {Protocol} from 'devtools-protocol';
|
||||
|
||||
import type {EventType} from '../common/EventEmitter.js';
|
||||
|
||||
import type {CdpTarget} from './Target.js';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export const enum TargetManagerEvent {
|
||||
TargetDiscovered = 'targetDiscovered',
|
||||
TargetAvailable = 'targetAvailable',
|
||||
TargetGone = 'targetGone',
|
||||
/**
|
||||
* Emitted after a target has been initialized and whenever its URL changes.
|
||||
*/
|
||||
TargetChanged = 'targetChanged',
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export interface TargetManagerEvents extends Record<EventType, unknown> {
|
||||
[TargetManagerEvent.TargetAvailable]: CdpTarget;
|
||||
[TargetManagerEvent.TargetDiscovered]: Protocol.Target.TargetInfo;
|
||||
[TargetManagerEvent.TargetGone]: CdpTarget;
|
||||
[TargetManagerEvent.TargetChanged]: {
|
||||
target: CdpTarget;
|
||||
wasInitialized: true;
|
||||
previousURL: string;
|
||||
};
|
||||
}
|
||||
442
node_modules/puppeteer-core/src/cdp/TargetManager.ts
generated
vendored
Normal file
442
node_modules/puppeteer-core/src/cdp/TargetManager.ts
generated
vendored
Normal 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
140
node_modules/puppeteer-core/src/cdp/Tracing.ts
generated
vendored
Normal file
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2017 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
import type {CDPSession} from '../api/CDPSession.js';
|
||||
import {
|
||||
getReadableAsTypedArray,
|
||||
getReadableFromProtocolStream,
|
||||
} from '../common/util.js';
|
||||
import {assert} from '../util/assert.js';
|
||||
import {Deferred} from '../util/Deferred.js';
|
||||
import {isErrorLike} from '../util/ErrorLike.js';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface TracingOptions {
|
||||
path?: string;
|
||||
screenshots?: boolean;
|
||||
categories?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* The Tracing class exposes the tracing audit interface.
|
||||
* @remarks
|
||||
* You can use `tracing.start` and `tracing.stop` to create a trace file
|
||||
* which can be opened in Chrome DevTools or {@link https://chromedevtools.github.io/timeline-viewer/ | timeline viewer}.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```ts
|
||||
* await page.tracing.start({path: 'trace.json'});
|
||||
* await page.goto('https://www.google.com');
|
||||
* await page.tracing.stop();
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class Tracing {
|
||||
#client: CDPSession;
|
||||
#recording = false;
|
||||
#path?: string;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
constructor(client: CDPSession) {
|
||||
this.#client = client;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
updateClient(client: CDPSession): void {
|
||||
this.#client = client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a trace for the current page.
|
||||
* @remarks
|
||||
* Only one trace can be active at a time per browser.
|
||||
*
|
||||
* @param options - Optional `TracingOptions`.
|
||||
*/
|
||||
async start(options: TracingOptions = {}): Promise<void> {
|
||||
assert(
|
||||
!this.#recording,
|
||||
'Cannot start recording trace while already recording trace.',
|
||||
);
|
||||
|
||||
const defaultCategories = [
|
||||
'-*',
|
||||
'devtools.timeline',
|
||||
'v8.execute',
|
||||
'disabled-by-default-devtools.timeline',
|
||||
'disabled-by-default-devtools.timeline.frame',
|
||||
'toplevel',
|
||||
'blink.console',
|
||||
'blink.user_timing',
|
||||
'latencyInfo',
|
||||
'disabled-by-default-devtools.timeline.stack',
|
||||
'disabled-by-default-v8.cpu_profiler',
|
||||
];
|
||||
const {path, screenshots = false, categories = defaultCategories} = options;
|
||||
|
||||
if (screenshots) {
|
||||
categories.push('disabled-by-default-devtools.screenshot');
|
||||
}
|
||||
|
||||
const excludedCategories = categories
|
||||
.filter(cat => {
|
||||
return cat.startsWith('-');
|
||||
})
|
||||
.map(cat => {
|
||||
return cat.slice(1);
|
||||
});
|
||||
const includedCategories = categories.filter(cat => {
|
||||
return !cat.startsWith('-');
|
||||
});
|
||||
|
||||
this.#path = path;
|
||||
this.#recording = true;
|
||||
await this.#client.send('Tracing.start', {
|
||||
transferMode: 'ReturnAsStream',
|
||||
traceConfig: {
|
||||
excludedCategories,
|
||||
includedCategories,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops a trace started with the `start` method.
|
||||
* @returns Promise which resolves to buffer with trace data.
|
||||
*/
|
||||
async stop(): Promise<Uint8Array | undefined> {
|
||||
const contentDeferred = Deferred.create<Uint8Array | undefined>();
|
||||
this.#client.once('Tracing.tracingComplete', async event => {
|
||||
try {
|
||||
assert(event.stream, 'Missing "stream"');
|
||||
const readable = await getReadableFromProtocolStream(
|
||||
this.#client,
|
||||
event.stream,
|
||||
);
|
||||
const typedArray = await getReadableAsTypedArray(readable, this.#path);
|
||||
contentDeferred.resolve(typedArray ?? undefined);
|
||||
} catch (error) {
|
||||
if (isErrorLike(error)) {
|
||||
contentDeferred.reject(error);
|
||||
} else {
|
||||
contentDeferred.reject(new Error(`Unknown error: ${error}`));
|
||||
}
|
||||
}
|
||||
});
|
||||
await this.#client.send('Tracing.end');
|
||||
this.#recording = false;
|
||||
return await contentDeferred.valueOrThrow();
|
||||
}
|
||||
}
|
||||
113
node_modules/puppeteer-core/src/cdp/WebWorker.ts
generated
vendored
Normal file
113
node_modules/puppeteer-core/src/cdp/WebWorker.ts
generated
vendored
Normal 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
43
node_modules/puppeteer-core/src/cdp/cdp.ts
generated
vendored
Normal 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
238
node_modules/puppeteer-core/src/cdp/utils.ts
generated
vendored
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user