From 1d4650cea2efa08865768f4ba2b8e722cb6bdf3c Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Fri, 1 Nov 2024 15:25:38 -0700 Subject: [PATCH] chore(snapshot): support aria-owns (#33404) --- .../src/server/injected/ariaSnapshot.ts | 22 ++++++++++- tests/page/page-aria-snapshot.spec.ts | 38 +++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/packages/playwright-core/src/server/injected/ariaSnapshot.ts b/packages/playwright-core/src/server/injected/ariaSnapshot.ts index ad09becd68..fa0f14343c 100644 --- a/packages/playwright-core/src/server/injected/ariaSnapshot.ts +++ b/packages/playwright-core/src/server/injected/ariaSnapshot.ts @@ -50,7 +50,12 @@ export type AriaTemplateRoleNode = AriaProps & { export type AriaTemplateNode = AriaTemplateRoleNode | AriaTemplateTextNode; export function generateAriaTree(rootElement: Element): AriaNode { + const visited = new Set(); const visit = (ariaNode: AriaNode, node: Node) => { + if (visited.has(node)) + return; + visited.add(node); + if (node.nodeType === Node.TEXT_NODE && node.nodeValue) { const text = node.nodeValue; if (text) @@ -65,13 +70,23 @@ export function generateAriaTree(rootElement: Element): AriaNode { if (roleUtils.isElementHiddenForAria(element)) return; + const ariaChildren: Element[] = []; + if (element.hasAttribute('aria-owns')) { + const ids = element.getAttribute('aria-owns')!.split(/\s+/); + for (const id of ids) { + const ownedElement = rootElement.ownerDocument.getElementById(id); + if (ownedElement) + ariaChildren.push(ownedElement); + } + } + const childAriaNode = toAriaNode(element); if (childAriaNode) ariaNode.children.push(childAriaNode); - processChildNodes(childAriaNode || ariaNode, element); + processElement(childAriaNode || ariaNode, element, ariaChildren); }; - function processChildNodes(ariaNode: AriaNode, element: Element) { + function processElement(ariaNode: AriaNode, element: Element, ariaChildren: Element[] = []) { // Surround every element with spaces for the sake of concatenated text nodes. const display = getElementComputedStyle(element)?.display || 'inline'; const treatAsBlock = (display !== 'inline' || element.nodeName === 'BR') ? ' ' : ''; @@ -94,6 +109,9 @@ export function generateAriaTree(rootElement: Element): AriaNode { } } + for (const child of ariaChildren) + visit(ariaNode, child); + ariaNode.children.push(roleUtils.getPseudoContent(element, '::after')); if (treatAsBlock) diff --git a/tests/page/page-aria-snapshot.spec.ts b/tests/page/page-aria-snapshot.spec.ts index d458c37f06..e9156ebad8 100644 --- a/tests/page/page-aria-snapshot.spec.ts +++ b/tests/page/page-aria-snapshot.spec.ts @@ -421,3 +421,41 @@ it('should treat input value as text in templates', async ({ page }) => { - textbox: hello world `); }); + +it('should respect aria-owns', async ({ page }) => { + await page.setContent(` + +
Link 1
+
+ +
Link 2
+
+ +

Paragraph

+ `); + + // - Different from Chrome DevTools which attributes ownership to the last element. + // - CDT also does not include non-owned children in accessible name. + // - Disregarding these as aria-owns can't suggest multiple parts by spec. + await checkAndMatchSnapshot(page.locator('body'), ` + - link "Link 1 Value Paragraph": + - region: Link 1 + - textbox: Value + - paragraph: Paragraph + - link "Link 2 Value Paragraph": + - region: Link 2 + `); +}); + +it('should be ok with circular ownership', async ({ page }) => { + await page.setContent(` + +
Hello
+
+ `); + + await checkAndMatchSnapshot(page.locator('body'), ` + - link "Hello": + - region: Hello + `); +});