fix(Page.click): throw a meaningful message for invisible elements (#1309)

This patch starts throwing a meaningful error message when trying to click the hidden
node.

References #1294
This commit is contained in:
JoelEinbinder 2017-11-07 13:54:40 -08:00 committed by Andrey Lushnikov
parent 3cb0f1af34
commit b58d319926
3 changed files with 46 additions and 8 deletions

View File

@ -1786,7 +1786,7 @@ The method runs `element.querySelectorAll` within the page. If no elements match
- width <[number]> the width of the element in pixels. - width <[number]> the width of the element in pixels.
- height <[number]> the height of the element in pixels. - height <[number]> the height of the element in pixels.
This method returns the bounding box of the element (relative to the main frame), or `null` if element is detached from dom. This method returns the bounding box of the element (relative to the main frame), or `null` if the element is not visible.
#### elementHandle.click([options]) #### elementHandle.click([options])
- `options` <[Object]> - `options` <[Object]>

View File

@ -15,7 +15,7 @@
*/ */
const path = require('path'); const path = require('path');
const {JSHandle} = require('./ExecutionContext'); const {JSHandle} = require('./ExecutionContext');
const {helper} = require('./helper'); const {helper, debugError} = require('./helper');
class ElementHandle extends JSHandle { class ElementHandle extends JSHandle {
/** /**
@ -59,6 +59,8 @@ class ElementHandle extends JSHandle {
async _visibleCenter() { async _visibleCenter() {
await this._scrollIntoViewIfNeeded(); await this._scrollIntoViewIfNeeded();
const box = await this.boundingBox(); const box = await this.boundingBox();
if (!box)
throw new Error('Node is not visible');
return { return {
x: box.x + box.width / 2, x: box.x + box.width / 2,
y: box.y + box.height / 2 y: box.y + box.height / 2
@ -116,16 +118,17 @@ class ElementHandle extends JSHandle {
} }
/** /**
* @return {!Promise<{x: number, y: number, width: number, height: number}>} * @return {!Promise<?{x: number, y: number, width: number, height: number}>}
*/ */
async boundingBox() { async boundingBox() {
const {model} = await this._client.send('DOM.getBoxModel', { const result = await this._client.send('DOM.getBoxModel', {
objectId: this._remoteObject.objectId objectId: this._remoteObject.objectId
}); }).catch(error => void debugError(error));
if (!model)
throw new Error('Node is detached from document');
const quad = model.border; if (!result)
return null;
const quad = result.model.border;
const x = Math.min(quad[0], quad[2], quad[4], quad[6]); const x = Math.min(quad[0], quad[2], quad[4], quad[6]);
const y = Math.min(quad[1], quad[3], quad[5], quad[7]); const y = Math.min(quad[1], quad[3], quad[5], quad[7]);
const width = Math.max(quad[0], quad[2], quad[4], quad[6]) - x; const width = Math.max(quad[0], quad[2], quad[4], quad[6]) - x;
@ -142,6 +145,8 @@ class ElementHandle extends JSHandle {
async screenshot(options = {}) { async screenshot(options = {}) {
await this._scrollIntoViewIfNeeded(); await this._scrollIntoViewIfNeeded();
const boundingBox = await this.boundingBox(); const boundingBox = await this.boundingBox();
if (!boundingBox)
throw new Error('Node is not visible');
return await this._page.screenshot(Object.assign({}, { return await this._page.screenshot(Object.assign({}, {
clip: boundingBox clip: boundingBox

View File

@ -1688,6 +1688,11 @@ describe('Page', function() {
const box = await elementHandle.boundingBox(); const box = await elementHandle.boundingBox();
expect(box).toEqual({ x: 28, y: 260, width: 264, height: 18 }); expect(box).toEqual({ x: 28, y: 260, width: 264, height: 18 });
})); }));
it('should return null for invisible elements', SX(async function() {
await page.setContent('<div style="display:none">hi</div>');
const element = await page.$('div');
expect(await element.boundingBox()).toBe(null);
}));
}); });
describe('ElementHandle.click', function() { describe('ElementHandle.click', function() {
@ -1718,6 +1723,26 @@ describe('Page', function() {
await button.click().catch(err => error = err); await button.click().catch(err => error = err);
expect(error.message).toBe('Node is detached from document'); expect(error.message).toBe('Node is detached from document');
})); }));
it('should throw for hidden nodes', SX(async function() {
await page.goto(PREFIX + '/input/button.html');
const button = await page.$('button');
await page.evaluate(button => button.style.display = 'none', button);
const error = await button.click().catch(err => err);
expect(error.message).toBe('Node is not visible');
}));
it('should throw for recursively hidden nodes', SX(async function() {
await page.goto(PREFIX + '/input/button.html');
const button = await page.$('button');
await page.evaluate(button => button.parentElement.style.display = 'none', button);
const error = await button.click().catch(err => err);
expect(error.message).toBe('Node is not visible');
}));
it('should throw for <br> elements', SX(async function() {
await page.setContent('hello<br>goodbye');
const br = await page.$('br');
const error = await br.click().catch(err => err);
expect(error.message).toBe('Node is not visible');
}));
}); });
describe('ElementHandle.hover', function() { describe('ElementHandle.hover', function() {
@ -2583,6 +2608,14 @@ describe('Page', function() {
expect(await page.evaluate(() => window.innerWidth)).toBe(375); expect(await page.evaluate(() => window.innerWidth)).toBe(375);
expect(await page.evaluate(() => navigator.userAgent)).toContain('Safari'); expect(await page.evaluate(() => navigator.userAgent)).toContain('Safari');
})); }));
it('should support clicking', SX(async function() {
await page.emulate(iPhone);
await page.goto(PREFIX + '/input/button.html');
const button = await page.$('button');
await page.evaluate(button => button.style.marginTop = '200px', button);
await button.click();
expect(await page.evaluate(() => result)).toBe('Clicked');
}));
}); });
describe('Page.emulateMedia', function() { describe('Page.emulateMedia', function() {