Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: JSHandles as argument for waitFor and return the success value #1712

Merged
merged 2 commits into from Jan 4, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
87 changes: 56 additions & 31 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,14 @@ 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;
return new WaitTask(this, pageFunction, polling, timeout, ...args).promise;
}

/**
Expand Down Expand Up @@ -658,11 +658,12 @@ helper.tracePublicAPI(Frame);
class WaitTask {
/**
* @param {!Frame} frame
* @param {string} predicateBody
* @param {Function|string} predicateBody
* @param {string|number} polling
* @param {number} timeout
* @param {!Array<*>} args
*/
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 +672,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) ? 'return ' + predicateBody : 'return (' + predicateBody + ')(...args)';
this._args = args;
this._runCount = 0;
frame._waitTasks.add(this);
this.promise = new Promise((resolve, reject) => {
Expand All @@ -695,20 +699,26 @@ 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) {
if (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();
else if (polling === 'mutation')
await pollMutation();
else if (typeof polling === 'number')
await pollInterval(polling);
return !timedOut;
return await pollRaf();
if (polling === 'mutation')
return await pollMutation();
if (typeof polling === 'number')
return await pollInterval(polling);

/**
* @return {!Promise}
* @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 @@ -777,7 +792,7 @@ async function waitForPredicatePageFunction(predicateBody, polling, timeout) {
}

/**
* @return {!Promise}
* @return {!Promise<*>}
*/
function pollRaf() {
let fulfill;
Expand All @@ -786,16 +801,21 @@ 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);
}
}

/**
* @param {number} pollInterval
* @return {!Promise}
* @return {!Promise<*>}
*/
function pollInterval(pollInterval) {
let fulfill;
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