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:
parent
62597bf897
commit
cb684ebbc4
66
docs/api.md
66
docs/api.md
@ -99,6 +99,7 @@
|
||||
* [page.waitForFunction(pageFunction[, options[, ...args]])](#pagewaitforfunctionpagefunction-options-args)
|
||||
* [page.waitForNavigation(options)](#pagewaitfornavigationoptions)
|
||||
* [page.waitForSelector(selector[, options])](#pagewaitforselectorselector-options)
|
||||
* [page.waitForXPath(xpath[, options])](#pagewaitforxpathxpath-options)
|
||||
- [class: Keyboard](#class-keyboard)
|
||||
* [keyboard.down(key[, options])](#keyboarddownkey-options)
|
||||
* [keyboard.press(key[, options])](#keyboardpresskey-options)
|
||||
@ -147,6 +148,7 @@
|
||||
* [frame.waitFor(selectorOrFunctionOrTimeout[, options[, ...args]])](#framewaitforselectororfunctionortimeout-options-args)
|
||||
* [frame.waitForFunction(pageFunction[, options[, ...args]])](#framewaitforfunctionpagefunction-options-args)
|
||||
* [frame.waitForSelector(selector[, options])](#framewaitforselectorselector-options)
|
||||
* [frame.waitForXPath(xpath[, options])](#framewaitforxpathxpath-options)
|
||||
- [class: ExecutionContext](#class-executioncontext)
|
||||
* [executionContext.evaluate(pageFunction, ...args)](#executioncontextevaluatepagefunction-args)
|
||||
* [executionContext.evaluateHandle(pageFunction, ...args)](#executioncontextevaluatehandlepagefunction-args)
|
||||
@ -1209,7 +1211,8 @@ This is a shortcut for [page.mainFrame().url()](#frameurl)
|
||||
- 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)
|
||||
- if `selectorOrFunctionOrTimeout` is a `string`, then the first argument is treated as a [selector] or [xpath], depending on whether or not it starts with '//', and the method is a shortcut for
|
||||
[page.waitForSelector](#pagewaitforselectorselector-options) or [page.waitForXPath](#pagewaitforxpathxpath-options)
|
||||
- if `selectorOrFunctionOrTimeout` is a `function`, then the first argument is treated as a predicate to wait for and the method is a shortcut for [page.waitForFunction()](#pagewaitforfunctionpagefunction-options-args).
|
||||
- if `selectorOrFunctionOrTimeout` is a `number`, then 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
|
||||
@ -1279,6 +1282,35 @@ puppeteer.launch().then(async browser => {
|
||||
```
|
||||
Shortcut for [page.mainFrame().waitForSelector(selector[, options])](#framewaitforselectorselector-options).
|
||||
|
||||
#### page.waitForXPath(xpath[, options])
|
||||
- `xpath` <[string]> A [xpath] of an element to wait for,
|
||||
- `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`.
|
||||
- `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]<[ElementHandle]>> Promise which resolves when element specified by xpath string is added to DOM.
|
||||
|
||||
Wait for the `xpath` to appear in page. If at the moment of calling
|
||||
the method the `xpath` already exists, the method will return
|
||||
immediately. If the xpath doesn't appear after the `timeout` milliseconds of waiting, the function will throw.
|
||||
|
||||
This method works across navigations:
|
||||
```js
|
||||
const puppeteer = require('puppeteer');
|
||||
|
||||
puppeteer.launch().then(async browser => {
|
||||
const page = await browser.newPage();
|
||||
let currentURL;
|
||||
page
|
||||
.waitForXPath('//img')
|
||||
.then(() => console.log('First URL with image: ' + currentURL));
|
||||
for (currentURL of ['https://example.com', 'https://google.com', 'https://bbc.com'])
|
||||
await page.goto(currentURL);
|
||||
await browser.close();
|
||||
});
|
||||
```
|
||||
Shortcut for [page.mainFrame().waitForXPath(xpath[, options])](#framewaitforxpathxpath-options).
|
||||
|
||||
|
||||
### class: Keyboard
|
||||
|
||||
@ -1677,7 +1709,8 @@ Returns frame's url.
|
||||
- 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)
|
||||
- if `selectorOrFunctionOrTimeout` is a `string`, then the first argument is treated as a [selector] or [xpath], depending on whether or not it starts with '//', and the method is a shortcut for
|
||||
[frame.waitForSelector](#framewaitforselectorselector-options) or [frame.waitForXPath](#framewaitforsxpathxpath-options)
|
||||
- if `selectorOrFunctionOrTimeout` is a `function`, then the first argument is treated as a predicate to wait for and the method is a shortcut for [frame.waitForFunction()](#framewaitforfunctionpagefunction-options-args).
|
||||
- if `selectorOrFunctionOrTimeout` is a `number`, then 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
|
||||
@ -1734,6 +1767,34 @@ puppeteer.launch().then(async browser => {
|
||||
});
|
||||
```
|
||||
|
||||
#### frame.waitForXPath(xpath[, options])
|
||||
- `xpath` <[string]> A [xpath] of an element to wait for
|
||||
- `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`.
|
||||
- `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]<[ElementHandle]>> Promise which resolves when element specified by xpath string is added to DOM.
|
||||
|
||||
Wait for the `xpath` to appear in page. If at the moment of calling
|
||||
the method the `xpath` already exists, the method will return
|
||||
immediately. If the xpath doesn't appear after the `timeout` milliseconds of waiting, the function will throw.
|
||||
|
||||
This method works across navigations:
|
||||
```js
|
||||
const puppeteer = require('puppeteer');
|
||||
|
||||
puppeteer.launch().then(async browser => {
|
||||
const page = await browser.newPage();
|
||||
let currentURL;
|
||||
page.mainFrame()
|
||||
.waitForXPath('//img')
|
||||
.then(() => console.log('First URL with image: ' + currentURL));
|
||||
for (currentURL of ['https://example.com', 'https://google.com', 'https://bbc.com'])
|
||||
await page.goto(currentURL);
|
||||
await browser.close();
|
||||
});
|
||||
```
|
||||
|
||||
### class: ExecutionContext
|
||||
|
||||
The class represents a context for JavaScript execution. Examples of JavaScript contexts are:
|
||||
@ -2340,3 +2401,4 @@ reported.
|
||||
[Touchscreen]: #class-touchscreen "Touchscreen"
|
||||
[Target]: #class-target "Target"
|
||||
[USKeyboardLayout]: ../lib/USKeyboardLayout.js "USKeyboardLayout"
|
||||
[xpath]: https://developer.mozilla.org/en-US/docs/Web/XPath "xpath"
|
||||
|
@ -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);
|
||||
|
@ -881,6 +881,15 @@ class Page extends EventEmitter {
|
||||
return this.mainFrame().waitForSelector(selector, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} xpath
|
||||
* @param {!Object=} options
|
||||
* @return {!Promise}
|
||||
*/
|
||||
waitForXPath(xpath, options = {}) {
|
||||
return this.mainFrame().waitForXPath(xpath, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {function()} pageFunction
|
||||
* @param {!Object=} options
|
||||
|
84
test/test.js
84
test/test.js
@ -797,7 +797,7 @@ describe('Page', function() {
|
||||
await FrameUtils.detachFrame(page, 'frame1');
|
||||
await waitPromise;
|
||||
expect(waitError).toBeTruthy();
|
||||
expect(waitError.message).toContain('waitForSelector failed: frame got detached.');
|
||||
expect(waitError.message).toContain('waitForFunction failed: frame got detached.');
|
||||
});
|
||||
it('should survive cross-process navigation', async({page, server}) => {
|
||||
let boxFound = false;
|
||||
@ -884,6 +884,73 @@ describe('Page', function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Frame.waitForXPath', function() {
|
||||
const addElement = tag => document.body.appendChild(document.createElement(tag));
|
||||
|
||||
it('should support some fancy xpath', async({page, server}) => {
|
||||
await page.setContent(`<p>red herring</p><p>hello world </p>`);
|
||||
const waitForXPath = page.waitForXPath('//p[normalize-space(.)="hello world"]');
|
||||
expect(await page.evaluate(x => x.textContent, await waitForXPath)).toBe('hello world ');
|
||||
});
|
||||
it('should run in specified frame', async({page, server}) => {
|
||||
await FrameUtils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
|
||||
await FrameUtils.attachFrame(page, 'frame2', server.EMPTY_PAGE);
|
||||
const frame1 = page.frames()[1];
|
||||
const frame2 = page.frames()[2];
|
||||
let added = false;
|
||||
frame2.waitForXPath('//div').then(() => added = true);
|
||||
expect(added).toBe(false);
|
||||
await frame1.evaluate(addElement, 'div');
|
||||
expect(added).toBe(false);
|
||||
await frame2.evaluate(addElement, 'div');
|
||||
expect(added).toBe(true);
|
||||
});
|
||||
it('should throw if evaluation failed', async({page, server}) => {
|
||||
await page.evaluateOnNewDocument(function() {
|
||||
document.evaluate = null;
|
||||
});
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
let error = null;
|
||||
await page.waitForXPath('*').catch(e => error = e);
|
||||
expect(error.message).toContain('document.evaluate is not a function');
|
||||
});
|
||||
it('should throw when frame is detached', async({page, server}) => {
|
||||
await FrameUtils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
|
||||
const frame = page.frames()[1];
|
||||
let waitError = null;
|
||||
const waitPromise = frame.waitForXPath('//*[@class="box"]').catch(e => waitError = e);
|
||||
await FrameUtils.detachFrame(page, 'frame1');
|
||||
await waitPromise;
|
||||
expect(waitError).toBeTruthy();
|
||||
expect(waitError.message).toContain('waitForFunction failed: frame got detached.');
|
||||
});
|
||||
it('hidden should wait for display: none', async({page, server}) => {
|
||||
let divHidden = false;
|
||||
await page.setContent(`<div style='display: block;'></div>`);
|
||||
const waitForXPath = page.waitForXPath('//div', {hidden: true}).then(() => divHidden = true);
|
||||
await page.waitForXPath('//div'); // do a round trip
|
||||
expect(divHidden).toBe(false);
|
||||
await page.evaluate(() => document.querySelector('div').style.setProperty('display', 'none'));
|
||||
expect(await waitForXPath).toBe(true);
|
||||
expect(divHidden).toBe(true);
|
||||
});
|
||||
it('should return the element handle', async({page, server}) => {
|
||||
const waitForXPath = page.waitForXPath('//*[@class="zombo"]');
|
||||
await page.setContent(`<div class='zombo'>anything</div>`);
|
||||
expect(await page.evaluate(x => x.textContent, await waitForXPath)).toBe('anything');
|
||||
});
|
||||
it('should allow you to select a text node', async({page, server}) => {
|
||||
await page.setContent(`<div>some text</div>`);
|
||||
const text = await page.waitForXPath('//div/text()');
|
||||
expect(await (await text.getProperty('nodeType')).jsonValue()).toBe(3 /* Node.TEXT_NODE */);
|
||||
});
|
||||
it('should allow you to select an element with single slash', async({page, server}) => {
|
||||
await page.setContent(`<div>some text</div>`);
|
||||
const waitForXPath = page.waitForXPath('/html/body/div');
|
||||
expect(await page.evaluate(x => x.textContent, await waitForXPath)).toBe('some text');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Page.waitFor', function() {
|
||||
it('should wait for selector', async({page, server}) => {
|
||||
let found = false;
|
||||
@ -894,6 +961,21 @@ describe('Page', function() {
|
||||
await waitFor;
|
||||
expect(found).toBe(true);
|
||||
});
|
||||
it('should wait for an xpath', async({page, server}) => {
|
||||
let found = false;
|
||||
const waitFor = page.waitFor('//div').then(() => found = true);
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
expect(found).toBe(false);
|
||||
await page.goto(server.PREFIX + '/grid.html');
|
||||
await waitFor;
|
||||
expect(found).toBe(true);
|
||||
});
|
||||
it('should not allow you to select an element with single slash xpath', async({page, server}) => {
|
||||
await page.setContent(`<div>some text</div>`);
|
||||
let error = null;
|
||||
await page.waitFor('/html/body/div').catch(e => error = e);
|
||||
expect(error).toBeTruthy();
|
||||
});
|
||||
it('should timeout', async({page, server}) => {
|
||||
const startTime = Date.now();
|
||||
const timeout = 42;
|
||||
|
Loading…
Reference in New Issue
Block a user