diff --git a/docs/api.md b/docs/api.md index c4761588026d2..cbd42b056554d 100644 --- a/docs/api.md +++ b/docs/api.md @@ -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 `` tag into the page with the desired url or a ` 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`. diff --git a/docs/core-concepts.md b/docs/core-concepts.md index 7b2672efd2f13..a9470be569435 100644 --- a/docs/core-concepts.md +++ b/docs/core-concepts.md @@ -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 diff --git a/docs/input.md b/docs/input.md index 75776a9e3ae4d..e5468acab7e09 100644 --- a/docs/input.md +++ b/docs/input.md @@ -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 diff --git a/src/injected/injected.ts b/src/injected/injected.ts index 41c6b7340ffff..9771159c47c99 100644 --- a/src/injected/injected.ts +++ b/src/injected/injected.ts @@ -25,13 +25,14 @@ export type InjectedResult = 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(predicate: Predicate, timeout: number): Promise { @@ -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; }); diff --git a/test/click.spec.js b/test/click.spec.js index 8f97903da7ffd..b93a9f63cc65c 100644 --- a/test/click.spec.js +++ b/test/click.spec.js @@ -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'); diff --git a/test/waittask.spec.js b/test/waittask.spec.js index f2717155c7afc..2b3a2a18db287 100644 --- a/test/waittask.spec.js +++ b/test/waittask.spec.js @@ -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(`
1
`); + 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(`
`); + await page.setContent(`
content
`); 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(`
`); + await page.setContent(`
content
`); 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(`
`); + await page.setContent(`
content
`); 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(`
`); + await page.setContent(`
content
`); 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'); });