Skip to content

Commit 031587a

Browse files
authored
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.
1 parent 4b0d977 commit 031587a

File tree

6 files changed

+67
-26
lines changed

6 files changed

+67
-26
lines changed

docs/api.md

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -947,7 +947,7 @@ Shortcut for [page.mainFrame().addStyleTag(options)](#frameaddstyletagoptions).
947947
- `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.
948948
- `options` <[Object]>
949949
- `force` <[boolean]> Whether to bypass the actionability checks. By default actions wait until the element is:
950-
- displayed (for example, no `display:none`),
950+
- displayed (for example, not empty, no `display:none`, no `visibility:hidden`),
951951
- is not moving (for example, waits until css transition finishes),
952952
- receives pointer events at the action point (for example, waits until element becomes non-obscured by other elements).
953953
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
971971
- y <[number]>
972972
- `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.
973973
- `force` <[boolean]> Whether to bypass the actionability checks. By default actions wait until the element is:
974-
- displayed (for example, no `display:none`),
974+
- displayed (for example, not empty, no `display:none`, no `visibility:hidden`),
975975
- is not moving (for example, waits until css transition finishes),
976976
- receives pointer events at the action point (for example, waits until element becomes non-obscured by other elements).
977977
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 [
10241024
- y <[number]>
10251025
- `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.
10261026
- `force` <[boolean]> Whether to bypass the actionability checks. By default actions wait until the element is:
1027-
- displayed (for example, no `display:none`),
1027+
- displayed (for example, not empty, no `display:none`, no `visibility:hidden`),
10281028
- is not moving (for example, waits until css transition finishes),
10291029
- receives pointer events at the action point (for example, waits until element becomes non-obscured by other elements).
10301030
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)
13331333
- y <[number]>
13341334
- `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.
13351335
- `force` <[boolean]> Whether to bypass the actionability checks. By default actions wait until the element is:
1336-
- displayed (for example, no `display:none`),
1336+
- displayed (for example, not empty, no `display:none`, no `visibility:hidden`),
13371337
- is not moving (for example, waits until css transition finishes),
13381338
- receives pointer events at the action point (for example, waits until element becomes non-obscured by other elements).
13391339
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
16581658
- `selector` <[string]> A selector to search for uncheckbox to check. If there are multiple elements satisfying the selector, the first will be checked.
16591659
- `options` <[Object]>
16601660
- `force` <[boolean]> Whether to bypass the actionability checks. By default actions wait until the element is:
1661-
- displayed (for example, no `display:none`),
1661+
- displayed (for example, not empty, no `display:none`, no `visibility:hidden`),
16621662
- is not moving (for example, waits until css transition finishes),
16631663
- receives pointer events at the action point (for example, waits until element becomes non-obscured by other elements).
16641664
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();
18191819

18201820
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.
18211821

1822+
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.
1823+
18221824
This method works across navigations:
18231825
```js
18241826
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
20022004
- `selector` <[string]> A selector to search for checkbox to check. If there are multiple elements satisfying the selector, the first will be checked.
20032005
- `options` <[Object]>
20042006
- `force` <[boolean]> Whether to bypass the actionability checks. By default actions wait until the element is:
2005-
- displayed (for example, no `display:none`),
2007+
- displayed (for example, not empty, no `display:none`, no `visibility:hidden`),
20062008
- is not moving (for example, waits until css transition finishes),
20072009
- receives pointer events at the action point (for example, waits until element becomes non-obscured by other elements).
20082010
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.
20272029
- y <[number]>
20282030
- `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.
20292031
- `force` <[boolean]> Whether to bypass the actionability checks. By default actions wait until the element is:
2030-
- displayed (for example, no `display:none`),
2032+
- displayed (for example, not empty, no `display:none`, no `visibility:hidden`),
20312033
- is not moving (for example, waits until css transition finishes),
20322034
- receives pointer events at the action point (for example, waits until element becomes non-obscured by other elements).
20332035
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.
20532055
- y <[number]>
20542056
- `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.
20552057
- `force` <[boolean]> Whether to bypass the actionability checks. By default actions wait until the element is:
2056-
- displayed (for example, no `display:none`),
2058+
- displayed (for example, not empty, no `display:none`, no `visibility:hidden`),
20572059
- is not moving (for example, waits until css transition finishes),
20582060
- receives pointer events at the action point (for example, waits until element becomes non-obscured by other elements).
20592061
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
22262228
- y <[number]>
22272229
- `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.
22282230
- `force` <[boolean]> Whether to bypass the actionability checks. By default actions wait until the element is:
2229-
- displayed (for example, no `display:none`),
2231+
- displayed (for example, not empty, no `display:none`, no `visibility:hidden`),
22302232
- is not moving (for example, waits until css transition finishes),
22312233
- receives pointer events at the action point (for example, waits until element becomes non-obscured by other elements).
22322234
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
23492351
- `selector` <[string]> A selector to search for uncheckbox to check. If there are multiple elements satisfying the selector, the first will be checked.
23502352
- `options` <[Object]>
23512353
- `force` <[boolean]> Whether to bypass the actionability checks. By default actions wait until the element is:
2352-
- displayed (for example, no `display:none`),
2354+
- displayed (for example, not empty, no `display:none`, no `visibility:hidden`),
23532355
- is not moving (for example, waits until css transition finishes),
23542356
- receives pointer events at the action point (for example, waits until element becomes non-obscured by other elements).
23552357
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)
25932595
#### elementHandle.check([options])
25942596
- `options` <[Object]>
25952597
- `force` <[boolean]> Whether to bypass the actionability checks. By default actions wait until the element is:
2596-
- displayed (for example, no `display:none`),
2598+
- displayed (for example, not empty, no `display:none`, no `visibility:hidden`),
25972599
- is not moving (for example, waits until css transition finishes),
25982600
- receives pointer events at the action point (for example, waits until element becomes non-obscured by other elements).
25992601
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
26132615
- y <[number]>
26142616
- `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.
26152617
- `force` <[boolean]> Whether to bypass the actionability checks. By default actions wait until the element is:
2616-
- displayed (for example, no `display:none`),
2618+
- displayed (for example, not empty, no `display:none`, no `visibility:hidden`),
26172619
- is not moving (for example, waits until css transition finishes),
26182620
- receives pointer events at the action point (for example, waits until element becomes non-obscured by other elements).
26192621
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.
26362638
- y <[number]>
26372639
- `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.
26382640
- `force` <[boolean]> Whether to bypass the actionability checks. By default actions wait until the element is:
2639-
- displayed (for example, no `display:none`),
2641+
- displayed (for example, not empty, no `display:none`, no `visibility:hidden`),
26402642
- is not moving (for example, waits until css transition finishes),
26412643
- receives pointer events at the action point (for example, waits until element becomes non-obscured by other elements).
26422644
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.
27092711
- y <[number]>
27102712
- `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.
27112713
- `force` <[boolean]> Whether to bypass the actionability checks. By default actions wait until the element is:
2712-
- displayed (for example, no `display:none`),
2714+
- displayed (for example, not empty, no `display:none`, no `visibility:hidden`),
27132715
- is not moving (for example, waits until css transition finishes),
27142716
- receives pointer events at the action point (for example, waits until element becomes non-obscured by other elements).
27152717
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');
28482850
#### elementHandle.uncheck([options])
28492851
- `options` <[Object]>
28502852
- `force` <[boolean]> Whether to bypass the actionability checks. By default actions wait until the element is:
2851-
- displayed (for example, no `display:none`),
2853+
- displayed (for example, not empty, no `display:none`, no `visibility:hidden`),
28522854
- is not moving (for example, waits until css transition finishes),
28532855
- receives pointer events at the action point (for example, waits until element becomes non-obscured by other elements).
28542856
Even if the action is forced, it will wait for the element matching selector to be in DOM. Defaults to `false`.

docs/core-concepts.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,10 +183,11 @@ const sectionText = await page.$eval('*css=section >> text=Selectors', e => e.te
183183

184184
Actions like `click` and `fill` auto-wait for the element to be visible and actionable. For example, click will:
185185
- wait for element with given selector to be in DOM
186-
- wait for it to become displayed, i.e. not `display:none`,
186+
- wait for it to become displayed, i.e. not empty, no `display:none`, no `visibility:hidden`
187187
- wait for it to stop moving, for example, until css transition finishes
188188
- scroll the element into view
189189
- wait for it to receive pointer events at the action point, for example, waits until element becomes non-obscured by other elements
190+
- retry if the element is detached during any of the above checks
190191

191192

192193
```js

docs/input.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,10 +115,11 @@ await page.click('button#submit');
115115
Performs a simple human click. Under the hood, this and other pointer-related methods:
116116

117117
- wait for element with given selector to be in DOM
118-
- wait for it to become displayed, i.e. not `display:none`,
118+
- wait for it to become displayed, i.e. not empty, no `display:none`, no `visibility:hidden`
119119
- wait for it to stop moving, for example, until css transition finishes
120120
- scroll the element into view
121121
- wait for it to receive pointer events at the action point, for example, waits until element becomes non-obscured by other elements
122+
- retry if the element is detached during any of the above checks
122123

123124
#### Variations
124125

src/injected/injected.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,14 @@ export type InjectedResult<T = undefined> =
2525

2626
export class Injected {
2727
isVisible(element: Element): boolean {
28+
// Note: this logic should be similar to waitForDisplayedAtStablePosition() to avoid surprises.
2829
if (!element.ownerDocument || !element.ownerDocument.defaultView)
2930
return true;
3031
const style = element.ownerDocument.defaultView.getComputedStyle(element);
3132
if (!style || style.visibility === 'hidden')
3233
return false;
3334
const rect = element.getBoundingClientRect();
34-
return !!(rect.top || rect.bottom || rect.width || rect.height);
35+
return rect.width > 0 && rect.height > 0;
3536
}
3637

3738
private _pollMutation<T>(predicate: Predicate<T>, timeout: number): Promise<T | undefined> {
@@ -311,9 +312,12 @@ export class Injected {
311312
return false;
312313
if (!node.isConnected)
313314
return 'notconnected';
315+
// Note: this logic should be similar to isVisible() to avoid surprises.
314316
const clientRect = element.getBoundingClientRect();
315317
const rect = { x: clientRect.top, y: clientRect.left, width: clientRect.width, height: clientRect.height };
316-
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;
318+
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;
319+
const style = element.ownerDocument && element.ownerDocument.defaultView ? element.ownerDocument.defaultView.getComputedStyle(element) : undefined;
320+
isDisplayedAndStable = isDisplayedAndStable && (!!style && style.visibility !== 'hidden');
317321
lastRect = rect;
318322
return !!isDisplayedAndStable;
319323
});

test/click.spec.js

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ describe('Page.click', function() {
146146
expect(error.message).toBe('Node is either not visible or not an HTMLElement');
147147
expect(await page.evaluate(() => result)).toBe('Was not clicked');
148148
});
149-
it('should waitFor visible', async({page, server}) => {
149+
it('should waitFor display:none to be gone', async({page, server}) => {
150150
let done = false;
151151
await page.goto(server.PREFIX + '/input/button.html');
152152
await page.$eval('button', b => b.style.display = 'none');
@@ -155,18 +155,41 @@ describe('Page.click', function() {
155155
// Do enough double rafs to check for possible races.
156156
await page.evaluate(() => new Promise(f => requestAnimationFrame(() => requestAnimationFrame(f))));
157157
}
158+
expect(await page.evaluate(() => result)).toBe('Was not clicked');
158159
expect(done).toBe(false);
159160
await page.$eval('button', b => b.style.display = 'block');
160161
await clicked;
161162
expect(done).toBe(true);
162163
expect(await page.evaluate(() => result)).toBe('Clicked');
163164
});
164-
it('should timeout waiting for visible', async({page, server}) => {
165+
it('should waitFor visibility:hidden to be gone', async({page, server}) => {
166+
let done = false;
167+
await page.goto(server.PREFIX + '/input/button.html');
168+
await page.$eval('button', b => b.style.visibility = 'hidden');
169+
const clicked = page.click('button', { timeout: 0 }).then(() => done = true);
170+
for (let i = 0; i < 10; i++) {
171+
// Do enough double rafs to check for possible races.
172+
await page.evaluate(() => new Promise(f => requestAnimationFrame(() => requestAnimationFrame(f))));
173+
}
174+
expect(await page.evaluate(() => result)).toBe('Was not clicked');
175+
expect(done).toBe(false);
176+
await page.$eval('button', b => b.style.visibility = 'visible');
177+
await clicked;
178+
expect(done).toBe(true);
179+
expect(await page.evaluate(() => result)).toBe('Clicked');
180+
});
181+
it('should timeout waiting for display:none to be gone', async({page, server}) => {
165182
await page.goto(server.PREFIX + '/input/button.html');
166183
await page.$eval('button', b => b.style.display = 'none');
167184
const error = await page.click('button', { timeout: 100 }).catch(e => e);
168185
expect(error.message).toContain('timeout exceeded');
169186
});
187+
it('should timeout waiting for visbility:hidden to be gone', async({page, server}) => {
188+
await page.goto(server.PREFIX + '/input/button.html');
189+
await page.$eval('button', b => b.style.visibility = 'hidden');
190+
const error = await page.click('button', { timeout: 100 }).catch(e => e);
191+
expect(error.message).toContain('timeout exceeded');
192+
});
170193
it('should waitFor visible when parent is hidden', async({page, server}) => {
171194
let done = false;
172195
await page.goto(server.PREFIX + '/input/button.html');

0 commit comments

Comments
 (0)