diff --git a/packages/playwright-core/src/server/injected/roleUtils.ts b/packages/playwright-core/src/server/injected/roleUtils.ts index b86bbfcc78..b7c8a48207 100644 --- a/packages/playwright-core/src/server/injected/roleUtils.ts +++ b/packages/playwright-core/src/server/injected/roleUtils.ts @@ -232,6 +232,8 @@ function getAriaBoolean(attr: string | null) { } // https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion, but including "none" and "presentation" roles +// Not implemented: +// `Any descendants of elements that have the characteristic "Children Presentational: True"` // 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)) @@ -242,17 +244,30 @@ export function isElementHiddenForAria(element: Element, cache: Map): boolean { +function belongsToDisplayNoneOrAriaHiddenOrNonSlotted(element: Element, cache: Map): boolean { if (!cache.has(element)) { - const style = getElementComputedStyle(element); - let hidden = !style || style.display === 'none' || getAriaBoolean(element.getAttribute('aria-hidden')) === true; + let hidden = false; + + // When parent has a shadow root, all light dom children must be assigned to a slot, + // otherwise they are not rendered and considered hidden for aria. + // Note: we can remove this logic once WebKit supports `Element.checkVisibility`. + if (element.parentElement && element.parentElement.shadowRoot && !element.assignedSlot) + hidden = true; + + // display:none and aria-hidden=true are considered hidden for aria. + if (!hidden) { + const style = getElementComputedStyle(element); + hidden = !style || style.display === 'none' || getAriaBoolean(element.getAttribute('aria-hidden')) === true; + } + + // Check recursively. if (!hidden) { const parent = parentElementOrShadowHost(element); if (parent) - hidden = hidden || belongsToDisplayNoneOrAriaHidden(parent, cache); + hidden = belongsToDisplayNoneOrAriaHiddenOrNonSlotted(parent, cache); } cache.set(element, hidden); } diff --git a/tests/page/selectors-role.spec.ts b/tests/page/selectors-role.spec.ts index 139887793f..4ba9f945f9 100644 --- a/tests/page/selectors-role.spec.ts +++ b/tests/page/selectors-role.spec.ts @@ -446,3 +446,36 @@ test('errors', async ({ page }) => { const e8 = await page.$('role=treeitem[expanded="none"]').catch(e => e); expect(e8.message).toContain(`"expanded" must be one of true, false`); }); + +test('hidden with shadow dom slots', async ({ page }) => { + await page.setContent(` +
+ +
+
+ +
+
+ +
+
+ +
+ + `); + expect(await page.locator(`role=button`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ + ``, + ``, + ]); + expect(await page.locator(`role=button[include-hidden]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ + ``, + ``, + ``, + ``, + ]); +});