diff --git a/packages/playwright-core/src/server/injected/roleUtils.ts b/packages/playwright-core/src/server/injected/roleUtils.ts index 57681e0542..99f3cafa29 100644 --- a/packages/playwright-core/src/server/injected/roleUtils.ts +++ b/packages/playwright-core/src/server/injected/roleUtils.ts @@ -261,12 +261,16 @@ function getAriaBoolean(attr: string | null) { return attr === null ? undefined : attr.toLowerCase() === 'true'; } +function isElementIgnoredForAria(element: Element) { + return ['STYLE', 'SCRIPT', 'NOSCRIPT', 'TEMPLATE'].includes(elementSafeTagName(element)); +} + // 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): boolean { - if (['STYLE', 'SCRIPT', 'NOSCRIPT', 'TEMPLATE'].includes(elementSafeTagName(element))) + if (isElementIgnoredForAria(element)) return true; const style = getElementComputedStyle(element); const isSlot = element.nodeName === 'SLOT'; @@ -496,14 +500,17 @@ function getTextAlternativeInternal(element: Element, options: AccessibleNameOpt // step 2a. Hidden Not Referenced: If the current node is hidden and is: // Not part of an aria-labelledby or aria-describedby traversal, where the node directly referenced by that relation was hidden. // Nor part of a native host language text alternative element (e.g. label in HTML) or attribute traversal, where the root of that traversal was hidden. - if (!options.includeHidden && - !options.embeddedInLabelledBy?.hidden && - !options.embeddedInDescribedBy?.hidden && - !options?.embeddedInNativeTextAlternative?.hidden && - !options?.embeddedInLabel?.hidden && - isElementHiddenForAria(element)) { - options.visitedElements.add(element); - return ''; + if (!options.includeHidden) { + const isEmbeddedInHiddenReferenceTraversal = + !!options.embeddedInLabelledBy?.hidden || + !!options.embeddedInDescribedBy?.hidden || + !!options.embeddedInNativeTextAlternative?.hidden || + !!options.embeddedInLabel?.hidden; + if (isElementIgnoredForAria(element) || + (!isEmbeddedInHiddenReferenceTraversal && isElementHiddenForAria(element))) { + options.visitedElements.add(element); + return ''; + } } const labelledBy = getAriaLabelledByElements(element); diff --git a/tests/library/role-utils.spec.ts b/tests/library/role-utils.spec.ts index 4acc7f8a85..067053ba11 100644 --- a/tests/library/role-utils.spec.ts +++ b/tests/library/role-utils.spec.ts @@ -462,6 +462,19 @@ test('should work with form and tricky input names', async ({ page }) => { expect.soft(await getNameAndRole(page, 'form')).toEqual({ role: 'form', name: 'my form' }); }); +test('should ignore stylesheet from hidden aria-labelledby subtree', async ({ page }) => { + await page.setContent(` +
+ + `); + expect.soft(await getNameAndRole(page, 'input')).toEqual({ role: 'textbox', name: 'hello' }); +}); + function toArray(x: any): any[] { return Array.isArray(x) ? x : [x]; }