mirror of
https://github.com/puppeteer/puppeteer
synced 2024-06-14 14:02:48 +00:00
Implement page.waitForFunction method
The page.waitForFunction method allows to wait for a general predicate. The predicate will be continiously polled for in page, until it either returns true or the timeout happens. The polling parameter could be one of the following: - 'raf' - to poll on every animation frame - 'mutation' - to poll on every dom mutation - <number> - to poll every X milliseconds References #91
This commit is contained in:
parent
47a0366b16
commit
ff5ed1c738
37
docs/api.md
37
docs/api.md
@ -59,6 +59,7 @@
|
|||||||
+ [page.url()](#pageurl)
|
+ [page.url()](#pageurl)
|
||||||
+ [page.viewport()](#pageviewport)
|
+ [page.viewport()](#pageviewport)
|
||||||
+ [page.waitFor(selectorOrTimeout[, options])](#pagewaitforselectorortimeout-options)
|
+ [page.waitFor(selectorOrTimeout[, options])](#pagewaitforselectorortimeout-options)
|
||||||
|
+ [page.waitForFunction(pageFunction[, options], ...args)](#pagewaitforfunctionpagefunction-options-args)
|
||||||
+ [page.waitForNavigation(options)](#pagewaitfornavigationoptions)
|
+ [page.waitForNavigation(options)](#pagewaitfornavigationoptions)
|
||||||
+ [page.waitForSelector(selector[, options])](#pagewaitforselectorselector-options)
|
+ [page.waitForSelector(selector[, options])](#pagewaitforselectorselector-options)
|
||||||
* [class: Keyboard](#class-keyboard)
|
* [class: Keyboard](#class-keyboard)
|
||||||
@ -91,6 +92,7 @@
|
|||||||
+ [frame.title()](#frametitle)
|
+ [frame.title()](#frametitle)
|
||||||
+ [frame.url()](#frameurl)
|
+ [frame.url()](#frameurl)
|
||||||
+ [frame.waitFor(selectorOrTimeout[, options])](#framewaitforselectorortimeout-options)
|
+ [frame.waitFor(selectorOrTimeout[, options])](#framewaitforselectorortimeout-options)
|
||||||
|
+ [frame.waitForFunction(pageFunction[, options], ...args)](#framewaitforfunctionpagefunction-options-args)
|
||||||
+ [frame.waitForSelector(selector[, options])](#framewaitforselectorselector-options)
|
+ [frame.waitForSelector(selector[, options])](#framewaitforselectorselector-options)
|
||||||
* [class: Request](#class-request)
|
* [class: Request](#class-request)
|
||||||
+ [request.headers](#requestheaders)
|
+ [request.headers](#requestheaders)
|
||||||
@ -653,6 +655,18 @@ This method behaves differently with respect to the type of the first parameter:
|
|||||||
|
|
||||||
The method is a shortcut for [page.mainFrame().waitFor()](#framewaitforselectorortimeout-options).
|
The method is a shortcut for [page.mainFrame().waitFor()](#framewaitforselectorortimeout-options).
|
||||||
|
|
||||||
|
#### page.waitForFunction(pageFunction[, options], ...args)
|
||||||
|
- `pageFunction` <[function]|[string]> Function to be evaluated in browser context
|
||||||
|
- `options` <[Object]> Optional waiting parameters
|
||||||
|
- `polling` <[string]|[number]> An interval at which the `pageFunction` is executed, defaults to `raf`. If `polling` is a number, then it is treated as an interval in milliseconds at which the function would be executed. If `polling` is a string, then it could be one of the following values:
|
||||||
|
- `raf` - to constantly execute `pageFunction` in `requestAnimationFrame` callback. This is the tightest polling mode which is suitable to observe styling changes.
|
||||||
|
- `mutation` - to execute `pageFunction` on every DOM mutation.
|
||||||
|
- `timeout` <[number]> maximum time to wait for in milliseconds. Defaults to `30000` (30 seconds).
|
||||||
|
- `...args` <...[Object]> Arguments to pass to `pageFunction`
|
||||||
|
- returns: <[Promise]> Promise which resolves when element specified by selector string is added to DOM.
|
||||||
|
|
||||||
|
Shortcut for [page.mainFrame().waitForFunction()](#framewaitforfunctionpagefunction-options-args).
|
||||||
|
|
||||||
#### page.waitForNavigation(options)
|
#### page.waitForNavigation(options)
|
||||||
- `options` <[Object]> Navigation parameters which might have the following properties:
|
- `options` <[Object]> Navigation parameters which might have the following properties:
|
||||||
- `timeout` <[number]> Maximum navigation time in milliseconds, defaults to 30 seconds.
|
- `timeout` <[number]> Maximum navigation time in milliseconds, defaults to 30 seconds.
|
||||||
@ -902,6 +916,29 @@ This method behaves differently with respect to the type of the first parameter:
|
|||||||
- if `selectorOrTimeout` is a `number`, than the first argument is treated as a timeout in milliseconds and the method returns a promise which resolves after the timeout
|
- if `selectorOrTimeout` is a `number`, than the first argument is treated as a timeout in milliseconds and the method returns a promise which resolves after the timeout
|
||||||
- otherwise, an exception is thrown
|
- otherwise, an exception is thrown
|
||||||
|
|
||||||
|
#### frame.waitForFunction(pageFunction[, options], ...args)
|
||||||
|
- `pageFunction` <[function]|[string]> Function to be evaluated in browser context
|
||||||
|
- `options` <[Object]> Optional waiting parameters
|
||||||
|
- `polling` <[string]|[number]> An interval at which the `pageFunction` is executed, defaults to `raf`. If `polling` is a number, then it is treated as an interval in milliseconds at which the function would be executed. If `polling` is a string, then it could be one of the following values:
|
||||||
|
- `raf` - to constantly execute `pageFunction` in `requestAnimationFrame` callback. This is the tightest polling mode which is suitable to observe styling changes.
|
||||||
|
- `mutation` - to execute `pageFunction` on every DOM mutation.
|
||||||
|
- `timeout` <[number]> maximum time to wait for in milliseconds. Defaults to `30000` (30 seconds).
|
||||||
|
- `...args` <...[Object]> Arguments to pass to `pageFunction`
|
||||||
|
- returns: <[Promise]> Promise which resolves when element specified by selector string is added to DOM.
|
||||||
|
|
||||||
|
The `waitForFunction` could be used to observe viewport size change:
|
||||||
|
```js
|
||||||
|
const {Browser} = require('.');
|
||||||
|
const browser = new Browser();
|
||||||
|
|
||||||
|
browser.newPage().then(async page => {
|
||||||
|
const watchDog = page.waitForFunction('window.innerWidth < 100);
|
||||||
|
page.setViewport({width: 50, height: 50});
|
||||||
|
await watchDog;
|
||||||
|
browser.close();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
#### frame.waitForSelector(selector[, options])
|
#### 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
|
||||||
|
@ -275,9 +275,9 @@ class Frame {
|
|||||||
* @return {!Promise}
|
* @return {!Promise}
|
||||||
*/
|
*/
|
||||||
waitFor(selectorOrTimeout, options = {}) {
|
waitFor(selectorOrTimeout, options = {}) {
|
||||||
if (typeof selectorOrTimeout === 'string' || selectorOrTimeout instanceof String)
|
if (helper.isString(selectorOrTimeout))
|
||||||
return this.waitForSelector(selectorOrTimeout, options);
|
return this.waitForSelector(selectorOrTimeout, options);
|
||||||
if (typeof selectorOrTimeout === 'number' || selectorOrTimeout instanceof Number)
|
if (helper.isNumber(selectorOrTimeout))
|
||||||
return new Promise(fulfill => setTimeout(fulfill, selectorOrTimeout));
|
return new Promise(fulfill => setTimeout(fulfill, selectorOrTimeout));
|
||||||
return Promise.reject(new Error('Unsupported target type: ' + (typeof selectorOrTimeout)));
|
return Promise.reject(new Error('Unsupported target type: ' + (typeof selectorOrTimeout)));
|
||||||
}
|
}
|
||||||
@ -290,8 +290,35 @@ class Frame {
|
|||||||
waitForSelector(selector, options = {}) {
|
waitForSelector(selector, options = {}) {
|
||||||
const timeout = options.timeout || 30000;
|
const timeout = options.timeout || 30000;
|
||||||
const waitForVisible = !!options.visible;
|
const waitForVisible = !!options.visible;
|
||||||
const pageScript = helper.evaluationString(waitForSelectorPageFunction, selector, waitForVisible, timeout);
|
const polling = waitForVisible ? 'raf' : 'mutation';
|
||||||
return new WaitTask(this, pageScript, timeout).promise;
|
return this.waitForFunction(predicate, {timeout, polling}, selector, waitForVisible);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} selector
|
||||||
|
* @param {boolean} waitForVisible
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
function predicate(selector, waitForVisible) {
|
||||||
|
const node = document.querySelector(selector);
|
||||||
|
if (!node)
|
||||||
|
return false;
|
||||||
|
if (!waitForVisible)
|
||||||
|
return true;
|
||||||
|
const style = window.getComputedStyle(node);
|
||||||
|
return style && style.display !== 'none' && style.visibility !== 'hidden';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {function()} pageFunction
|
||||||
|
* @param {!Object=} options
|
||||||
|
* @return {!Promise}
|
||||||
|
*/
|
||||||
|
waitForFunction(pageFunction, options = {}, ...args) {
|
||||||
|
const timeout = options.timeout || 30000;
|
||||||
|
const polling = options.polling || 'raf';
|
||||||
|
const predicateCode = 'return ' + helper.evaluationString(pageFunction, ...args);
|
||||||
|
return new WaitTask(this, predicateCode, polling, timeout).promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -415,12 +442,20 @@ helper.tracePublicAPI(Frame);
|
|||||||
class WaitTask {
|
class WaitTask {
|
||||||
/**
|
/**
|
||||||
* @param {!Frame} frame
|
* @param {!Frame} frame
|
||||||
* @param {string} pageScript
|
* @param {string} predicateBody
|
||||||
|
* @param {string} polling
|
||||||
* @param {number} timeout
|
* @param {number} timeout
|
||||||
*/
|
*/
|
||||||
constructor(frame, pageScript, timeout) {
|
constructor(frame, predicateBody, polling, timeout) {
|
||||||
|
if (helper.isString(polling))
|
||||||
|
console.assert(polling === 'raf' || polling === 'mutation', 'Unknown polling option: ' + polling);
|
||||||
|
else if (helper.isNumber(polling))
|
||||||
|
console.assert(polling > 0, 'Cannot poll with non-positive interval: ' + polling);
|
||||||
|
else
|
||||||
|
throw new Error('Unknown polling options: ' + polling);
|
||||||
|
|
||||||
this._frame = frame;
|
this._frame = frame;
|
||||||
this._pageScript = pageScript;
|
this._pageScript = helper.evaluationString(waitForPredicatePageFunction, predicateBody, polling, timeout);
|
||||||
this._runCount = 0;
|
this._runCount = 0;
|
||||||
frame._waitTasks.add(this);
|
frame._waitTasks.add(this);
|
||||||
this.promise = new Promise((resolve, reject) => {
|
this.promise = new Promise((resolve, reject) => {
|
||||||
@ -475,31 +510,34 @@ class WaitTask {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} selector
|
* @param {string} predicateBody
|
||||||
* @param {boolean} waitForVisible
|
* @param {string} polling
|
||||||
* @param {number} timeout
|
* @param {number} timeout
|
||||||
* @return {!Promise<boolean>}
|
* @return {!Promise<boolean>}
|
||||||
*/
|
*/
|
||||||
async function waitForSelectorPageFunction(selector, visible, timeout) {
|
async function waitForPredicatePageFunction(predicateBody, polling, timeout) {
|
||||||
|
const predicate = new Function(predicateBody);
|
||||||
let timedOut = false;
|
let timedOut = false;
|
||||||
setTimeout(() => timedOut = true, timeout);
|
setTimeout(() => timedOut = true, timeout);
|
||||||
await waitForDOM();
|
if (polling === 'raf')
|
||||||
await waitForVisible();
|
await pollRaf();
|
||||||
|
else if (polling === 'mutation')
|
||||||
|
await pollMutation();
|
||||||
|
else if (typeof polling === 'number')
|
||||||
|
await pollInterval(polling);
|
||||||
return !timedOut;
|
return !timedOut;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return {!Promise<!Element>}
|
* @return {!Promise<!Element>}
|
||||||
*/
|
*/
|
||||||
function waitForDOM() {
|
function pollMutation() {
|
||||||
let node = document.querySelector(selector);
|
if (predicate())
|
||||||
if (node)
|
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
|
|
||||||
let fulfill;
|
let fulfill;
|
||||||
const result = new Promise(x => fulfill = x);
|
const result = new Promise(x => fulfill = x);
|
||||||
const observer = new MutationObserver(mutations => {
|
const observer = new MutationObserver(mutations => {
|
||||||
const node = document.querySelector(selector);
|
if (timedOut || predicate()) {
|
||||||
if (node || timedOut) {
|
|
||||||
observer.disconnect();
|
observer.disconnect();
|
||||||
fulfill();
|
fulfill();
|
||||||
}
|
}
|
||||||
@ -512,26 +550,37 @@ async function waitForSelectorPageFunction(selector, visible, timeout) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return {!Promise<!Element>}
|
* @return {!Promise}
|
||||||
*/
|
*/
|
||||||
function waitForVisible() {
|
function pollRaf() {
|
||||||
let fulfill;
|
let fulfill;
|
||||||
const result = new Promise(x => fulfill = x);
|
const result = new Promise(x => fulfill = x);
|
||||||
onRaf();
|
onRaf();
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
function onRaf() {
|
function onRaf() {
|
||||||
if (timedOut) {
|
if (timedOut || predicate())
|
||||||
fulfill();
|
fulfill();
|
||||||
return;
|
else
|
||||||
}
|
|
||||||
const node = document.querySelector(selector);
|
|
||||||
const style = node ? window.getComputedStyle(node) : null;
|
|
||||||
if (!style || style.display === 'none' || style.visibility === 'hidden') {
|
|
||||||
requestAnimationFrame(onRaf);
|
requestAnimationFrame(onRaf);
|
||||||
return;
|
}
|
||||||
}
|
}
|
||||||
fulfill();
|
|
||||||
|
/**
|
||||||
|
* @param {number} pollInterval
|
||||||
|
* @return {!Promise}
|
||||||
|
*/
|
||||||
|
function pollInterval(pollInterval) {
|
||||||
|
let fulfill;
|
||||||
|
const result = new Promise(x => fulfill = x);
|
||||||
|
onTimeout();
|
||||||
|
return result;
|
||||||
|
|
||||||
|
function onTimeout() {
|
||||||
|
if (timedOut || predicate())
|
||||||
|
fulfill();
|
||||||
|
else
|
||||||
|
setTimeout(onTimeout, pollInterval);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
14
lib/Page.js
14
lib/Page.js
@ -571,6 +571,16 @@ class Page extends EventEmitter {
|
|||||||
return this.mainFrame().waitForSelector(selector, options);
|
return this.mainFrame().waitForSelector(selector, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {function()} pageFunction
|
||||||
|
* @param {!Object=} options
|
||||||
|
* @param {!Array<*>} args
|
||||||
|
* @return {!Promise}
|
||||||
|
*/
|
||||||
|
waitForFunction(pageFunction, options = {}, ...args) {
|
||||||
|
return this.mainFrame().waitForFunction(pageFunction, options, ...args);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} selector
|
* @param {string} selector
|
||||||
* @param {!Array<string>} filePaths
|
* @param {!Array<string>} filePaths
|
||||||
@ -637,10 +647,10 @@ function convertPrintParameterToInches(parameter) {
|
|||||||
if (typeof parameter === 'undefined')
|
if (typeof parameter === 'undefined')
|
||||||
return undefined;
|
return undefined;
|
||||||
let pixels;
|
let pixels;
|
||||||
if (typeof parameter === 'number') {
|
if (helper.isNumber(parameter)) {
|
||||||
// Treat numbers as pixel values to be aligned with phantom's paperSize.
|
// Treat numbers as pixel values to be aligned with phantom's paperSize.
|
||||||
pixels = /** @type {number} */ (parameter);
|
pixels = /** @type {number} */ (parameter);
|
||||||
} else if (typeof parameter === 'string') {
|
} else if (helper.isString(parameter)) {
|
||||||
let text = parameter;
|
let text = parameter;
|
||||||
let unit = text.substring(text.length - 2).toLowerCase();
|
let unit = text.substring(text.length - 2).toLowerCase();
|
||||||
let valueText = '';
|
let valueText = '';
|
||||||
|
@ -23,7 +23,7 @@ class Helper {
|
|||||||
* @return {string}
|
* @return {string}
|
||||||
*/
|
*/
|
||||||
static evaluationString(fun, ...args) {
|
static evaluationString(fun, ...args) {
|
||||||
if (typeof fun === 'string') {
|
if (Helper.isString(fun)) {
|
||||||
console.assert(args.length === 0, 'Cannot evaluate a string with arguments');
|
console.assert(args.length === 0, 'Cannot evaluate a string with arguments');
|
||||||
return fun;
|
return fun;
|
||||||
}
|
}
|
||||||
@ -181,6 +181,22 @@ class Helper {
|
|||||||
static recordPublicAPICoverage() {
|
static recordPublicAPICoverage() {
|
||||||
apiCoverage = new Map();
|
apiCoverage = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {!Object} obj
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
static isString(obj) {
|
||||||
|
return typeof obj === 'string' || obj instanceof String;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {!Object} obj
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
static isNumber(obj) {
|
||||||
|
return typeof obj === 'number' || obj instanceof Number;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Helper;
|
module.exports = Helper;
|
||||||
|
54
test/test.js
54
test/test.js
@ -198,6 +198,60 @@ describe('Puppeteer', function() {
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Frame.waitForFunction', function() {
|
||||||
|
it('should accept a string', SX(async function() {
|
||||||
|
const watchdog = page.waitForFunction('window.__FOO === 1');
|
||||||
|
await page.evaluate(() => window.__FOO = 1);
|
||||||
|
await watchdog;
|
||||||
|
}));
|
||||||
|
it('should poll on interval', SX(async function() {
|
||||||
|
let success = false;
|
||||||
|
const startTime = Date.now();
|
||||||
|
const polling = 100;
|
||||||
|
const watchdog = page.waitForFunction(() => window.__FOO === 'hit', {polling})
|
||||||
|
.then(() => success = true);
|
||||||
|
await page.evaluate(() => window.__FOO = 'hit');
|
||||||
|
expect(success).toBe(false);
|
||||||
|
await page.evaluate(() => document.body.appendChild(document.createElement('div')));
|
||||||
|
await watchdog;
|
||||||
|
expect(Date.now() - startTime).not.toBeLessThan(polling / 2);
|
||||||
|
}));
|
||||||
|
it('should poll on mutation', SX(async function() {
|
||||||
|
let success = false;
|
||||||
|
const watchdog = page.waitForFunction(() => window.__FOO === 'hit', {polling: 'mutation'})
|
||||||
|
.then(() => success = true);
|
||||||
|
await page.evaluate(() => window.__FOO = 'hit');
|
||||||
|
expect(success).toBe(false);
|
||||||
|
await page.evaluate(() => document.body.appendChild(document.createElement('div')));
|
||||||
|
await watchdog;
|
||||||
|
}));
|
||||||
|
it('should poll on raf', SX(async function() {
|
||||||
|
const watchdog = page.waitForFunction(() => window.__FOO === 'hit', {polling: 'raf'});
|
||||||
|
await page.evaluate(() => window.__FOO = 'hit');
|
||||||
|
await watchdog;
|
||||||
|
}));
|
||||||
|
it('should throw on bad polling value', SX(async function() {
|
||||||
|
let error = null;
|
||||||
|
try {
|
||||||
|
await page.waitForFunction(() => !!document.body, {polling: 'unknown'});
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
expect(error).toBeTruthy();
|
||||||
|
expect(error.message).toContain('polling');
|
||||||
|
}));
|
||||||
|
it('should throw negative polling interval', SX(async function() {
|
||||||
|
let error = null;
|
||||||
|
try {
|
||||||
|
await page.waitForFunction(() => !!document.body, {polling: -10});
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
expect(error).toBeTruthy();
|
||||||
|
expect(error.message).toContain('Cannot poll with non-positive interval');
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
describe('Frame.waitForSelector', 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));
|
||||||
|
Loading…
Reference in New Issue
Block a user