fix(visibility): unify visibilty checks (#1998)

This applies a common definition of visibility to clicks and waitfors:
- non-empty bounding box - implies non-empty content and no display:none;
- no visibility:hidden.
This commit is contained in:
Dmitry Gozman 2020-04-27 15:40:46 -07:00 committed by GitHub
parent 4b0d977489
commit 031587a9cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 67 additions and 26 deletions

View File

@ -947,7 +947,7 @@ Shortcut for [page.mainFrame().addStyleTag(options)](#frameaddstyletagoptions).
- `selector` <[string]> A selector to search for checkbox or radio button to check. If there are multiple elements satisfying the selector, the first will be checked.
- `options` <[Object]>
- `force` <[boolean]> Whether to bypass the actionability checks. By default actions wait until the element is:
- displayed (for example, no `display:none`),
- displayed (for example, not empty, no `display:none`, no `visibility:hidden`),
- is not moving (for example, waits until css transition finishes),
- receives pointer events at the action point (for example, waits until element becomes non-obscured by other elements).
Even if the action is forced, it will wait for the element matching selector to be in DOM. Defaults to `false`.
@ -971,7 +971,7 @@ Shortcut for [page.mainFrame().check(selector[, options])](#framecheckselector-o
- y <[number]>
- `modifiers` <[Array]<"Alt"|"Control"|"Meta"|"Shift">> Modifier keys to press. Ensures that only these modifiers are pressed during the click, and then restores current modifiers back. If not specified, currently pressed modifiers are used.
- `force` <[boolean]> Whether to bypass the actionability checks. By default actions wait until the element is:
- displayed (for example, no `display:none`),
- displayed (for example, not empty, no `display:none`, no `visibility:hidden`),
- is not moving (for example, waits until css transition finishes),
- receives pointer events at the action point (for example, waits until element becomes non-obscured by other elements).
Even if the action is forced, it will wait for the element matching selector to be in DOM. Defaults to `false`.
@ -1024,7 +1024,7 @@ Browser-specific Coverage implementation, only available for Chromium atm. See [
- y <[number]>
- `modifiers` <[Array]<"Alt"|"Control"|"Meta"|"Shift">> Modifier keys to press. Ensures that only these modifiers are pressed during the double click, and then restores current modifiers back. If not specified, currently pressed modifiers are used.
- `force` <[boolean]> Whether to bypass the actionability checks. By default actions wait until the element is:
- displayed (for example, no `display:none`),
- displayed (for example, not empty, no `display:none`, no `visibility:hidden`),
- is not moving (for example, waits until css transition finishes),
- receives pointer events at the action point (for example, waits until element becomes non-obscured by other elements).
Even if the action is forced, it will wait for the element matching selector to be in DOM. Defaults to `false`.
@ -1333,7 +1333,7 @@ Shortcut for [page.mainFrame().goto(url[, options])](#framegotourl-options)
- y <[number]>
- `modifiers` <[Array]<"Alt"|"Control"|"Meta"|"Shift">> Modifier keys to press. Ensures that only these modifiers are pressed during the hover, and then restores current modifiers back. If not specified, currently pressed modifiers are used.
- `force` <[boolean]> Whether to bypass the actionability checks. By default actions wait until the element is:
- displayed (for example, no `display:none`),
- displayed (for example, not empty, no `display:none`, no `visibility:hidden`),
- is not moving (for example, waits until css transition finishes),
- receives pointer events at the action point (for example, waits until element becomes non-obscured by other elements).
Even if the action is forced, it will wait for the element matching selector to be in DOM. Defaults to `false`.
@ -1658,7 +1658,7 @@ Shortcut for [page.mainFrame().type(selector, text[, options])](#frametypeselect
- `selector` <[string]> A selector to search for uncheckbox to check. If there are multiple elements satisfying the selector, the first will be checked.
- `options` <[Object]>
- `force` <[boolean]> Whether to bypass the actionability checks. By default actions wait until the element is:
- displayed (for example, no `display:none`),
- displayed (for example, not empty, no `display:none`, no `visibility:hidden`),
- is not moving (for example, waits until css transition finishes),
- receives pointer events at the action point (for example, waits until element becomes non-obscured by other elements).
Even if the action is forced, it will wait for the element matching selector to be in DOM. Defaults to `false`.
@ -1819,6 +1819,8 @@ return finalResponse.ok();
Wait for the `selector` to satisfy `waitFor` option (either appear/disappear from dom, or become visible/hidden). If at the moment of calling the method `selector` already satisfies the condition, the method will return immediately. If the selector doesn't satisfy the condition for the `timeout` milliseconds, the function will throw.
Element is considered `visible` when it has non-empty bounding box (for example, it has some content and no `display:none`) and no `visibility:hidden`. Element is considired `hidden` when it is not `visible` as defined above.
This method works across navigations:
```js
const { chromium } = require('playwright'); // Or 'firefox' or 'webkit'.
@ -2002,7 +2004,7 @@ Adds a `<link rel="stylesheet">` tag into the page with the desired url or a `<s
- `selector` <[string]> A selector to search for checkbox to check. If there are multiple elements satisfying the selector, the first will be checked.
- `options` <[Object]>
- `force` <[boolean]> Whether to bypass the actionability checks. By default actions wait until the element is:
- displayed (for example, no `display:none`),
- displayed (for example, not empty, no `display:none`, no `visibility:hidden`),
- is not moving (for example, waits until css transition finishes),
- receives pointer events at the action point (for example, waits until element becomes non-obscured by other elements).
Even if the action is forced, it will wait for the element matching selector to be in DOM. Defaults to `false`.
@ -2027,7 +2029,7 @@ If there's no element matching `selector`, the method throws an error.
- y <[number]>
- `modifiers` <[Array]<"Alt"|"Control"|"Meta"|"Shift">> Modifier keys to press. Ensures that only these modifiers are pressed during the click, and then restores current modifiers back. If not specified, currently pressed modifiers are used.
- `force` <[boolean]> Whether to bypass the actionability checks. By default actions wait until the element is:
- displayed (for example, no `display:none`),
- displayed (for example, not empty, no `display:none`, no `visibility:hidden`),
- is not moving (for example, waits until css transition finishes),
- receives pointer events at the action point (for example, waits until element becomes non-obscured by other elements).
Even if the action is forced, it will wait for the element matching selector to be in DOM. Defaults to `false`.
@ -2053,7 +2055,7 @@ Gets the full HTML contents of the frame, including the doctype.
- y <[number]>
- `modifiers` <[Array]<"Alt"|"Control"|"Meta"|"Shift">> Modifier keys to press. Ensures that only these modifiers are pressed during the double click, and then restores current modifiers back. If not specified, currently pressed modifiers are used.
- `force` <[boolean]> Whether to bypass the actionability checks. By default actions wait until the element is:
- displayed (for example, no `display:none`),
- displayed (for example, not empty, no `display:none`, no `visibility:hidden`),
- is not moving (for example, waits until css transition finishes),
- receives pointer events at the action point (for example, waits until element becomes non-obscured by other elements).
Even if the action is forced, it will wait for the element matching selector to be in DOM. Defaults to `false`.
@ -2226,7 +2228,7 @@ console.log(frame === contentFrame); // -> true
- y <[number]>
- `modifiers` <[Array]<"Alt"|"Control"|"Meta"|"Shift">> Modifier keys to press. Ensures that only these modifiers are pressed during the hover, and then restores current modifiers back. If not specified, currently pressed modifiers are used.
- `force` <[boolean]> Whether to bypass the actionability checks. By default actions wait until the element is:
- displayed (for example, no `display:none`),
- displayed (for example, not empty, no `display:none`, no `visibility:hidden`),
- is not moving (for example, waits until css transition finishes),
- receives pointer events at the action point (for example, waits until element becomes non-obscured by other elements).
Even if the action is forced, it will wait for the element matching selector to be in DOM. Defaults to `false`.
@ -2349,7 +2351,7 @@ await frame.type('#mytextarea', 'World', {delay: 100}); // Types slower, like a
- `selector` <[string]> A selector to search for uncheckbox to check. If there are multiple elements satisfying the selector, the first will be checked.
- `options` <[Object]>
- `force` <[boolean]> Whether to bypass the actionability checks. By default actions wait until the element is:
- displayed (for example, no `display:none`),
- displayed (for example, not empty, no `display:none`, no `visibility:hidden`),
- is not moving (for example, waits until css transition finishes),
- receives pointer events at the action point (for example, waits until element becomes non-obscured by other elements).
Even if the action is forced, it will wait for the element matching selector to be in DOM. Defaults to `false`.
@ -2593,7 +2595,7 @@ This method returns the bounding box of the element (relative to the main frame)
#### elementHandle.check([options])
- `options` <[Object]>
- `force` <[boolean]> Whether to bypass the actionability checks. By default actions wait until the element is:
- displayed (for example, no `display:none`),
- displayed (for example, not empty, no `display:none`, no `visibility:hidden`),
- is not moving (for example, waits until css transition finishes),
- receives pointer events at the action point (for example, waits until element becomes non-obscured by other elements).
Even if the action is forced, it will wait for the element matching selector to be in DOM. Defaults to `false`.
@ -2613,7 +2615,7 @@ If element is not already checked, it scrolls it into view if needed, and then u
- y <[number]>
- `modifiers` <[Array]<"Alt"|"Control"|"Meta"|"Shift">> Modifier keys to press. Ensures that only these modifiers are pressed during the click, and then restores current modifiers back. If not specified, currently pressed modifiers are used.
- `force` <[boolean]> Whether to bypass the actionability checks. By default actions wait until the element is:
- displayed (for example, no `display:none`),
- displayed (for example, not empty, no `display:none`, no `visibility:hidden`),
- is not moving (for example, waits until css transition finishes),
- receives pointer events at the action point (for example, waits until element becomes non-obscured by other elements).
Even if the action is forced, it will wait for the element matching selector to be in DOM. Defaults to `false`.
@ -2636,7 +2638,7 @@ If the element is detached from DOM, the method throws an error.
- y <[number]>
- `modifiers` <[Array]<"Alt"|"Control"|"Meta"|"Shift">> Modifier keys to press. Ensures that only these modifiers are pressed during the double click, and then restores current modifiers back. If not specified, currently pressed modifiers are used.
- `force` <[boolean]> Whether to bypass the actionability checks. By default actions wait until the element is:
- displayed (for example, no `display:none`),
- displayed (for example, not empty, no `display:none`, no `visibility:hidden`),
- is not moving (for example, waits until css transition finishes),
- receives pointer events at the action point (for example, waits until element becomes non-obscured by other elements).
Even if the action is forced, it will wait for the element matching selector to be in DOM. Defaults to `false`.
@ -2709,7 +2711,7 @@ Returns element attribute value.
- y <[number]>
- `modifiers` <[Array]<"Alt"|"Control"|"Meta"|"Shift">> Modifier keys to press. Ensures that only these modifiers are pressed during the hover, and then restores current modifiers back. If not specified, currently pressed modifiers are used.
- `force` <[boolean]> Whether to bypass the actionability checks. By default actions wait until the element is:
- displayed (for example, no `display:none`),
- displayed (for example, not empty, no `display:none`, no `visibility:hidden`),
- is not moving (for example, waits until css transition finishes),
- receives pointer events at the action point (for example, waits until element becomes non-obscured by other elements).
Even if the action is forced, it will wait for the element matching selector to be in DOM. Defaults to `false`.
@ -2848,7 +2850,7 @@ await elementHandle.press('Enter');
#### elementHandle.uncheck([options])
- `options` <[Object]>
- `force` <[boolean]> Whether to bypass the actionability checks. By default actions wait until the element is:
- displayed (for example, no `display:none`),
- displayed (for example, not empty, no `display:none`, no `visibility:hidden`),
- is not moving (for example, waits until css transition finishes),
- receives pointer events at the action point (for example, waits until element becomes non-obscured by other elements).
Even if the action is forced, it will wait for the element matching selector to be in DOM. Defaults to `false`.

View File

@ -183,10 +183,11 @@ const sectionText = await page.$eval('*css=section >> text=Selectors', e => e.te
Actions like `click` and `fill` auto-wait for the element to be visible and actionable. For example, click will:
- wait for element with given selector to be in DOM
- wait for it to become displayed, i.e. not `display:none`,
- wait for it to become displayed, i.e. not empty, no `display:none`, no `visibility:hidden`
- wait for it to stop moving, for example, until css transition finishes
- scroll the element into view
- wait for it to receive pointer events at the action point, for example, waits until element becomes non-obscured by other elements
- retry if the element is detached during any of the above checks
```js

View File

@ -115,10 +115,11 @@ await page.click('button#submit');
Performs a simple human click. Under the hood, this and other pointer-related methods:
- wait for element with given selector to be in DOM
- wait for it to become displayed, i.e. not `display:none`,
- wait for it to become displayed, i.e. not empty, no `display:none`, no `visibility:hidden`
- wait for it to stop moving, for example, until css transition finishes
- scroll the element into view
- wait for it to receive pointer events at the action point, for example, waits until element becomes non-obscured by other elements
- retry if the element is detached during any of the above checks
#### Variations

View File

@ -25,13 +25,14 @@ export type InjectedResult<T = undefined> =
export class Injected {
isVisible(element: Element): boolean {
// Note: this logic should be similar to waitForDisplayedAtStablePosition() to avoid surprises.
if (!element.ownerDocument || !element.ownerDocument.defaultView)
return true;
const style = element.ownerDocument.defaultView.getComputedStyle(element);
if (!style || style.visibility === 'hidden')
return false;
const rect = element.getBoundingClientRect();
return !!(rect.top || rect.bottom || rect.width || rect.height);
return rect.width > 0 && rect.height > 0;
}
private _pollMutation<T>(predicate: Predicate<T>, timeout: number): Promise<T | undefined> {
@ -311,9 +312,12 @@ export class Injected {
return false;
if (!node.isConnected)
return 'notconnected';
// Note: this logic should be similar to isVisible() to avoid surprises.
const clientRect = element.getBoundingClientRect();
const rect = { x: clientRect.top, y: clientRect.left, width: clientRect.width, height: clientRect.height };
const isDisplayedAndStable = lastRect && rect.x === lastRect.x && rect.y === lastRect.y && rect.width === lastRect.width && rect.height === lastRect.height && rect.width > 0 && rect.height > 0;
let isDisplayedAndStable = lastRect && rect.x === lastRect.x && rect.y === lastRect.y && rect.width === lastRect.width && rect.height === lastRect.height && rect.width > 0 && rect.height > 0;
const style = element.ownerDocument && element.ownerDocument.defaultView ? element.ownerDocument.defaultView.getComputedStyle(element) : undefined;
isDisplayedAndStable = isDisplayedAndStable && (!!style && style.visibility !== 'hidden');
lastRect = rect;
return !!isDisplayedAndStable;
});

View File

@ -146,7 +146,7 @@ describe('Page.click', function() {
expect(error.message).toBe('Node is either not visible or not an HTMLElement');
expect(await page.evaluate(() => result)).toBe('Was not clicked');
});
it('should waitFor visible', async({page, server}) => {
it('should waitFor display:none to be gone', async({page, server}) => {
let done = false;
await page.goto(server.PREFIX + '/input/button.html');
await page.$eval('button', b => b.style.display = 'none');
@ -155,18 +155,41 @@ describe('Page.click', function() {
// Do enough double rafs to check for possible races.
await page.evaluate(() => new Promise(f => requestAnimationFrame(() => requestAnimationFrame(f))));
}
expect(await page.evaluate(() => result)).toBe('Was not clicked');
expect(done).toBe(false);
await page.$eval('button', b => b.style.display = 'block');
await clicked;
expect(done).toBe(true);
expect(await page.evaluate(() => result)).toBe('Clicked');
});
it('should timeout waiting for visible', async({page, server}) => {
it('should waitFor visibility:hidden to be gone', async({page, server}) => {
let done = false;
await page.goto(server.PREFIX + '/input/button.html');
await page.$eval('button', b => b.style.visibility = 'hidden');
const clicked = page.click('button', { timeout: 0 }).then(() => done = true);
for (let i = 0; i < 10; i++) {
// Do enough double rafs to check for possible races.
await page.evaluate(() => new Promise(f => requestAnimationFrame(() => requestAnimationFrame(f))));
}
expect(await page.evaluate(() => result)).toBe('Was not clicked');
expect(done).toBe(false);
await page.$eval('button', b => b.style.visibility = 'visible');
await clicked;
expect(done).toBe(true);
expect(await page.evaluate(() => result)).toBe('Clicked');
});
it('should timeout waiting for display:none to be gone', async({page, server}) => {
await page.goto(server.PREFIX + '/input/button.html');
await page.$eval('button', b => b.style.display = 'none');
const error = await page.click('button', { timeout: 100 }).catch(e => e);
expect(error.message).toContain('timeout exceeded');
});
it('should timeout waiting for visbility:hidden to be gone', async({page, server}) => {
await page.goto(server.PREFIX + '/input/button.html');
await page.$eval('button', b => b.style.visibility = 'hidden');
const error = await page.click('button', { timeout: 100 }).catch(e => e);
expect(error.message).toContain('timeout exceeded');
});
it('should waitFor visible when parent is hidden', async({page, server}) => {
let done = false;
await page.goto(server.PREFIX + '/input/button.html');

View File

@ -252,6 +252,16 @@ describe('Frame.waitForSelector', function() {
expect(await waitForSelector).toBe(true);
expect(divFound).toBe(true);
});
it('should not consider visible when zero-sized', async({page, server}) => {
await page.setContent(`<div style='width: 0; height: 0;'>1</div>`);
let error = await page.waitForSelector('div', { waitFor: 'visible', timeout: 1000 }).catch(e => e);
expect(error.message).toContain('timeout exceeded');
await page.evaluate(() => document.querySelector('div').style.width = '10px');
error = await page.waitForSelector('div', { waitFor: 'visible', timeout: 1000 }).catch(e => e);
expect(error.message).toContain('timeout exceeded');
await page.evaluate(() => document.querySelector('div').style.height = '10px');
expect(await page.waitForSelector('div', { waitFor: 'visible', timeout: 1000 })).toBeTruthy();
});
it('should wait for visible recursively', async({page, server}) => {
let divVisible = false;
const waitForSelector = page.waitForSelector('div#inner', { waitFor: 'visible' }).then(() => divVisible = true);
@ -265,7 +275,7 @@ describe('Frame.waitForSelector', function() {
});
it('hidden should wait for hidden', async({page, server}) => {
let divHidden = false;
await page.setContent(`<div style='display: block;'></div>`);
await page.setContent(`<div style='display: block;'>content</div>`);
const waitForSelector = page.waitForSelector('div', { waitFor: 'hidden' }).then(() => divHidden = true);
await page.waitForSelector('div'); // do a round trip
expect(divHidden).toBe(false);
@ -275,7 +285,7 @@ describe('Frame.waitForSelector', function() {
});
it('hidden should wait for display: none', async({page, server}) => {
let divHidden = false;
await page.setContent(`<div style='display: block;'></div>`);
await page.setContent(`<div style='display: block;'>content</div>`);
const waitForSelector = page.waitForSelector('div', { waitFor: 'hidden' }).then(() => divHidden = true);
await page.waitForSelector('div'); // do a round trip
expect(divHidden).toBe(false);
@ -284,7 +294,7 @@ describe('Frame.waitForSelector', function() {
expect(divHidden).toBe(true);
});
it('hidden should wait for removal', async({page, server}) => {
await page.setContent(`<div></div>`);
await page.setContent(`<div>content</div>`);
let divRemoved = false;
const waitForSelector = page.waitForSelector('div', { waitFor: 'hidden' }).then(() => divRemoved = true);
await page.waitForSelector('div'); // do a round trip
@ -305,9 +315,9 @@ describe('Frame.waitForSelector', function() {
expect(error).toBeInstanceOf(playwright.errors.TimeoutError);
});
it('should have an error message specifically for awaiting an element to be hidden', async({page, server}) => {
await page.setContent(`<div></div>`);
await page.setContent(`<div>content</div>`);
let error = null;
await page.waitForSelector('div', { waitFor: 'hidden', timeout: 10 }).catch(e => error = e);
await page.waitForSelector('div', { waitFor: 'hidden', timeout: 1000 }).catch(e => error = e);
expect(error).toBeTruthy();
expect(error.message).toContain('waiting for selector "[hidden] div" failed: timeout');
});