diff --git a/docs/api.md b/docs/api.md index 5ad3f39176c..986d29386f1 100644 --- a/docs/api.md +++ b/docs/api.md @@ -9,6 +9,8 @@ - [browser.newPage()](#browsernewpage) - [browser.version()](#browserversion) + [class: Page](#class-page) + - [page.$(querySelector, fun, args)](#pagequeryselector-fun-args) + - [page.$$(querySelector, fun, args)](#pagequeryselector-fun-args-1) - [page.addScriptTag(url)](#pageaddscripttagurl) - [page.close()](#pageclose) - [page.evaluate(fun, args)](#pageevaluatefun-args) @@ -104,6 +106,20 @@ browser.newPage().then(page => { ``` Pages could be closed by `page.close()` method. +#### page.$(querySelector, fun, args) + +- `querySelector` <[string]> Query selector to be run on the page +- `fun` <[function<[Element]>]> Function to be evaluated with first element matching `querySelector` +- `args` <[Array]<[string]>> Arguments to pass to `fun` +- returns: <[Promise<[Object]]> Promise which resolves to function return value. + +#### page.$$(querySelector, fun, args) + +- `querySelector` <[string]> Query selector to be run on the page +- `fun` <[function<[Element]>]> Function to be evaluted for every element matching `querySelector`. +- `args` <[Array]<[string]>> Arguments to pass to `fun` +- returns: <[Promise<[Array<[Object]>]>]> Promise which resolves to array of function return values. + #### page.addScriptTag(url) - `url` <[string]> Url of a script to be added @@ -295,3 +311,4 @@ Pages could be closed by `page.close()` method. [Page]: https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#class-page "Page" [Promise]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise "Promise" [string]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type "String" +[Element]: https://developer.mozilla.org/en-US/docs/Web/API/element "Element" diff --git a/lib/FrameManager.js b/lib/FrameManager.js index 41ad7fb1fc5..8b39de13d0d 100644 --- a/lib/FrameManager.js +++ b/lib/FrameManager.js @@ -174,31 +174,11 @@ class FrameManager extends EventEmitter { let message = await helper.getExceptionMessage(this._client, exceptionDetails); throw new Error('Evaluation failed: ' + message); } - if (remoteObject.unserializableValue) { - switch (remoteObject.unserializableValue) { - case '-0': - return -0; - case 'NaN': - return NaN; - case 'Infinity': - return Infinity; - case '-Infinity': - return -Infinity; - default: - throw new Error('Unsupported unserializable value: ' + remoteObject.unserializableValue); - } - } - if (!remoteObject.objectId) - return remoteObject.value; - let response = await this._client.send('Runtime.callFunctionOn', { - objectId: remoteObject.objectId, - functionDeclaration: 'function() { return this; }', - returnByValue: true, - }); - return response.result.value; + return helper.serializeRemoteObjet(this._client, remoteObject); } } + /** @enum {string} */ FrameManager.Events = { FrameAttached: 'frameattached', diff --git a/lib/Page.js b/lib/Page.js index ae4877e8371..a0fe9198f85 100644 --- a/lib/Page.js +++ b/lib/Page.js @@ -499,6 +499,17 @@ class Page extends EventEmitter { })).nodeId; } + /** + * @param {string} selector + * @param {!Promise>} + */ + async _querySelectorAll(selector) { + return (await this._client.send('DOM.querySelectorAll', { + nodeId: await this._rootNodeId(), + selector + })).nodeIds; + } + /** * @param {string} selector * @param {!Promise} @@ -561,6 +572,53 @@ class Page extends EventEmitter { }); } } + + /** + * @param {number} nodeId + * @param {function(!Element):T} fun + * @param {!Array<*>} args + * @return {!Promise} + */ + async _callFunctionOnNode(nodeId, fun, ...args) { + let { objectId } = (await this._client.send('DOM.resolveNode', { nodeId })).object; + let argsString = ['this'].concat(args.map(x => JSON.stringify(x))).join(','); + let {result, exceptionDetails} = await this._client.send('Runtime.callFunctionOn', { + objectId, + functionDeclaration: `function(){return (${fun})(${argsString})}`, + returnByValue: false, + }); + this._client.send('Runtime.releaseObject', {objectId}); + if (exceptionDetails) { + let message = await helper.getExceptionMessage(this._client, exceptionDetails); + throw new Error('Evaluation failed: ' + message); + } + let value = helper.serializeRemoteObjet(this._client, result); + return value; + } + + /** + * @param {string} querySelector + * @param {function(!Element):T} fun + * @param {!Array<*>} args + * @return {!Promise} + */ + async $(querySelector, fun, ...args) { + let nodeId = await this._querySelector(querySelector); + if (!nodeId) + return null; + return this._callFunctionOnNode(await this._querySelector(querySelector), fun, ...args); + } + + /** + * @param {string} querySelector + * @param {function(!Element):T} fun + * @param {!Array<*>} args + * @return {!Promise>} + */ + async $$(querySelector, fun, ...args) { + let nodeIds = await this._querySelectorAll(querySelector); + return Promise.all(nodeIds.map((nodeId, index) => this._callFunctionOnNode(nodeId, fun, index, ...args))); + } } /** @enum {string} */ diff --git a/lib/helper.js b/lib/helper.js index 794090f953f..182a91c9b52 100644 --- a/lib/helper.js +++ b/lib/helper.js @@ -52,6 +52,36 @@ class Helper { } return message; } + + /** + * @param {!Connection} client + * @param {!Object} remoteObject + */ + static async serializeRemoteObjet(client, remoteObject) { + if (remoteObject.unserializableValue) { + switch (remoteObject.unserializableValue) { + case '-0': + return -0; + case 'NaN': + return NaN; + case 'Infinity': + return Infinity; + case '-Infinity': + return -Infinity; + default: + throw new Error('Unsupported unserializable value: ' + remoteObject.unserializableValue); + } + } + if (!remoteObject.objectId) + return remoteObject.value; + let response = client.send('Runtime.callFunctionOn', { + objectId: remoteObject.objectId, + functionDeclaration: 'function() { return this; }', + returnByValue: true, + }); + client.send('Runtime.releaseObject', {objectId: remoteObject.objectId}); + return (await response).result.value; + } } module.exports = Helper; diff --git a/test/assets/playground.html b/test/assets/playground.html new file mode 100644 index 00000000000..828cfb1c701 --- /dev/null +++ b/test/assets/playground.html @@ -0,0 +1,15 @@ + + + + Playground + + + + +
First div
+
+ Second div + Inner span +
+ + \ No newline at end of file diff --git a/test/test.js b/test/test.js index 2448543157b..e73013da0fd 100644 --- a/test/test.js +++ b/test/test.js @@ -630,6 +630,33 @@ describe('Puppeteer', function() { ]); })); }); + + describe('Query selector', function() { + it('Page.$', SX(async function() { + await page.navigate(STATIC_PREFIX + '/playground.html'); + expect(await page.$('#first', element => element.textContent)).toBe('First div'); + expect(await page.$('#second span', element => element.textContent)).toBe('Inner span'); + expect(await page.$('#first', (element, arg1) => arg1, 'value1')).toBe('value1'); + expect(await page.$('#first', (element, arg1, arg2) => arg2, 'value1', 'value2')).toBe('value2'); + expect(await page.$('doesnot-exist', element => 5)).toBe(null); + expect(await page.$('button', function(element, arg1) { + element.textContent = arg1; + return true; + }, 'new button text')).toBe(true); + expect(await page.$('button', function(element) { + return element.textContent; + })).toBe('new button text'); + })); + it('Page.$$', SX(async function() { + await page.navigate(STATIC_PREFIX + '/playground.html'); + expect((await page.$$('div', element => element.textContent)).length).toBe(2); + expect((await page.$$('div', (element, index) => index))[0]).toBe(0); + expect((await page.$$('div', (element, index) => index))[1]).toBe(1); + expect((await page.$$('doesnotexist', function(){})).length).toBe(0); + expect((await page.$$('div', element => element.textContent))[0]).toBe('First div'); + expect((await page.$$('span', (element, index, arg1) => arg1, 'value1'))[0]).toBe('value1'); + })); + }); }); // Since Jasmine doesn't like async functions, they should be wrapped