feat(api): add element.select and element.evaluate for consistency (#4892)

This commit is contained in:
Pavel Feldman 2019-09-04 15:19:34 -07:00 committed by Andrey Lushnikov
parent 135bb424ba
commit 73fd7ff822
3 changed files with 142 additions and 39 deletions

View File

@ -237,6 +237,8 @@
- [class: JSHandle](#class-jshandle) - [class: JSHandle](#class-jshandle)
* [jsHandle.asElement()](#jshandleaselement) * [jsHandle.asElement()](#jshandleaselement)
* [jsHandle.dispose()](#jshandledispose) * [jsHandle.dispose()](#jshandledispose)
* [jsHandle.evaluate(pageFunction[, ...args])](#jshandleevaluatepagefunction-args)
* [jsHandle.evaluateHandle(pageFunction[, ...args])](#jshandleevaluatehandlepagefunction-args)
* [jsHandle.executionContext()](#jshandleexecutioncontext) * [jsHandle.executionContext()](#jshandleexecutioncontext)
* [jsHandle.getProperties()](#jshandlegetproperties) * [jsHandle.getProperties()](#jshandlegetproperties)
* [jsHandle.getProperty(propertyName)](#jshandlegetpropertypropertyname) * [jsHandle.getProperty(propertyName)](#jshandlegetpropertypropertyname)
@ -253,6 +255,8 @@
* [elementHandle.click([options])](#elementhandleclickoptions) * [elementHandle.click([options])](#elementhandleclickoptions)
* [elementHandle.contentFrame()](#elementhandlecontentframe) * [elementHandle.contentFrame()](#elementhandlecontentframe)
* [elementHandle.dispose()](#elementhandledispose) * [elementHandle.dispose()](#elementhandledispose)
* [elementHandle.evaluate(pageFunction[, ...args])](#elementhandleevaluatepagefunction-args)
* [elementHandle.evaluateHandle(pageFunction[, ...args])](#elementhandleevaluatehandlepagefunction-args)
* [elementHandle.executionContext()](#elementhandleexecutioncontext) * [elementHandle.executionContext()](#elementhandleexecutioncontext)
* [elementHandle.focus()](#elementhandlefocus) * [elementHandle.focus()](#elementhandlefocus)
* [elementHandle.getProperties()](#elementhandlegetproperties) * [elementHandle.getProperties()](#elementhandlegetproperties)
@ -262,6 +266,7 @@
* [elementHandle.jsonValue()](#elementhandlejsonvalue) * [elementHandle.jsonValue()](#elementhandlejsonvalue)
* [elementHandle.press(key[, options])](#elementhandlepresskey-options) * [elementHandle.press(key[, options])](#elementhandlepresskey-options)
* [elementHandle.screenshot([options])](#elementhandlescreenshotoptions) * [elementHandle.screenshot([options])](#elementhandlescreenshotoptions)
* [elementHandle.select(...values)](#elementhandleselectvalues)
* [elementHandle.tap()](#elementhandletap) * [elementHandle.tap()](#elementhandletap)
* [elementHandle.toString()](#elementhandletostring) * [elementHandle.toString()](#elementhandletostring)
* [elementHandle.type(text[, options])](#elementhandletypetext-options) * [elementHandle.type(text[, options])](#elementhandletypetext-options)
@ -3030,6 +3035,34 @@ Returns either `null` or the object handle itself, if the object handle is an in
The `jsHandle.dispose` method stops referencing the element handle. The `jsHandle.dispose` method stops referencing the element handle.
#### jsHandle.evaluate(pageFunction[, ...args])
- `pageFunction` <[function]\([Object]\)> 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 passes this handle as the first argument to `pageFunction`.
If `pageFunction` returns a [Promise], then `handle.evaluate` would wait for the promise to resolve and return its value.
Examples:
```js
const tweetHandle = await page.$('.tweet .retweets');
expect(await tweetHandle.evaluate(node => node.innerText)).toBe('10');
```
#### jsHandle.evaluateHandle(pageFunction[, ...args])
- `pageFunction` <[function]|[string]> Function to be evaluated
- `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction`
- returns: <[Promise]<[JSHandle]>> Promise which resolves to the return value of `pageFunction` as in-page object (JSHandle)
This method passes this handle as the first argument to `pageFunction`.
The only difference between `jsHandle.evaluate` and `jsHandle.evaluateHandle` is that `executionContext.evaluateHandle` returns in-page object (JSHandle).
If the function passed to the `jsHandle.evaluateHandle` returns a [Promise], then `jsHandle.evaluateHandle` would wait for the promise to resolve and return its value.
See [Page.evaluateHandle](#pageevaluatehandlepagefunction-args) for more details.
#### jsHandle.executionContext() #### jsHandle.executionContext()
- returns: <[ExecutionContext]> - returns: <[ExecutionContext]>
@ -3190,6 +3223,34 @@ If the element is detached from DOM, the method throws an error.
The `elementHandle.dispose` method stops referencing the element handle. The `elementHandle.dispose` method stops referencing the element handle.
#### elementHandle.evaluate(pageFunction[, ...args])
- `pageFunction` <[function]\([Object]\)> 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 passes this handle as the first argument to `pageFunction`.
If `pageFunction` returns a [Promise], then `handle.evaluate` would wait for the promise to resolve and return its value.
Examples:
```js
const tweetHandle = await page.$('.tweet .retweets');
expect(await tweetHandle.evaluate(node => node.innerText)).toBe('10');
```
#### elementHandle.evaluateHandle(pageFunction[, ...args])
- `pageFunction` <[function]|[string]> Function to be evaluated
- `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction`
- returns: <[Promise]<[JSHandle]>> Promise which resolves to the return value of `pageFunction` as in-page object (JSHandle)
This method passes this handle as the first argument to `pageFunction`.
The only difference between `evaluateHandle.evaluate` and `evaluateHandle.evaluateHandle` is that `executionContext.evaluateHandle` returns in-page object (JSHandle).
If the function passed to the `evaluateHandle.evaluateHandle` returns a [Promise], then `evaluateHandle.evaluateHandle` would wait for the promise to resolve and return its value.
See [Page.evaluateHandle](#pageevaluatehandlepagefunction-args) for more details.
#### elementHandle.executionContext() #### elementHandle.executionContext()
- returns: <[ExecutionContext]> - returns: <[ExecutionContext]>
@ -3257,6 +3318,18 @@ If `key` is a single character and no modifier keys besides `Shift` are being he
This method scrolls element into view if needed, and then uses [page.screenshot](#pagescreenshotoptions) to take a screenshot of the element. This method scrolls element into view if needed, and then uses [page.screenshot](#pagescreenshotoptions) to take a screenshot of the element.
If the element is detached from DOM, the method throws an error. If the element is detached from DOM, the method throws an error.
#### elementHandle.select(...values)
- `...values` <...[string]> Values of options to select. If the `<select>` has the `multiple` attribute, all values are considered, otherwise only the first one is taken into account.
- returns: <[Promise]<[Array]<[string]>>> An array of option values that have been successfully selected.
Triggers a `change` and `input` event once all the provided options have been selected.
If there's no `<select>` element matching `selector`, the method throws an error.
```js
handle.select('blue'); // single selection
handle.select('red', 'green', 'blue'); // multiple selections
```
#### elementHandle.tap() #### elementHandle.tap()
- returns: <[Promise]> Promise which resolves when the element is successfully tapped. Promise gets rejected if the element is detached from DOM. - returns: <[Promise]> Promise which resolves when the element is successfully tapped. Promise gets rejected if the element is detached from DOM.

View File

@ -389,28 +389,16 @@ class DOMWorld {
} }
/** /**
* @param {string} selector * @param {string} selector
* @param {!Array<string>} values * @param {!Array<string>} values
* @return {!Promise<!Array<string>>} * @return {!Promise<!Array<string>>}
*/ */
select(selector, ...values){ async select(selector, ...values) {
for (const value of values) const handle = await this.$(selector);
assert(helper.isString(value), 'Values must be strings. Found value "' + value + '" of type "' + (typeof value) + '"'); assert(handle, 'No node found for selector: ' + selector);
return this.$eval(selector, (element, values) => { const result = await handle.select(...values);
if (element.nodeName.toLowerCase() !== 'select') await handle.dispose();
throw new Error('Element is not a <select> element.'); return result;
const options = Array.from(element.options);
element.value = undefined;
for (const option of options) {
option.selected = values.includes(option.value);
if (option.selected && !element.multiple)
break;
}
element.dispatchEvent(new Event('input', { 'bubbles': true }));
element.dispatchEvent(new Event('change', { 'bubbles': true }));
return options.filter(option => option.selected).map(option => option.value);
}, values);
} }
/** /**

View File

@ -46,16 +46,34 @@ class JSHandle {
return this._context; return this._context;
} }
/**
* @param {Function|String} pageFunction
* @param {!Array<*>} args
* @return {!Promise<(!Object|undefined)>}
*/
async evaluate(pageFunction, ...args) {
return await this.executionContext().evaluate(pageFunction, this, ...args);
}
/**
* @param {Function|string} pageFunction
* @param {!Array<*>} args
* @return {!Promise<!Puppeteer.JSHandle>}
*/
async evaluateHandle(pageFunction, ...args) {
return await this.executionContext().evaluateHandle(pageFunction, this, ...args);
}
/** /**
* @param {string} propertyName * @param {string} propertyName
* @return {!Promise<?JSHandle>} * @return {!Promise<?JSHandle>}
*/ */
async getProperty(propertyName) { async getProperty(propertyName) {
const objectHandle = await this._context.evaluateHandle((object, propertyName) => { const objectHandle = await this.evaluateHandle((object, propertyName) => {
const result = {__proto__: null}; const result = {__proto__: null};
result[propertyName] = object[propertyName]; result[propertyName] = object[propertyName];
return result; return result;
}, this, propertyName); }, propertyName);
const properties = await objectHandle.getProperties(); const properties = await objectHandle.getProperties();
const result = properties.get(propertyName) || null; const result = properties.get(propertyName) || null;
await objectHandle.dispose(); await objectHandle.dispose();
@ -160,7 +178,7 @@ class ElementHandle extends JSHandle {
} }
async _scrollIntoViewIfNeeded() { async _scrollIntoViewIfNeeded() {
const error = await this.executionContext().evaluate(async(element, pageJavascriptEnabled) => { const error = await this.evaluate(async(element, pageJavascriptEnabled) => {
if (!element.isConnected) if (!element.isConnected)
return 'Node is detached from document'; return 'Node is detached from document';
if (element.nodeType !== Node.ELEMENT_NODE) if (element.nodeType !== Node.ELEMENT_NODE)
@ -180,7 +198,7 @@ class ElementHandle extends JSHandle {
if (visibleRatio !== 1.0) if (visibleRatio !== 1.0)
element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'}); element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'});
return false; return false;
}, this, this._page._javascriptEnabled); }, this._page._javascriptEnabled);
if (error) if (error)
throw new Error(error); throw new Error(error);
} }
@ -266,6 +284,30 @@ class ElementHandle extends JSHandle {
await this._page.mouse.click(x, y, options); await this._page.mouse.click(x, y, options);
} }
/**
* @param {!Array<string>} values
* @return {!Promise<!Array<string>>}
*/
async select(...values) {
for (const value of values)
assert(helper.isString(value), 'Values must be strings. Found value "' + value + '" of type "' + (typeof value) + '"');
return this.evaluate((element, values) => {
if (element.nodeName.toLowerCase() !== 'select')
throw new Error('Element is not a <select> element.');
const options = Array.from(element.options);
element.value = undefined;
for (const option of options) {
option.selected = values.includes(option.value);
if (option.selected && !element.multiple)
break;
}
element.dispatchEvent(new Event('input', { 'bubbles': true }));
element.dispatchEvent(new Event('change', { 'bubbles': true }));
return options.filter(option => option.selected).map(option => option.value);
}, values);
}
/** /**
* @param {!Array<string>} filePaths * @param {!Array<string>} filePaths
*/ */
@ -282,7 +324,7 @@ class ElementHandle extends JSHandle {
} }
async focus() { async focus() {
await this.executionContext().evaluate(element => element.focus(), this); await this.evaluate(element => element.focus());
} }
/** /**
@ -392,9 +434,9 @@ class ElementHandle extends JSHandle {
* @return {!Promise<?ElementHandle>} * @return {!Promise<?ElementHandle>}
*/ */
async $(selector) { async $(selector) {
const handle = await this.executionContext().evaluateHandle( const handle = await this.evaluateHandle(
(element, selector) => element.querySelector(selector), (element, selector) => element.querySelector(selector),
this, selector selector
); );
const element = handle.asElement(); const element = handle.asElement();
if (element) if (element)
@ -408,9 +450,9 @@ class ElementHandle extends JSHandle {
* @return {!Promise<!Array<!ElementHandle>>} * @return {!Promise<!Array<!ElementHandle>>}
*/ */
async $$(selector) { async $$(selector) {
const arrayHandle = await this.executionContext().evaluateHandle( const arrayHandle = await this.evaluateHandle(
(element, selector) => element.querySelectorAll(selector), (element, selector) => element.querySelectorAll(selector),
this, selector selector
); );
const properties = await arrayHandle.getProperties(); const properties = await arrayHandle.getProperties();
await arrayHandle.dispose(); await arrayHandle.dispose();
@ -433,7 +475,7 @@ class ElementHandle extends JSHandle {
const elementHandle = await this.$(selector); const elementHandle = await this.$(selector);
if (!elementHandle) if (!elementHandle)
throw new Error(`Error: failed to find element matching selector "${selector}"`); throw new Error(`Error: failed to find element matching selector "${selector}"`);
const result = await this.executionContext().evaluate(pageFunction, elementHandle, ...args); const result = await elementHandle.evaluate(pageFunction, ...args);
await elementHandle.dispose(); await elementHandle.dispose();
return result; return result;
} }
@ -445,12 +487,12 @@ class ElementHandle extends JSHandle {
* @return {!Promise<(!Object|undefined)>} * @return {!Promise<(!Object|undefined)>}
*/ */
async $$eval(selector, pageFunction, ...args) { async $$eval(selector, pageFunction, ...args) {
const arrayHandle = await this.executionContext().evaluateHandle( const arrayHandle = await this.evaluateHandle(
(element, selector) => Array.from(element.querySelectorAll(selector)), (element, selector) => Array.from(element.querySelectorAll(selector)),
this, selector selector
); );
const result = await this.executionContext().evaluate(pageFunction, arrayHandle, ...args); const result = await arrayHandle.evaluate(pageFunction, ...args);
await arrayHandle.dispose(); await arrayHandle.dispose();
return result; return result;
} }
@ -460,7 +502,7 @@ class ElementHandle extends JSHandle {
* @return {!Promise<!Array<!ElementHandle>>} * @return {!Promise<!Array<!ElementHandle>>}
*/ */
async $x(expression) { async $x(expression) {
const arrayHandle = await this.executionContext().evaluateHandle( const arrayHandle = await this.evaluateHandle(
(element, expression) => { (element, expression) => {
const document = element.ownerDocument || element; const document = element.ownerDocument || element;
const iterator = document.evaluate(expression, element, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE); const iterator = document.evaluate(expression, element, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE);
@ -470,7 +512,7 @@ class ElementHandle extends JSHandle {
array.push(item); array.push(item);
return array; return array;
}, },
this, expression expression
); );
const properties = await arrayHandle.getProperties(); const properties = await arrayHandle.getProperties();
await arrayHandle.dispose(); await arrayHandle.dispose();
@ -487,7 +529,7 @@ class ElementHandle extends JSHandle {
* @returns {!Promise<boolean>} * @returns {!Promise<boolean>}
*/ */
isIntersectingViewport() { isIntersectingViewport() {
return this.executionContext().evaluate(async element => { return this.evaluate(async element => {
const visibleRatio = await new Promise(resolve => { const visibleRatio = await new Promise(resolve => {
const observer = new IntersectionObserver(entries => { const observer = new IntersectionObserver(entries => {
resolve(entries[0].intersectionRatio); resolve(entries[0].intersectionRatio);
@ -496,7 +538,7 @@ class ElementHandle extends JSHandle {
observer.observe(element); observer.observe(element);
}); });
return visibleRatio > 0; return visibleRatio > 0;
}, this); });
} }
} }