Skip to content

Commit

Permalink
feat: accept JSHandles as arguments to waitFor and return the success…
Browse files Browse the repository at this point in the history
… value
  • Loading branch information
JoelEinbinder committed Jan 3, 2018
1 parent bd73e4b commit 814126c
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 43 deletions.
34 changes: 17 additions & 17 deletions docs/api.md
Expand Up @@ -496,7 +496,7 @@ Shortcut for [page.mainFrame().$$(selector)](#frameselector-1).
#### page.$$eval(selector, pageFunction[, ...args])
- `selector` <[string]> A [selector] to query frame for
- `pageFunction` <[function]> Function to be evaluated in browser context
- `...args` <...[Serializable]|[ElementHandle]> Arguments to pass to `pageFunction`
- `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction`
- returns: <[Promise]<[Serializable]>> Promise which resolves to the return value of `pageFunction`

This method runs `document.querySelectorAll` within the page and passes it as the first argument to `pageFunction`.
Expand All @@ -511,7 +511,7 @@ const divsCounts = await page.$$eval('div', divs => divs.length);
#### page.$eval(selector, pageFunction[, ...args])
- `selector` <[string]> A [selector] to query page for
- `pageFunction` <[function]> Function to be evaluated in browser context
- `...args` <...[Serializable]|[ElementHandle]> Arguments to pass to `pageFunction`
- `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction`
- returns: <[Promise]<[Serializable]>> Promise which resolves to the return value of `pageFunction`

This method runs `document.querySelector` within the page and passes it as the first argument to `pageFunction`. If there's no element matching `selector`, the method throws an error.
Expand Down Expand Up @@ -649,7 +649,7 @@ List of all available devices is available in the source code: [DeviceDescriptor

#### page.evaluate(pageFunction, ...args)
- `pageFunction` <[function]|[string]> Function to be evaluated in the page context
- `...args` <...[Serializable]|[ElementHandle]> Arguments to pass to `pageFunction`
- `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction`
- returns: <[Promise]<[Serializable]>> Resolves to the return value of `pageFunction`

If the function, passed to the `page.evaluate`, returns a [Promise], then `page.evaluate` would wait for the promise to resolve and return its value.
Expand Down Expand Up @@ -1144,8 +1144,8 @@ This is a shortcut for [page.mainFrame().url()](#frameurl)
#### page.waitFor(selectorOrFunctionOrTimeout[, options[, ...args]])
- `selectorOrFunctionOrTimeout` <[string]|[number]|[function]> A [selector], predicate or timeout to wait for
- `options` <[Object]> Optional waiting parameters
- `...args` <...[Serializable]> Arguments to pass to `pageFunction`
- returns: <[Promise]>
- `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction`
- returns: <[Promise]<[JSHandle]>> Promise which resolves to a JSHandle of the success value

This method behaves differently with respect to the type of the first parameter:
- if `selectorOrFunctionOrTimeout` is a `string`, then the first argument is treated as a [selector] to wait for and the method is a shortcut for [page.waitForSelector](#pagewaitforselectorselector-options)
Expand All @@ -1162,8 +1162,8 @@ Shortcut for [page.mainFrame().waitFor(selectorOrFunctionOrTimeout[, options[, .
- `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` <...[Serializable]> Arguments to pass to `pageFunction`
- returns: <[Promise]> Promise which resolves when the `pageFunction` returns a truthy value.
- `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction`
- returns: <[Promise]<[JSHandle]>> Promise which resolves when the `pageFunction` returns a truthy value. It resolves to a JSHandle of the truthy value.

The `waitForFunction` can be used to observe viewport size change:
```js
Expand Down Expand Up @@ -1195,7 +1195,7 @@ Shortcut for [page.mainFrame().waitForFunction(pageFunction[, options[, ...args]
- `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`.
- `hidden` <[boolean]> wait for element to not be found in the DOM or to be hidden, i.e. have `display: none` or `visibility: hidden` CSS properties. Defaults to `false`.
- `timeout` <[number]> maximum time to wait for in milliseconds. Defaults to `30000` (30 seconds).
- returns: <[Promise]> Promise which resolves when element specified by selector string is added to DOM.
- returns: <[Promise]<[ElementHandle]>> 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
the method the `selector` already exists, the method will return
Expand Down Expand Up @@ -1481,7 +1481,7 @@ The method runs `document.querySelectorAll` within the frame. If no elements mat
#### frame.$$eval(selector, pageFunction[, ...args])
- `selector` <[string]> A [selector] to query frame for
- `pageFunction` <[function]> Function to be evaluated in browser context
- `...args` <...[Serializable]|[ElementHandle]> Arguments to pass to `pageFunction`
- `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction`
- returns: <[Promise]<[Serializable]>> Promise which resolves to the return value of `pageFunction`

This method runs `document.querySelectorAll` within the frame and passes it as the first argument to `pageFunction`.
Expand All @@ -1496,7 +1496,7 @@ const divsCounts = await frame.$$eval('div', divs => divs.length);
#### frame.$eval(selector, pageFunction[, ...args])
- `selector` <[string]> A [selector] to query frame for
- `pageFunction` <[function]> Function to be evaluated in browser context
- `...args` <...[Serializable]|[ElementHandle]> Arguments to pass to `pageFunction`
- `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction`
- returns: <[Promise]<[Serializable]>> Promise which resolves to the return value of `pageFunction`

This method runs `document.querySelector` within the frame and passes it as the first argument to `pageFunction`. If there's no element matching `selector`, the method throws an error.
Expand Down Expand Up @@ -1538,7 +1538,7 @@ Gets the full HTML contents of the frame, including the doctype.

#### frame.evaluate(pageFunction, ...args)
- `pageFunction` <[function]|[string]> Function to be evaluated in browser context
- `...args` <...[Serializable]|[ElementHandle]> Arguments to pass to `pageFunction`
- `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction`
- returns: <[Promise]<[Serializable]>> Promise which resolves to function return value

If the function, passed to the `frame.evaluate`, returns a [Promise], then `frame.evaluate` would wait for the promise to resolve and return its value.
Expand Down Expand Up @@ -1613,8 +1613,8 @@ Returns frame's url.
#### frame.waitFor(selectorOrFunctionOrTimeout[, options[, ...args]])
- `selectorOrFunctionOrTimeout` <[string]|[number]|[function]> A [selector], predicate or timeout to wait for
- `options` <[Object]> Optional waiting parameters
- `...args` <...[Serializable]> Arguments to pass to `pageFunction`
- returns: <[Promise]>
- `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction`
- returns: <[Promise]<[JSHandle]>> Promise which resolves to a JSHandle of the success value

This method behaves differently with respect to the type of the first parameter:
- if `selectorOrFunctionOrTimeout` is a `string`, then the first argument is treated as a [selector] to wait for and the method is a shortcut for [frame.waitForSelector](#framewaitforselectorselector-options)
Expand All @@ -1630,8 +1630,8 @@ This method behaves differently with respect to the type of the first parameter:
- `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` <...[Serializable]> Arguments to pass to `pageFunction`
- returns: <[Promise]> Promise which resolves when the `pageFunction` returns a truthy value.
- `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction`
- returns: <[Promise]<[JSHandle]>> Promise which resolves when the `pageFunction` returns a truthy value. It resolves to a JSHandle of the truthy value.

The `waitForFunction` can be used to observe viewport size change:
```js
Expand All @@ -1652,7 +1652,7 @@ puppeteer.launch().then(async browser => {
- `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`.
- `hidden` <[boolean]> wait for element to not be found in the DOM or to be hidden, i.e. have `display: none` or `visibility: hidden` CSS properties. Defaults to `false`.
- `timeout` <[number]> maximum time to wait for in milliseconds. Defaults to `30000` (30 seconds).
- returns: <[Promise]> Promise which resolves when element specified by selector string is added to DOM.
- returns: <[Promise]<[ElementHandle]>> 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
the method the `selector` already exists, the method will return
Expand Down Expand Up @@ -1688,7 +1688,7 @@ The class represents a context for JavaScript execution. Examples of JavaScript

#### executionContext.evaluate(pageFunction, ...args)
- `pageFunction` <[function]|[string]> Function to be evaluated in `executionContext`
- `...args` <...[Serializable]|[ElementHandle]> Arguments to pass to `pageFunction`
- `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction`
- returns: <[Promise]<[Serializable]>> Promise which resolves to function return value

If the function, passed to the `executionContext.evaluate`, returns a [Promise], then `executionContext.evaluate` would wait for the promise to resolve and return its value.
Expand Down
77 changes: 51 additions & 26 deletions lib/FrameManager.js
Expand Up @@ -583,17 +583,18 @@ class Frame {
* @param {string} selector
* @param {boolean} waitForVisible
* @param {boolean} waitForHidden
* @return {boolean}
* @return {?Node|boolean}
*/
function predicate(selector, waitForVisible, waitForHidden) {
const node = document.querySelector(selector);
if (!node)
return waitForHidden;
if (!waitForVisible && !waitForHidden)
return true;
return node;
const style = window.getComputedStyle(node);
const isVisible = style && style.visibility !== 'hidden' && hasVisibleBoundingBox();
return (waitForVisible === isVisible || waitForHidden === !isVisible);
const success = (waitForVisible === isVisible || waitForHidden === !isVisible);
return success ? node : null;

/**
* @return {boolean}
Expand All @@ -606,15 +607,16 @@ class Frame {
}

/**
* @param {Function} pageFunction
* @param {Function|string} 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;
if (helper.isString(pageFunction))
pageFunction = 'return ' + pageFunction;
return new WaitTask(this, pageFunction, polling, timeout, ...args).promise;
}

/**
Expand Down Expand Up @@ -658,11 +660,11 @@ helper.tracePublicAPI(Frame);
class WaitTask {
/**
* @param {!Frame} frame
* @param {string} predicateBody
* @param {Function|string} predicateBody
* @param {string|number} polling
* @param {number} timeout
*/
constructor(frame, predicateBody, polling, timeout) {
constructor(frame, predicateBody, polling, timeout, ...args) {
if (helper.isString(polling))
console.assert(polling === 'raf' || polling === 'mutation', 'Unknown polling option: ' + polling);
else if (helper.isNumber(polling))
Expand All @@ -671,7 +673,10 @@ class WaitTask {
throw new Error('Unknown polling options: ' + polling);

this._frame = frame;
this._pageScript = helper.evaluationString(waitForPredicatePageFunction, predicateBody, polling, timeout);
this._polling = polling;
this._timeout = timeout;
this._predicateBody = helper.isString(predicateBody) ? predicateBody : 'return (' + predicateBody + ')(...args)';
this._args = args;
this._runCount = 0;
frame._waitTasks.add(this);
this.promise = new Promise((resolve, reject) => {
Expand All @@ -695,20 +700,25 @@ class WaitTask {

async rerun() {
const runCount = ++this._runCount;
let success = false;
/** @type {?JSHandle} */
let success = null;
let error = null;
try {
success = await this._frame.evaluate(this._pageScript);
success = await (await this._frame.executionContext()).evaluateHandle(waitForPredicatePageFunction, this._predicateBody, this._polling, this._timeout, ...this._args);
} catch (e) {
error = e;
}

if (this._terminated || runCount !== this._runCount)
if (this._terminated || runCount !== this._runCount) {
success && await success.dispose();
return;
}

// Ignore timeouts in pageScript - we track timeouts ourselves.
if (!success && !error)
if (!error && !(await success.jsonValue())) {
await success.dispose();
return;
}

// When the page is navigated, the promise is rejected.
// We will try again in the new execution context.
Expand All @@ -723,7 +733,7 @@ class WaitTask {
if (error)
this._reject(error);
else
this._resolve();
this._resolve(success);

this._cleanup();
}
Expand All @@ -739,34 +749,39 @@ class WaitTask {
* @param {string} predicateBody
* @param {string} polling
* @param {number} timeout
* @return {!Promise<boolean>}
* @return {!Promise<*>}
*/
async function waitForPredicatePageFunction(predicateBody, polling, timeout) {
const predicate = new Function(predicateBody);
async function waitForPredicatePageFunction(predicateBody, polling, timeout, ...args) {
const predicate = new Function('...args', predicateBody);
let timedOut = false;
setTimeout(() => timedOut = true, timeout);
if (polling === 'raf')
await pollRaf();
return await pollRaf();
else if (polling === 'mutation')
await pollMutation();
return await pollMutation();
else if (typeof polling === 'number')
await pollInterval(polling);
return !timedOut;
return await pollInterval(polling);

/**
* @return {!Promise}
*/
function pollMutation() {
if (predicate())
return Promise.resolve();
const success = predicate.apply(null, args);
if (success)
return Promise.resolve(success);

let fulfill;
const result = new Promise(x => fulfill = x);
const observer = new MutationObserver(mutations => {
if (timedOut || predicate()) {
if (timedOut) {
observer.disconnect();
fulfill();
}
const success = predicate.apply(null, args);
if (success) {
observer.disconnect();
fulfill(success);
}
});
observer.observe(document, {
childList: true,
Expand All @@ -786,8 +801,13 @@ async function waitForPredicatePageFunction(predicateBody, polling, timeout) {
return result;

function onRaf() {
if (timedOut || predicate())
if (timedOut) {
fulfill();
return;
}
const success = predicate.apply(null, args);
if (success)
fulfill(success);
else
requestAnimationFrame(onRaf);
}
Expand All @@ -804,8 +824,13 @@ async function waitForPredicatePageFunction(predicateBody, polling, timeout) {
return result;

function onTimeout() {
if (timedOut || predicate())
if (timedOut) {
fulfill();
return;
}
const success = predicate.apply(null, args);
if (success)
fulfill(success);
else
setTimeout(onTimeout, pollInterval);
}
Expand Down
17 changes: 17 additions & 0 deletions test/test.js
Expand Up @@ -688,6 +688,18 @@ describe('Page', function() {
expect(error).toBeTruthy();
expect(error.message).toContain('Cannot poll with non-positive interval');
});
it('should return the success value as a JSHandle', async({page}) => {
expect(await (await page.waitForFunction(() => 5)).jsonValue()).toBe(5);
});
it('should accept ElementHandle arguments', async({page}) => {
await page.setContent('<div></div>');
const div = await page.$('div');
let resolved = false;
const waitForFunction = page.waitForFunction(element => !element.parentElement, {}, div).then(() => resolved = true);
expect(resolved).toBe(false);
await page.evaluate(element => element.remove(), div);
await waitForFunction;
});
});

describe('Frame.waitForSelector', function() {
Expand Down Expand Up @@ -854,6 +866,11 @@ describe('Page', function() {
await page.evaluate(() => document.querySelector('div').className = 'zombo');
expect(await waitForSelector).toBe(true);
});
it('should return the element handle', async({page, server}) => {
const waitForSelector = page.waitForSelector('.zombo');
await page.setContent(`<div class='zombo'>anything</div>`);
expect(await page.evaluate(x => x.textContent, await waitForSelector)).toBe('anything');
});
});

describe('Page.waitFor', function() {
Expand Down

0 comments on commit 814126c

Please sign in to comment.