chore: refactor JSHandle and ExecutionContext (#8773)

This commit is contained in:
jrandolf 2022-08-11 11:45:35 +02:00 committed by GitHub
parent ee2540baef
commit a238f5758d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 298 additions and 333 deletions

View File

@ -7,7 +7,7 @@ sidebar_label: API
## Classes
| Class | Description |
| --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Accessibility](./puppeteer.accessibility.md) | The Accessibility class provides methods for inspecting Chromium's accessibility tree. The accessibility tree is used by assistive technology such as [screen readers](https://en.wikipedia.org/wiki/Screen_reader) or [switches](https://en.wikipedia.org/wiki/Switch_access). |
| [Browser](./puppeteer.browser.md) | A Browser is created when Puppeteer connects to a Chromium instance, either through [PuppeteerNode.launch()](./puppeteer.puppeteernode.launch.md) or [Puppeteer.connect()](./puppeteer.puppeteer.connect.md). |
| [BrowserContext](./puppeteer.browsercontext.md) | BrowserContexts provide a way to operate multiple independent browser sessions. When a browser is launched, it has a single BrowserContext used by default. The method [Browser.newPage](./puppeteer.browser.newpage.md) creates a page in the default browser context. |
@ -21,13 +21,13 @@ sidebar_label: API
| [Dialog](./puppeteer.dialog.md) | Dialog instances are dispatched by the [Page](./puppeteer.page.md) via the <code>dialog</code> event. |
| [ElementHandle](./puppeteer.elementhandle.md) | ElementHandle represents an in-page DOM element. |
| [EventEmitter](./puppeteer.eventemitter.md) | The EventEmitter class that many Puppeteer classes extend. |
| [ExecutionContext](./puppeteer.executioncontext.md) | <p>This class represents a context for JavaScript execution. A \[Page\] might have many execution contexts: - each [frame](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe) has "default" execution context that is always created after frame is attached to DOM. This context is returned by the [Frame.executionContext()](./puppeteer.frame.executioncontext.md) method. - [Extension](https://developer.chrome.com/extensions)'s content scripts create additional execution contexts.</p><p>Besides pages, execution contexts can be found in [workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API).</p> |
| [ExecutionContext](./puppeteer.executioncontext.md) | Represents a context for JavaScript execution. |
| [FileChooser](./puppeteer.filechooser.md) | File choosers let you react to the page requesting for a file. |
| [Frame](./puppeteer.frame.md) | At every point of time, page exposes its current frame tree via the [page.mainFrame](./puppeteer.page.mainframe.md) and [frame.childFrames](./puppeteer.frame.childframes.md) methods. |
| [HTTPRequest](./puppeteer.httprequest.md) | Represents an HTTP request sent by a page. |
| [HTTPResponse](./puppeteer.httpresponse.md) | The HTTPResponse class represents responses which are received by the [Page](./puppeteer.page.md) class. |
| [JSCoverage](./puppeteer.jscoverage.md) | |
| [JSHandle](./puppeteer.jshandle.md) | Represents an in-page JavaScript object. JSHandles can be created with the [page.evaluateHandle](./puppeteer.page.evaluatehandle.md) method. |
| [JSHandle](./puppeteer.jshandle.md) | <p>Represents a reference to a JavaScript object. Instances can be created using [Page.evaluateHandle()](./puppeteer.page.evaluatehandle.md).</p><p>Handles prevent the referenced JavaScript object from being garbage-collected unless the handle is purposely [disposed](./puppeteer.jshandle.dispose.md). JSHandles are auto-disposed when their associated frame is navigated away or the parent context gets destroyed.</p><p>Handles can be used as arguments for any evaluation function such as [Page.$eval()](./puppeteer.page._eval.md), [Page.evaluate()](./puppeteer.page.evaluate.md), and [Page.evaluateHandle()](./puppeteer.page.evaluatehandle.md). They are resolved to their referenced object.</p> |
| [Keyboard](./puppeteer.keyboard.md) | Keyboard provides an api for managing a virtual keyboard. The high level api is [Keyboard.type()](./puppeteer.keyboard.type.md), which takes raw characters and generates proper keydown, keypress/input, and keyup events on your page. |
| [Mouse](./puppeteer.mouse.md) | The Mouse class operates in main-frame CSS pixels relative to the top-left corner of the viewport. |
| [Page](./puppeteer.page.md) | <p>Page provides methods to interact with a single tab or [extension background page](https://developer.chrome.com/extensions/background_pages) in Chromium.</p><p>:::note</p><p>One Browser instance might have multiple Page instances.</p><p>:::</p> |

View File

@ -4,6 +4,8 @@ sidebar_label: ExecutionContext.evaluate
# ExecutionContext.evaluate() method
Evaluates the given function.
**Signature:**
```typescript
@ -21,19 +23,15 @@ class ExecutionContext {
## Parameters
| Parameter | Type | Description |
| ------------ | -------------- | --------------------------------------------------------------- |
| pageFunction | Func \| string | a function to be evaluated in the <code>executionContext</code> |
| args | Params | argument to pass to the page function |
| ------------ | -------------- | ----------------------------------------------- |
| pageFunction | Func \| string | The function to evaluate. |
| args | Params | Additional arguments to pass into the function. |
**Returns:**
Promise&lt;Awaited&lt;ReturnType&lt;Func&gt;&gt;&gt;
A promise that resolves to the return value of the given function.
## Remarks
If the function passed to the `executionContext.evaluate` returns a Promise, then `executionContext.evaluate` would wait for the promise to resolve and return its value. If the function passed to the `executionContext.evaluate` returns a non-serializable value, then `executionContext.evaluate` resolves to `undefined`. DevTools Protocol also supports transferring some additional values that are not serializable by `JSON`: `-0`, `NaN`, `Infinity`, `-Infinity`, and bigint literals.
The result of evaluating the function. If the result is an object, a vanilla object containing the serializable properties of the result is returned.
## Example 1
@ -45,7 +43,7 @@ console.log(result); // prints "56"
## Example 2
A string can also be passed in instead of a function.
A string can also be passed in instead of a function:
```ts
console.log(await executionContext.evaluate('1 + 2')); // prints "3"
@ -53,13 +51,15 @@ console.log(await executionContext.evaluate('1 + 2')); // prints "3"
## Example 3
[JSHandle](./puppeteer.jshandle.md) instances can be passed as arguments to the `executionContext.* evaluate`:
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
(a, b) => a + b,
oneHandle,
twoHandle
);
await oneHandle.dispose();
await twoHandle.dispose();

View File

@ -4,6 +4,12 @@ sidebar_label: ExecutionContext.evaluateHandle
# ExecutionContext.evaluateHandle() method
Evaluates the given function.
Unlike [evaluate](./puppeteer.executioncontext.evaluate.md), 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.
**Signature:**
```typescript
@ -21,26 +27,23 @@ class ExecutionContext {
## Parameters
| Parameter | Type | Description |
| ------------ | -------------- | --------------------------------------------------------------- |
| pageFunction | Func \| string | a function to be evaluated in the <code>executionContext</code> |
| args | Params | argument to pass to the page function |
| ------------ | -------------- | ----------------------------------------------- |
| pageFunction | Func \| string | The function to evaluate. |
| args | Params | Additional arguments to pass into the function. |
**Returns:**
Promise&lt;[HandleFor](./puppeteer.handlefor.md)&lt;Awaited&lt;ReturnType&lt;Func&gt;&gt;&gt;&gt;
A promise that resolves to the return value of the given function as an in-page object (a [JSHandle](./puppeteer.jshandle.md)).
## Remarks
The only difference between `executionContext.evaluate` and `executionContext.evaluateHandle` is that `executionContext.evaluateHandle` returns an in-page object (a [JSHandle](./puppeteer.jshandle.md)). If the function passed to the `executionContext.evaluateHandle` returns a Promise, then `executionContext.evaluateHandle` would wait for the promise to resolve and return its value.
A [handle](./puppeteer.jshandle.md) to the result of evaluating the function. If the result is a `Node`, then this will return an [element handle](./puppeteer.elementhandle.md).
## Example 1
```ts
const context = await page.mainFrame().executionContext();
const aHandle = await context.evaluateHandle(() => Promise.resolve(self));
aHandle; // Handle for the global object.
const handle: JSHandle<typeof globalThis> = await context.evaluateHandle(() =>
Promise.resolve(self)
);
```
## Example 2
@ -48,18 +51,25 @@ aHandle; // Handle for the global object.
A string can also be passed in instead of a function.
```ts
// Handle for the '3' * object.
const aHandle = await context.evaluateHandle('1 + 2');
const handle: JSHandle<number> = await context.evaluateHandle('1 + 2');
```
## Example 3
JSHandle instances can be passed as arguments to the `executionContext.* evaluateHandle`:
Handles can also be passed as `args`. They resolve to their referenced object:
```ts
const aHandle = await context.evaluateHandle(() => document.body);
const resultHandle = await context.evaluateHandle(body => body.innerHTML, * aHandle);
console.log(await resultHandle.jsonValue()); // prints body's innerHTML
await aHandle.dispose();
await resultHandle.dispose();
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();
```

View File

@ -20,4 +20,4 @@ The frame associated with this execution context.
## Remarks
Not every execution context is associated with a frame. For example, workers and extensions have execution contexts that are not associated with frames.
Not every execution context is associated with a frame. For example, [workers](./puppeteer.webworker.md) have execution contexts that are not associated with frames.

View File

@ -4,9 +4,7 @@ sidebar_label: ExecutionContext
# ExecutionContext class
This class represents a context for JavaScript execution. A \[Page\] might have many execution contexts: - each [frame](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe) has "default" execution context that is always created after frame is attached to DOM. This context is returned by the [Frame.executionContext()](./puppeteer.frame.executioncontext.md) method. - [Extension](https://developer.chrome.com/extensions)'s content scripts create additional execution contexts.
Besides pages, execution contexts can be found in [workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API).
Represents a context for JavaScript execution.
**Signature:**
@ -16,13 +14,21 @@ export declare class ExecutionContext
## Remarks
Besides pages, execution contexts can be found in [workers](./puppeteer.webworker.md).
The constructor for this class is marked as internal. Third-party code should not call the constructor directly or create subclasses that extend the `ExecutionContext` class.
## Example
A [Page](./puppeteer.page.md) can have several execution contexts:
- Each [Frame](./puppeteer.frame.md) of a [page](./puppeteer.page.md) has a "default" execution context that is always created after frame is attached to DOM. This context is returned by the [Frame.executionContext()](./puppeteer.frame.executioncontext.md) method. - Each [Chrome extensions](https://developer.chrome.com/extensions) creates additional execution contexts to isolate their code.
## Methods
| Method | Modifiers | Description |
| ------------------------------------------------------------------------------------ | --------- | -------------------------------------------------------------------------------------------- |
| [evaluate(pageFunction, args)](./puppeteer.executioncontext.evaluate.md) | | |
| [evaluateHandle(pageFunction, args)](./puppeteer.executioncontext.evaluatehandle.md) | | |
| ------------------------------------------------------------------------------------ | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [evaluate(pageFunction, args)](./puppeteer.executioncontext.evaluate.md) | | Evaluates the given function. |
| [evaluateHandle(pageFunction, args)](./puppeteer.executioncontext.evaluatehandle.md) | | <p>Evaluates the given function.</p><p>Unlike [evaluate](./puppeteer.executioncontext.evaluate.md), this method returns a handle to the result of the function.</p><p>This method may be better suited if the object cannot be serialized (e.g. <code>Map</code>) and requires further manipulation.</p> |
| [frame()](./puppeteer.executioncontext.frame.md) | | |
| [queryObjects(prototypeHandle)](./puppeteer.executioncontext.queryobjects.md) | | This method iterates the JavaScript heap and finds all the objects with the given prototype. |
| [queryObjects(prototypeHandle)](./puppeteer.executioncontext.queryobjects.md) | | Iterates through the JavaScript heap and finds all the objects with the given prototype. |

View File

@ -4,7 +4,7 @@ sidebar_label: ExecutionContext.queryObjects
# ExecutionContext.queryObjects() method
This method iterates the JavaScript heap and finds all the objects with the given prototype.
Iterates through the JavaScript heap and finds all the objects with the given prototype.
**Signature:**
@ -28,8 +28,6 @@ Promise&lt;[HandleFor](./puppeteer.handlefor.md)&lt;Prototype\[\]&gt;&gt;
A handle to an array of objects with the given prototype.
## Remarks
## Example
```ts

View File

@ -16,4 +16,4 @@ class JSHandle {
[ElementHandle](./puppeteer.elementhandle.md)&lt;Node&gt; \| null
Either `null` or the object handle itself, if the object handle is an instance of [ElementHandle](./puppeteer.elementhandle.md).
Either `null` or the handle itself if the handle is an instance of [ElementHandle](./puppeteer.elementhandle.md).

View File

@ -4,7 +4,7 @@ sidebar_label: JSHandle.dispose
# JSHandle.dispose() method
Stops referencing the element handle, and resolves when the object handle is successfully disposed of.
Releases the object referenced by the handle for garbage collection.
**Signature:**

View File

@ -4,7 +4,7 @@ sidebar_label: JSHandle.evaluate
# JSHandle.evaluate() method
This method passes this handle as the first argument to `pageFunction`. If `pageFunction` returns a Promise, then `handle.evaluate` would wait for the promise to resolve and return its value.
Evaluates the given function with the current handle as its first argument.
**Signature:**
@ -32,10 +32,3 @@ class JSHandle {
**Returns:**
Promise&lt;Awaited&lt;ReturnType&lt;Func&gt;&gt;&gt;
## Example
```ts
const tweetHandle = await page.$('.tweet .retweets');
expect(await tweetHandle.evaluate(node => node.innerText)).toBe('10');
```

View File

@ -4,7 +4,7 @@ sidebar_label: JSHandle.evaluateHandle
# JSHandle.evaluateHandle() method
This method passes this handle as the first argument to `pageFunction`.
Evaluates the given function with the current handle as its first argument.
**Signature:**
@ -32,11 +32,3 @@ class JSHandle {
**Returns:**
Promise&lt;[HandleFor](./puppeteer.handlefor.md)&lt;Awaited&lt;ReturnType&lt;Func&gt;&gt;&gt;&gt;
## Remarks
The only difference between `jsHandle.evaluate` and `jsHandle.evaluateHandle` is that `jsHandle.evaluateHandle` returns an in-page object (JSHandle).
If the function passed to `jsHandle.evaluateHandle` returns a Promise, then `evaluateHandle.evaluateHandle` waits for the promise to resolve and returns its value.
See [Page.evaluateHandle()](./puppeteer.page.evaluatehandle.md) for more details.

View File

@ -4,8 +4,6 @@ sidebar_label: JSHandle.executionContext
# JSHandle.executionContext() method
Returns the execution context the handle belongs to.
**Signature:**
```typescript
@ -17,3 +15,5 @@ class JSHandle {
**Returns:**
[ExecutionContext](./puppeteer.executioncontext.md)
The execution context the handle belongs to.

View File

@ -4,7 +4,7 @@ sidebar_label: JSHandle.getProperties
# JSHandle.getProperties() method
The method returns a map with property names as keys and JSHandle instances for the property values.
Gets a map of handles representing the properties of the current handle.
**Signature:**
@ -26,7 +26,9 @@ const properties = await listHandle.getProperties();
const children = [];
for (const property of properties.values()) {
const element = property.asElement();
if (element) children.push(element);
if (element) {
children.push(element);
}
}
children; // holds elementHandles to all children of document.body
```

View File

@ -8,7 +8,7 @@ sidebar_label: JSHandle.jsonValue
```typescript
class JSHandle {
jsonValue<T = unknown>(): Promise<T>;
jsonValue(): Promise<T>;
}
```
@ -16,8 +16,12 @@ class JSHandle {
Promise&lt;T&gt;
Returns a JSON representation of the object.If the object has a `toJSON` function, it will not be called.
A vanilla object representing the serializable portions of the referenced object.
## Exceptions
Throws if the object cannot be serialized due to circularity.
## Remarks
The JSON is generated by running [JSON.stringify](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) on the object in page and consequent [JSON.parse](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse) in puppeteer. \*\*NOTE\*\* The method throws if the referenced object is not stringifiable.
If the object has a `toJSON` function, it \*will not\* be called.

View File

@ -4,7 +4,11 @@ sidebar_label: JSHandle
# JSHandle class
Represents an in-page JavaScript object. JSHandles can be created with the [page.evaluateHandle](./puppeteer.page.evaluatehandle.md) method.
Represents a reference to a JavaScript object. Instances can be created using [Page.evaluateHandle()](./puppeteer.page.evaluatehandle.md).
Handles prevent the referenced JavaScript object from being garbage-collected unless the handle is purposely [disposed](./puppeteer.jshandle.dispose.md). JSHandles are auto-disposed when their associated frame is navigated away or the parent context gets destroyed.
Handles can be used as arguments for any evaluation function such as [Page.$eval()](./puppeteer.page._eval.md), [Page.evaluate()](./puppeteer.page.evaluate.md), and [Page.evaluateHandle()](./puppeteer.page.evaluatehandle.md). They are resolved to their referenced object.
**Signature:**
@ -22,10 +26,6 @@ The constructor for this class is marked as internal. Third-party code should no
const windowHandle = await page.evaluateHandle(() => window);
```
JSHandle prevents the referenced JavaScript object from being garbage-collected unless the handle is [disposed](./puppeteer.jshandle.dispose.md). JSHandles are auto- disposed when their origin frame gets navigated or the parent context gets destroyed.
JSHandle instances can be used as arguments for [Page.$eval()](./puppeteer.page._eval.md), [Page.evaluate()](./puppeteer.page.evaluate.md), and [Page.evaluateHandle()](./puppeteer.page.evaluatehandle.md).
## Properties
| Property | Modifiers | Type | Description |
@ -35,15 +35,15 @@ JSHandle instances can be used as arguments for [Page.$eval()](./puppeteer.page.
## Methods
| Method | Modifiers | Description |
| ---------------------------------------------------------------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ---------------------------------------------------------------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [asElement()](./puppeteer.jshandle.aselement.md) | | |
| [dispose()](./puppeteer.jshandle.dispose.md) | | Stops referencing the element handle, and resolves when the object handle is successfully disposed of. |
| [evaluate(pageFunction, args)](./puppeteer.jshandle.evaluate.md) | | This method passes this handle as the first argument to <code>pageFunction</code>. If <code>pageFunction</code> returns a Promise, then <code>handle.evaluate</code> would wait for the promise to resolve and return its value. |
| [evaluateHandle(pageFunction, args)](./puppeteer.jshandle.evaluatehandle.md) | | This method passes this handle as the first argument to <code>pageFunction</code>. |
| [executionContext()](./puppeteer.jshandle.executioncontext.md) | | Returns the execution context the handle belongs to. |
| [getProperties()](./puppeteer.jshandle.getproperties.md) | | The method returns a map with property names as keys and JSHandle instances for the property values. |
| [dispose()](./puppeteer.jshandle.dispose.md) | | Releases the object referenced by the handle for garbage collection. |
| [evaluate(pageFunction, args)](./puppeteer.jshandle.evaluate.md) | | Evaluates the given function with the current handle as its first argument. |
| [evaluateHandle(pageFunction, args)](./puppeteer.jshandle.evaluatehandle.md) | | Evaluates the given function with the current handle as its first argument. |
| [executionContext()](./puppeteer.jshandle.executioncontext.md) | | |
| [getProperties()](./puppeteer.jshandle.getproperties.md) | | Gets a map of handles representing the properties of the current handle. |
| [getProperty(propertyName)](./puppeteer.jshandle.getproperty.md) | | Fetches a single property from the referenced object. |
| [getProperty(propertyName)](./puppeteer.jshandle.getproperty_1.md) | | |
| [jsonValue()](./puppeteer.jshandle.jsonvalue.md) | | |
| [remoteObject()](./puppeteer.jshandle.remoteobject.md) | | Provides access to \[Protocol.Runtime.RemoteObject\](https://chromedevtools.github.io/devtools-protocol/tot/Runtime/\#type-RemoteObject) backing this JSHandle. |
| [remoteObject()](./puppeteer.jshandle.remoteobject.md) | | Provides access to the \[Protocol.Runtime.RemoteObject\](https://chromedevtools.github.io/devtools-protocol/tot/Runtime/\#type-RemoteObject) backing this handle. |
| [toString()](./puppeteer.jshandle.tostring.md) | | Returns a string representation of the JSHandle. |

View File

@ -4,7 +4,7 @@ sidebar_label: JSHandle.remoteObject
# JSHandle.remoteObject() method
Provides access to \[Protocol.Runtime.RemoteObject\](https://chromedevtools.github.io/devtools-protocol/tot/Runtime/\#type-RemoteObject) backing this JSHandle.
Provides access to the \[Protocol.Runtime.RemoteObject\](https://chromedevtools.github.io/devtools-protocol/tot/Runtime/\#type-RemoteObject) backing this handle.
**Signature:**

View File

@ -185,7 +185,7 @@ export class Accessibility {
let backendNodeId: number | undefined;
if (root) {
const {node} = await this.#client.send('DOM.describeNode', {
objectId: root._remoteObject.objectId,
objectId: root.remoteObject().objectId,
});
backendNodeId = node.backendNodeId;
}

View File

@ -33,7 +33,7 @@ async function queryAXTree(
role?: string
): Promise<Protocol.Accessibility.AXNode[]> {
const {nodes} = await client.send('Accessibility.queryAXTree', {
objectId: element._remoteObject.objectId,
objectId: element.remoteObject().objectId,
accessibleName,
role,
});

View File

@ -277,7 +277,7 @@ export class ElementHandle<
selector: Selector,
options: Exclude<WaitForSelectorOptions, 'root'> = {}
): Promise<ElementHandle<NodeFor<Selector>> | null> {
const frame = this._context.frame();
const frame = this.executionContext().frame();
assert(frame);
const adoptedRoot = await frame.worlds[PUPPETEER_WORLD].adoptHandle(this);
const handle = await frame.worlds[PUPPETEER_WORLD].waitForSelector(
@ -376,8 +376,8 @@ export class ElementHandle<
* iframe nodes, or null otherwise
*/
async contentFrame(): Promise<Frame | null> {
const nodeInfo = await this._client.send('DOM.describeNode', {
objectId: this._remoteObject.objectId,
const nodeInfo = await this.client.send('DOM.describeNode', {
objectId: this.remoteObject().objectId,
});
if (typeof nodeInfo.node.frameId !== 'string') {
return null;
@ -403,8 +403,8 @@ export class ElementHandle<
}
try {
await this._client.send('DOM.scrollIntoViewIfNeeded', {
objectId: this._remoteObject.objectId,
await this.client.send('DOM.scrollIntoViewIfNeeded', {
objectId: this.remoteObject().objectId,
});
} catch (_err) {
// Fallback to Element.scrollIntoView if DOM.scrollIntoViewIfNeeded is not supported
@ -470,9 +470,9 @@ export class ElementHandle<
*/
async clickablePoint(offset?: Offset): Promise<Point> {
const [result, layoutMetrics] = await Promise.all([
this._client
this.client
.send('DOM.getContentQuads', {
objectId: this._remoteObject.objectId,
objectId: this.remoteObject().objectId,
})
.catch(debugError),
this.#page._client().send('Page.getLayoutMetrics'),
@ -539,9 +539,9 @@ export class ElementHandle<
#getBoxModel(): Promise<void | Protocol.DOM.GetBoxModelResponse> {
const params: Protocol.DOM.GetBoxModelRequest = {
objectId: this._remoteObject.objectId,
objectId: this.remoteObject().objectId,
};
return this._client.send('DOM.getBoxModel', params).catch(error => {
return this.client.send('DOM.getBoxModel', params).catch(error => {
return debugError(error);
});
}
@ -758,8 +758,8 @@ export class ElementHandle<
return path.resolve(filePath);
}
});
const {objectId} = this._remoteObject;
const {node} = await this._client.send('DOM.describeNode', {objectId});
const {objectId} = this.remoteObject();
const {node} = await this.client.send('DOM.describeNode', {objectId});
const {backendNodeId} = node;
/* The zero-length array is a special case, it seems that
@ -775,7 +775,7 @@ export class ElementHandle<
element.dispatchEvent(new Event('change', {bubbles: true}));
});
} else {
await this._client.send('DOM.setFileInputFiles', {
await this.client.send('DOM.setFileInputFiles', {
objectId,
files,
backendNodeId,
@ -954,7 +954,7 @@ export class ElementHandle<
assert(boundingBox.width !== 0, 'Node has 0 width.');
assert(boundingBox.height !== 0, 'Node has 0 height.');
const layoutMetrics = await this._client.send('Page.getLayoutMetrics');
const layoutMetrics = await this.client.send('Page.getLayoutMetrics');
// Fallback to `layoutViewport` in case of using Firefox.
const {pageX, pageY} =
layoutMetrics.cssVisualViewport || layoutMetrics.layoutViewport;

View File

@ -17,16 +17,15 @@
import {Protocol} from 'devtools-protocol';
import {assert} from './assert.js';
import {CDPSession} from './Connection.js';
import {IsolatedWorld} from './IsolatedWorld.js';
import {ElementHandle} from './ElementHandle.js';
import {Frame} from './FrameManager.js';
import {IsolatedWorld} from './IsolatedWorld.js';
import {JSHandle} from './JSHandle.js';
import {EvaluateFunc, HandleFor} from './types.js';
import {
createJSHandle,
getExceptionMessage,
isString,
valueFromRemoteObject,
createJSHandle,
} from './util.js';
/**
@ -36,18 +35,24 @@ export const EVALUATION_SCRIPT_URL = 'pptr://__puppeteer_evaluation_script__';
const SOURCE_URL_REGEX = /^[\040\t]*\/\/[@#] sourceURL=\s*(\S*?)\s*$/m;
/**
* This class represents a context for JavaScript execution. A [Page] might have
* many execution contexts:
* - each {@link
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe | frame}
* has "default" execution context that is always created after frame is
* attached to DOM. This context is returned by the
* {@link Frame.executionContext} method.
* - {@link https://developer.chrome.com/extensions | Extension}'s content
* scripts create additional execution contexts.
* Represents a context for JavaScript execution.
*
* Besides pages, execution contexts can be found in {@link
* https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API | workers}.
* @example
* A {@link Page} can have several execution contexts:
*
* - Each {@link Frame} of a {@link Page | page} has a "default" execution
* context that is always created after frame is attached to DOM. This context
* is returned by the {@link Frame.executionContext} method.
* - Each {@link https://developer.chrome.com/extensions | Chrome extensions}
* creates additional execution contexts to isolate their code.
*
* @remarks
* By definition, each context is isolated from one another, however they are
* all able to manipulate non-JavaScript resources (such as DOM).
*
* @remarks
* Besides pages, execution contexts can be found in
* {@link WebWorker | workers}.
*/
export class ExecutionContext {
/**
@ -82,28 +87,19 @@ export class ExecutionContext {
}
/**
* @remarks
*
* Not every execution context is associated with a frame. For example,
* workers and extensions have execution contexts that are not associated with
* frames.
*
* @returns The frame associated with this execution context.
*
* @remarks
* Not every execution context is associated with a frame. For example,
* {@link WebWorker | workers} have execution contexts that are not associated
* with frames.
*/
frame(): Frame | null {
return this._world ? this._world.frame() : null;
}
/**
* @remarks
* If the function passed to the `executionContext.evaluate` returns a
* Promise, then `executionContext.evaluate` would wait for the promise to
* resolve and return its value. If the function passed to the
* `executionContext.evaluate` returns a non-serializable value, then
* `executionContext.evaluate` resolves to `undefined`. DevTools Protocol also
* supports transferring some additional values that are not serializable by
* `JSON`: `-0`, `NaN`, `Infinity`, `-Infinity`, and bigint literals.
*
* Evaluates the given function.
*
* @example
* ```ts
@ -113,29 +109,29 @@ export class ExecutionContext {
* ```
*
* @example
* A string can also be passed in instead of a function.
*
* A string can also be passed in instead of a function:
* ```ts
* console.log(await executionContext.evaluate('1 + 2')); // prints "3"
* ```
*
* @example
* {@link JSHandle} instances can be passed as arguments to the
* `executionContext.* evaluate`:
* 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
* (a, b) => a + b, oneHandle, twoHandle
* );
* await oneHandle.dispose();
* await twoHandle.dispose();
* console.log(result); // prints '3'.
* ```
* @param pageFunction - a function to be evaluated in the `executionContext`
* @param args - argument to pass to the page function
*
* @returns A promise that resolves to the return value of the given function.
* @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[],
@ -148,46 +144,51 @@ export class ExecutionContext {
}
/**
* @remarks
* The only difference between `executionContext.evaluate` and
* `executionContext.evaluateHandle` is that `executionContext.evaluateHandle`
* returns an in-page object (a {@link JSHandle}).
* If the function passed to the `executionContext.evaluateHandle` returns a
* Promise, then `executionContext.evaluateHandle` would wait for the
* promise to resolve and return its value.
* 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 aHandle = await context.evaluateHandle(() => Promise.resolve(self));
* aHandle; // Handle for the global object.
* const handle: JSHandle<typeof globalThis> = await context.evaluateHandle(() =>
* Promise.resolve(self)
* );
* ```
*
* @example
* A string can also be passed in instead of a function.
*
* ```ts
* // Handle for the '3' * object.
* const aHandle = await context.evaluateHandle('1 + 2');
* const handle: JSHandle<number> = await context.evaluateHandle('1 + 2');
* ```
*
* @example
* JSHandle instances can be passed as arguments
* to the `executionContext.* evaluateHandle`:
*
* Handles can also be passed as `args`. They resolve to their referenced object:
* ```ts
* const aHandle = await context.evaluateHandle(() => document.body);
* const resultHandle = await context.evaluateHandle(body => body.innerHTML, * aHandle);
* console.log(await resultHandle.jsonValue()); // prints body's innerHTML
* await aHandle.dispose();
* await resultHandle.dispose();
* 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 - a function to be evaluated in the `executionContext`
* @param args - argument to pass to the page function
*
* @returns A promise that resolves to the return value of the given function
* as an in-page object (a {@link JSHandle}).
* @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[],
@ -253,12 +254,6 @@ export class ExecutionContext {
: createJSHandle(this, remoteObject);
}
if (typeof pageFunction !== 'function') {
throw new Error(
`Expected to get |string| or |function| as the first argument, but got "${pageFunction}" instead.`
);
}
let functionText = pageFunction.toString();
try {
new Function('(' + functionText + ')');
@ -330,23 +325,24 @@ export class ExecutionContext {
}
const objectHandle = arg && arg instanceof JSHandle ? arg : null;
if (objectHandle) {
if (objectHandle._context !== this) {
if (objectHandle.executionContext() !== this) {
throw new Error(
'JSHandles can be evaluated only in the context they were created!'
);
}
if (objectHandle._disposed) {
if (objectHandle.disposed) {
throw new Error('JSHandle is disposed!');
}
if (objectHandle._remoteObject.unserializableValue) {
if (objectHandle.remoteObject().unserializableValue) {
return {
unserializableValue: objectHandle._remoteObject.unserializableValue,
unserializableValue:
objectHandle.remoteObject().unserializableValue,
};
}
if (!objectHandle._remoteObject.objectId) {
return {value: objectHandle._remoteObject.value};
if (!objectHandle.remoteObject().objectId) {
return {value: objectHandle.remoteObject().value};
}
return {objectId: objectHandle._remoteObject.objectId};
return {objectId: objectHandle.remoteObject().objectId};
}
return {value: arg};
}
@ -372,9 +368,9 @@ export class ExecutionContext {
}
/**
* This method iterates the JavaScript heap and finds all the objects with the
* Iterates through the JavaScript heap and finds all the objects with the
* given prototype.
* @remarks
*
* @example
* ```ts
* // Create a Map object
@ -390,33 +386,20 @@ export class ExecutionContext {
* ```
*
* @param prototypeHandle - a handle to the object prototype
*
* @returns A handle to an array of objects with the given prototype.
*/
async queryObjects<Prototype>(
prototypeHandle: JSHandle<Prototype>
): Promise<HandleFor<Prototype[]>> {
assert(!prototypeHandle._disposed, 'Prototype JSHandle is disposed!');
assert(!prototypeHandle.disposed, 'Prototype JSHandle is disposed!');
const remoteObject = prototypeHandle.remoteObject();
assert(
prototypeHandle._remoteObject.objectId,
remoteObject.objectId,
'Prototype JSHandle must not be referencing primitive value'
);
const response = await this._client.send('Runtime.queryObjects', {
prototypeObjectId: prototypeHandle._remoteObject.objectId,
prototypeObjectId: remoteObject.objectId,
});
return createJSHandle(this, response.objects) as HandleFor<Prototype[]>;
}
/**
* @internal
*/
async _adoptBackendNodeId(
backendNodeId?: Protocol.DOM.BackendNodeId
): Promise<ElementHandle<Node>> {
const {object} = await this._client.send('DOM.resolveNode', {
backendNodeId: backendNodeId,
executionContextId: this._contextId,
});
return createJSHandle(this, object) as ElementHandle<Node>;
}
}

View File

@ -789,7 +789,7 @@ export class IsolatedWorld {
'Cannot adopt handle that already belongs to this execution context'
);
const nodeInfo = await this.#client.send('DOM.describeNode', {
objectId: handle._remoteObject.objectId,
objectId: handle.remoteObject().objectId,
});
return (await this.adoptBackendNode(nodeInfo.node.backendNodeId)) as T;
}

View File

@ -17,11 +17,11 @@
import {Protocol} from 'devtools-protocol';
import {assert} from './assert.js';
import {CDPSession} from './Connection.js';
import {EvaluateFunc, HandleFor, HandleOr} from './types.js';
import type {ElementHandle} from './ElementHandle.js';
import {ExecutionContext} from './ExecutionContext.js';
import {MouseButton} from './Input.js';
import {releaseObject, valueFromRemoteObject, createJSHandle} from './util.js';
import type {ElementHandle} from './ElementHandle.js';
import {EvaluateFunc, HandleFor, HandleOr} from './types.js';
import {createJSHandle, releaseObject, valueFromRemoteObject} from './util.js';
declare const __JSHandleSymbol: unique symbol;
@ -52,21 +52,23 @@ export interface BoundingBox extends Point {
}
/**
* Represents an in-page JavaScript object. JSHandles can be created with the
* {@link Page.evaluateHandle | page.evaluateHandle} method.
* Represents a reference to a JavaScript object. Instances can be created using
* {@link Page.evaluateHandle}.
*
* Handles prevent the referenced JavaScript object from being garbage-collected
* unless the handle is purposely {@link JSHandle.dispose | disposed}. JSHandles
* are auto-disposed when their associated frame is navigated away or the parent
* context gets destroyed.
*
* Handles can be used as arguments for any evaluation function such as
* {@link Page.$eval}, {@link Page.evaluate}, and {@link Page.evaluateHandle}.
* They are resolved to their referenced object.
*
* @example
* ```ts
* const windowHandle = await page.evaluateHandle(() => window);
* ```
*
* JSHandle prevents the referenced JavaScript object from being garbage-collected
* unless the handle is {@link JSHandle.dispose | disposed}. JSHandles are auto-
* disposed when their origin frame gets navigated or the parent context gets destroyed.
*
* JSHandle instances can be used as arguments for {@link Page.$eval},
* {@link Page.evaluate}, and {@link Page.evaluateHandle}.
*
* @public
*/
export class JSHandle<T = unknown> {
@ -83,31 +85,17 @@ export class JSHandle<T = unknown> {
/**
* @internal
*/
get _client(): CDPSession {
get client(): CDPSession {
return this.#client;
}
/**
* @internal
*/
get _disposed(): boolean {
get disposed(): boolean {
return this.#disposed;
}
/**
* @internal
*/
get _remoteObject(): Protocol.Runtime.RemoteObject {
return this.#remoteObject;
}
/**
* @internal
*/
get _context(): ExecutionContext {
return this.#context;
}
/**
* @internal
*/
@ -121,24 +109,18 @@ export class JSHandle<T = unknown> {
this.#remoteObject = remoteObject;
}
/** Returns the execution context the handle belongs to.
/**
* @returns The execution context the handle belongs to.
*/
executionContext(): ExecutionContext {
return this.#context;
}
/**
* This method passes this handle as the first argument to `pageFunction`. If
* `pageFunction` returns a Promise, then `handle.evaluate` would wait for the
* promise to resolve and return its value.
* Evaluates the given function with the current handle as its first argument.
*
* @example
* ```ts
* const tweetHandle = await page.$('.tweet .retweets');
* expect(await tweetHandle.evaluate(node => node.innerText)).toBe('10');
* ```
* @see {@link ExecutionContext.evaluate} for more details.
*/
async evaluate<
Params extends unknown[],
Func extends EvaluateFunc<[this, ...Params]> = EvaluateFunc<
@ -154,19 +136,9 @@ export class JSHandle<T = unknown> {
}
/**
* This method passes this handle as the first argument to `pageFunction`.
* Evaluates the given function with the current handle as its first argument.
*
* @remarks
*
* The only difference between `jsHandle.evaluate` and
* `jsHandle.evaluateHandle` is that `jsHandle.evaluateHandle` returns an
* in-page object (JSHandle).
*
* If the function passed to `jsHandle.evaluateHandle` returns a Promise, then
* `evaluateHandle.evaluateHandle` waits for the promise to resolve and
* returns its value.
*
* See {@link Page.evaluateHandle} for more details.
* @see {@link ExecutionContext.evaluateHandle} for more details.
*/
async evaluateHandle<
Params extends unknown[],
@ -196,14 +168,13 @@ export class JSHandle<T = unknown> {
async getProperty<K extends keyof T>(
propertyName: HandleOr<K>
): Promise<HandleFor<T[K]>> {
return await this.evaluateHandle((object, propertyName) => {
return this.evaluateHandle((object, propertyName) => {
return object[propertyName];
}, propertyName);
}
/**
* The method returns a map with property names as keys and JSHandle instances
* for the property values.
* Gets a map of handles representing the properties of the current handle.
*
* @example
* ```ts
@ -212,14 +183,17 @@ export class JSHandle<T = unknown> {
* const children = [];
* for (const property of properties.values()) {
* const element = property.asElement();
* if (element)
* if (element) {
* children.push(element);
* }
* }
* children; // holds elementHandles to all children of document.body
* ```
*/
async getProperties(): Promise<Map<string, JSHandle>> {
assert(this.#remoteObject.objectId);
// We use Runtime.getProperties rather than iterative building because the
// iterative approach might create a distorted snapshot.
const response = await this.#client.send('Runtime.getProperties', {
objectId: this.#remoteObject.objectId,
ownProperties: true,
@ -235,41 +209,36 @@ export class JSHandle<T = unknown> {
}
/**
* @returns Returns a JSON representation of the object.If the object has a
* `toJSON` function, it will not be called.
* @remarks
* @returns A vanilla object representing the serializable portions of the
* referenced object.
* @throws Throws if the object cannot be serialized due to circularity.
*
* The JSON is generated by running {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify | JSON.stringify}
* on the object in page and consequent {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse | JSON.parse} in puppeteer.
* **NOTE** The method throws if the referenced object is not stringifiable.
* @remarks
* If the object has a `toJSON` function, it *will not* be called.
*/
async jsonValue<T = unknown>(): Promise<T> {
if (this.#remoteObject.objectId) {
const response = await this.#client.send('Runtime.callFunctionOn', {
functionDeclaration: 'function() { return this; }',
objectId: this.#remoteObject.objectId,
returnByValue: true,
awaitPromise: true,
});
return valueFromRemoteObject(response.result) as T;
async jsonValue(): Promise<T> {
if (!this.#remoteObject.objectId) {
return valueFromRemoteObject(this.#remoteObject);
}
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;
}
/**
* @returns Either `null` or the object handle itself, if the object
* handle is an instance of {@link ElementHandle}.
* @returns Either `null` or the handle itself if the handle is an
* instance of {@link ElementHandle}.
*/
asElement(): ElementHandle<Node> | null {
/* This always returns null, but subclasses can override this and return an
ElementHandle.
*/
return null;
}
/**
* Stops referencing the element handle, and resolves when the object handle is
* successfully disposed of.
* Releases the object referenced by the handle for garbage collection.
*/
async dispose(): Promise<void> {
if (this.#disposed) {
@ -282,18 +251,21 @@ export class JSHandle<T = unknown> {
/**
* Returns a string representation of the JSHandle.
*
* @remarks Useful during debugging.
* @remarks
* Useful during debugging.
*/
toString(): string {
if (this.#remoteObject.objectId) {
if (!this.#remoteObject.objectId) {
return 'JSHandle:' + valueFromRemoteObject(this.#remoteObject);
}
const type = this.#remoteObject.subtype || this.#remoteObject.type;
return 'JSHandle@' + type;
}
return 'JSHandle:' + valueFromRemoteObject(this.#remoteObject);
}
/**
* Provides access to [Protocol.Runtime.RemoteObject](https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#type-RemoteObject) backing this JSHandle.
* Provides access to the
* [Protocol.Runtime.RemoteObject](https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#type-RemoteObject)
* backing this handle.
*/
remoteObject(): Protocol.Runtime.RemoteObject {
return this.#remoteObject;

View File

@ -1710,7 +1710,7 @@ export class Page extends EventEmitter {
}
const textTokens = [];
for (const arg of args) {
const remoteObject = arg._remoteObject;
const remoteObject = arg.remoteObject();
if (remoteObject.objectId) {
textTokens.push(arg.toString());
} else {

View File

@ -353,7 +353,7 @@ describe('Evaluation specs', function () {
const windowHandle = await page.evaluateHandle(() => {
return window;
});
const errorText = await windowHandle.jsonValue<string>().catch(error_ => {
const errorText = await windowHandle.jsonValue().catch(error_ => {
return error_.message;
});
const error = await page

View File

@ -73,14 +73,9 @@ describe('JSHandle', function () {
test.obj = test;
let error!: Error;
await page
.evaluateHandle(
opts => {
// @ts-expect-error we are deliberately passing a bad type here
// (nested object)
return opts.elem;
},
{test}
)
.evaluateHandle(opts => {
return opts;
}, test)
.catch(error_ => {
return (error = error_);
});
@ -136,7 +131,7 @@ describe('JSHandle', function () {
const aHandle = await page.evaluateHandle(() => {
return {foo: 'bar'};
});
const json = await aHandle.jsonValue<Record<string, string>>();
const json = await aHandle.jsonValue();
expect(json).toEqual({foo: 'bar'});
});
@ -146,7 +141,7 @@ describe('JSHandle', function () {
const aHandle = await page.evaluateHandle(() => {
return ['a', 'b'];
});
const json = await aHandle.jsonValue<string[]>();
const json = await aHandle.jsonValue();
expect(json).toEqual(['a', 'b']);
});
@ -156,8 +151,12 @@ describe('JSHandle', function () {
const aHandle = await page.evaluateHandle(() => {
return 'foo';
});
const json = await aHandle.jsonValue<string>();
expect(json).toEqual('foo');
expect(await aHandle.jsonValue()).toEqual('foo');
const bHandle = await page.evaluateHandle(() => {
return undefined;
});
expect(await bHandle.jsonValue()).toEqual(undefined);
});
itFailsFirefox('should not work with dates', async () => {
@ -172,13 +171,19 @@ describe('JSHandle', function () {
it('should throw for circular objects', async () => {
const {page, isChrome} = getTestState();
const windowHandle = await page.evaluateHandle('window');
const handle = await page.evaluateHandle(() => {
const t: {t?: unknown; g: number} = {g: 1};
t.t = t;
return t;
});
let error!: Error;
await windowHandle.jsonValue().catch(error_ => {
await handle.jsonValue().catch(error_ => {
return (error = error_);
});
if (isChrome) {
expect(error.message).toContain('Object reference chain is too long');
expect(error.message).toContain(
'Could not serialize referenced object'
);
} else {
expect(error.message).toContain('Object is not serializable');
}