page.evaluate takes a string in addition to function (#135)

This patch improves on page.evaluate to accept a string.
The string can have a trailing '//# sourceURL=' comment which would
name the evaluation to make stacks beautiful.

In order to make sourceURL comments possible, this patch:
- removes wrapping of the client function into `Promise.resolve()`
- stops passing `awaitPromise` parameter to `Runtime.evaluate`
- starts to await promise via the `Runtime.awaitPromise` if the return type of the evaluation
  is promise

closes #118
This commit is contained in:
JoelEinbinder 2017-07-27 12:23:41 -07:00 committed by Andrey Lushnikov
parent 8870aaee17
commit bbde8fd1c2
6 changed files with 62 additions and 36 deletions

View File

@ -354,14 +354,34 @@ Adds a `<script></script>` tag to the page with the desired url. Alternatively,
- returns: <[Promise]> Returns promise which resolves when page gets closed.
#### page.evaluate(pageFunction, ...args)
- `pageFunction` <[function]> Function to be evaluated in browser context
- `pageFunction` <[function]|[string]> Function to be evaluated in browser context
- `...args` <...[string]> Arguments to pass to `pageFunction`
- returns: <[Promise]<[Object]>> Promise which resolves to function return value
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.
```js
const {Browser} = require('puppeteer');
const browser = new Browser();
browser.newPage().then(async page =>
const result = await page.evaluate(() => {
return Promise.resolve().then(() => 8 * 7);
});
console.log(result); // prints "56"
browser.close();
});
```
A string can also be passed in instead of a function.
```js
console.log(await page.evaluate('1 + 2')); // prints "3"
```
This is a shortcut for [page.mainFrame().evaluate()](#frameevaluatefun-args) method.
#### page.evaluateOnNewDocument(pageFunction, ...args)
- `pageFunction` <[function]> Function to be evaluated in browser context
- `pageFunction` <[function]|[string]> Function to be evaluated in browser context
- `...args` <...[string]> Arguments to pass to `pageFunction`
- returns: <[Promise]<[Object]>> Promise which resolves to function
@ -825,23 +845,11 @@ Adds a `<script></script>` tag to the frame with the desired url. Alternatively,
- returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully clicked. Promise gets rejected if there's no element matching `selector`.
#### frame.evaluate(pageFunction, ...args)
- `pageFunction` <[function]> Function to be evaluated in browser context
- `pageFunction` <[function]|[string]> Function to be evaluated in browser context
- `...args` <...[string]> Arguments to pass to `pageFunction`
- returns: <[Promise]<[Object]>> Promise which resolves to function return value
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.
```js
const {Browser} = require('puppeteer');
const browser = new Browser();
browser.newPage().then(async page =>
const result = await page.evaluate(() => {
return Promise.resolve().then(() => 8 * 7);
});
console.log(result); // prints "56"
browser.close();
});
```
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.
#### frame.focus(selector)
- `selector` <[string]> A query selector of element to focus. If there are multiple elements satisfying the selector, the first will be focused.

View File

@ -187,24 +187,14 @@ class Frame {
}
/**
* @param {function()} pageFunction
* @param {function()|string} pageFunction
* @param {!Array<*>} args
* @return {!Promise<(!Object|undefined)>}
*/
async evaluate(pageFunction, ...args) {
return this._evaluateExpression(helper.evaluationString(pageFunction, ...args), true);
}
/**
* @param {string} expression
* @param {boolean} awaitPromise
* @return {!Promise<(!Object|undefined)>}
*/
async _evaluateExpression(expression, awaitPromise) {
let expression = helper.evaluationString(pageFunction, ...args);
const contextId = this._defaultContextId;
if (awaitPromise)
expression = `Promise.resolve(${expression})`;
let { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.evaluate', { expression, contextId, returnByValue: false, awaitPromise});
let { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.evaluate', { expression, contextId, returnByValue: false});
if (exceptionDetails)
throw new Error('Evaluation failed: ' + helper.getExceptionMessage(exceptionDetails));
return await helper.serializeRemoteObject(this._client, remoteObject);
@ -257,7 +247,7 @@ class Frame {
});
});
contents += `//# sourceURL=` + filePath.replace(/\n/g,'');
return this._evaluateExpression(contents, false);
return this.evaluate(contents);
}
/**
@ -319,7 +309,7 @@ class Frame {
return null;
return (${pageFunction})(${argsString});
})()`;
return this._evaluateExpression(expression, true);
return this.evaluate(expression);
}
/**
@ -335,7 +325,7 @@ class Frame {
let nodes = document.querySelectorAll(${JSON.stringify(selector)});
return Array.prototype.map.call(nodes, (node, index) => (${pageFunction})(${argsString}));
})()`;
return this._evaluateExpression(expression, true);
return this.evaluate(expression);
}
/**
@ -457,7 +447,7 @@ class WaitTask {
let success = false;
let error = null;
try {
success = await this._frame._evaluateExpression(this._pageScript, true);
success = await this._frame.evaluate(this._pageScript);
} catch (e) {
error = e;
}

View File

@ -345,7 +345,7 @@ class Page extends EventEmitter {
}
/**
* @param {function()} pageFunction
* @param {function()|string} pageFunction
* @param {!Array<*>} args
* @return {!Promise}
*/

View File

@ -16,11 +16,15 @@
class Helper {
/**
* @param {function()} fun
* @param {function()|string} fun
* @param {!Array<*>} args
* @return {string}
*/
static evaluationString(fun, ...args) {
if (typeof fun === 'string') {
console.assert(args.length === 0, 'Cannot evaluate a string with arguments');
return fun;
}
return `(${fun})(${args.map(x => JSON.stringify(x)).join(',')})`;
}
@ -48,6 +52,16 @@ class Helper {
* @return {!Promise<!Object>}
*/
static async serializeRemoteObject(client, remoteObject) {
if (remoteObject.subtype === 'promise') {
let response = (await client.send('Runtime.awaitPromise', {
promiseObjectId: remoteObject.objectId,
returnByValue: false
}));
Helper.releaseObject(client, remoteObject);
if (response.exceptionDetails)
throw new Error('Evaluation failed: ' + Helper.getExceptionMessage(response.exceptionDetails));
remoteObject = response.result;
}
if (remoteObject.unserializableValue) {
switch (remoteObject.unserializableValue) {
case '-0':

View File

@ -533,10 +533,12 @@ class WebPage {
}
/**
* @param {function()} fun
* @param {function()|string} fun
* @param {!Array<!Object>} args
*/
evaluate(fun, ...args) {
if (typeof fun === 'string')
fun = `(${fun})()`;
if (this._deferEvaluate)
return await(this._page.evaluateOnNewDocument(fun, ...args));
return await(this._currentFrame.evaluate(fun, ...args));

View File

@ -144,6 +144,18 @@ describe('Puppeteer', function() {
let result = await page.evaluate(() => window);
expect(result).toBe('Window');
}));
it('should accept a string', SX(async function() {
let result = await page.evaluate('1 + 2');
expect(result).toBe(3);
}));
it('should accept a string with semi colons', SX(async function() {
let result = await page.evaluate('1 + 5;');
expect(result).toBe(6);
}));
it('should accept a string with comments', SX(async function() {
let result = await page.evaluate('2 + 5;\n// do some math!');
expect(result).toBe(7);
}));
});
describe('Page.injectFile', function() {