Introduce Page.$ and Page.$$ (#75)

This patch introduces Page.$ and Page.$$ methods which are
aliases for `document.querySelector` and `document.querySelectorAll`. 

Fixes #78.
This commit is contained in:
JoelEinbinder 2017-07-17 18:56:56 -07:00 committed by Andrey Lushnikov
parent bf7698e8f8
commit 117a128b42
8 changed files with 158 additions and 9 deletions

View File

@ -24,6 +24,8 @@
* [event: 'requestfailed'](#event-requestfailed) * [event: 'requestfailed'](#event-requestfailed)
* [event: 'requestfinished'](#event-requestfinished) * [event: 'requestfinished'](#event-requestfinished)
* [event: 'response'](#event-response) * [event: 'response'](#event-response)
* [page.$(selector, pageFunction, ...args)](#pageselector-pagefunction-args)
* [page.$$(selector, pageFunction, ...args)](#pageselector-pagefunction-args)
* [page.addScriptTag(url)](#pageaddscripttagurl) * [page.addScriptTag(url)](#pageaddscripttagurl)
* [page.click(selector)](#pageclickselector) * [page.click(selector)](#pageclickselector)
* [page.close()](#pageclose) * [page.close()](#pageclose)
@ -68,6 +70,8 @@
* [dialog.message()](#dialogmessage) * [dialog.message()](#dialogmessage)
* [dialog.type](#dialogtype) * [dialog.type](#dialogtype)
- [class: Frame](#class-frame) - [class: Frame](#class-frame)
* [frame.$(selector, pageFunction, ...args)](#frameselector-pagefunction-args)
* [frame.$$(selector, pageFunction, ...args)](#frameselector-pagefunction-args)
* [frame.childFrames()](#framechildframes) * [frame.childFrames()](#framechildframes)
* [frame.evaluate(pageFunction, ...args)](#frameevaluatepagefunction-args) * [frame.evaluate(pageFunction, ...args)](#frameevaluatepagefunction-args)
* [frame.isDetached()](#frameisdetached) * [frame.isDetached()](#frameisdetached)
@ -255,6 +259,35 @@ Emitted when a request is successfully finished.
Emitted when a [response] is received. 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) #### page.addScriptTag(url)
- `url` <[string]> Url of a script to be added - `url` <[string]> Url of a script to be added
- returns: <[Promise]> Promise which resolves as the script gets added and loads. - 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() #### frame.childFrames()
- returns: <[Array]<[Frame]>> - returns: <[Array]<[Frame]>>
#### frame.evaluate(pageFunction, ...args) #### frame.evaluate(pageFunction, ...args)
- `pageFunction` <[function]> Function to be evaluated in browser context - `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 - 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. 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" [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" [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" [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" [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" [Dialog]: https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#class-dialog "Dialog"

View File

@ -154,18 +154,16 @@ class FrameManager extends EventEmitter {
/** /**
* @param {!Frame} frame * @param {!Frame} frame
* @param {function()} pageFunction * @param {string} expression
* @param {!Array<*>} args
* @return {!Promise<(!Object|undefined)>} * @return {!Promise<(!Object|undefined)>}
*/ */
async _evaluateOnFrame(frame, pageFunction, ...args) { async _evaluateOnFrame(frame, expression) {
let contextId = undefined; let contextId = undefined;
if (!frame.isMainFrame()) { if (!frame.isMainFrame()) {
contextId = this._frameIdToExecutionContextId.get(frame._id); contextId = this._frameIdToExecutionContextId.get(frame._id);
console.assert(contextId, 'Frame does not have default context to evaluate in!'); console.assert(contextId, 'Frame does not have default context to evaluate in!');
} }
let syncExpression = helper.evaluationString(pageFunction, ...args); expression = `Promise.resolve(${expression})`;
let expression = `Promise.resolve(${syncExpression})`;
let { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.evaluate', { expression, contextId, returnByValue: false, awaitPromise: true }); let { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.evaluate', { expression, contextId, returnByValue: false, awaitPromise: true });
if (exceptionDetails) { if (exceptionDetails) {
let message = await helper.getExceptionMessage(this._client, exceptionDetails); let message = await helper.getExceptionMessage(this._client, exceptionDetails);
@ -192,6 +190,7 @@ class FrameManager extends EventEmitter {
functionDeclaration: 'function() { return this; }', functionDeclaration: 'function() { return this; }',
returnByValue: true, returnByValue: true,
}); });
this._client.send('Runtime.releaseObject', {objectId: remoteObject.objectId});
return response.result.value; return response.result.value;
} }
@ -275,7 +274,7 @@ class Frame {
* @return {!Promise<(!Object|undefined)>} * @return {!Promise<(!Object|undefined)>}
*/ */
async evaluate(pageFunction, ...args) { 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); await this._frameManager._waitForInFrame(selector, this);
} }
/**
* @param {string} selector
* @param {function(!Element):T} pageFunction
* @param {!Array<*>} args
* @return {!Promise<?T>}
*/
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<!Array<T>>}
*/
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 * @param {?Object} framePayload
*/ */

View File

@ -592,6 +592,26 @@ class Page extends EventEmitter {
await this._client.send('DOM.disable'); await this._client.send('DOM.disable');
} }
} }
/**
* @param {string} selector
* @param {function(!Element):T} pageFunction
* @param {!Array<*>} args
* @return {!Promise<?T>}
*/
async $(selector, pageFunction, ...args) {
return this.mainFrame().$(selector, pageFunction, ...args);
}
/**
* @param {string} selector
* @param {function(!Element):T} pageFunction
* @param {!Array<*>} args
* @return {!Promise<!Array<T>>}
*/
async $$(selector, pageFunction, ...args) {
return this.mainFrame().$$(selector, pageFunction, ...args);
}
} }
/** @enum {string} */ /** @enum {string} */

View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<title>Playground</title>
</head>
<body>
<button>A button</button>
<textarea>A text area</textarea>
<div id="first">First div</div>
<div id="second">
Second div
<span class="inner">Inner span</span>
</div>
</body>
</html>

View File

@ -960,6 +960,34 @@ describe('Puppeteer', function() {
expect(await page.title()).toBe('Button test'); 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 // Since Jasmine doesn't like async functions, they should be wrapped

View File

@ -63,7 +63,7 @@ class MDOutline {
this.errors = errors; this.errors = errors;
const classHeading = /^class: (\w+)$/; const classHeading = /^class: (\w+)$/;
const constructorRegex = /^new (\w+)\((.*)\)$/; const constructorRegex = /^new (\w+)\((.*)\)$/;
const methodRegex = /^(\w+)\.(\w+)\((.*)\)$/; const methodRegex = /^(\w+)\.([\w$]+)\((.*)\)$/;
const propertyRegex = /^(\w+)\.(\w+)$/; const propertyRegex = /^(\w+)\.(\w+)$/;
const eventRegex = /^event: '(\w+)'$/; const eventRegex = /^event: '(\w+)'$/;
let currentClassName = null; let currentClassName = null;

View File

@ -1,5 +1,9 @@
### class: Foo ### class: Foo
#### foo.$()
#### foo.money$$money()
#### foo.proceed() #### foo.proceed()
#### foo.start() #### foo.start()

View File

@ -7,4 +7,10 @@ class Foo {
get zzz() { get zzz() {
} }
$() {
}
money$$money() {
}
} }