From 117a128b427b9ddc9c319016c62749117dab6145 Mon Sep 17 00:00:00 2001 From: JoelEinbinder Date: Mon, 17 Jul 2017 18:56:56 -0700 Subject: [PATCH] Introduce Page.$ and Page.$$ (#75) This patch introduces Page.$ and Page.$$ methods which are aliases for `document.querySelector` and `document.querySelectorAll`. Fixes #78. --- docs/api.md | 49 +++++++++++++++++++++- lib/FrameManager.js | 43 ++++++++++++++++--- lib/Page.js | 20 +++++++++ test/assets/playground.html | 15 +++++++ test/test.js | 28 +++++++++++++ utils/doclint/MDBuilder.js | 2 +- utils/doclint/test/02-method-errors/doc.md | 4 ++ utils/doclint/test/02-method-errors/foo.js | 6 +++ 8 files changed, 158 insertions(+), 9 deletions(-) create mode 100644 test/assets/playground.html diff --git a/docs/api.md b/docs/api.md index e97be309e37..eccc2e85f2e 100644 --- a/docs/api.md +++ b/docs/api.md @@ -24,6 +24,8 @@ * [event: 'requestfailed'](#event-requestfailed) * [event: 'requestfinished'](#event-requestfinished) * [event: 'response'](#event-response) + * [page.$(selector, pageFunction, ...args)](#pageselector-pagefunction-args) + * [page.$$(selector, pageFunction, ...args)](#pageselector-pagefunction-args) * [page.addScriptTag(url)](#pageaddscripttagurl) * [page.click(selector)](#pageclickselector) * [page.close()](#pageclose) @@ -68,6 +70,8 @@ * [dialog.message()](#dialogmessage) * [dialog.type](#dialogtype) - [class: Frame](#class-frame) + * [frame.$(selector, pageFunction, ...args)](#frameselector-pagefunction-args) + * [frame.$$(selector, pageFunction, ...args)](#frameselector-pagefunction-args) * [frame.childFrames()](#framechildframes) * [frame.evaluate(pageFunction, ...args)](#frameevaluatepagefunction-args) * [frame.isDetached()](#frameisdetached) @@ -255,6 +259,35 @@ Emitted when a request is successfully finished. Emitted when a [response] is received. +#### page.$(selector, pageFunction, ...args) + +- `selector` <[string]> A selector to be matched in the page +- `pageFunction` <[function]\([Element]\)> Function to be evaluated in-page with first element matching `selector` +- `...args` <...[string]> Arguments to pass to `pageFunction` +- returns: <[Promise]<[Object]>> Promise which resolves to function return value. + +Example: +```js +const outerhtml = await page.$('#box', e => e.outerHTML); +``` + +Shortcut for [page.mainFrame().$(selector, pageFunction, ...args)](#pageselector-fun-args). + +#### page.$$(selector, pageFunction, ...args) + +- `selector` <[string]> A selector to be matched in the page +- `pageFunction` <[function]\([Element]\)> Function to be evaluated in-page for every matching element. +- `...args` <...[string]> Arguments to pass to `pageFunction` +- returns: <[Promise]<[Array]<[Object]>>> Promise which resolves to array of function return values. + +Example: +```js +const headings = await page.$$('h1,h2,h3,h4', el => el.textContent); +for (const heading of headings) console.log(heading); +``` + +Shortcut for [page.mainFrame().$$(selector, pageFunction, ...args)](#pageselector-fun-args). + #### page.addScriptTag(url) - `url` <[string]> Url of a script to be added - returns: <[Promise]> Promise which resolves as the script gets added and loads. @@ -632,13 +665,24 @@ browser.newPage().then(async page => { }); ``` +#### frame.$(selector, pageFunction, ...args) +- `selector` <[string]> A selector to be matched in the page +- `pageFunction` <[function]\([Element]\)> Function to be evaluated with first element matching `selector` +- `...args` <...[string]> Arguments to pass to `pageFunction` +- returns: <[Promise]<[Object]>> Promise which resolves to function return value. + +#### frame.$$(selector, pageFunction, ...args) +- `selector` <[string]> A selector to be matched in the page +- `pageFunction` <[function]\([Element]\)> Function to be evaluted for every element matching `selector`. +- `...args` <...[string]> Arguments to pass to `pageFunction` +- returns: <[Promise]<[Array]<[Object]>>> Promise which resolves to array of function return values. + #### frame.childFrames() - returns: <[Array]<[Frame]>> - #### frame.evaluate(pageFunction, ...args) - `pageFunction` <[function]> Function to be evaluated in browser context -- `...args` <[Array]<[string]>> Arguments to pass to `pageFunction` +- `...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. @@ -868,5 +912,6 @@ If there's already a header with name `name`, the header gets overwritten. [Request]: https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#class-request "Request" [Browser]: https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#class-browser "Browser" [Body]: https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#class-body "Body" +[Element]: https://developer.mozilla.org/en-US/docs/Web/API/element "Element" [Keyboard]: https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#class-keyboard "Keyboard" [Dialog]: https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#class-dialog "Dialog" diff --git a/lib/FrameManager.js b/lib/FrameManager.js index d6a4bf18b1a..73fc8a64dd9 100644 --- a/lib/FrameManager.js +++ b/lib/FrameManager.js @@ -154,18 +154,16 @@ class FrameManager extends EventEmitter { /** * @param {!Frame} frame - * @param {function()} pageFunction - * @param {!Array<*>} args + * @param {string} expression * @return {!Promise<(!Object|undefined)>} */ - async _evaluateOnFrame(frame, pageFunction, ...args) { + async _evaluateOnFrame(frame, expression) { let contextId = undefined; if (!frame.isMainFrame()) { contextId = this._frameIdToExecutionContextId.get(frame._id); console.assert(contextId, 'Frame does not have default context to evaluate in!'); } - let syncExpression = helper.evaluationString(pageFunction, ...args); - let expression = `Promise.resolve(${syncExpression})`; + expression = `Promise.resolve(${expression})`; let { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.evaluate', { expression, contextId, returnByValue: false, awaitPromise: true }); if (exceptionDetails) { let message = await helper.getExceptionMessage(this._client, exceptionDetails); @@ -192,6 +190,7 @@ class FrameManager extends EventEmitter { functionDeclaration: 'function() { return this; }', returnByValue: true, }); + this._client.send('Runtime.releaseObject', {objectId: remoteObject.objectId}); return response.result.value; } @@ -275,7 +274,7 @@ class Frame { * @return {!Promise<(!Object|undefined)>} */ async evaluate(pageFunction, ...args) { - return this._frameManager._evaluateOnFrame(this, pageFunction, ...args); + return this._frameManager._evaluateOnFrame(this, helper.evaluationString(pageFunction, ...args)); } /** @@ -328,6 +327,38 @@ class Frame { await this._frameManager._waitForInFrame(selector, this); } + /** + * @param {string} selector + * @param {function(!Element):T} pageFunction + * @param {!Array<*>} args + * @return {!Promise} + */ + async $(selector, pageFunction, ...args) { + let argsString = ['node'].concat(args.map(x => JSON.stringify(x))).join(','); + let expression = `(()=>{ + let node = document.querySelector(${JSON.stringify(selector)}); + if (!node) + return null; + return (${pageFunction})(${argsString}); + })()`; + return this._frameManager._evaluateOnFrame(this, expression); + } + + /** + * @param {string} selector + * @param {function(!Element):T} pageFunction + * @param {!Array<*>} args + * @return {!Promise>} + */ + async $$(selector, pageFunction, ...args) { + let argsString = ['node, index'].concat(args.map(x => JSON.stringify(x))).join(','); + let expression = `(()=>{ + let nodes = document.querySelectorAll(${JSON.stringify(selector)}); + return Array.prototype.map.call(nodes, (node, index) => (${pageFunction})(${argsString})); + })()`; + return this._frameManager._evaluateOnFrame(this, expression); + } + /** * @param {?Object} framePayload */ diff --git a/lib/Page.js b/lib/Page.js index 83d948984a2..700c413b93f 100644 --- a/lib/Page.js +++ b/lib/Page.js @@ -592,6 +592,26 @@ class Page extends EventEmitter { await this._client.send('DOM.disable'); } } + + /** + * @param {string} selector + * @param {function(!Element):T} pageFunction + * @param {!Array<*>} args + * @return {!Promise} + */ + async $(selector, pageFunction, ...args) { + return this.mainFrame().$(selector, pageFunction, ...args); + } + + /** + * @param {string} selector + * @param {function(!Element):T} pageFunction + * @param {!Array<*>} args + * @return {!Promise>} + */ + async $$(selector, pageFunction, ...args) { + return this.mainFrame().$$(selector, pageFunction, ...args); + } } /** @enum {string} */ 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 c283fd70f65..8697a7b25ba 100644 --- a/test/test.js +++ b/test/test.js @@ -960,6 +960,34 @@ describe('Puppeteer', function() { expect(await page.title()).toBe('Button test'); })); }); + + describe('Query selector', function() { + it('Page.$', SX(async function() { + await page.navigate(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(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 diff --git a/utils/doclint/MDBuilder.js b/utils/doclint/MDBuilder.js index c3e091eeacb..434dad60759 100644 --- a/utils/doclint/MDBuilder.js +++ b/utils/doclint/MDBuilder.js @@ -63,7 +63,7 @@ class MDOutline { this.errors = errors; const classHeading = /^class: (\w+)$/; const constructorRegex = /^new (\w+)\((.*)\)$/; - const methodRegex = /^(\w+)\.(\w+)\((.*)\)$/; + const methodRegex = /^(\w+)\.([\w$]+)\((.*)\)$/; const propertyRegex = /^(\w+)\.(\w+)$/; const eventRegex = /^event: '(\w+)'$/; let currentClassName = null; diff --git a/utils/doclint/test/02-method-errors/doc.md b/utils/doclint/test/02-method-errors/doc.md index 3b479423a5e..489d0149883 100644 --- a/utils/doclint/test/02-method-errors/doc.md +++ b/utils/doclint/test/02-method-errors/doc.md @@ -1,5 +1,9 @@ ### class: Foo +#### foo.$() + +#### foo.money$$money() + #### foo.proceed() #### foo.start() diff --git a/utils/doclint/test/02-method-errors/foo.js b/utils/doclint/test/02-method-errors/foo.js index 3f499572431..76f44edab77 100644 --- a/utils/doclint/test/02-method-errors/foo.js +++ b/utils/doclint/test/02-method-errors/foo.js @@ -7,4 +7,10 @@ class Foo { get zzz() { } + + $() { + } + + money$$money() { + } }