Skip to content

Commit

Permalink
feat(api): add element.select and element.evaluate for consistency (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
pavelfeldman authored and aslushnikov committed Sep 4, 2019
1 parent 135bb42 commit 73fd7ff
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 39 deletions.
73 changes: 73 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,8 @@
- [class: JSHandle](#class-jshandle)
* [jsHandle.asElement()](#jshandleaselement)
* [jsHandle.dispose()](#jshandledispose)
* [jsHandle.evaluate(pageFunction[, ...args])](#jshandleevaluatepagefunction-args)
* [jsHandle.evaluateHandle(pageFunction[, ...args])](#jshandleevaluatehandlepagefunction-args)
* [jsHandle.executionContext()](#jshandleexecutioncontext)
* [jsHandle.getProperties()](#jshandlegetproperties)
* [jsHandle.getProperty(propertyName)](#jshandlegetpropertypropertyname)
Expand All @@ -253,6 +255,8 @@
* [elementHandle.click([options])](#elementhandleclickoptions)
* [elementHandle.contentFrame()](#elementhandlecontentframe)
* [elementHandle.dispose()](#elementhandledispose)
* [elementHandle.evaluate(pageFunction[, ...args])](#elementhandleevaluatepagefunction-args)
* [elementHandle.evaluateHandle(pageFunction[, ...args])](#elementhandleevaluatehandlepagefunction-args)
* [elementHandle.executionContext()](#elementhandleexecutioncontext)
* [elementHandle.focus()](#elementhandlefocus)
* [elementHandle.getProperties()](#elementhandlegetproperties)
Expand All @@ -262,6 +266,7 @@
* [elementHandle.jsonValue()](#elementhandlejsonvalue)
* [elementHandle.press(key[, options])](#elementhandlepresskey-options)
* [elementHandle.screenshot([options])](#elementhandlescreenshotoptions)
* [elementHandle.select(...values)](#elementhandleselectvalues)
* [elementHandle.tap()](#elementhandletap)
* [elementHandle.toString()](#elementhandletostring)
* [elementHandle.type(text[, options])](#elementhandletypetext-options)
Expand Down Expand Up @@ -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.

#### 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()
- returns: <[ExecutionContext]>

Expand Down Expand Up @@ -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.

#### 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()
- returns: <[ExecutionContext]>

Expand Down Expand Up @@ -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.
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()
- returns: <[Promise]> Promise which resolves when the element is successfully tapped. Promise gets rejected if the element is detached from DOM.

Expand Down
32 changes: 10 additions & 22 deletions lib/DOMWorld.js
Original file line number Diff line number Diff line change
Expand Up @@ -389,28 +389,16 @@ class DOMWorld {
}

/**
* @param {string} selector
* @param {!Array<string>} values
* @return {!Promise<!Array<string>>}
*/
select(selector, ...values){
for (const value of values)
assert(helper.isString(value), 'Values must be strings. Found value "' + value + '" of type "' + (typeof value) + '"');
return this.$eval(selector, (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 {string} selector
* @param {!Array<string>} values
* @return {!Promise<!Array<string>>}
*/
async select(selector, ...values) {
const handle = await this.$(selector);
assert(handle, 'No node found for selector: ' + selector);
const result = await handle.select(...values);
await handle.dispose();
return result;
}

/**
Expand Down
76 changes: 59 additions & 17 deletions lib/JSHandle.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,34 @@ class JSHandle {
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
* @return {!Promise<?JSHandle>}
*/
async getProperty(propertyName) {
const objectHandle = await this._context.evaluateHandle((object, propertyName) => {
const objectHandle = await this.evaluateHandle((object, propertyName) => {
const result = {__proto__: null};
result[propertyName] = object[propertyName];
return result;
}, this, propertyName);
}, propertyName);
const properties = await objectHandle.getProperties();
const result = properties.get(propertyName) || null;
await objectHandle.dispose();
Expand Down Expand Up @@ -160,7 +178,7 @@ class ElementHandle extends JSHandle {
}

async _scrollIntoViewIfNeeded() {
const error = await this.executionContext().evaluate(async(element, pageJavascriptEnabled) => {
const error = await this.evaluate(async(element, pageJavascriptEnabled) => {
if (!element.isConnected)
return 'Node is detached from document';
if (element.nodeType !== Node.ELEMENT_NODE)
Expand All @@ -180,7 +198,7 @@ class ElementHandle extends JSHandle {
if (visibleRatio !== 1.0)
element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'});
return false;
}, this, this._page._javascriptEnabled);
}, this._page._javascriptEnabled);
if (error)
throw new Error(error);
}
Expand Down Expand Up @@ -266,6 +284,30 @@ class ElementHandle extends JSHandle {
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
*/
Expand All @@ -282,7 +324,7 @@ class ElementHandle extends JSHandle {
}

async focus() {
await this.executionContext().evaluate(element => element.focus(), this);
await this.evaluate(element => element.focus());
}

/**
Expand Down Expand Up @@ -392,9 +434,9 @@ class ElementHandle extends JSHandle {
* @return {!Promise<?ElementHandle>}
*/
async $(selector) {
const handle = await this.executionContext().evaluateHandle(
const handle = await this.evaluateHandle(
(element, selector) => element.querySelector(selector),
this, selector
selector
);
const element = handle.asElement();
if (element)
Expand All @@ -408,9 +450,9 @@ class ElementHandle extends JSHandle {
* @return {!Promise<!Array<!ElementHandle>>}
*/
async $$(selector) {
const arrayHandle = await this.executionContext().evaluateHandle(
const arrayHandle = await this.evaluateHandle(
(element, selector) => element.querySelectorAll(selector),
this, selector
selector
);
const properties = await arrayHandle.getProperties();
await arrayHandle.dispose();
Expand All @@ -433,7 +475,7 @@ class ElementHandle extends JSHandle {
const elementHandle = await this.$(selector);
if (!elementHandle)
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();
return result;
}
Expand All @@ -445,12 +487,12 @@ class ElementHandle extends JSHandle {
* @return {!Promise<(!Object|undefined)>}
*/
async $$eval(selector, pageFunction, ...args) {
const arrayHandle = await this.executionContext().evaluateHandle(
const arrayHandle = await this.evaluateHandle(
(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();
return result;
}
Expand All @@ -460,7 +502,7 @@ class ElementHandle extends JSHandle {
* @return {!Promise<!Array<!ElementHandle>>}
*/
async $x(expression) {
const arrayHandle = await this.executionContext().evaluateHandle(
const arrayHandle = await this.evaluateHandle(
(element, expression) => {
const document = element.ownerDocument || element;
const iterator = document.evaluate(expression, element, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE);
Expand All @@ -470,7 +512,7 @@ class ElementHandle extends JSHandle {
array.push(item);
return array;
},
this, expression
expression
);
const properties = await arrayHandle.getProperties();
await arrayHandle.dispose();
Expand All @@ -487,7 +529,7 @@ class ElementHandle extends JSHandle {
* @returns {!Promise<boolean>}
*/
isIntersectingViewport() {
return this.executionContext().evaluate(async element => {
return this.evaluate(async element => {
const visibleRatio = await new Promise(resolve => {
const observer = new IntersectionObserver(entries => {
resolve(entries[0].intersectionRatio);
Expand All @@ -496,7 +538,7 @@ class ElementHandle extends JSHandle {
observer.observe(element);
});
return visibleRatio > 0;
}, this);
});
}
}

Expand Down

0 comments on commit 73fd7ff

Please sign in to comment.