Introduce polymorphic page.waitFor method

This patch:
- introduces page.waitForSelector to wait for the selector to appear
- introduces polymorphic page.waitFor method, which accepts
either string (and in this case is a shortcut for page.waitForSelector)
or number (and in this case it's a promisified timeout).

References #91.
This commit is contained in:
Andrey Lushnikov 2017-07-21 12:41:49 -07:00
parent 1891a49962
commit dc032b42b9
6 changed files with 108 additions and 39 deletions

View File

@ -59,8 +59,9 @@
+ [page.url()](#pageurl) + [page.url()](#pageurl)
+ [page.userAgent()](#pageuseragent) + [page.userAgent()](#pageuseragent)
+ [page.viewport()](#pageviewport) + [page.viewport()](#pageviewport)
+ [page.waitFor(selector[, options])](#pagewaitforselector-options) + [page.waitFor(target[, options])](#pagewaitfortarget-options)
+ [page.waitForNavigation(options)](#pagewaitfornavigationoptions) + [page.waitForNavigation(options)](#pagewaitfornavigationoptions)
+ [page.waitForSelector(selector[, options])](#pagewaitforselectorselector-options)
* [class: Keyboard](#class-keyboard) * [class: Keyboard](#class-keyboard)
+ [keyboard.down(key[, options])](#keyboarddownkey-options) + [keyboard.down(key[, options])](#keyboarddownkey-options)
+ [keyboard.modifiers()](#keyboardmodifiers) + [keyboard.modifiers()](#keyboardmodifiers)
@ -83,7 +84,8 @@
+ [frame.name()](#framename) + [frame.name()](#framename)
+ [frame.parentFrame()](#frameparentframe) + [frame.parentFrame()](#frameparentframe)
+ [frame.url()](#frameurl) + [frame.url()](#frameurl)
+ [frame.waitFor(selector[, options])](#framewaitforselector-options) + [frame.waitFor(target[, options])](#framewaitfortarget-options)
+ [frame.waitForSelector(selector[, options])](#framewaitforselectorselector-options)
* [class: Request](#class-request) * [class: Request](#class-request)
+ [request.headers](#requestheaders) + [request.headers](#requestheaders)
+ [request.method](#requestmethod) + [request.method](#requestmethod)
@ -592,18 +594,24 @@ This is a shortcut for [page.mainFrame().url()](#frameurl)
#### page.viewport() #### page.viewport()
- 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(target[, options])
- `target` <[string]|[number]> A target to wait for.
- `options` <[Object]> Optional waiting parameters.
- returns: <[Promise]>
#### page.waitFor(selector[, options]) Shortcut for [page.mainFrame().waitFor()](#framewaitfortargetoptions).
- `selector` <[string]> A query selector to wait for on the page.
- `options` <[Object]> Optional waiting parameters. Same as options for the [frame.waitFor](#framewaitforselector)
- returns: <[Promise]> Promise which resolves when the element matching `selector` appears in the page.
Shortcut for [page.mainFrame().waitFor(selector)](#framewaitforselector).
#### 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.
#### page.waitForSelector(selector[, options])
- `selector` <[string]> A query selector to wait for on the page.
- `options` <[Object]> Optional waiting parameters. Same as options for the [frame.waitFor](#framewaitforselector)
- returns: <[Promise]> Promise which resolves when the element matching `selector` appears in the page.
Shortcut for [page.mainFrame().waitForSelector()](#framewaitforselectorselectoroptions).
### 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.
@ -800,7 +808,17 @@ Returns frame's name as specified in the tag.
Returns frame's url. Returns frame's url.
#### frame.waitFor(selector[, options]) #### frame.waitFor(target[, options])
- `target` <[string]|[number]> A target to wait for
- `options` <[Object]> Optional waiting parameters
- returns: <[Promise]>
This method behaves differently wrt the type of the first parameter:
- if `target` is a `string`, than target is treated as a selector to wait for and the method is a shortcut for [frame.waitForSelector](#framewaitforselectorselectoroptions)
- if `target` is a `number`, than target is treated as timeout in milliseconds and the method returns a promise which resolves after the timeout
- otherwise, an exception is thrown
#### frame.waitForSelector(selector[, options])
- `selector` <[string]> CSS selector of awaited element, - `selector` <[string]> CSS selector of awaited element,
- `options` <[Object]> Optional waiting parameters - `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. Defaults to `false`. - `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. Defaults to `false`.
@ -818,7 +836,7 @@ const browser = new Browser();
browser.newPage().then(async page => { browser.newPage().then(async page => {
let currentURL; let currentURL;
page.waitFor('img').then(() => console.log('First URL with image: ' + currentURL)); page.waitForSelector('img').then(() => console.log('First URL with image: ' + currentURL));
for (currentURL of ['https://example.com', 'https://google.com', 'https://bbc.com']) for (currentURL of ['https://example.com', 'https://google.com', 'https://bbc.com'])
await page.navigate(currentURL); await page.navigate(currentURL);
browser.close(); browser.close();

View File

@ -201,7 +201,7 @@ class FrameManager extends EventEmitter {
async function inPageWatchdog(selector, visible, timeout) { async function inPageWatchdog(selector, visible, timeout) {
const resultPromise = visible ? waitForVisible(selector) : waitInDOM(selector); const resultPromise = visible ? waitForVisible(selector) : waitInDOM(selector);
const timeoutPromise = new Promise((resolve, reject) => { const timeoutPromise = new Promise((resolve, reject) => {
setTimeout(reject.bind(null, new Error(`waitFor failed: timeout ${timeout}ms exceeded.`)), timeout); setTimeout(reject.bind(null, new Error(`waitForSelector failed: timeout ${timeout}ms exceeded.`)), timeout);
}); });
await Promise.race([resultPromise, timeoutPromise]); await Promise.race([resultPromise, timeoutPromise]);
@ -342,17 +342,30 @@ class Frame {
return this._detached; return this._detached;
} }
/**
* @param {(string|number)} target
* @param {!Object=} options
* @return {!Promise}
*/
waitFor(target, options = {}) {
if (typeof target === 'string' || target instanceof String)
return this.waitForSelector(target, options);
if (typeof target === 'number' || target instanceof Number)
return new Promise(fulfill => setTimeout(fulfill, target));
return Promise.reject(new Error('Unsupported target type: ' + (typeof target)));
}
/** /**
* @param {string} selector * @param {string} selector
* @param {!Object=} options * @param {!Object=} options
* @return {!Promise} * @return {!Promise}
*/ */
async waitFor(selector, options = {}) { waitForSelector(selector, options = {}) {
const timeout = options.timeout || 30000; const timeout = options.timeout || 30000;
const awaitedElement = new AwaitedElement(() => this._frameManager._waitForSelector(this, selector, !!options.visible, timeout)); const awaitedElement = new AwaitedElement(() => this._frameManager._waitForSelector(this, selector, !!options.visible, timeout));
// Since navigation will re-install page watchdogs, we should timeout on our // Since navigation will re-install page watchdogs, we should timeout on our
// end as well. // end as well.
setTimeout(() => awaitedElement.terminate(new Error(`waitFor failed: timeout ${timeout}ms exceeded`)), timeout); setTimeout(() => awaitedElement.terminate(new Error(`waitForSelector failed: timeout ${timeout}ms exceeded`)), timeout);
this._awaitedElements.add(awaitedElement); this._awaitedElements.add(awaitedElement);
let cleanup = () => this._awaitedElements.delete(awaitedElement); let cleanup = () => this._awaitedElements.delete(awaitedElement);

View File

@ -597,12 +597,21 @@ class Page extends EventEmitter {
} }
/** /**
* @param {string} selector * @param {string} target
* @param {!Object=} options * @param {!Object=} options
* @return {!Promise<undefined>} * @return {!Promise<undefined>}
*/ */
waitFor(selector, options) { waitFor(target, options) {
return this.mainFrame().waitFor(selector, options); return this.mainFrame().waitFor(target, options);
}
/**
* @param {string} selector
* @param {!Object=} options
* @return {!Promise}
*/
waitForSelector(selector, options = {}) {
return this.mainFrame().waitForSelector(selector, options);
} }
/** /**

View File

@ -162,7 +162,7 @@ describe('Puppeteer', function() {
})); }));
}); });
describe('Frame.waitFor', function() { describe('Frame.waitForSelector', function() {
let FrameUtils = require('./frame-utils'); let FrameUtils = require('./frame-utils');
let addElement = tag => document.body.appendChild(document.createElement(tag)); let addElement = tag => document.body.appendChild(document.createElement(tag));
@ -170,12 +170,12 @@ describe('Puppeteer', function() {
await page.navigate(EMPTY_PAGE); await page.navigate(EMPTY_PAGE);
let frame = page.mainFrame(); let frame = page.mainFrame();
let added = false; let added = false;
await frame.waitFor('*').then(() => added = true); await frame.waitForSelector('*').then(() => added = true);
expect(added).toBe(true); expect(added).toBe(true);
added = false; added = false;
await frame.evaluate(addElement, 'div'); await frame.evaluate(addElement, 'div');
await frame.waitFor('div').then(() => added = true); await frame.waitForSelector('div').then(() => added = true);
expect(added).toBe(true); expect(added).toBe(true);
})); }));
@ -183,10 +183,10 @@ describe('Puppeteer', function() {
await page.navigate(EMPTY_PAGE); await page.navigate(EMPTY_PAGE);
let frame = page.mainFrame(); let frame = page.mainFrame();
let added = false; let added = false;
frame.waitFor('div').then(() => added = true); frame.waitForSelector('div').then(() => added = true);
// run nop function.. // run nop function..
await frame.evaluate(() => 42); await frame.evaluate(() => 42);
// .. to be sure that waitFor promise is not resolved yet. // .. to be sure that waitForSelector promise is not resolved yet.
expect(added).toBe(false); expect(added).toBe(false);
await frame.evaluate(addElement, 'br'); await frame.evaluate(addElement, 'br');
expect(added).toBe(false); expect(added).toBe(false);
@ -198,19 +198,19 @@ describe('Puppeteer', function() {
await page.navigate(EMPTY_PAGE); await page.navigate(EMPTY_PAGE);
let frame = page.mainFrame(); let frame = page.mainFrame();
let added = false; let added = false;
frame.waitFor('h3 div').then(() => added = true); frame.waitForSelector('h3 div').then(() => added = true);
expect(added).toBe(false); expect(added).toBe(false);
await frame.evaluate(addElement, 'span'); await frame.evaluate(addElement, 'span');
await page.$('span', span => span.innerHTML = '<h3><div></div></h3>'); await page.$('span', span => span.innerHTML = '<h3><div></div></h3>');
expect(added).toBe(true); expect(added).toBe(true);
})); }));
it('Page.waitFor is shortcut for main frame', SX(async function() { it('Page.waitForSelector is shortcut for main frame', SX(async function() {
await page.navigate(EMPTY_PAGE); await page.navigate(EMPTY_PAGE);
await FrameUtils.attachFrame(page, 'frame1', EMPTY_PAGE); await FrameUtils.attachFrame(page, 'frame1', EMPTY_PAGE);
let otherFrame = page.frames()[1]; let otherFrame = page.frames()[1];
let added = false; let added = false;
page.waitFor('div').then(() => added = true); page.waitForSelector('div').then(() => added = true);
await otherFrame.evaluate(addElement, 'div'); await otherFrame.evaluate(addElement, 'div');
expect(added).toBe(false); expect(added).toBe(false);
await page.evaluate(addElement, 'div'); await page.evaluate(addElement, 'div');
@ -223,7 +223,7 @@ describe('Puppeteer', function() {
let frame1 = page.frames()[1]; let frame1 = page.frames()[1];
let frame2 = page.frames()[2]; let frame2 = page.frames()[2];
let added = false; let added = false;
frame2.waitFor('div').then(() => added = true); frame2.waitForSelector('div').then(() => added = true);
expect(added).toBe(false); expect(added).toBe(false);
await frame1.evaluate(addElement, 'div'); await frame1.evaluate(addElement, 'div');
expect(added).toBe(false); expect(added).toBe(false);
@ -237,8 +237,8 @@ describe('Puppeteer', function() {
}); });
await page.navigate(EMPTY_PAGE); await page.navigate(EMPTY_PAGE);
try { try {
await page.waitFor('*'); await page.waitForSelector('*');
fail('Failed waitFor did not throw.'); fail('Failed waitForSelector did not throw.');
} catch (e) { } catch (e) {
expect(e.message).toContain('document.querySelector is not a function'); expect(e.message).toContain('document.querySelector is not a function');
} }
@ -247,7 +247,7 @@ describe('Puppeteer', function() {
await FrameUtils.attachFrame(page, 'frame1', EMPTY_PAGE); await FrameUtils.attachFrame(page, 'frame1', EMPTY_PAGE);
let frame = page.frames()[1]; let frame = page.frames()[1];
let waitError = null; let waitError = null;
let waitPromise = frame.waitFor('.box').catch(e => waitError = e); let waitPromise = frame.waitForSelector('.box').catch(e => waitError = e);
await FrameUtils.detachFrame(page, 'frame1'); await FrameUtils.detachFrame(page, 'frame1');
await waitPromise; await waitPromise;
expect(waitError).toBeTruthy(); expect(waitError).toBeTruthy();
@ -255,30 +255,56 @@ describe('Puppeteer', function() {
})); }));
it('should survive navigation', SX(async function() { it('should survive navigation', SX(async function() {
let boxFound = false; let boxFound = false;
let waitFor = page.waitFor('.box').then(() => boxFound = true); let waitForSelector = page.waitForSelector('.box').then(() => boxFound = true);
await page.navigate(EMPTY_PAGE); await page.navigate(EMPTY_PAGE);
expect(boxFound).toBe(false); expect(boxFound).toBe(false);
await page.reload(); await page.reload();
expect(boxFound).toBe(false); expect(boxFound).toBe(false);
await page.navigate(PREFIX + '/grid.html'); await page.navigate(PREFIX + '/grid.html');
await waitFor; await waitForSelector;
expect(boxFound).toBe(true); expect(boxFound).toBe(true);
})); }));
it('should wait for visible', SX(async function() { it('should wait for visible', SX(async function() {
let divFound = false; let divFound = false;
let waitFor = page.waitFor('div', {visible: true}).then(() => divFound = true); let waitForSelector = page.waitForSelector('div', {visible: true}).then(() => divFound = true);
await page.setContent(`<div style='display: none;visibility: hidden'></div>`); await page.setContent(`<div style='display: none;visibility: hidden'></div>`);
expect(divFound).toBe(false); expect(divFound).toBe(false);
await page.evaluate(() => document.querySelector('div').style.removeProperty('display')); await page.evaluate(() => document.querySelector('div').style.removeProperty('display'));
expect(divFound).toBe(false); expect(divFound).toBe(false);
await page.evaluate(() => document.querySelector('div').style.removeProperty('visibility')); await page.evaluate(() => document.querySelector('div').style.removeProperty('visibility'));
expect(await waitFor).toBe(true); expect(await waitForSelector).toBe(true);
})); }));
it('should respect timeout', SX(async function() { it('should respect timeout', SX(async function() {
let error = null; let error = null;
await page.waitFor('div', {timeout: 10}).catch(e => error = e); await page.waitForSelector('div', {timeout: 10}).catch(e => error = e);
expect(error).toBeTruthy(); expect(error).toBeTruthy();
expect(error.message).toContain('waitFor failed: timeout'); expect(error.message).toContain('waitForSelector failed: timeout');
}));
});
describe('Page.waitFor', function() {
it('should wait for selector', SX(async function() {
let found = false;
let waitFor = page.waitFor('div').then(() => found = true);
await page.navigate(EMPTY_PAGE);
expect(found).toBe(false);
await page.navigate(PREFIX + '/grid.html');
await waitFor;
expect(found).toBe(true);
}));
it('should timeout', SX(async function() {
startTime = Date.now();
const timeout = 42;
await page.waitFor(timeout);
expect(Date.now() - startTime).not.toBeLessThan(timeout);
}));
it('should throw when unknown type', SX(async function() {
try {
await page.waitFor({foo: 'bar'});
fail('Failed to throw exception');
} catch (e) {
expect(e.message).toContain('Unsupported target type');
}
})); }));
}); });

View File

@ -94,10 +94,13 @@ function lintMarkdown(doc) {
if (member1.type === 'method' && member1.name === 'constructor') if (member1.type === 'method' && member1.name === 'constructor')
continue; continue;
if (member1.name > member2.name) { if (member1.name > member2.name) {
let memberName = `${cls.name}.${member1.name}`; let memberName1 = `${cls.name}.${member1.name}`;
if (member1.type === 'method') if (member1.type === 'method')
memberName += '()'; memberName1 += '()';
errors.push(`${memberName} breaks alphabetic ordering of class members.`); let memberName2 = `${cls.name}.${member2.name}`;
if (member2.type === 'method')
memberName2 += '()';
errors.push(`Bad alphabetic ordering of ${cls.name} members: ${memberName1} should go after ${memberName2}`);
} }
} }
} }

View File

@ -1,5 +1,5 @@
[MarkDown] Events should go first. Event 'b' in class Foo breaks order [MarkDown] Events should go first. Event 'b' in class Foo breaks order
[MarkDown] Constructor of Foo should go before other methods [MarkDown] Constructor of Foo should go before other methods
[MarkDown] Event 'c' in class Foo breaks alphabetic ordering of events [MarkDown] Event 'c' in class Foo breaks alphabetic ordering of events
[MarkDown] Foo.ddd breaks alphabetic ordering of class members. [MarkDown] Bad alphabetic ordering of Foo members: Foo.ddd should go after Foo.constructor()
[MarkDown] Foo.ccc() breaks alphabetic ordering of class members. [MarkDown] Bad alphabetic ordering of Foo members: Foo.ccc() should go after Foo.bbb()