Skip to content

Commit 73fd7ff

Browse files
pavelfeldmanaslushnikov
authored andcommitted
feat(api): add element.select and element.evaluate for consistency (#4892)
1 parent 135bb42 commit 73fd7ff

File tree

3 files changed

+142
-39
lines changed

3 files changed

+142
-39
lines changed

docs/api.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,8 @@
237237
- [class: JSHandle](#class-jshandle)
238238
* [jsHandle.asElement()](#jshandleaselement)
239239
* [jsHandle.dispose()](#jshandledispose)
240+
* [jsHandle.evaluate(pageFunction[, ...args])](#jshandleevaluatepagefunction-args)
241+
* [jsHandle.evaluateHandle(pageFunction[, ...args])](#jshandleevaluatehandlepagefunction-args)
240242
* [jsHandle.executionContext()](#jshandleexecutioncontext)
241243
* [jsHandle.getProperties()](#jshandlegetproperties)
242244
* [jsHandle.getProperty(propertyName)](#jshandlegetpropertypropertyname)
@@ -253,6 +255,8 @@
253255
* [elementHandle.click([options])](#elementhandleclickoptions)
254256
* [elementHandle.contentFrame()](#elementhandlecontentframe)
255257
* [elementHandle.dispose()](#elementhandledispose)
258+
* [elementHandle.evaluate(pageFunction[, ...args])](#elementhandleevaluatepagefunction-args)
259+
* [elementHandle.evaluateHandle(pageFunction[, ...args])](#elementhandleevaluatehandlepagefunction-args)
256260
* [elementHandle.executionContext()](#elementhandleexecutioncontext)
257261
* [elementHandle.focus()](#elementhandlefocus)
258262
* [elementHandle.getProperties()](#elementhandlegetproperties)
@@ -262,6 +266,7 @@
262266
* [elementHandle.jsonValue()](#elementhandlejsonvalue)
263267
* [elementHandle.press(key[, options])](#elementhandlepresskey-options)
264268
* [elementHandle.screenshot([options])](#elementhandlescreenshotoptions)
269+
* [elementHandle.select(...values)](#elementhandleselectvalues)
265270
* [elementHandle.tap()](#elementhandletap)
266271
* [elementHandle.toString()](#elementhandletostring)
267272
* [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
30303035

30313036
The `jsHandle.dispose` method stops referencing the element handle.
30323037

3038+
#### jsHandle.evaluate(pageFunction[, ...args])
3039+
- `pageFunction` <[function]\([Object]\)> Function to be evaluated in browser context
3040+
- `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction`
3041+
- returns: <[Promise]<[Serializable]>> Promise which resolves to the return value of `pageFunction`
3042+
3043+
This method passes this handle as the first argument to `pageFunction`.
3044+
3045+
If `pageFunction` returns a [Promise], then `handle.evaluate` would wait for the promise to resolve and return its value.
3046+
3047+
Examples:
3048+
```js
3049+
const tweetHandle = await page.$('.tweet .retweets');
3050+
expect(await tweetHandle.evaluate(node => node.innerText)).toBe('10');
3051+
```
3052+
3053+
#### jsHandle.evaluateHandle(pageFunction[, ...args])
3054+
- `pageFunction` <[function]|[string]> Function to be evaluated
3055+
- `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction`
3056+
- returns: <[Promise]<[JSHandle]>> Promise which resolves to the return value of `pageFunction` as in-page object (JSHandle)
3057+
3058+
This method passes this handle as the first argument to `pageFunction`.
3059+
3060+
The only difference between `jsHandle.evaluate` and `jsHandle.evaluateHandle` is that `executionContext.evaluateHandle` returns in-page object (JSHandle).
3061+
3062+
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.
3063+
3064+
See [Page.evaluateHandle](#pageevaluatehandlepagefunction-args) for more details.
3065+
30333066
#### jsHandle.executionContext()
30343067
- returns: <[ExecutionContext]>
30353068

@@ -3190,6 +3223,34 @@ If the element is detached from DOM, the method throws an error.
31903223

31913224
The `elementHandle.dispose` method stops referencing the element handle.
31923225

3226+
#### elementHandle.evaluate(pageFunction[, ...args])
3227+
- `pageFunction` <[function]\([Object]\)> Function to be evaluated in browser context
3228+
- `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction`
3229+
- returns: <[Promise]<[Serializable]>> Promise which resolves to the return value of `pageFunction`
3230+
3231+
This method passes this handle as the first argument to `pageFunction`.
3232+
3233+
If `pageFunction` returns a [Promise], then `handle.evaluate` would wait for the promise to resolve and return its value.
3234+
3235+
Examples:
3236+
```js
3237+
const tweetHandle = await page.$('.tweet .retweets');
3238+
expect(await tweetHandle.evaluate(node => node.innerText)).toBe('10');
3239+
```
3240+
3241+
#### elementHandle.evaluateHandle(pageFunction[, ...args])
3242+
- `pageFunction` <[function]|[string]> Function to be evaluated
3243+
- `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction`
3244+
- returns: <[Promise]<[JSHandle]>> Promise which resolves to the return value of `pageFunction` as in-page object (JSHandle)
3245+
3246+
This method passes this handle as the first argument to `pageFunction`.
3247+
3248+
The only difference between `evaluateHandle.evaluate` and `evaluateHandle.evaluateHandle` is that `executionContext.evaluateHandle` returns in-page object (JSHandle).
3249+
3250+
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.
3251+
3252+
See [Page.evaluateHandle](#pageevaluatehandlepagefunction-args) for more details.
3253+
31933254
#### elementHandle.executionContext()
31943255
- returns: <[ExecutionContext]>
31953256

@@ -3257,6 +3318,18 @@ If `key` is a single character and no modifier keys besides `Shift` are being he
32573318
This method scrolls element into view if needed, and then uses [page.screenshot](#pagescreenshotoptions) to take a screenshot of the element.
32583319
If the element is detached from DOM, the method throws an error.
32593320

3321+
#### elementHandle.select(...values)
3322+
- `...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.
3323+
- returns: <[Promise]<[Array]<[string]>>> An array of option values that have been successfully selected.
3324+
3325+
Triggers a `change` and `input` event once all the provided options have been selected.
3326+
If there's no `<select>` element matching `selector`, the method throws an error.
3327+
3328+
```js
3329+
handle.select('blue'); // single selection
3330+
handle.select('red', 'green', 'blue'); // multiple selections
3331+
```
3332+
32603333
#### elementHandle.tap()
32613334
- returns: <[Promise]> Promise which resolves when the element is successfully tapped. Promise gets rejected if the element is detached from DOM.
32623335

lib/DOMWorld.js

Lines changed: 10 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -389,28 +389,16 @@ class DOMWorld {
389389
}
390390

391391
/**
392-
* @param {string} selector
393-
* @param {!Array<string>} values
394-
* @return {!Promise<!Array<string>>}
395-
*/
396-
select(selector, ...values){
397-
for (const value of values)
398-
assert(helper.isString(value), 'Values must be strings. Found value "' + value + '" of type "' + (typeof value) + '"');
399-
return this.$eval(selector, (element, values) => {
400-
if (element.nodeName.toLowerCase() !== 'select')
401-
throw new Error('Element is not a <select> element.');
402-
403-
const options = Array.from(element.options);
404-
element.value = undefined;
405-
for (const option of options) {
406-
option.selected = values.includes(option.value);
407-
if (option.selected && !element.multiple)
408-
break;
409-
}
410-
element.dispatchEvent(new Event('input', { 'bubbles': true }));
411-
element.dispatchEvent(new Event('change', { 'bubbles': true }));
412-
return options.filter(option => option.selected).map(option => option.value);
413-
}, values);
392+
* @param {string} selector
393+
* @param {!Array<string>} values
394+
* @return {!Promise<!Array<string>>}
395+
*/
396+
async select(selector, ...values) {
397+
const handle = await this.$(selector);
398+
assert(handle, 'No node found for selector: ' + selector);
399+
const result = await handle.select(...values);
400+
await handle.dispose();
401+
return result;
414402
}
415403

416404
/**

lib/JSHandle.js

Lines changed: 59 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -46,16 +46,34 @@ class JSHandle {
4646
return this._context;
4747
}
4848

49+
/**
50+
* @param {Function|String} pageFunction
51+
* @param {!Array<*>} args
52+
* @return {!Promise<(!Object|undefined)>}
53+
*/
54+
async evaluate(pageFunction, ...args) {
55+
return await this.executionContext().evaluate(pageFunction, this, ...args);
56+
}
57+
58+
/**
59+
* @param {Function|string} pageFunction
60+
* @param {!Array<*>} args
61+
* @return {!Promise<!Puppeteer.JSHandle>}
62+
*/
63+
async evaluateHandle(pageFunction, ...args) {
64+
return await this.executionContext().evaluateHandle(pageFunction, this, ...args);
65+
}
66+
4967
/**
5068
* @param {string} propertyName
5169
* @return {!Promise<?JSHandle>}
5270
*/
5371
async getProperty(propertyName) {
54-
const objectHandle = await this._context.evaluateHandle((object, propertyName) => {
72+
const objectHandle = await this.evaluateHandle((object, propertyName) => {
5573
const result = {__proto__: null};
5674
result[propertyName] = object[propertyName];
5775
return result;
58-
}, this, propertyName);
76+
}, propertyName);
5977
const properties = await objectHandle.getProperties();
6078
const result = properties.get(propertyName) || null;
6179
await objectHandle.dispose();
@@ -160,7 +178,7 @@ class ElementHandle extends JSHandle {
160178
}
161179

162180
async _scrollIntoViewIfNeeded() {
163-
const error = await this.executionContext().evaluate(async(element, pageJavascriptEnabled) => {
181+
const error = await this.evaluate(async(element, pageJavascriptEnabled) => {
164182
if (!element.isConnected)
165183
return 'Node is detached from document';
166184
if (element.nodeType !== Node.ELEMENT_NODE)
@@ -180,7 +198,7 @@ class ElementHandle extends JSHandle {
180198
if (visibleRatio !== 1.0)
181199
element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'});
182200
return false;
183-
}, this, this._page._javascriptEnabled);
201+
}, this._page._javascriptEnabled);
184202
if (error)
185203
throw new Error(error);
186204
}
@@ -266,6 +284,30 @@ class ElementHandle extends JSHandle {
266284
await this._page.mouse.click(x, y, options);
267285
}
268286

287+
/**
288+
* @param {!Array<string>} values
289+
* @return {!Promise<!Array<string>>}
290+
*/
291+
async select(...values) {
292+
for (const value of values)
293+
assert(helper.isString(value), 'Values must be strings. Found value "' + value + '" of type "' + (typeof value) + '"');
294+
return this.evaluate((element, values) => {
295+
if (element.nodeName.toLowerCase() !== 'select')
296+
throw new Error('Element is not a <select> element.');
297+
298+
const options = Array.from(element.options);
299+
element.value = undefined;
300+
for (const option of options) {
301+
option.selected = values.includes(option.value);
302+
if (option.selected && !element.multiple)
303+
break;
304+
}
305+
element.dispatchEvent(new Event('input', { 'bubbles': true }));
306+
element.dispatchEvent(new Event('change', { 'bubbles': true }));
307+
return options.filter(option => option.selected).map(option => option.value);
308+
}, values);
309+
}
310+
269311
/**
270312
* @param {!Array<string>} filePaths
271313
*/
@@ -282,7 +324,7 @@ class ElementHandle extends JSHandle {
282324
}
283325

284326
async focus() {
285-
await this.executionContext().evaluate(element => element.focus(), this);
327+
await this.evaluate(element => element.focus());
286328
}
287329

288330
/**
@@ -392,9 +434,9 @@ class ElementHandle extends JSHandle {
392434
* @return {!Promise<?ElementHandle>}
393435
*/
394436
async $(selector) {
395-
const handle = await this.executionContext().evaluateHandle(
437+
const handle = await this.evaluateHandle(
396438
(element, selector) => element.querySelector(selector),
397-
this, selector
439+
selector
398440
);
399441
const element = handle.asElement();
400442
if (element)
@@ -408,9 +450,9 @@ class ElementHandle extends JSHandle {
408450
* @return {!Promise<!Array<!ElementHandle>>}
409451
*/
410452
async $$(selector) {
411-
const arrayHandle = await this.executionContext().evaluateHandle(
453+
const arrayHandle = await this.evaluateHandle(
412454
(element, selector) => element.querySelectorAll(selector),
413-
this, selector
455+
selector
414456
);
415457
const properties = await arrayHandle.getProperties();
416458
await arrayHandle.dispose();
@@ -433,7 +475,7 @@ class ElementHandle extends JSHandle {
433475
const elementHandle = await this.$(selector);
434476
if (!elementHandle)
435477
throw new Error(`Error: failed to find element matching selector "${selector}"`);
436-
const result = await this.executionContext().evaluate(pageFunction, elementHandle, ...args);
478+
const result = await elementHandle.evaluate(pageFunction, ...args);
437479
await elementHandle.dispose();
438480
return result;
439481
}
@@ -445,12 +487,12 @@ class ElementHandle extends JSHandle {
445487
* @return {!Promise<(!Object|undefined)>}
446488
*/
447489
async $$eval(selector, pageFunction, ...args) {
448-
const arrayHandle = await this.executionContext().evaluateHandle(
490+
const arrayHandle = await this.evaluateHandle(
449491
(element, selector) => Array.from(element.querySelectorAll(selector)),
450-
this, selector
492+
selector
451493
);
452494

453-
const result = await this.executionContext().evaluate(pageFunction, arrayHandle, ...args);
495+
const result = await arrayHandle.evaluate(pageFunction, ...args);
454496
await arrayHandle.dispose();
455497
return result;
456498
}
@@ -460,7 +502,7 @@ class ElementHandle extends JSHandle {
460502
* @return {!Promise<!Array<!ElementHandle>>}
461503
*/
462504
async $x(expression) {
463-
const arrayHandle = await this.executionContext().evaluateHandle(
505+
const arrayHandle = await this.evaluateHandle(
464506
(element, expression) => {
465507
const document = element.ownerDocument || element;
466508
const iterator = document.evaluate(expression, element, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE);
@@ -470,7 +512,7 @@ class ElementHandle extends JSHandle {
470512
array.push(item);
471513
return array;
472514
},
473-
this, expression
515+
expression
474516
);
475517
const properties = await arrayHandle.getProperties();
476518
await arrayHandle.dispose();
@@ -487,7 +529,7 @@ class ElementHandle extends JSHandle {
487529
* @returns {!Promise<boolean>}
488530
*/
489531
isIntersectingViewport() {
490-
return this.executionContext().evaluate(async element => {
532+
return this.evaluate(async element => {
491533
const visibleRatio = await new Promise(resolve => {
492534
const observer = new IntersectionObserver(entries => {
493535
resolve(entries[0].intersectionRatio);
@@ -496,7 +538,7 @@ class ElementHandle extends JSHandle {
496538
observer.observe(element);
497539
});
498540
return visibleRatio > 0;
499-
}, this);
541+
});
500542
}
501543
}
502544

0 commit comments

Comments
 (0)