diff --git a/packages/playwright-core/src/server/injected/domUtils.ts b/packages/playwright-core/src/server/injected/domUtils.ts index 8a7abe8b63..e4a8e66e71 100644 --- a/packages/playwright-core/src/server/injected/domUtils.ts +++ b/packages/playwright-core/src/server/injected/domUtils.ts @@ -55,11 +55,38 @@ export function closestCrossShadow(element: Element | undefined, css: string): E } } +export function getElementComputedStyle(element: Element, pseudo?: string): CSSStyleDeclaration | undefined { + return element.ownerDocument && element.ownerDocument.defaultView ? element.ownerDocument.defaultView.getComputedStyle(element, pseudo) : undefined; +} + +export function isElementStyleVisibilityVisible(element: Element, style?: CSSStyleDeclaration): boolean { + style = style ?? getElementComputedStyle(element); + if (!style) + return true; + // Element.checkVisibility checks for content-visibility and also looks at + // styles up the flat tree including user-agent ShadowRoots, such as the + // details element for example. + // @ts-ignore Typescript doesn't know that checkVisibility exists yet. + if (Element.prototype.checkVisibility) { + // @ts-ignore Typescript doesn't know that checkVisibility exists yet. + if (!element.checkVisibility({ checkOpacity: false, checkVisibilityCSS: false })) + return false; + } else { + // Manual workaround for WebKit that does not have checkVisibility. + const detailsOrSummary = element.closest('details,summary'); + if (detailsOrSummary !== element && detailsOrSummary?.nodeName === 'DETAILS' && !(detailsOrSummary as HTMLDetailsElement).open) + return false; + } + if (style.visibility !== 'visible') + return false; + return true; +} + export function isElementVisible(element: Element): boolean { // Note: this logic should be similar to waitForDisplayedAtStablePosition() to avoid surprises. - if (!element.ownerDocument || !element.ownerDocument.defaultView) + const style = getElementComputedStyle(element); + if (!style) return true; - const style = element.ownerDocument.defaultView.getComputedStyle(element); if (style.display === 'contents') { // display:contents is not rendered itself, but its child nodes are. for (let child = element.firstChild; child; child = child.nextSibling) { @@ -70,13 +97,7 @@ export function isElementVisible(element: Element): boolean { } return false; } - // Element.checkVisibility checks for content-visibility and also looks at - // styles up the flat tree including user-agent ShadowRoots, such as the - // details element for example. - // @ts-ignore Typescript doesn't know that checkVisibility exists yet. - if (Element.prototype.checkVisibility && !element.checkVisibility({ checkOpacity: false, checkVisibilityCSS: false })) - return false; - if (!style || style.visibility === 'hidden') + if (!isElementStyleVisibilityVisible(element, style)) return false; const rect = element.getBoundingClientRect(); return rect.width > 0 && rect.height > 0; diff --git a/packages/playwright-core/src/server/injected/roleUtils.ts b/packages/playwright-core/src/server/injected/roleUtils.ts index cc3820a94d..c3c5eecb9c 100644 --- a/packages/playwright-core/src/server/injected/roleUtils.ts +++ b/packages/playwright-core/src/server/injected/roleUtils.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { closestCrossShadow, enclosingShadowRootOrDocument, parentElementOrShadowHost } from './domUtils'; +import { closestCrossShadow, enclosingShadowRootOrDocument, getElementComputedStyle, isElementStyleVisibilityVisible, parentElementOrShadowHost } from './domUtils'; function hasExplicitAccessibleName(e: Element) { return e.hasAttribute('aria-label') || e.hasAttribute('aria-labelledby'); @@ -225,24 +225,23 @@ function getAriaBoolean(attr: string | null) { return attr === null ? undefined : attr.toLowerCase() === 'true'; } -function getComputedStyle(element: Element, pseudo?: string): CSSStyleDeclaration | undefined { - return element.ownerDocument && element.ownerDocument.defaultView ? element.ownerDocument.defaultView.getComputedStyle(element, pseudo) : undefined; -} - // https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion, but including "none" and "presentation" roles // https://www.w3.org/TR/wai-aria-1.2/#aria-hidden export function isElementHiddenForAria(element: Element, cache: Map): boolean { if (['STYLE', 'SCRIPT', 'NOSCRIPT', 'TEMPLATE'].includes(element.tagName)) return true; - const style: CSSStyleDeclaration | undefined = getComputedStyle(element); - if (!style || style.visibility === 'hidden') + // Note: