feat(JSHandles): introduce JSHandles (#943)

This patch:
- introduces ExecutionContext class that incapsulates javascript
  execution context. An examples of execution contexts are workers and
  frames
- introduces JSHandle that holds a references to the javascript
  object in ExecutionContext
- inherits ElementHandle from JSHandle

Fixes #382.
This commit is contained in:
Andrey Lushnikov 2017-10-06 15:35:02 -07:00 committed by GitHub
parent 59bcc2ee56
commit 0d0f9b7984
8 changed files with 654 additions and 112 deletions

View File

@ -44,6 +44,7 @@
+ [page.emulate(options)](#pageemulateoptions) + [page.emulate(options)](#pageemulateoptions)
+ [page.emulateMedia(mediaType)](#pageemulatemediamediatype) + [page.emulateMedia(mediaType)](#pageemulatemediamediatype)
+ [page.evaluate(pageFunction, ...args)](#pageevaluatepagefunction-args) + [page.evaluate(pageFunction, ...args)](#pageevaluatepagefunction-args)
+ [page.evaluateHandle(pageFunction, ...args)](#pageevaluatehandlepagefunction-args)
+ [page.evaluateOnNewDocument(pageFunction, ...args)](#pageevaluateonnewdocumentpagefunction-args) + [page.evaluateOnNewDocument(pageFunction, ...args)](#pageevaluateonnewdocumentpagefunction-args)
+ [page.exposeFunction(name, puppeteerFunction)](#pageexposefunctionname-puppeteerfunction) + [page.exposeFunction(name, puppeteerFunction)](#pageexposefunctionname-puppeteerfunction)
+ [page.focus(selector)](#pagefocusselector) + [page.focus(selector)](#pagefocusselector)
@ -112,6 +113,7 @@
+ [frame.addStyleTag(url)](#frameaddstyletagurl) + [frame.addStyleTag(url)](#frameaddstyletagurl)
+ [frame.childFrames()](#framechildframes) + [frame.childFrames()](#framechildframes)
+ [frame.evaluate(pageFunction, ...args)](#frameevaluatepagefunction-args) + [frame.evaluate(pageFunction, ...args)](#frameevaluatepagefunction-args)
+ [frame.executionContext()](#frameexecutioncontext)
+ [frame.injectFile(filePath)](#frameinjectfilefilepath) + [frame.injectFile(filePath)](#frameinjectfilefilepath)
+ [frame.isDetached()](#frameisdetached) + [frame.isDetached()](#frameisdetached)
+ [frame.name()](#framename) + [frame.name()](#framename)
@ -121,11 +123,28 @@
+ [frame.waitFor(selectorOrFunctionOrTimeout[, options[, ...args]])](#framewaitforselectororfunctionortimeout-options-args) + [frame.waitFor(selectorOrFunctionOrTimeout[, options[, ...args]])](#framewaitforselectororfunctionortimeout-options-args)
+ [frame.waitForFunction(pageFunction[, options[, ...args]])](#framewaitforfunctionpagefunction-options-args) + [frame.waitForFunction(pageFunction[, options[, ...args]])](#framewaitforfunctionpagefunction-options-args)
+ [frame.waitForSelector(selector[, options])](#framewaitforselectorselector-options) + [frame.waitForSelector(selector[, options])](#framewaitforselectorselector-options)
* [class: ExecutionContext](#class-executioncontext)
+ [executionContext.evaluate(pageFunction, ...args)](#executioncontextevaluatepagefunction-args)
+ [executionContext.evaluateHandle(pageFunction, ...args)](#executioncontextevaluatehandlepagefunction-args)
* [class: JSHandle](#class-jshandle)
+ [jsHandle.asElement()](#jshandleaselement)
+ [jsHandle.dispose()](#jshandledispose)
+ [jsHandle.executionContext()](#jshandleexecutioncontext)
+ [jsHandle.getProperties()](#jshandlegetproperties)
+ [jsHandle.getProperty(propertyName)](#jshandlegetpropertypropertyname)
+ [jsHandle.jsonValue()](#jshandlejsonvalue)
+ [jsHandle.toString()](#jshandletostring)
* [class: ElementHandle](#class-elementhandle) * [class: ElementHandle](#class-elementhandle)
+ [elementHandle.asElement()](#elementhandleaselement)
+ [elementHandle.click([options])](#elementhandleclickoptions) + [elementHandle.click([options])](#elementhandleclickoptions)
+ [elementHandle.dispose()](#elementhandledispose) + [elementHandle.dispose()](#elementhandledispose)
+ [elementHandle.executionContext()](#elementhandleexecutioncontext)
+ [elementHandle.getProperties()](#elementhandlegetproperties)
+ [elementHandle.getProperty(propertyName)](#elementhandlegetpropertypropertyname)
+ [elementHandle.hover()](#elementhandlehover) + [elementHandle.hover()](#elementhandlehover)
+ [elementHandle.jsonValue()](#elementhandlejsonvalue)
+ [elementHandle.tap()](#elementhandletap) + [elementHandle.tap()](#elementhandletap)
+ [elementHandle.toString()](#elementhandletostring)
+ [elementHandle.uploadFile(...filePaths)](#elementhandleuploadfilefilepaths) + [elementHandle.uploadFile(...filePaths)](#elementhandleuploadfilefilepaths)
* [class: Request](#class-request) * [class: Request](#class-request)
+ [request.abort()](#requestabort) + [request.abort()](#requestabort)
@ -352,7 +371,7 @@ Shortcut for [page.mainFrame().$$(selector)](#frameselector-1).
This method runs `document.querySelector` within the page and passes it as the first argument to `pageFunction`. If there's no element matching `selector`, the method throws an error. This method runs `document.querySelector` within the page and passes it as the first argument to `pageFunction`. If there's no element matching `selector`, the method throws an error.
If `pageFunction` returns a [Promise], then `page.$eval` would wait for the promise to resolve and return it's value. If `pageFunction` returns a [Promise], then `page.$eval` would wait for the promise to resolve and return its value.
Examples: Examples:
```js ```js
@ -475,7 +494,7 @@ List of all available devices is available in the source code: [DeviceDescriptor
- `...args` <...[Serializable]|[ElementHandle]> Arguments to pass to `pageFunction` - `...args` <...[Serializable]|[ElementHandle]> Arguments to pass to `pageFunction`
- returns: <[Promise]<[Serializable]>> Resolves to the return value of `pageFunction` - returns: <[Promise]<[Serializable]>> Resolves to the return value of `pageFunction`
If the function, passed to the `page.evaluate`, returns a [Promise], then `page.evaluate` would wait for the promise to resolve and return it's value. If the function, passed to the `page.evaluate`, returns a [Promise], then `page.evaluate` would wait for the promise to resolve and return its value.
```js ```js
const result = await page.evaluate(() => { const result = await page.evaluate(() => {
@ -499,6 +518,35 @@ await bodyHandle.dispose();
Shortcut for [page.mainFrame().evaluate(pageFunction, ...args)](#frameevaluatepagefunction-args). Shortcut for [page.mainFrame().evaluate(pageFunction, ...args)](#frameevaluatepagefunction-args).
#### page.evaluateHandle(pageFunction, ...args)
- `pageFunction` <[function]|[string]> Function to be evaluated in the page context
- `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction`
- returns: <[Promise]<[JSHandle]>> Resolves to the return value of `pageFunction`
If the function, passed to the `page.evaluateHandle`, returns a [Promise], then `page.evaluateHandle` would wait for the promise to resolve and return its value.
```js
const aWindowHandle = await page.evaluateHandle(() => Promise.resolve(window));
aWindowHandle; // Handle for the window object.
```
A string can also be passed in instead of a function.
```js
const aHandle = await page.evaluateHandle('document'); // Handle for the 'document'.
```
[JSHandle] instances could be passed as arguments to the `page.evaluateHandle`:
```js
const aHandle = await page.evaluateHandle(() => document.body);
const resultHandle = await page.evaluateHandle(body => body.innerHTML, aHandle);
console.log(await resultHandle.jsonValue());
await resultHandle.dispose();
```
Shortcut for [page.mainFrame().executionContext().evaluateHandle(pageFunction, ...args)](#frameobjectpagefunction-args).
#### page.evaluateOnNewDocument(pageFunction, ...args) #### page.evaluateOnNewDocument(pageFunction, ...args)
- `pageFunction` <[function]|[string]> Function to be evaluated in browser context - `pageFunction` <[function]|[string]> Function to be evaluated in browser context
- `...args` <...[Serializable]> Arguments to pass to `pageFunction` - `...args` <...[Serializable]> Arguments to pass to `pageFunction`
@ -1171,7 +1219,7 @@ The method runs `document.querySelectorAll` within the frame. If no elements mat
This method runs `document.querySelector` within the frame and passes it as the first argument to `pageFunction`. If there's no element matching `selector`, the method throws an error. This method runs `document.querySelector` within the frame and passes it as the first argument to `pageFunction`. If there's no element matching `selector`, the method throws an error.
If `pageFunction` returns a [Promise], then `frame.$eval` would wait for the promise to resolve and return it's value. If `pageFunction` returns a [Promise], then `frame.$eval` would wait for the promise to resolve and return its value.
Examples: Examples:
```js ```js
@ -1200,7 +1248,7 @@ Adds a `<link rel="stylesheet">` tag to the frame with the desired url.
- `...args` <...[Serializable]|[ElementHandle]> Arguments to pass to `pageFunction` - `...args` <...[Serializable]|[ElementHandle]> Arguments to pass to `pageFunction`
- returns: <[Promise]<[Serializable]>> Promise which resolves to function return value - returns: <[Promise]<[Serializable]>> Promise which resolves to function return value
If the function, passed to the `frame.evaluate`, returns a [Promise], then `frame.evaluate` would wait for the promise to resolve and return it's value. If the function, passed to the `frame.evaluate`, returns a [Promise], then `frame.evaluate` would wait for the promise to resolve and return its value.
```js ```js
const result = await frame.evaluate(() => { const result = await frame.evaluate(() => {
@ -1222,6 +1270,9 @@ const html = await frame.evaluate(body => body.innerHTML, bodyHandle);
await bodyHandle.dispose(); await bodyHandle.dispose();
``` ```
#### frame.executionContext()
- returns: <[ExecutionContext]> Execution context associated with this frame.
#### frame.injectFile(filePath) #### frame.injectFile(filePath)
- `filePath` <[string]> Path to the JavaScript file to be injected into frame. If `filePath` is a relative path, then it is resolved relative to [current working directory](https://nodejs.org/api/process.html#process_process_cwd). - `filePath` <[string]> Path to the JavaScript file to be injected into frame. If `filePath` is a relative path, then it is resolved relative to [current working directory](https://nodejs.org/api/process.html#process_process_cwd).
- returns: <[Promise]> Promise which resolves when file gets successfully evaluated in frame. - returns: <[Promise]> Promise which resolves when file gets successfully evaluated in frame.
@ -1314,8 +1365,129 @@ puppeteer.launch().then(async browser => {
}); });
``` ```
### class: ExecutionContext
The class represents a context for JavaScript execution. Examples of JavaScript contexts are:
- each [frame](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe) has a separate execution context
- all kind of [workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) have their own contexts
#### executionContext.evaluate(pageFunction, ...args)
- `pageFunction` <[function]|[string]> Function to be evaluated in browser context
- `...args` <...[Serializable]|[ElementHandle]> Arguments to pass to `pageFunction`
- returns: <[Promise]<[Serializable]>> Promise which resolves to function return value
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.
```js
const result = await executionContext.evaluate(() => Promise.resolve(8 * 7));
console.log(result); // prints "56"
```
A string can also be passed in instead of a function.
```js
console.log(await executionContext.evaluate('1 + 2')); // prints "3"
```
[JSHandle] instances can be passed as arguments to the `frame.evaluate`:
```js
const oneHandle = await executionContext.evaluateHandle(() => 1);
const twoHandle = await executionContext.evaluateHandle(() => 2);
const result = await executionContext.evaluate((a, b) => a + b, oneHandle, twoHandle);
await oneHandle.dispose();
await twoHandle.dispose();
console.log(result); // prints '3'.
```
#### executionContext.evaluateHandle(pageFunction, ...args)
- `pageFunction` <[function]|[string]> Function to be evaluated in the page context
- `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction`
- returns: <[Promise]<[JSHandle]>> Resolves to the return value of `pageFunction`
If the function, passed to the `executionContext.evaluateHandle`, returns a [Promise], then `executionContext.evaluteHandle` would wait for the promise to resolve and return its value.
```js
const aHandle = await context.evaluateHandle(() => Promise.resolve(self));
aHandle; // Handle for the global object.
```
A string can also be passed in instead of a function.
```js
const aHandle = await context.evaluateHandle('1 + 2'); // Handle for the '3' object.
```
[JSHandle] instances could be passed as arguments to the `executionContext.evaluateHandle`:
```js
const context = page.mainFrame().executionContext();
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();
```
### class: JSHandle
JSHandle represents an in-page javascript object. JSHandles could be created with the [page.evaluateHandle](#pageobjectpagefunction-args) method.
```js
await windowHandle = await page.evaluateHandle(() => window);
// ...
```
JSHandle prevents references javascript objects from garbage collection unless the handle is [disposed](#objecthandledispose). JSHandles are auto-disposed when their origin frame gets navigated or the parent context gets destroyed.
JSHandle instances can be used as arguments in [`page.$eval()`](#pageevalselector-pagefunction-args), [`page.evaluate()`](#pageevaluatepagefunction-args) and [`page.evaluateHandle`](#pageobjectpagefunction-args) methods.
#### jsHandle.asElement()
- returns: <[ElementHandle]>
Returns either `null` or the object handle itself, if the object handle is an instance of [ElementHandle].
#### jsHandle.dispose()
- returns: <[Promise]> Promise which resolves when the object handle is successfully disposed.
The `jsHandle.dispose` method stops referencing the element handle.
#### jsHandle.executionContext()
- returns: [ExecutionContext]
Returns execution context the handle belongs to.
#### jsHandle.getProperties()
- returns: <[Promise]<[Map]<[string], [JSHandle]>>>
The method returns a map with property names as keys and JSHandle instances for the property values.
```js
const handle = await page.evaluateHandle(() => {window, document});
const properties = await handle.getProperties();
const windowHandle = properties.get('window');
const documentHandle = properties.get('document');
await handle.dispose();
```
#### jsHandle.getProperty(propertyName)
- `propertyName` <[string]> property to get
- returns: <[Promise]<[JSHandle]>>
Fetches a single property from the referenced object.
#### jsHandle.jsonValue()
- returns: <[Promise]<[Object]>>
Returns a JSON representation of the object. 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 will throw if the referenced object is not stringifiable.
#### jsHandle.toString()
- returns: <[string]>
### class: ElementHandle ### class: ElementHandle
> **NOTE** Class [ElementHandle] extends [JSHandle].
ElementHandle represents an in-page DOM element. ElementHandles could be created with the [page.$](#pageselector) method. ElementHandle represents an in-page DOM element. ElementHandles could be created with the [page.$](#pageselector) method.
```js ```js
@ -1334,6 +1506,9 @@ ElementHandle prevents DOM element from garbage collection unless the handle is
ElementHandle instances can be used as arguments in [`page.$eval()`](#pageevalselector-pagefunction-args) and [`page.evaluate()`](#pageevaluatepagefunction-args) methods. ElementHandle instances can be used as arguments in [`page.$eval()`](#pageevalselector-pagefunction-args) and [`page.evaluate()`](#pageevaluatepagefunction-args) methods.
#### elementHandle.asElement()
- returns: <[ElementHandle]>
#### elementHandle.click([options]) #### elementHandle.click([options])
- `options` <[Object]> - `options` <[Object]>
- `button` <[string]> `left`, `right`, or `middle`, defaults to `left`. - `button` <[string]> `left`, `right`, or `middle`, defaults to `left`.
@ -1349,18 +1524,54 @@ If the element is detached from DOM, the method throws an error.
The `elementHandle.dispose` method stops referencing the element handle. The `elementHandle.dispose` method stops referencing the element handle.
#### elementHandle.executionContext()
- returns: [ExecutionContext]
#### elementHandle.getProperties()
- returns: <[Promise]<[Map]<[string], [JSHandle]>>>
The method returns a map with property names as keys and JSHandle instances for the property values.
```js
const listHandle = await page.evaluateHandle(() => document.body.children);
const properties = await containerHandle.getProperties();
const children = [];
for (const property of properties.values()) {
const element = property.asElement();
if (element)
children.push(element);
}
children; // holds elementHandles to all children of document.body
```
#### elementHandle.getProperty(propertyName)
- `propertyName` <[string]> property to get
- returns: <[Promise]<[JSHandle]>>
Fetches a single property from the objectHandle.
#### elementHandle.hover() #### elementHandle.hover()
- returns: <[Promise]> Promise which resolves when the element is successfully hovered. - returns: <[Promise]> Promise which resolves when the element is successfully hovered.
This method scrolls element into view if needed, and then uses [page.mouse](#pagemouse) to hover over the center of the element. This method scrolls element into view if needed, and then uses [page.mouse](#pagemouse) to hover over the center of the element.
If the element is detached from DOM, the method throws an error. If the element is detached from DOM, the method throws an error.
#### elementHandle.jsonValue()
- returns: <[Promise]<[Object]>>
Returns a JSON representation of the object. 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 will throw if the referenced object is not stringifiable.
#### elementHandle.tap() #### elementHandle.tap()
- returns: <[Promise]> Promise which resolves when the element is successfully tapped. Promise gets rejected if the element is detached from DOM. - returns: <[Promise]> Promise which resolves when the element is successfully tapped. Promise gets rejected if the element is detached from DOM.
This method scrolls element into view if needed, and then uses [touchscreen.tap](#touchscreentapx-y) to tap in the center of the element. This method scrolls element into view if needed, and then uses [touchscreen.tap](#touchscreentapx-y) to tap in the center of the element.
If the element is detached from DOM, the method throws an error. If the element is detached from DOM, the method throws an error.
#### elementHandle.toString()
- returns: <[string]>
#### elementHandle.uploadFile(...filePaths) #### elementHandle.uploadFile(...filePaths)
- `...filePaths` <...[string]> Sets the value of the file input these paths. If some of the `filePaths` are relative paths, then they are resolved relative to [current working directory](https://nodejs.org/api/process.html#process_process_cwd). - `...filePaths` <...[string]> Sets the value of the file input these paths. If some of the `filePaths` are relative paths, then they are resolved relative to [current working directory](https://nodejs.org/api/process.html#process_process_cwd).
- returns: <[Promise]> - returns: <[Promise]>
@ -1479,6 +1690,8 @@ Contains the URL of the response.
[Element]: https://developer.mozilla.org/en-US/docs/Web/API/element "Element" [Element]: https://developer.mozilla.org/en-US/docs/Web/API/element "Element"
[Keyboard]: #class-keyboard "Keyboard" [Keyboard]: #class-keyboard "Keyboard"
[Dialog]: #class-dialog "Dialog" [Dialog]: #class-dialog "Dialog"
[JSHandle]: #class-objecthandle "JSHandle"
[ExecutionContext]: #class-executioncontext "ExecutionContext"
[Mouse]: #class-mouse "Mouse" [Mouse]: #class-mouse "Mouse"
[Map]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map "Map" [Map]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map "Map"
[selector]: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors "selector" [selector]: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors "selector"

View File

@ -14,55 +14,50 @@
* limitations under the License. * limitations under the License.
*/ */
const path = require('path'); const path = require('path');
const {JSHandle} = require('./ExecutionContext');
const {helper} = require('./helper'); const {helper} = require('./helper');
class ElementHandle { class ElementHandle extends JSHandle {
/** /**
* @param {!Frame} frame * @param {!ExecutionContext} context
* @param {!Connection} client * @param {!Session} client
* @param {!Object} remoteObject * @param {!Object} remoteObject
* @param {!Mouse} mouse * @param {!Mouse} mouse
* @param {!Touchscreen} touchscreen; * @param {!Touchscreen} touchscreen;
*/ */
constructor(frame, client, remoteObject, mouse, touchscreen) { constructor(context, client, remoteObject, mouse, touchscreen) {
this._frame = frame; super(context, client, remoteObject);
this._client = client;
this._remoteObject = remoteObject;
this._mouse = mouse; this._mouse = mouse;
this._touchscreen = touchscreen; this._touchscreen = touchscreen;
this._disposed = false;
} }
/** /**
* @return {?string} * @override
* @return {?ElementHandle}
*/ */
_remoteObjectId() { asElement() {
return this._disposed ? null : this._remoteObject.objectId; return this;
}
async dispose() {
if (this._disposed)
return;
this._disposed = true;
await helper.releaseObject(this._client, this._remoteObject);
} }
/** /**
* @return {!Promise<{x: number, y: number}>} * @return {!Promise<{x: number, y: number}>}
*/ */
async _visibleCenter() { async _visibleCenter() {
const center = await this._frame.evaluate(element => { const {center, error} = await this.executionContext().evaluate(element => {
if (!element.ownerDocument.contains(element)) if (!element.ownerDocument.contains(element))
return null; return {center: null, error: 'Node is detached from document'};
if (element.nodeType !== HTMLElement.ELEMENT_NODE)
return {center: null, error: 'Node is not of type HTMLElement'};
element.scrollIntoViewIfNeeded(); element.scrollIntoViewIfNeeded();
const rect = element.getBoundingClientRect(); const rect = element.getBoundingClientRect();
return { const center = {
x: (Math.max(rect.left, 0) + Math.min(rect.right, window.innerWidth)) / 2, x: (Math.max(rect.left, 0) + Math.min(rect.right, window.innerWidth)) / 2,
y: (Math.max(rect.top, 0) + Math.min(rect.bottom, window.innerHeight)) / 2 y: (Math.max(rect.top, 0) + Math.min(rect.bottom, window.innerHeight)) / 2
}; };
return {center, error: null};
}, this); }, this);
if (!center) if (error)
throw new Error('No node found for selector: ' + selector); throw new Error(error);
return center; return center;
} }

192
lib/ExecutionContext.js Normal file
View File

@ -0,0 +1,192 @@
/**
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const {helper} = require('./helper');
class ExecutionContext {
/**
* @param {!Session} client
* @param {string} contextId
* @param {function(*):!JSHandle} objectHandleFactory
*/
constructor(client, contextId, objectHandleFactory) {
this._client = client;
this._contextId = contextId;
this._objectHandleFactory = objectHandleFactory;
}
/**
* @param {function()|string} pageFunction
* @param {!Array<*>} args
* @return {!Promise<(!Object|undefined)>}
*/
async evaluate(pageFunction, ...args) {
const handle = await this.evaluateHandle(pageFunction, ...args);
const result = await handle.jsonValue();
await handle.dispose();
return result;
}
/**
* @param {function()|string} pageFunction
* @param {!Array<*>} args
* @return {!Promise<!JSHandle>}
*/
async evaluateHandle(pageFunction, ...args) {
if (helper.isString(pageFunction)) {
const contextId = this._contextId;
const expression = pageFunction;
const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.evaluate', { expression, contextId, returnByValue: false, awaitPromise: true});
if (exceptionDetails)
throw new Error('Evaluation failed: ' + helper.getExceptionMessage(exceptionDetails));
return this._objectHandleFactory(remoteObject);
}
const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.callFunctionOn', {
functionDeclaration: pageFunction.toString(),
executionContextId: this._contextId,
arguments: args.map(convertArgument.bind(this)),
returnByValue: false,
awaitPromise: true
});
if (exceptionDetails)
throw new Error('Evaluation failed: ' + helper.getExceptionMessage(exceptionDetails));
return this._objectHandleFactory(remoteObject);
/**
* @param {*} arg
* @return {*}
* @this {Frame}
*/
function convertArgument(arg) {
if (Object.is(arg, -0))
return { unserializableValue: '-0' };
if (Object.is(arg, Infinity))
return { unserializableValue: 'Infinity' };
if (Object.is(arg, -Infinity))
return { unserializableValue: '-Infinity' };
if (Object.is(arg, NaN))
return { unserializableValue: 'NaN' };
const objectHandle = arg && (arg instanceof JSHandle) ? arg : null;
if (objectHandle) {
if (objectHandle._context !== this)
throw new Error('JSHandles can be evaluated only in the context they were created!');
if (objectHandle._disposed)
throw new Error('JSHandle is disposed!');
if (objectHandle._remoteObject.unserializableValue)
return { unserializableValue: objectHandle._remoteObject.unserializableValue };
if (!objectHandle._remoteObject.objectId)
return { value: objectHandle._remoteObject.value };
return { objectId: objectHandle._remoteObject.objectId };
}
return { value: arg };
}
}
}
class JSHandle {
/**
* @param {!ExecutionContext} context
* @param {!Session} client
* @param {!Object} remoteObject
*/
constructor(context, client, remoteObject) {
this._context = context;
this._client = client;
this._remoteObject = remoteObject;
this._disposed = false;
}
/**
* @return {!ExecutionContext}
*/
executionContext() {
return this._context;
}
/**
* @param {string} propertyName
* @return {!Promise<?JSHandle>}
*/
async getProperty(propertyName) {
const objectHandle = await this._context.evaluateHandle((object, propertyName) => {
const result = {__proto__: null};
result[propertyName] = object[propertyName];
return result;
}, this, propertyName);
const properties = await objectHandle.getProperties();
const result = properties.get(propertyName) || null;
await objectHandle.dispose();
return result;
}
/**
* @return {!Property<Map<string, !ObjectHandle>>}
*/
async getProperties() {
const response = await this._client.send('Runtime.getProperties', {
objectId: this._remoteObject.objectId,
ownProperties: true
});
const result = new Map();
for (const property of response.result) {
if (!property.enumerable)
continue;
result.set(property.name, this._context._objectHandleFactory(property.value));
}
return result;
}
/**
* @return {!Promise<?Object>}
*/
async jsonValue() {
if (this._remoteObject.objectId) {
const jsonString = await this._context.evaluate(object => JSON.stringify(object), this);
return JSON.parse(jsonString);
}
return helper.valueFromRemoteObject(this._remoteObject);
}
/**
* @return {?ElementHandle}
*/
asElement() {
return null;
}
async dispose() {
if (this._disposed)
return;
this._disposed = true;
await helper.releaseObject(this._client, this._remoteObject);
}
/**
* @override
* @return {string}
*/
toString() {
if (this._remoteObject.objectId) {
const type = this._remoteObject.subtype || this._remoteObject.type;
return 'JSHandle@' + type;
}
return helper.valueFromRemoteObject(this._remoteObject) + '';
}
}
helper.tracePublicAPI(JSHandle);
module.exports = {ExecutionContext, JSHandle};

View File

@ -17,6 +17,7 @@
const fs = require('fs'); const fs = require('fs');
const EventEmitter = require('events'); const EventEmitter = require('events');
const {helper} = require('./helper'); const {helper} = require('./helper');
const {ExecutionContext, JSHandle} = require('./ExecutionContext');
const ElementHandle = require('./ElementHandle'); const ElementHandle = require('./ElementHandle');
class FrameManager extends EventEmitter { class FrameManager extends EventEmitter {
@ -33,6 +34,8 @@ class FrameManager extends EventEmitter {
this._touchscreen = touchscreen; this._touchscreen = touchscreen;
/** @type {!Map<string, !Frame>} */ /** @type {!Map<string, !Frame>} */
this._frames = new Map(); this._frames = new Map();
/** @type {!Map<string, !ExecutionContext>} */
this._contextIdToContext = new Map();
this._client.on('Page.frameAttached', event => this._onFrameAttached(event.frameId, event.parentFrameId)); this._client.on('Page.frameAttached', event => this._onFrameAttached(event.frameId, event.parentFrameId));
this._client.on('Page.frameNavigated', event => this._onFrameNavigated(event.frame)); this._client.on('Page.frameNavigated', event => this._onFrameNavigated(event.frame));
@ -112,16 +115,36 @@ class FrameManager extends EventEmitter {
this._removeFramesRecursively(frame); this._removeFramesRecursively(frame);
} }
_onExecutionContextCreated(context) { _onExecutionContextCreated(contextPayload) {
const frameId = context.auxData && context.auxData.isDefault ? context.auxData.frameId : null; const context = new ExecutionContext(this._client, contextPayload.id, this.createJSHandle.bind(this, contextPayload.id));
this._contextIdToContext.set(contextPayload.id, context);
const frameId = contextPayload.auxData && contextPayload.auxData.isDefault ? contextPayload.auxData.frameId : null;
const frame = this._frames.get(frameId); const frame = this._frames.get(frameId);
if (!frame) if (!frame)
return; return;
frame._defaultContextId = context.id; frame._context = context;
for (const waitTask of frame._waitTasks) for (const waitTask of frame._waitTasks)
waitTask.rerun(); waitTask.rerun();
} }
_onExecutionContextDestroyed(contextPayload) {
this._contextIdToContext.delete(contextPayload.id);
}
/**
* @param {string} contextId
* @param {*} remoteObject
* @return {!JSHandle}
*/
createJSHandle(contextId, remoteObject) {
const context = this._contextIdToContext.get(contextId);
console.assert(context, 'INTERNAL ERROR: missing context with id = ' + contextId);
if (remoteObject.subtype === 'node')
return new ElementHandle(context, this._client, remoteObject, this._mouse, this._touchscreen);
return new JSHandle(context, this._client, remoteObject);
}
/** /**
* @param {!Frame} frame * @param {!Frame} frame
*/ */
@ -167,7 +190,7 @@ class Frame {
this._parentFrame = parentFrame; this._parentFrame = parentFrame;
this._url = ''; this._url = '';
this._id = frameId; this._id = frameId;
this._defaultContextId = '<not-initialized>'; this._context = null;
/** @type {!Set<!WaitTask>} */ /** @type {!Set<!WaitTask>} */
this._waitTasks = new Set(); this._waitTasks = new Set();
@ -177,14 +200,20 @@ class Frame {
this._parentFrame._childFrames.add(this); this._parentFrame._childFrames.add(this);
} }
/**
* @return {!ExecutionContext}
*/
executionContext() {
return this._context;
}
/** /**
* @param {function()|string} pageFunction * @param {function()|string} pageFunction
* @param {!Array<*>} args * @param {!Array<*>} args
* @return {!Promise<(!Object|undefined)>} * @return {!Promise<(!Object|undefined)>}
*/ */
async evaluate(pageFunction, ...args) { async evaluate(pageFunction, ...args) {
const remoteObject = await this._rawEvaluate(pageFunction, ...args); return this._context.evaluate(pageFunction, ...args);
return await helper.serializeRemoteObject(this._client, remoteObject);
} }
/** /**
@ -192,10 +221,11 @@ class Frame {
* @return {!Promise<?ElementHandle>} * @return {!Promise<?ElementHandle>}
*/ */
async $(selector) { async $(selector) {
const remoteObject = await this._rawEvaluate(selector => document.querySelector(selector), selector); const handle = await this._context.evaluateHandle(selector => document.querySelector(selector), selector);
if (remoteObject.subtype === 'node') const element = handle.asElement();
return new ElementHandle(this, this._client, remoteObject, this._mouse, this._touchscreen); if (element)
await helper.releaseObject(this._client, remoteObject); return element;
await handle.dispose();
return null; return null;
} }
@ -220,76 +250,18 @@ class Frame {
* @return {!Promise<!Array<!ElementHandle>>} * @return {!Promise<!Array<!ElementHandle>>}
*/ */
async $$(selector) { async $$(selector) {
const remoteObject = await this._rawEvaluate(selector => Array.from(document.querySelectorAll(selector)), selector); const arrayHandle = await this._context.evaluateHandle(selector => document.querySelectorAll(selector), selector);
const response = await this._client.send('Runtime.getProperties', { const properties = await arrayHandle.getProperties();
objectId: remoteObject.objectId, await arrayHandle.dispose();
ownProperties: true
});
const properties = response.result;
const result = []; const result = [];
const releasePromises = [helper.releaseObject(this._client, remoteObject)]; for (const property of properties.values()) {
for (const property of properties) { const elementHandle = property.asElement();
if (property.enumerable && property.value.subtype === 'node') if (elementHandle)
result.push(new ElementHandle(this, this._client, property.value, this._mouse, this._touchscreen)); result.push(elementHandle);
else
releasePromises.push(helper.releaseObject(this._client, property.value));
} }
await Promise.all(releasePromises);
return result; return result;
} }
/**
* @param {function()|string} pageFunction
* @param {!Array<*>} args
* @return {!Promise<(!Object|undefined)>}
*/
async _rawEvaluate(pageFunction, ...args) {
if (helper.isString(pageFunction)) {
const contextId = this._defaultContextId;
const expression = pageFunction;
const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.evaluate', { expression, contextId, returnByValue: false, awaitPromise: true});
if (exceptionDetails)
throw new Error('Evaluation failed: ' + helper.getExceptionMessage(exceptionDetails));
return remoteObject;
}
const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.callFunctionOn', {
functionDeclaration: pageFunction.toString(),
executionContextId: this._defaultContextId,
arguments: args.map(convertArgument.bind(this)),
returnByValue: false,
awaitPromise: true
});
if (exceptionDetails)
throw new Error('Evaluation failed: ' + helper.getExceptionMessage(exceptionDetails));
return remoteObject;
/**
* @param {*} arg
* @return {*}
* @this {Frame}
*/
function convertArgument(arg) {
if (Object.is(arg, -0))
return { unserializableValue: '-0' };
if (Object.is(arg, Infinity))
return { unserializableValue: 'Infinity' };
if (Object.is(arg, -Infinity))
return { unserializableValue: '-Infinity' };
if (Object.is(arg, NaN))
return { unserializableValue: 'NaN' };
if (arg instanceof ElementHandle) {
if (arg._frame !== this)
throw new Error('ElementHandles passed as arguments should belong to the frame that does evaluation');
const objectId = arg._remoteObjectId();
if (!objectId)
throw new Error('ElementHandle is disposed!');
return { objectId };
}
return { value: arg };
}
}
/** /**
* @return {string} * @return {string}
*/ */

View File

@ -166,6 +166,16 @@ class Page extends EventEmitter {
return this.mainFrame().$(selector); return this.mainFrame().$(selector);
} }
/**
* @param {string} selector
* @param {function()|string} pageFunction
* @param {!Array<*>} args
* @return {!Promise<!JSHandle>}
*/
async evaluateHandle(pageFunction, ...args) {
return this.mainFrame().executionContext().evaluateHandle(pageFunction, ...args);
}
/** /**
* @param {string} selector * @param {string} selector
* @param {function()|string} pageFunction * @param {function()|string} pageFunction

View File

@ -64,7 +64,8 @@ class Helper {
* @param {!Object} remoteObject * @param {!Object} remoteObject
* @return {!Promise<!Object>} * @return {!Promise<!Object>}
*/ */
static async serializeRemoteObject(client, remoteObject) { static valueFromRemoteObject(remoteObject) {
console.assert(!remoteObject.objectId, 'Cannot extract value when objectId is given');
if (remoteObject.unserializableValue) { if (remoteObject.unserializableValue) {
switch (remoteObject.unserializableValue) { switch (remoteObject.unserializableValue) {
case '-0': case '-0':
@ -79,8 +80,17 @@ class Helper {
throw new Error('Unsupported unserializable value: ' + remoteObject.unserializableValue); throw new Error('Unsupported unserializable value: ' + remoteObject.unserializableValue);
} }
} }
return remoteObject.value;
}
/**
* @param {!Session} client
* @param {!Object} remoteObject
* @return {!Promise<!Object>}
*/
static async serializeRemoteObject(client, remoteObject) {
if (!remoteObject.objectId) if (!remoteObject.objectId)
return remoteObject.value; return Helper.valueFromRemoteObject(remoteObject);
if (remoteObject.subtype === 'promise') if (remoteObject.subtype === 'promise')
return remoteObject.description; return remoteObject.description;
try { try {

View File

@ -265,9 +265,10 @@ describe('Page', function() {
const result = await page.evaluate((a, b) => Object.is(a, undefined) && Object.is(b, 'foo'), undefined, 'foo'); const result = await page.evaluate((a, b) => Object.is(a, undefined) && Object.is(b, 'foo'), undefined, 'foo');
expect(result).toBe(true); expect(result).toBe(true);
})); }));
it('should not fail for window object', SX(async function() { it('should fail for window object', SX(async function() {
const result = await page.evaluate(() => window); let error = null;
expect(result).toBe('Window'); await page.evaluate(() => window).catch(e => error = e);
expect(error.message).toContain('Converting circular structure to JSON');
})); }));
it('should accept a string', SX(async function() { it('should accept a string', SX(async function() {
const result = await page.evaluate('1 + 2'); const result = await page.evaluate('1 + 2');
@ -294,7 +295,7 @@ describe('Page', function() {
await element.dispose(); await element.dispose();
let error = null; let error = null;
await page.evaluate(e => e.textContent, element).catch(e => error = e); await page.evaluate(e => e.textContent, element).catch(e => error = e);
expect(error.message).toContain('ElementHandle is disposed'); expect(error.message).toContain('JSHandle is disposed');
})); }));
it('should throw if elementHandles are from other frames', SX(async function() { it('should throw if elementHandles are from other frames', SX(async function() {
const FrameUtils = require('./frame-utils'); const FrameUtils = require('./frame-utils');
@ -303,7 +304,117 @@ describe('Page', function() {
let error = null; let error = null;
await page.evaluate(body => body.innerHTML, bodyHandle).catch(e => error = e); await page.evaluate(body => body.innerHTML, bodyHandle).catch(e => error = e);
expect(error).toBeTruthy(); expect(error).toBeTruthy();
expect(error.message).toContain('ElementHandles passed as arguments should belong'); expect(error.message).toContain('JSHandles can be evaluated only in the context they were created');
}));
it('should accept object handle as an argument', SX(async function() {
const navigatorHandle = await page.evaluateHandle(() => navigator);
const text = await page.evaluate(e => e.userAgent, navigatorHandle);
expect(text).toContain('Mozilla');
}));
it('should accept object handle to primitive types', SX(async function() {
const aHandle = await page.evaluateHandle(() => 5);
const isFive = await page.evaluate(e => Object.is(e, 5), aHandle);
expect(isFive).toBeTruthy();
}));
});
describe('Page.evaluateHandle', function() {
it('should work', SX(async function() {
const windowHandle = await page.evaluateHandle(() => window);
expect(windowHandle).toBeTruthy();
}));
});
describe('JSHandle.getProperty', function() {
it('should work', SX(async function() {
const aHandle = await page.evaluateHandle(() => ({
one: 1,
two: 2,
three: 3
}));
const twoHandle = await aHandle.getProperty('two');
expect(await twoHandle.jsonValue()).toEqual(2);
}));
});
describe('JSHandle.jsonValue', function() {
it('should work', SX(async function() {
const aHandle = await page.evaluateHandle(() => ({foo: 'bar'}));
const json = await aHandle.jsonValue();
expect(json).toEqual({foo: 'bar'});
}));
it('should work with dates', SX(async function() {
const dateHandle = await page.evaluateHandle(() => new Date('2017-09-26T00:00:00.000Z'));
const json = await dateHandle.jsonValue();
expect(json).toBe('2017-09-26T00:00:00.000Z');
}));
it('should throw for circular objects', SX(async function() {
const windowHandle = await page.evaluateHandle('window');
let error = null;
await windowHandle.jsonValue().catch(e => error = e);
expect(error.message).toContain('Converting circular structure to JSON');
}));
});
describe('JSHandle.getProperties', function() {
it('should work', SX(async function() {
const aHandle = await page.evaluateHandle(() => ({
foo: 'bar'
}));
const properties = await aHandle.getProperties();
const foo = properties.get('foo');
expect(foo).toBeTruthy();
expect(await foo.jsonValue()).toBe('bar');
}));
it('should return even non-own properties', SX(async function() {
const aHandle = await page.evaluateHandle(() => {
class A {
constructor() {
this.a = '1';
}
}
class B extends A {
constructor() {
super();
this.b = '2';
}
}
return new B();
});
const properties = await aHandle.getProperties();
expect(await properties.get('a').jsonValue()).toBe('1');
expect(await properties.get('b').jsonValue()).toBe('2');
}));
});
describe('JSHandle.asElement', function() {
it('should work', SX(async function() {
const aHandle = await page.evaluateHandle(() => document.body);
const element = aHandle.asElement();
expect(element).toBeTruthy();
}));
it('should return null for non-elements', SX(async function() {
const aHandle = await page.evaluateHandle(() => 2);
const element = aHandle.asElement();
expect(element).toBeFalsy();
}));
it('should return ElementHandle for TextNodes', SX(async function() {
await page.setContent('<div>ee!</div>');
const aHandle = await page.evaluateHandle(() => document.querySelector('div').firstChild);
const element = aHandle.asElement();
expect(element).toBeTruthy();
expect(await page.evaluate(e => e.nodeType === HTMLElement.TEXT_NODE, element));
}));
});
describe('JSHandle.toString', function() {
it('should work for primitives', SX(async function() {
const aHandle = await page.evaluateHandle(() => 2);
expect(aHandle.toString()).toBe('2');
}));
it('should work for complicated objects', SX(async function() {
const aHandle = await page.evaluateHandle(() => window);
expect(aHandle.toString()).toBe('JSHandle@object');
})); }));
}); });
@ -322,6 +433,30 @@ describe('Page', function() {
})); }));
}); });
describe('Frame.context', function() {
const FrameUtils = require('./frame-utils');
it('should work', SX(async function() {
await page.goto(EMPTY_PAGE);
await FrameUtils.attachFrame(page, 'frame1', EMPTY_PAGE);
expect(page.frames().length).toBe(2);
const [frame1, frame2] = page.frames();
expect(frame1.executionContext()).toBeTruthy();
expect(frame2.executionContext()).toBeTruthy();
expect(frame1.executionContext() !== frame2.executionContext()).toBeTruthy();
await Promise.all([
frame1.executionContext().evaluate(() => window.a = 1),
frame2.executionContext().evaluate(() => window.a = 2)
]);
const [a1, a2] = await Promise.all([
frame1.executionContext().evaluate(() => window.a),
frame2.executionContext().evaluate(() => window.a)
]);
expect(a1).toBe(1);
expect(a2).toBe(2);
}));
});
describe('Frame.evaluate', function() { describe('Frame.evaluate', function() {
const FrameUtils = require('./frame-utils'); const FrameUtils = require('./frame-utils');
it('should have different execution contexts', SX(async function() { it('should have different execution contexts', SX(async function() {
@ -1241,9 +1376,24 @@ describe('Page', function() {
it('should work', SX(async function() { it('should work', SX(async function() {
await page.goto(PREFIX + '/input/button.html'); await page.goto(PREFIX + '/input/button.html');
const button = await page.$('button'); const button = await page.$('button');
await button.click('button'); await button.click();
expect(await page.evaluate(() => result)).toBe('Clicked'); expect(await page.evaluate(() => result)).toBe('Clicked');
})); }));
it('should work for TextNodes', SX(async function() {
await page.goto(PREFIX + '/input/button.html');
const buttonTextNode = await page.evaluateHandle(() => document.querySelector('button').firstChild);
let error = null;
await buttonTextNode.click().catch(err => error = err);
expect(error.message).toBe('Node is not of type HTMLElement');
}));
it('should throw for detached nodes', SX(async function() {
await page.goto(PREFIX + '/input/button.html');
const button = await page.$('button');
await page.evaluate(button => button.remove(), button);
let error = null;
await button.click().catch(err => error = err);
expect(error.message).toBe('Node is detached from document');
}));
}); });
describe('ElementHandle.hover', function() { describe('ElementHandle.hover', function() {

View File

@ -59,8 +59,8 @@ class MDOutline {
let actualText = element.firstChild.textContent; let actualText = element.firstChild.textContent;
let angleIndex = actualText.indexOf('<'); let angleIndex = actualText.indexOf('<');
let spaceIndex = actualText.indexOf(' '); let spaceIndex = actualText.indexOf(' ');
angleIndex = angleIndex === -1 ? angleText.length : angleIndex; angleIndex = angleIndex === -1 ? actualText.length : angleIndex;
spaceIndex = spaceIndex === -1 ? spaceIndex.length : spaceIndex + 1; spaceIndex = spaceIndex === -1 ? actualText.length : spaceIndex + 1;
actualText = actualText.substring(0, Math.min(angleIndex, spaceIndex)); actualText = actualText.substring(0, Math.min(angleIndex, spaceIndex));
if (actualText !== expectedText) if (actualText !== expectedText)
errors.push(`${member.name} has mistyped 'return' type declaration: expected exactly '${expectedText}', found '${actualText}'.`); errors.push(`${member.name} has mistyped 'return' type declaration: expected exactly '${expectedText}', found '${actualText}'.`);