From a238f5758dd2c0e92186966478cd382affac2280 Mon Sep 17 00:00:00 2001 From: jrandolf <101637635+jrandolf@users.noreply.github.com> Date: Thu, 11 Aug 2022 11:45:35 +0200 Subject: [PATCH] chore: refactor JSHandle and ExecutionContext (#8773) --- docs/api/index.md | 68 +++---- .../puppeteer.executioncontext.evaluate.md | 24 +-- ...ppeteer.executioncontext.evaluatehandle.md | 48 +++-- docs/api/puppeteer.executioncontext.frame.md | 2 +- docs/api/puppeteer.executioncontext.md | 24 ++- ...puppeteer.executioncontext.queryobjects.md | 4 +- docs/api/puppeteer.jshandle.aselement.md | 2 +- docs/api/puppeteer.jshandle.dispose.md | 2 +- docs/api/puppeteer.jshandle.evaluate.md | 9 +- docs/api/puppeteer.jshandle.evaluatehandle.md | 10 +- .../puppeteer.jshandle.executioncontext.md | 4 +- docs/api/puppeteer.jshandle.getproperties.md | 6 +- docs/api/puppeteer.jshandle.jsonvalue.md | 10 +- docs/api/puppeteer.jshandle.md | 36 ++-- docs/api/puppeteer.jshandle.remoteobject.md | 2 +- src/common/Accessibility.ts | 2 +- src/common/AriaQueryHandler.ts | 2 +- src/common/ElementHandle.ts | 26 +-- src/common/ExecutionContext.ts | 171 ++++++++---------- src/common/IsolatedWorld.ts | 2 +- src/common/JSHandle.ts | 138 ++++++-------- src/common/Page.ts | 2 +- test/src/evaluation.spec.ts | 2 +- test/src/jshandle.spec.ts | 35 ++-- 24 files changed, 298 insertions(+), 333 deletions(-) diff --git a/docs/api/index.md b/docs/api/index.md index 88e7d46f..aab590d9 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -6,40 +6,40 @@ 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. | -| [BrowserFetcher](./puppeteer.browserfetcher.md) | BrowserFetcher can download and manage different versions of Chromium and Firefox. | -| [CDPSession](./puppeteer.cdpsession.md) | The CDPSession instances are used to talk raw Chrome Devtools Protocol. | -| [Connection](./puppeteer.connection.md) | | -| [ConsoleMessage](./puppeteer.consolemessage.md) | ConsoleMessage objects are dispatched by page via the 'console' event. | -| [Coverage](./puppeteer.coverage.md) | The Coverage class provides methods to gathers information about parts of JavaScript and CSS that were used by the page. | -| [CSSCoverage](./puppeteer.csscoverage.md) | | -| [CustomError](./puppeteer.customerror.md) | | -| [Dialog](./puppeteer.dialog.md) | Dialog instances are dispatched by the [Page](./puppeteer.page.md) via the dialog 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) |

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).

| -| [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. | -| [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) |

Page provides methods to interact with a single tab or [extension background page](https://developer.chrome.com/extensions/background_pages) in Chromium.

:::note

One Browser instance might have multiple Page instances.

:::

| -| [ProtocolError](./puppeteer.protocolerror.md) | ProtocolError is emitted whenever there is an error from the protocol. | -| [Puppeteer](./puppeteer.puppeteer.md) |

The main Puppeteer class.

IMPORTANT: if you are using Puppeteer in a Node environment, you will get an instance of [PuppeteerNode](./puppeteer.puppeteernode.md) when you import or require puppeteer. That class extends Puppeteer, so has all the methods documented below as well as all that are defined on [PuppeteerNode](./puppeteer.puppeteernode.md).

| -| [PuppeteerNode](./puppeteer.puppeteernode.md) |

Extends the main [Puppeteer](./puppeteer.puppeteer.md) class with Node specific behaviour for fetching and downloading browsers.

If you're using Puppeteer in a Node environment, this is the class you'll get when you run require('puppeteer') (or the equivalent ES import).

| -| [SecurityDetails](./puppeteer.securitydetails.md) | The SecurityDetails class represents the security details of a response that was received over a secure connection. | -| [Target](./puppeteer.target.md) | | -| [TimeoutError](./puppeteer.timeouterror.md) | TimeoutError is emitted whenever certain operations are terminated due to timeout. | -| [Touchscreen](./puppeteer.touchscreen.md) | The Touchscreen class exposes touchscreen events. | -| [Tracing](./puppeteer.tracing.md) | The Tracing class exposes the tracing audit interface. | -| [WebWorker](./puppeteer.webworker.md) | This class represents a [WebWorker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API). | +| 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. | +| [BrowserFetcher](./puppeteer.browserfetcher.md) | BrowserFetcher can download and manage different versions of Chromium and Firefox. | +| [CDPSession](./puppeteer.cdpsession.md) | The CDPSession instances are used to talk raw Chrome Devtools Protocol. | +| [Connection](./puppeteer.connection.md) | | +| [ConsoleMessage](./puppeteer.consolemessage.md) | ConsoleMessage objects are dispatched by page via the 'console' event. | +| [Coverage](./puppeteer.coverage.md) | The Coverage class provides methods to gathers information about parts of JavaScript and CSS that were used by the page. | +| [CSSCoverage](./puppeteer.csscoverage.md) | | +| [CustomError](./puppeteer.customerror.md) | | +| [Dialog](./puppeteer.dialog.md) | Dialog instances are dispatched by the [Page](./puppeteer.page.md) via the dialog 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) | 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 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.

| +| [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) |

Page provides methods to interact with a single tab or [extension background page](https://developer.chrome.com/extensions/background_pages) in Chromium.

:::note

One Browser instance might have multiple Page instances.

:::

| +| [ProtocolError](./puppeteer.protocolerror.md) | ProtocolError is emitted whenever there is an error from the protocol. | +| [Puppeteer](./puppeteer.puppeteer.md) |

The main Puppeteer class.

IMPORTANT: if you are using Puppeteer in a Node environment, you will get an instance of [PuppeteerNode](./puppeteer.puppeteernode.md) when you import or require puppeteer. That class extends Puppeteer, so has all the methods documented below as well as all that are defined on [PuppeteerNode](./puppeteer.puppeteernode.md).

| +| [PuppeteerNode](./puppeteer.puppeteernode.md) |

Extends the main [Puppeteer](./puppeteer.puppeteer.md) class with Node specific behaviour for fetching and downloading browsers.

If you're using Puppeteer in a Node environment, this is the class you'll get when you run require('puppeteer') (or the equivalent ES import).

| +| [SecurityDetails](./puppeteer.securitydetails.md) | The SecurityDetails class represents the security details of a response that was received over a secure connection. | +| [Target](./puppeteer.target.md) | | +| [TimeoutError](./puppeteer.timeouterror.md) | TimeoutError is emitted whenever certain operations are terminated due to timeout. | +| [Touchscreen](./puppeteer.touchscreen.md) | The Touchscreen class exposes touchscreen events. | +| [Tracing](./puppeteer.tracing.md) | The Tracing class exposes the tracing audit interface. | +| [WebWorker](./puppeteer.webworker.md) | This class represents a [WebWorker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API). | ## Enumerations diff --git a/docs/api/puppeteer.executioncontext.evaluate.md b/docs/api/puppeteer.executioncontext.evaluate.md index 3c7abd30..54999787 100644 --- a/docs/api/puppeteer.executioncontext.evaluate.md +++ b/docs/api/puppeteer.executioncontext.evaluate.md @@ -4,6 +4,8 @@ sidebar_label: ExecutionContext.evaluate # ExecutionContext.evaluate() method +Evaluates the given function. + **Signature:** ```typescript @@ -20,20 +22,16 @@ class ExecutionContext { ## Parameters -| Parameter | Type | Description | -| ------------ | -------------- | --------------------------------------------------------------- | -| pageFunction | Func \| string | a function to be evaluated in the executionContext | -| args | Params | argument to pass to the page function | +| Parameter | Type | Description | +| ------------ | -------------- | ----------------------------------------------- | +| pageFunction | Func \| string | The function to evaluate. | +| args | Params | Additional arguments to pass into the function. | **Returns:** Promise<Awaited<ReturnType<Func>>> -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(); diff --git a/docs/api/puppeteer.executioncontext.evaluatehandle.md b/docs/api/puppeteer.executioncontext.evaluatehandle.md index 103b8d7e..d35dcfa9 100644 --- a/docs/api/puppeteer.executioncontext.evaluatehandle.md +++ b/docs/api/puppeteer.executioncontext.evaluatehandle.md @@ -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 @@ -20,27 +26,24 @@ class ExecutionContext { ## Parameters -| Parameter | Type | Description | -| ------------ | -------------- | --------------------------------------------------------------- | -| pageFunction | Func \| string | a function to be evaluated in the executionContext | -| args | Params | argument to pass to the page function | +| Parameter | Type | Description | +| ------------ | -------------- | ----------------------------------------------- | +| pageFunction | Func \| string | The function to evaluate. | +| args | Params | Additional arguments to pass into the function. | **Returns:** Promise<[HandleFor](./puppeteer.handlefor.md)<Awaited<ReturnType<Func>>>> -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 = 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 = 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 = await context.evaluateHandle( + () => { + return document.body; + } +); +const stringHandle: JSHandle = 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(); ``` diff --git a/docs/api/puppeteer.executioncontext.frame.md b/docs/api/puppeteer.executioncontext.frame.md index feef9b8d..07afe93b 100644 --- a/docs/api/puppeteer.executioncontext.frame.md +++ b/docs/api/puppeteer.executioncontext.frame.md @@ -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. diff --git a/docs/api/puppeteer.executioncontext.md b/docs/api/puppeteer.executioncontext.md index a0e5999b..55167305 100644 --- a/docs/api/puppeteer.executioncontext.md +++ b/docs/api/puppeteer.executioncontext.md @@ -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) | | | -| [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. | +| Method | Modifiers | Description | +| ------------------------------------------------------------------------------------ | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [evaluate(pageFunction, args)](./puppeteer.executioncontext.evaluate.md) | | Evaluates the given function. | +| [evaluateHandle(pageFunction, args)](./puppeteer.executioncontext.evaluatehandle.md) | |

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.

| +| [frame()](./puppeteer.executioncontext.frame.md) | | | +| [queryObjects(prototypeHandle)](./puppeteer.executioncontext.queryobjects.md) | | Iterates through the JavaScript heap and finds all the objects with the given prototype. | diff --git a/docs/api/puppeteer.executioncontext.queryobjects.md b/docs/api/puppeteer.executioncontext.queryobjects.md index 400fdb41..8b55d68b 100644 --- a/docs/api/puppeteer.executioncontext.queryobjects.md +++ b/docs/api/puppeteer.executioncontext.queryobjects.md @@ -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<[HandleFor](./puppeteer.handlefor.md)<Prototype\[\]>> A handle to an array of objects with the given prototype. -## Remarks - ## Example ```ts diff --git a/docs/api/puppeteer.jshandle.aselement.md b/docs/api/puppeteer.jshandle.aselement.md index 98c9b510..0a733c22 100644 --- a/docs/api/puppeteer.jshandle.aselement.md +++ b/docs/api/puppeteer.jshandle.aselement.md @@ -16,4 +16,4 @@ class JSHandle { [ElementHandle](./puppeteer.elementhandle.md)<Node> \| 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). diff --git a/docs/api/puppeteer.jshandle.dispose.md b/docs/api/puppeteer.jshandle.dispose.md index 940d20c7..aff4389d 100644 --- a/docs/api/puppeteer.jshandle.dispose.md +++ b/docs/api/puppeteer.jshandle.dispose.md @@ -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:** diff --git a/docs/api/puppeteer.jshandle.evaluate.md b/docs/api/puppeteer.jshandle.evaluate.md index 84ce9c5f..f96769cb 100644 --- a/docs/api/puppeteer.jshandle.evaluate.md +++ b/docs/api/puppeteer.jshandle.evaluate.md @@ -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<Awaited<ReturnType<Func>>> - -## Example - -```ts -const tweetHandle = await page.$('.tweet .retweets'); -expect(await tweetHandle.evaluate(node => node.innerText)).toBe('10'); -``` diff --git a/docs/api/puppeteer.jshandle.evaluatehandle.md b/docs/api/puppeteer.jshandle.evaluatehandle.md index 67eedfeb..caddb5df 100644 --- a/docs/api/puppeteer.jshandle.evaluatehandle.md +++ b/docs/api/puppeteer.jshandle.evaluatehandle.md @@ -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<[HandleFor](./puppeteer.handlefor.md)<Awaited<ReturnType<Func>>>> - -## 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. diff --git a/docs/api/puppeteer.jshandle.executioncontext.md b/docs/api/puppeteer.jshandle.executioncontext.md index 9e04acc1..60f9c0ee 100644 --- a/docs/api/puppeteer.jshandle.executioncontext.md +++ b/docs/api/puppeteer.jshandle.executioncontext.md @@ -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. diff --git a/docs/api/puppeteer.jshandle.getproperties.md b/docs/api/puppeteer.jshandle.getproperties.md index 49736f29..234eaf3c 100644 --- a/docs/api/puppeteer.jshandle.getproperties.md +++ b/docs/api/puppeteer.jshandle.getproperties.md @@ -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 ``` diff --git a/docs/api/puppeteer.jshandle.jsonvalue.md b/docs/api/puppeteer.jshandle.jsonvalue.md index 8c0feedc..4d6871d0 100644 --- a/docs/api/puppeteer.jshandle.jsonvalue.md +++ b/docs/api/puppeteer.jshandle.jsonvalue.md @@ -8,7 +8,7 @@ sidebar_label: JSHandle.jsonValue ```typescript class JSHandle { - jsonValue(): Promise; + jsonValue(): Promise; } ``` @@ -16,8 +16,12 @@ class JSHandle { Promise<T> -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. diff --git a/docs/api/puppeteer.jshandle.md b/docs/api/puppeteer.jshandle.md index c207f2c9..cc1a3e54 100644 --- a/docs/api/puppeteer.jshandle.md +++ b/docs/api/puppeteer.jshandle.md @@ -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 | @@ -34,16 +34,16 @@ 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 pageFunction. If pageFunction returns a Promise, then handle.evaluate 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 pageFunction. | -| [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. | -| [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. | -| [toString()](./puppeteer.jshandle.tostring.md) | | Returns a string representation of the JSHandle. | +| Method | Modifiers | Description | +| ---------------------------------------------------------------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [asElement()](./puppeteer.jshandle.aselement.md) | | | +| [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 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. | diff --git a/docs/api/puppeteer.jshandle.remoteobject.md b/docs/api/puppeteer.jshandle.remoteobject.md index 868762a1..8f8286bf 100644 --- a/docs/api/puppeteer.jshandle.remoteobject.md +++ b/docs/api/puppeteer.jshandle.remoteobject.md @@ -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:** diff --git a/src/common/Accessibility.ts b/src/common/Accessibility.ts index c3459332..e1f13e82 100644 --- a/src/common/Accessibility.ts +++ b/src/common/Accessibility.ts @@ -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; } diff --git a/src/common/AriaQueryHandler.ts b/src/common/AriaQueryHandler.ts index 935a7ce3..a8239557 100644 --- a/src/common/AriaQueryHandler.ts +++ b/src/common/AriaQueryHandler.ts @@ -33,7 +33,7 @@ async function queryAXTree( role?: string ): Promise { const {nodes} = await client.send('Accessibility.queryAXTree', { - objectId: element._remoteObject.objectId, + objectId: element.remoteObject().objectId, accessibleName, role, }); diff --git a/src/common/ElementHandle.ts b/src/common/ElementHandle.ts index 7fe2898a..2d65453f 100644 --- a/src/common/ElementHandle.ts +++ b/src/common/ElementHandle.ts @@ -277,7 +277,7 @@ export class ElementHandle< selector: Selector, options: Exclude = {} ): Promise> | 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 { - 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 { 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 { 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; diff --git a/src/common/ExecutionContext.ts b/src/common/ExecutionContext.ts index ba19d714..fdcb8898 100644 --- a/src/common/ExecutionContext.ts +++ b/src/common/ExecutionContext.ts @@ -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 = 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 = 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 = await context.evaluateHandle( + * () => { + * return document.body; + * } + * ); + * const stringHandle: JSHandle = 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( prototypeHandle: JSHandle ): Promise> { - 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; } - - /** - * @internal - */ - async _adoptBackendNodeId( - backendNodeId?: Protocol.DOM.BackendNodeId - ): Promise> { - const {object} = await this._client.send('DOM.resolveNode', { - backendNodeId: backendNodeId, - executionContextId: this._contextId, - }); - return createJSHandle(this, object) as ElementHandle; - } } diff --git a/src/common/IsolatedWorld.ts b/src/common/IsolatedWorld.ts index 6b07b2de..0550bfd1 100644 --- a/src/common/IsolatedWorld.ts +++ b/src/common/IsolatedWorld.ts @@ -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; } diff --git a/src/common/JSHandle.ts b/src/common/JSHandle.ts index 4cdb08c0..5dad33e9 100644 --- a/src/common/JSHandle.ts +++ b/src/common/JSHandle.ts @@ -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 { @@ -83,31 +85,17 @@ export class JSHandle { /** * @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 { 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 { } /** - * 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 { async getProperty( propertyName: HandleOr ): Promise> { - 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 { * 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> { 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 { } /** - * @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(): Promise { - 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 { + 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 | 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 { if (this.#disposed) { @@ -282,18 +251,21 @@ export class JSHandle { /** * Returns a string representation of the JSHandle. * - * @remarks Useful during debugging. + * @remarks + * Useful during debugging. */ toString(): string { - if (this.#remoteObject.objectId) { - const type = this.#remoteObject.subtype || this.#remoteObject.type; - return 'JSHandle@' + type; + if (!this.#remoteObject.objectId) { + return 'JSHandle:' + valueFromRemoteObject(this.#remoteObject); } - return 'JSHandle:' + valueFromRemoteObject(this.#remoteObject); + const type = this.#remoteObject.subtype || this.#remoteObject.type; + return 'JSHandle@' + type; } /** - * 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; diff --git a/src/common/Page.ts b/src/common/Page.ts index cb7fb10a..2cac4371 100644 --- a/src/common/Page.ts +++ b/src/common/Page.ts @@ -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 { diff --git a/test/src/evaluation.spec.ts b/test/src/evaluation.spec.ts index a4a39f57..307bf605 100644 --- a/test/src/evaluation.spec.ts +++ b/test/src/evaluation.spec.ts @@ -353,7 +353,7 @@ describe('Evaluation specs', function () { const windowHandle = await page.evaluateHandle(() => { return window; }); - const errorText = await windowHandle.jsonValue().catch(error_ => { + const errorText = await windowHandle.jsonValue().catch(error_ => { return error_.message; }); const error = await page diff --git a/test/src/jshandle.spec.ts b/test/src/jshandle.spec.ts index 0bbd1c39..7147fffb 100644 --- a/test/src/jshandle.spec.ts +++ b/test/src/jshandle.spec.ts @@ -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>(); + 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(); + 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(); - 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'); }