From 1e07925e26783d586ab63a6eb88063ca51fb5647 Mon Sep 17 00:00:00 2001 From: Vasyl Pahut Date: Sat, 26 May 2018 02:56:51 +0300 Subject: [PATCH] feat(elementhandle): add elementHandle.$$eval method (#2589) Fixes #2401 --- docs/api.md | 23 +++++++++++++++++++++++ lib/ElementHandle.js | 19 +++++++++++++++++++ lib/FrameManager.js | 8 +++----- test/elementhandle.spec.js | 24 ++++++++++++++++++++++++ 4 files changed, 69 insertions(+), 5 deletions(-) diff --git a/docs/api.md b/docs/api.md index 4d06575ddc6..ee5826ef8a8 100644 --- a/docs/api.md +++ b/docs/api.md @@ -205,6 +205,7 @@ - [class: ElementHandle](#class-elementhandle) * [elementHandle.$(selector)](#elementhandleselector) * [elementHandle.$$(selector)](#elementhandleselector) + * [elementHandle.$$eval(selector, pageFunction, ...args)](#elementhandleevalselector-pagefunction-args) * [elementHandle.$eval(selector, pageFunction, ...args)](#elementhandleevalselector-pagefunction-args) * [elementHandle.$x(expression)](#elementhandlexexpression) * [elementHandle.asElement()](#elementhandleaselement) @@ -2397,6 +2398,28 @@ The method runs `element.querySelector` within the page. If no element matches t The method runs `element.querySelectorAll` within the page. If no elements match the selector, the return value resolve to `[]`. +#### elementHandle.$$eval(selector, pageFunction, ...args) +- `selector` <[string]> A [selector] to query page for +- `pageFunction` <[function]> Function to be evaluated in browser context +- `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction` +- returns: <[Promise]<[Serializable]>> Promise which resolves to the return value of `pageFunction` + +This method runs `document.querySelectorAll` within the element 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 its value. + +Examples: +```html +
+
Hello!
+
Hi!
+
+``` +```js +const feedHandle = await page.$('.feed'); +expect(await feedHandle.$$eval('.tweet', nodes => nodes.map(n => n.innerText)).toEqual(['Hello!', 'Hi!']); +``` + #### elementHandle.$eval(selector, pageFunction, ...args) - `selector` <[string]> A [selector] to query page for - `pageFunction` <[function]> Function to be evaluated in browser context diff --git a/lib/ElementHandle.js b/lib/ElementHandle.js index c5bec74c547..5bba6af281b 100644 --- a/lib/ElementHandle.js +++ b/lib/ElementHandle.js @@ -295,6 +295,25 @@ class ElementHandle extends JSHandle { return result; } + /** + * @param {string} selector + * @param {Function|String} pageFunction + * @param {!Array<*>} args + * @return {!Promise<(!Object|undefined)>} + */ + async $$eval(selector, pageFunction, ...args) { + const arrayHandle = await this.executionContext().evaluateHandle( + (element, selector) => Array.from(element.querySelectorAll(selector)), + this, selector + ); + if (!(await arrayHandle.jsonValue()).length) + throw new Error(`Error: failed to find elements matching selector "${selector}"`); + + const result = await this.executionContext().evaluate(pageFunction, arrayHandle, ...args); + await arrayHandle.dispose(); + return result; + } + /** * @param {string} expression * @return {!Promise>} diff --git a/lib/FrameManager.js b/lib/FrameManager.js index 8ea3b8fce8f..65d43e163cb 100644 --- a/lib/FrameManager.js +++ b/lib/FrameManager.js @@ -378,11 +378,9 @@ class Frame { * @return {!Promise<(!Object|undefined)>} */ async $$eval(selector, pageFunction, ...args) { - const context = await this._contextPromise; - const arrayHandle = await context.evaluateHandle(selector => Array.from(document.querySelectorAll(selector)), selector); - const result = await this.evaluate(pageFunction, arrayHandle, ...args); - await arrayHandle.dispose(); - return result; + const document = await this._document(); + const value = await document.$$eval(selector, pageFunction, ...args); + return value; } /** diff --git a/test/elementhandle.spec.js b/test/elementhandle.spec.js index e44ef8a1c17..111afd7363e 100644 --- a/test/elementhandle.spec.js +++ b/test/elementhandle.spec.js @@ -318,6 +318,30 @@ module.exports.addTests = function({testRunner, expect}) { expect(errorMessage).toBe(`Error: failed to find element matching selector ".a"`); }); }); + describe('ElementHandle.$$eval', function() { + it('should work', async({page, server}) => { + await page.setContent('
'); + const tweet = await page.$('.tweet'); + const content = await tweet.$$eval('.like', nodes => nodes.map(n => n.innerText)); + expect(content).toEqual(['100', '10']); + }); + + it('should retrieve content from subtree', async({page, server}) => { + const htmlContent = '
not-a-child-div
a1-child-div
a2-child-div
'; + await page.setContent(htmlContent); + const elementHandle = await page.$('#myId'); + const content = await elementHandle.$$eval('.a', nodes => nodes.map(n => n.innerText)); + expect(content).toEqual(['a1-child-div', 'a2-child-div']); + }); + + it('should throw in case of missing selector', async({page, server}) => { + const htmlContent = '
not-a-child-div
'; + await page.setContent(htmlContent); + const elementHandle = await page.$('#myId'); + const errorMessage = await elementHandle.$$eval('.a', nodes => nodes.map(n => n.innerText)).catch(error => error.message); + expect(errorMessage).toBe(`Error: failed to find elements matching selector ".a"`); + }); + }); describe('ElementHandle.$$', function() { it('should query existing elements', async({page, server}) => {