Implement visible option for Page.waitFor method

This patch adds a 'visible' option to the Page.waitFor method, making
it possible to wait for the element to become actually visible.

References #89, #91.
This commit is contained in:
Andrey Lushnikov 2017-07-21 00:58:38 -07:00
parent 139b9e9b6d
commit 52de75742b
8 changed files with 117 additions and 55 deletions

View File

@ -59,7 +59,7 @@
+ [page.url()](#pageurl) + [page.url()](#pageurl)
+ [page.userAgent()](#pageuseragent) + [page.userAgent()](#pageuseragent)
+ [page.viewport()](#pageviewport) + [page.viewport()](#pageviewport)
+ [page.waitFor(selector)](#pagewaitforselector) + [page.waitFor(selector[, options])](#pagewaitforselector-options)
+ [page.waitForNavigation(options)](#pagewaitfornavigationoptions) + [page.waitForNavigation(options)](#pagewaitfornavigationoptions)
* [class: Keyboard](#class-keyboard) * [class: Keyboard](#class-keyboard)
+ [keyboard.down(key[, options])](#keyboarddownkey-options) + [keyboard.down(key[, options])](#keyboarddownkey-options)
@ -83,7 +83,7 @@
+ [frame.name()](#framename) + [frame.name()](#framename)
+ [frame.parentFrame()](#frameparentframe) + [frame.parentFrame()](#frameparentframe)
+ [frame.url()](#frameurl) + [frame.url()](#frameurl)
+ [frame.waitFor(selector)](#framewaitforselector) + [frame.waitFor(selector[, options])](#framewaitforselector-options)
* [class: Request](#class-request) * [class: Request](#class-request)
+ [request.headers](#requestheaders) + [request.headers](#requestheaders)
+ [request.method](#requestmethod) + [request.method](#requestmethod)
@ -593,30 +593,18 @@ This is a shortcut for [page.mainFrame().url()](#frameurl)
- returns: <[Object]> An object with the save fields as described in [page.setViewport](#pagesetviewportviewport) - returns: <[Object]> An object with the save fields as described in [page.setViewport](#pagesetviewportviewport)
#### page.waitFor(selector) #### page.waitFor(selector[, options])
- `selector` <[string]> A query selector to wait for on the page. - `selector` <[string]> A query selector to wait for on the page.
- `options` <[Object]> Optional waiting parameters
- `visible` <[boolean]> wait for element to be present in DOM and to be visible, i.e. to not have `display: none` or `visibility: hidden` CSS properties.
- returns: <[Promise]> Promise which resolves when the element matching `selector` appears in the page. - returns: <[Promise]> Promise which resolves when the element matching `selector` appears in the page.
The `page.waitFor` successfully survives page navigations: Shortcut for [page.mainFrame().waitFor(selector)](#framewaitforselector).
```js
const {Browser} = new require('puppeteer');
const browser = new Browser();
browser.newPage().then(async page => {
let currentURL;
page.waitFor('img').then(() => console.log('First URL with image: ' + currentURL));
for (currentURL of ['https://example.com', 'https://google.com', 'https://bbc.com'])
await page.navigate(currentURL);
browser.close();
});
```
#### page.waitForNavigation(options) #### page.waitForNavigation(options)
- `options` <[Object]> Navigation parameters, same as in [page.navigate](#pagenavigateurl-options). - `options` <[Object]> Navigation parameters, same as in [page.navigate](#pagenavigateurl-options).
- returns: <[Promise]<[Response]>> Promise which resolves to the main resource response. In case of multiple redirects, the navigation will resolve with the response of the last redirect. - returns: <[Promise]<[Response]>> Promise which resolves to the main resource response. In case of multiple redirects, the navigation will resolve with the response of the last redirect.
Shortcut for [page.mainFrame().waitFor(selector)](#framewaitforselector).
### class: Keyboard ### class: Keyboard
Keyboard provides an api for managing a virtual keyboard. The high level api is [`keyboard.type`](#keyboardtypetext), which takes raw characters and generates proper keydown, keypress/input, and keyup events on your page. Keyboard provides an api for managing a virtual keyboard. The high level api is [`keyboard.type`](#keyboardtypetext), which takes raw characters and generates proper keydown, keypress/input, and keyup events on your page.
@ -813,14 +801,30 @@ Returns frame's name as specified in the tag.
Returns frame's url. Returns frame's url.
#### frame.waitFor(selector) #### frame.waitFor(selector[, options])
- `selector` <[string]> CSS selector of awaited element, - `selector` <[string]> CSS selector of awaited element,
- `options` <[Object]> Optional waiting parameters
- `visible` <[boolean]> wait for element to be present in DOM and to be visible, i.e. to not have `display: none` or `visibility: hidden` CSS properties.
- returns: <[Promise]> Promise which resolves when element specified by selector string is added to DOM. - returns: <[Promise]> Promise which resolves when element specified by selector string is added to DOM.
Wait for the `selector` to appear in page. If at the moment of calling Wait for the `selector` to appear in page. If at the moment of calling
the method the `selector` already exists, the method will return the method the `selector` already exists, the method will return
immediately. immediately.
This method works across navigations:
```js
const {Browser} = new require('puppeteer');
const browser = new Browser();
browser.newPage().then(async page => {
let currentURL;
page.waitFor('img').then(() => console.log('First URL with image: ' + currentURL));
for (currentURL of ['https://example.com', 'https://google.com', 'https://bbc.com'])
await page.navigate(currentURL);
browser.close();
});
```
### class: Request ### class: Request

View File

@ -173,40 +173,19 @@ class FrameManager extends EventEmitter {
} }
/** /**
* @param {string} selector
* @param {!Frame} frame * @param {!Frame} frame
* @param {string} selector
* @param {boolean} waitForVisible
* @return {!Promise<undefined>} * @return {!Promise<undefined>}
*/ */
async _waitForSelector(selector, frame) { async _waitForSelector(frame, selector, waitForVisible) {
function code(selector) {
if (document.querySelector(selector))
return Promise.resolve();
let callback;
const result = new Promise(fulfill => callback = fulfill);
const mo = new MutationObserver((mutations, observer) => {
if (document.querySelector(selector)) {
observer.disconnect();
callback();
return;
}
});
mo.observe(document, {
childList: true,
subtree: true
});
return result;
}
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 { exceptionDetails } = await this._client.send('Runtime.evaluate', { let { exceptionDetails } = await this._client.send('Runtime.evaluate', {
expression: helper.evaluationString(code, selector), expression: helper.evaluationString(inPageWatchdog, selector, waitForVisible),
contextId, contextId,
awaitPromise: true, awaitPromise: true,
returnByValue: false, returnByValue: false,
@ -215,6 +194,65 @@ class FrameManager extends EventEmitter {
let message = await helper.getExceptionMessage(this._client, exceptionDetails); let message = await helper.getExceptionMessage(this._client, exceptionDetails);
throw new Error('Evaluation failed: ' + message); throw new Error('Evaluation failed: ' + message);
} }
/**
* @param {string} selector
* @param {boolean} waitForVisible
* @return {!Promise}
*/
function inPageWatchdog(selector, visible) {
return visible ? waitForVisible(selector) : waitInDOM(selector);
/**
* @param {string} selector
* @return {!Promise<!Element>}
*/
function waitInDOM(selector) {
let node = document.querySelector(selector);
if (node)
return Promise.resolve(node);
let fulfill;
const result = new Promise(x => fulfill = x);
const observer = new MutationObserver(mutations => {
const node = document.querySelector(selector);
if (node) {
observer.disconnect();
fulfill(node);
}
});
observer.observe(document, {
childList: true,
subtree: true
});
return result;
}
/**
* @param {string} selector
* @return {!Promise<!Element>}
*/
async function waitForVisible(selector) {
let fulfill;
const result = new Promise(x => fulfill = x);
onRaf();
return result;
async function onRaf() {
const node = await waitInDOM(selector);
if (!node) {
fulfill(null);
return;
}
const style = window.getComputedStyle(node);
if (style.display === 'none' || style.visibility === 'hidden') {
requestAnimationFrame(onRaf);
return;
}
fulfill(node);
}
}
}
} }
} }
@ -304,10 +342,11 @@ class Frame {
/** /**
* @param {string} selector * @param {string} selector
* @param {!Object=} options
* @return {!Promise} * @return {!Promise}
*/ */
async waitFor(selector) { async waitFor(selector, options = {}) {
const awaitedElement = new AwaitedElement(() => this._frameManager._waitForSelector(selector, this)); const awaitedElement = new AwaitedElement(() => this._frameManager._waitForSelector(this, selector, !!options.visible));
this._awaitedElements.add(awaitedElement); this._awaitedElements.add(awaitedElement);
let cleanup = () => this._awaitedElements.delete(awaitedElement); let cleanup = () => this._awaitedElements.delete(awaitedElement);

View File

@ -598,10 +598,11 @@ class Page extends EventEmitter {
/** /**
* @param {string} selector * @param {string} selector
* @param {!Object=} options
* @return {!Promise<undefined>} * @return {!Promise<undefined>}
*/ */
waitFor(selector) { waitFor(selector, options) {
return this.mainFrame().waitFor(selector); return this.mainFrame().waitFor(selector, options);
} }
/** /**

View File

@ -264,6 +264,16 @@ describe('Puppeteer', function() {
await waitFor; await waitFor;
expect(boxFound).toBe(true); expect(boxFound).toBe(true);
})); }));
it('should wait for visible', SX(async function() {
let divFound = false;
let waitFor = page.waitFor('div', {visible: true}).then(() => divFound = true);
await page.setContent(`<div style='display: none;visibility: hidden'></div>`);
expect(divFound).toBe(false);
await page.evaluate(() => document.querySelector('div').style.removeProperty('display'));
expect(divFound).toBe(false);
await page.evaluate(() => document.querySelector('div').style.removeProperty('visibility'));
expect(await waitFor).toBe(true);
}));
}); });
describe('Page.Events.Console', function() { describe('Page.Events.Console', function() {

View File

@ -66,8 +66,16 @@ class JSOutline {
walker.walk(node.value.body); walker.walk(node.value.body);
} }
const args = []; const args = [];
for (let param of node.value.params) for (let param of node.value.params) {
args.push(new Documentation.Argument(this._extractText(param))); if (param.type === 'AssignmentPattern')
args.push(new Documentation.Argument(param.left.name));
else if (param.type === 'RestElement')
args.push(new Documentation.Argument('...' + param.argument.name));
else if (param.type === 'Identifier')
args.push(new Documentation.Argument(param.name));
else
this.errors.push('JS Parsing issue: cannot support parameter of type ' + param.type + ' in method ' + methodName);
}
let method = Documentation.Member.createMethod(methodName, args, hasReturn, node.value.async); let method = Documentation.Member.createMethod(methodName, args, hasReturn, node.value.async);
this._currentClassMembers.push(method); this._currentClassMembers.push(method);
return ESTreeWalker.SkipSubtree; return ESTreeWalker.SkipSubtree;

View File

@ -3,5 +3,5 @@
- `arg1` <[string]> - `arg1` <[string]>
- `arg2` <[string]> - `arg2` <[string]>
#### foo.test(fileNames) #### foo.test(...files)
- `filePaths` <[Array]> - `...filePaths` <[string]>

View File

@ -1,7 +1,7 @@
class Foo { class Foo {
constructor(arg1, arg3) { constructor(arg1, arg3 = {}) {
} }
test(filePaths) { test(...filePaths) {
} }
} }

View File

@ -1,4 +1,4 @@
[MarkDown] Method Foo.constructor() fails to describe its parameters: [MarkDown] Method Foo.constructor() fails to describe its parameters:
- Argument not found: arg3 - Argument not found: arg3
- Non-existing argument found: arg2 - Non-existing argument found: arg2
[MarkDown] Heading arguments for "foo.test(fileNames)" do not match described ones, i.e. "fileNames" != "filePaths" [MarkDown] Heading arguments for "foo.test(...files)" do not match described ones, i.e. "...files" != "...filePaths"