diff --git a/src/server/dom.ts b/src/server/dom.ts index 3dbf6f4833..dde2c6685d 100644 --- a/src/server/dom.ts +++ b/src/server/dom.ts @@ -697,7 +697,9 @@ export class ElementHandle extends js.JSHandle { async isVisible(): Promise { const result = await this.evaluateInUtility(([injected, node]) => injected.checkElementState(node, 'visible'), {}); - return throwRetargetableDOMError(throwFatalDOMError(result)); + if (result === 'error:notconnected') + return false; + return throwFatalDOMError(result); } async isHidden(): Promise { diff --git a/tests/page/elementhandle-convenience.spec.ts b/tests/page/elementhandle-convenience.spec.ts index 3ecd81be62..7422c98dcd 100644 --- a/tests/page/elementhandle-convenience.spec.ts +++ b/tests/page/elementhandle-convenience.spec.ts @@ -211,6 +211,25 @@ it('element state checks should work for label with zero-sized input', async ({p expect(await page.isDisabled('text=Click me')).toBe(true); }); +it('isVisible should not throw when the DOM element is not connected', async ({page}) => { + await page.setContent(`
`); + await page.evaluate(() => { + function insert() { + document.getElementById('root').innerHTML = '
Problem
'; + window.requestAnimationFrame(remove); + } + function remove() { + const node = document.getElementById('problem'); + node?.parentNode?.removeChild(node); + window.requestAnimationFrame(insert); + } + window.requestAnimationFrame(insert); + }); + + for (let i = 0; i < 10; i++) + await page.isVisible('#problem'); +}); + it('isEnabled and isDisabled should work', async ({ page }) => { await page.setContent(`