From 4784dae10fa1c0e2c044548b45f4a7d9258f1c1b Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Tue, 6 Dec 2022 09:31:26 -0800 Subject: [PATCH] fix(role): accessibleName computation should walk the flat dom tree (#19301) - When visiting `` element, descend into assigned nodes. - When node has `assignedSlot`, skip it during regular traversal. Fixes #18989. --- .../src/server/injected/roleUtils.ts | 24 +++++--- tests/page/selectors-role.spec.ts | 60 +++++++++++++++++++ 2 files changed, 76 insertions(+), 8 deletions(-) diff --git a/packages/playwright-core/src/server/injected/roleUtils.ts b/packages/playwright-core/src/server/injected/roleUtils.ts index 9d9c1be95c..1952962a6b 100644 --- a/packages/playwright-core/src/server/injected/roleUtils.ts +++ b/packages/playwright-core/src/server/injected/roleUtils.ts @@ -579,7 +579,9 @@ function getElementAccessibleNameInternal(element: Element, options: AccessibleN if (allowsNameFromContent || options.embeddedInLabelledBy !== 'none' || options.embeddedInLabel !== 'none' || options.embeddedInTextAlternativeElement || options.embeddedInTargetElement === 'descendant') { options.visitedElements.add(element); const tokens: string[] = []; - const visit = (node: Node) => { + const visit = (node: Node, skipSlotted: boolean) => { + if (skipSlotted && (node as Element | Text).assignedSlot) + return; if (node.nodeType === 1 /* Node.ELEMENT_NODE */) { const display = getComputedStyle(node as Element)?.getPropertyValue('display') || 'inline'; let token = getElementAccessibleNameInternal(node as Element, childOptions); @@ -596,14 +598,20 @@ function getElementAccessibleNameInternal(element: Element, options: AccessibleN } }; tokens.push(getPseudoContent(getComputedStyle(element, '::before'))); - for (let child = element.firstChild; child; child = child.nextSibling) - visit(child); - if (element.shadowRoot) { - for (let child = element.shadowRoot.firstChild; child; child = child.nextSibling) - visit(child); + const assignedNodes = element.nodeName === 'SLOT' ? (element as HTMLSlotElement).assignedNodes() : []; + if (assignedNodes.length) { + for (const child of assignedNodes) + visit(child, false); + } else { + for (let child = element.firstChild; child; child = child.nextSibling) + visit(child, true); + if (element.shadowRoot) { + for (let child = element.shadowRoot.firstChild; child; child = child.nextSibling) + visit(child, true); + } + for (const owned of getIdRefs(element, element.getAttribute('aria-owns'))) + visit(owned, true); } - for (const owned of getIdRefs(element, element.getAttribute('aria-owns'))) - visit(owned); tokens.push(getPseudoContent(getComputedStyle(element, '::after'))); const accessibleName = tokens.join(''); if (accessibleName.trim()) diff --git a/tests/page/selectors-role.spec.ts b/tests/page/selectors-role.spec.ts index 253afd9475..0e45304a09 100644 --- a/tests/page/selectors-role.spec.ts +++ b/tests/page/selectors-role.spec.ts @@ -423,3 +423,63 @@ 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('should detect accessible name with slots', async ({ page }) => { + // Text "foo" is assigned to the slot, should not be used twice. + await page.setContent(` + + + `); + expect(await page.locator(`role=button[name="foo"]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ + ``, + ]); + + // Text "foo" is assigned to the slot, should be used instead of slot content. + await page.setContent(` +
foo
+ + `); + expect(await page.locator(`role=button[name="foo"]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ + ``, + ]); + + // Nothing is assigned to the slot, should use slot content. + await page.setContent(` +
+ + `); + expect(await page.locator(`role=button[name="pre"]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ + ``, + ]); +});