diff --git a/docs/api.md b/docs/api.md
index 87c72050600..c7e80ab3ee1 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -44,6 +44,7 @@
+ [page.emulate(options)](#pageemulateoptions)
+ [page.emulateMedia(mediaType)](#pageemulatemediamediatype)
+ [page.evaluate(pageFunction, ...args)](#pageevaluatepagefunction-args)
+ + [page.evaluateHandle(pageFunction, ...args)](#pageevaluatehandlepagefunction-args)
+ [page.evaluateOnNewDocument(pageFunction, ...args)](#pageevaluateonnewdocumentpagefunction-args)
+ [page.exposeFunction(name, puppeteerFunction)](#pageexposefunctionname-puppeteerfunction)
+ [page.focus(selector)](#pagefocusselector)
@@ -112,6 +113,7 @@
+ [frame.addStyleTag(url)](#frameaddstyletagurl)
+ [frame.childFrames()](#framechildframes)
+ [frame.evaluate(pageFunction, ...args)](#frameevaluatepagefunction-args)
+ + [frame.executionContext()](#frameexecutioncontext)
+ [frame.injectFile(filePath)](#frameinjectfilefilepath)
+ [frame.isDetached()](#frameisdetached)
+ [frame.name()](#framename)
@@ -121,11 +123,28 @@
+ [frame.waitFor(selectorOrFunctionOrTimeout[, options[, ...args]])](#framewaitforselectororfunctionortimeout-options-args)
+ [frame.waitForFunction(pageFunction[, options[, ...args]])](#framewaitforfunctionpagefunction-options-args)
+ [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)
+ + [elementHandle.asElement()](#elementhandleaselement)
+ [elementHandle.click([options])](#elementhandleclickoptions)
+ [elementHandle.dispose()](#elementhandledispose)
+ + [elementHandle.executionContext()](#elementhandleexecutioncontext)
+ + [elementHandle.getProperties()](#elementhandlegetproperties)
+ + [elementHandle.getProperty(propertyName)](#elementhandlegetpropertypropertyname)
+ [elementHandle.hover()](#elementhandlehover)
+ + [elementHandle.jsonValue()](#elementhandlejsonvalue)
+ [elementHandle.tap()](#elementhandletap)
+ + [elementHandle.toString()](#elementhandletostring)
+ [elementHandle.uploadFile(...filePaths)](#elementhandleuploadfilefilepaths)
* [class: Request](#class-request)
+ [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.
-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:
```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`
- 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
const result = await page.evaluate(() => {
@@ -499,6 +518,35 @@ await bodyHandle.dispose();
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)
- `pageFunction` <[function]|[string]> Function to be evaluated in browser context
- `...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.
-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:
```js
@@ -1200,7 +1248,7 @@ Adds a `` tag to the frame with the desired url.
- `...args` <...[Serializable]|[ElementHandle]> Arguments to pass to `pageFunction`
- 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
const result = await frame.evaluate(() => {
@@ -1222,6 +1270,9 @@ const html = await frame.evaluate(body => body.innerHTML, bodyHandle);
await bodyHandle.dispose();
```
+#### frame.executionContext()
+- returns: <[ExecutionContext]> Execution context associated with this frame.
+
#### 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).
- 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
+> **NOTE** Class [ElementHandle] extends [JSHandle].
+
ElementHandle represents an in-page DOM element. ElementHandles could be created with the [page.$](#pageselector) method.
```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.asElement()
+- returns: <[ElementHandle]>
+
#### elementHandle.click([options])
- `options` <[Object]>
- `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.
+#### 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()
- 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.
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()
- 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.
If the element is detached from DOM, the method throws an error.
+#### elementHandle.toString()
+- returns: <[string]>
+
#### 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).
- returns: <[Promise]>
@@ -1479,6 +1690,8 @@ Contains the URL of the response.
[Element]: https://developer.mozilla.org/en-US/docs/Web/API/element "Element"
[Keyboard]: #class-keyboard "Keyboard"
[Dialog]: #class-dialog "Dialog"
+[JSHandle]: #class-objecthandle "JSHandle"
+[ExecutionContext]: #class-executioncontext "ExecutionContext"
[Mouse]: #class-mouse "Mouse"
[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"
diff --git a/lib/ElementHandle.js b/lib/ElementHandle.js
index ad4a5e41495..4239e8abb48 100644
--- a/lib/ElementHandle.js
+++ b/lib/ElementHandle.js
@@ -14,55 +14,50 @@
* limitations under the License.
*/
const path = require('path');
+const {JSHandle} = require('./ExecutionContext');
const {helper} = require('./helper');
-class ElementHandle {
+class ElementHandle extends JSHandle {
/**
- * @param {!Frame} frame
- * @param {!Connection} client
+ * @param {!ExecutionContext} context
+ * @param {!Session} client
* @param {!Object} remoteObject
* @param {!Mouse} mouse
* @param {!Touchscreen} touchscreen;
*/
- constructor(frame, client, remoteObject, mouse, touchscreen) {
- this._frame = frame;
- this._client = client;
- this._remoteObject = remoteObject;
+ constructor(context, client, remoteObject, mouse, touchscreen) {
+ super(context, client, remoteObject);
this._mouse = mouse;
this._touchscreen = touchscreen;
- this._disposed = false;
}
/**
- * @return {?string}
+ * @override
+ * @return {?ElementHandle}
*/
- _remoteObjectId() {
- return this._disposed ? null : this._remoteObject.objectId;
- }
-
- async dispose() {
- if (this._disposed)
- return;
- this._disposed = true;
- await helper.releaseObject(this._client, this._remoteObject);
+ asElement() {
+ return this;
}
/**
* @return {!Promise<{x: number, y: number}>}
*/
async _visibleCenter() {
- const center = await this._frame.evaluate(element => {
+ const {center, error} = await this.executionContext().evaluate(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();
const rect = element.getBoundingClientRect();
- return {
+ const center = {
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
};
+ return {center, error: null};
}, this);
- if (!center)
- throw new Error('No node found for selector: ' + selector);
+ if (error)
+ throw new Error(error);
return center;
}
diff --git a/lib/ExecutionContext.js b/lib/ExecutionContext.js
new file mode 100644
index 00000000000..6bcda5dfa2e
--- /dev/null
+++ b/lib/ExecutionContext.js
@@ -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}
+ */
+ 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}
+ */
+ 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