feat(Page): introduce Page.waitForXPath (#1767)

This patch:
- introduces `page.waitForXPath` method
- introduces `frame.waitForXPath` method
- amends `page.waitFor` to treat strings that start with `//` as xpath queries.

Fixes #1757.
This commit is contained in:
Ram Dobson
2018-01-22 18:16:20 -05:00
committed by Andrey Lushnikov
parent 62597bf897
commit cb684ebbc4
4 changed files with 219 additions and 36 deletions

View File

@@ -566,8 +566,14 @@ class Frame {
* @return {!Promise}
*/
waitFor(selectorOrFunctionOrTimeout, options = {}, ...args) {
if (helper.isString(selectorOrFunctionOrTimeout))
return this.waitForSelector(/** @type {string} */(selectorOrFunctionOrTimeout), options);
const xPathPattern = '//';
if (helper.isString(selectorOrFunctionOrTimeout)) {
const string = /** @type {string} */ (selectorOrFunctionOrTimeout);
if (string.startsWith(xPathPattern))
return this.waitForXPath(string, options);
return this.waitForSelector(string, options);
}
if (helper.isNumber(selectorOrFunctionOrTimeout))
return new Promise(fulfill => setTimeout(fulfill, selectorOrFunctionOrTimeout));
if (typeof selectorOrFunctionOrTimeout === 'function')
@@ -581,37 +587,16 @@ class Frame {
* @return {!Promise}
*/
waitForSelector(selector, options = {}) {
const timeout = options.timeout || 30000;
const waitForVisible = !!options.visible;
const waitForHidden = !!options.hidden;
const polling = waitForVisible || waitForHidden ? 'raf' : 'mutation';
return this.waitForFunction(predicate, {timeout, polling}, selector, waitForVisible, waitForHidden);
return this._waitForSelectorOrXPath(selector, false, options);
}
/**
* @param {string} selector
* @param {boolean} waitForVisible
* @param {boolean} waitForHidden
* @return {?Node|boolean}
*/
function predicate(selector, waitForVisible, waitForHidden) {
const node = document.querySelector(selector);
if (!node)
return waitForHidden;
if (!waitForVisible && !waitForHidden)
return node;
const style = window.getComputedStyle(node);
const isVisible = style && style.visibility !== 'hidden' && hasVisibleBoundingBox();
const success = (waitForVisible === isVisible || waitForHidden === !isVisible);
return success ? node : null;
/**
* @return {boolean}
*/
function hasVisibleBoundingBox() {
const rect = node.getBoundingClientRect();
return !!(rect.top || rect.bottom || rect.width || rect.height);
}
}
/**
* @param {string} xpath
* @param {!Object=} options
* @return {!Promise}
*/
waitForXPath(xpath, options = {}) {
return this._waitForSelectorOrXPath(xpath, true, options);
}
/**
@@ -632,6 +617,51 @@ class Frame {
return this.evaluate(() => document.title);
}
/**
* @param {string} selectorOrXPath
* @param {boolean} isXPath
* @param {!Object=} options
* @return {!Promise}
*/
_waitForSelectorOrXPath(selectorOrXPath, isXPath, options = {}) {
const timeout = options.timeout || 30000;
const waitForVisible = !!options.visible;
const waitForHidden = !!options.hidden;
const polling = waitForVisible || waitForHidden ? 'raf' : 'mutation';
return this.waitForFunction(predicate, {timeout, polling}, selectorOrXPath, isXPath, waitForVisible, waitForHidden);
/**
* @param {string} selectorOrXPath
* @param {boolean} isXPath
* @param {boolean} waitForVisible
* @param {boolean} waitForHidden
* @return {?Node|boolean}
*/
function predicate(selectorOrXPath, isXPath, waitForVisible, waitForHidden) {
const node = isXPath
? document.evaluate(selectorOrXPath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue
: document.querySelector(selectorOrXPath);
if (!node)
return waitForHidden;
if (!waitForVisible && !waitForHidden)
return node;
const element = /** @type {Element} */ (node.nodeType === Node.TEXT_NODE ? node.parentElement : node);
const style = window.getComputedStyle(element);
const isVisible = style && style.visibility !== 'hidden' && hasVisibleBoundingBox();
const success = (waitForVisible === isVisible || waitForHidden === !isVisible);
return success ? node : null;
/**
* @return {boolean}
*/
function hasVisibleBoundingBox() {
const rect = element.getBoundingClientRect();
return !!(rect.top || rect.bottom || rect.width || rect.height);
}
}
}
/**
* @param {!Object} framePayload
*/
@@ -654,7 +684,7 @@ class Frame {
_detach() {
for (const waitTask of this._waitTasks)
waitTask.terminate(new Error('waitForSelector failed: frame got detached.'));
waitTask.terminate(new Error('waitForFunction failed: frame got detached.'));
this._detached = true;
if (this._parentFrame)
this._parentFrame._childFrames.delete(this);