From 63e5257a4c349a3ce1db81caf6847a45152e2fb0 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 11 Mar 2025 10:04:52 -0700 Subject: [PATCH] chrome: expose link/url in aria (#35134) --- .../src/server/injected/ariaSnapshot.ts | 19 +++- .../src/utils/isomorphic/ariaSnapshot.ts | 16 ++++ tests/page/page-aria-snapshot.spec.ts | 88 +++++++++++++------ tests/page/to-match-aria-snapshot.spec.ts | 11 +++ .../update-aria-snapshot.spec.ts | 11 ++- 5 files changed, 109 insertions(+), 36 deletions(-) diff --git a/packages/playwright-core/src/server/injected/ariaSnapshot.ts b/packages/playwright-core/src/server/injected/ariaSnapshot.ts index 9acf2a8159..bfc54c9922 100644 --- a/packages/playwright-core/src/server/injected/ariaSnapshot.ts +++ b/packages/playwright-core/src/server/injected/ariaSnapshot.ts @@ -27,6 +27,7 @@ export type AriaNode = AriaProps & { name: string; children: (AriaNode | string)[]; element: Element; + props: Record; }; export type AriaSnapshot = { @@ -40,7 +41,7 @@ export function generateAriaTree(rootElement: Element, generation: number): Aria const visited = new Set(); const snapshot: AriaSnapshot = { - root: { role: 'fragment', name: '', children: [], element: rootElement }, + root: { role: 'fragment', name: '', children: [], element: rootElement, props: {} }, elements: new Map(), generation, ids: new Map(), @@ -124,6 +125,11 @@ export function generateAriaTree(rootElement: Element, generation: number): Aria if (ariaNode.children.length === 1 && ariaNode.name === ariaNode.children[0]) ariaNode.children = []; + + if (ariaNode.role === 'link' && element.hasAttribute('href')) { + const href = element.getAttribute('href')!; + ariaNode.props['url'] = href; + } } roleUtils.beginAriaCaches(); @@ -143,7 +149,7 @@ function toAriaNode(element: Element): AriaNode | null { return null; const name = normalizeWhiteSpace(roleUtils.getElementAccessibleName(element, false) || ''); - const result: AriaNode = { role, name, children: [], element }; + const result: AriaNode = { role, name, children: [], props: {}, element }; if (roleUtils.kAriaCheckedRoles.includes(role)) result.checked = roleUtils.getAriaChecked(element); @@ -263,6 +269,8 @@ function matchesNode(node: AriaNode | string, template: AriaTemplateNode, depth: return false; if (!matchesName(node.name, template)) return false; + if (!matchesText(node.props.url, template.props?.url)) + return false; if (!containsList(node.children || [], template.children || [], depth)) return false; return true; @@ -355,9 +363,10 @@ export function renderAriaTree(ariaSnapshot: AriaSnapshot, options?: { mode?: 'r } const escapedKey = indent + '- ' + yamlEscapeKeyIfNeeded(key); - if (!ariaNode.children.length) { + const hasProps = !!Object.keys(ariaNode.props).length; + if (!ariaNode.children.length && !hasProps) { lines.push(escapedKey); - } else if (ariaNode.children.length === 1 && typeof ariaNode.children[0] === 'string') { + } else if (ariaNode.children.length === 1 && typeof ariaNode.children[0] === 'string' && !hasProps) { const text = includeText(ariaNode, ariaNode.children[0]) ? renderString(ariaNode.children[0] as string) : null; if (text) lines.push(escapedKey + ': ' + yamlEscapeValueIfNeeded(text)); @@ -365,6 +374,8 @@ export function renderAriaTree(ariaSnapshot: AriaSnapshot, options?: { mode?: 'r lines.push(escapedKey); } else { lines.push(escapedKey + ':'); + for (const [name, value] of Object.entries(ariaNode.props)) + lines.push(indent + ' - /' + name + ': ' + yamlEscapeValueIfNeeded(value)); for (const child of ariaNode.children || []) visit(child, ariaNode, indent + ' '); } diff --git a/packages/playwright-core/src/utils/isomorphic/ariaSnapshot.ts b/packages/playwright-core/src/utils/isomorphic/ariaSnapshot.ts index 5fbc23ec44..04e5b7c471 100644 --- a/packages/playwright-core/src/utils/isomorphic/ariaSnapshot.ts +++ b/packages/playwright-core/src/utils/isomorphic/ariaSnapshot.ts @@ -46,6 +46,7 @@ export type AriaTemplateRoleNode = AriaProps & { role: AriaRole | 'fragment'; name?: AriaRegex | string; children?: AriaTemplateNode[]; + props?: Record; }; export type AriaTemplateNode = AriaTemplateRoleNode | AriaTemplateTextNode; @@ -151,6 +152,21 @@ export function parseAriaSnapshot(yaml: YamlLibrary, text: string, options: yaml continue; } + // - /url: "about:blank" + if (key.value.startsWith('/')) { + const valueIsString = value instanceof yaml.Scalar && typeof value.value === 'string'; + if (!valueIsString) { + errors.push({ + message: 'Property value should be a string', + range: convertRange(((entry.value as any).range || map.range)), + }); + continue; + } + container.props = container.props ?? {}; + container.props[key.value.slice(1)] = valueOrRegex(value.value); + continue; + } + // role "name": ... const childNode = KeyParser.parse(key, parseOptions, errors); if (!childNode) diff --git a/tests/page/page-aria-snapshot.spec.ts b/tests/page/page-aria-snapshot.spec.ts index e568fc6227..1a7adc168f 100644 --- a/tests/page/page-aria-snapshot.spec.ts +++ b/tests/page/page-aria-snapshot.spec.ts @@ -79,7 +79,8 @@ it('should snapshot complex', async ({ page }) => { await checkAndMatchSnapshot(page.locator('body'), ` - list: - listitem: - - link "link" + - link "link": + - /url: about:blank `); }); @@ -149,7 +150,8 @@ it('should snapshot integration', async ({ page }) => { - listitem: - group: Verified - listitem: - - link "Sponsor" + - link "Sponsor": + - /url: about:blank `); }); @@ -220,7 +222,8 @@ it('should include pseudo in text', async ({ page }) => { `); await checkAndMatchSnapshot(page.locator('body'), ` - - link "worldhello hellobye" + - link "worldhello hellobye": + - /url: about:blank `); }); @@ -243,7 +246,8 @@ it('should not include hidden pseudo in text', async ({ page }) => { `); await checkAndMatchSnapshot(page.locator('body'), ` - - link "hello hello" + - link "hello hello": + - /url: about:blank `); }); @@ -266,7 +270,8 @@ it('should include new line for block pseudo', async ({ page }) => { `); await checkAndMatchSnapshot(page.locator('body'), ` - - link "world hello hello bye" + - link "world hello hello bye": + - /url: about:blank `); }); @@ -450,10 +455,12 @@ it('should respect aria-owns', async ({ page }) => { // - Disregarding these as aria-owns can't suggest multiple parts by spec. await checkAndMatchSnapshot(page.locator('body'), ` - link "Link 1 Value Paragraph": + - /url: about:blank - region: Link 1 - textbox: Value - paragraph: Paragraph - link "Link 2 Value Paragraph": + - /url: about:blank - region: Link 2 `); }); @@ -467,6 +474,7 @@ it('should be ok with circular ownership', async ({ page }) => { await checkAndMatchSnapshot(page.locator('body'), ` - link "Hello": + - /url: about:blank - region: Hello `); }); @@ -488,22 +496,30 @@ it('should escape yaml text in text nodes', async ({ page }) => { await checkAndMatchSnapshot(page.locator('body'), ` - group: - text: "one:" - - link "link1" + - link "link1": + - /url: "#" - text: "\\\"two" - - link "link2" + - link "link2": + - /url: "#" - text: "'three" - - link "link3" + - link "link3": + - /url: "#" - text: "\`four" - list: - - link "one" + - link "one": + - /url: "#" - text: "," - - link "two" + - link "two": + - /url: "#" - text: ( - - link "three" + - link "three": + - /url: "#" - text: ") {" - - link "four" + - link "four": + - /url: "#" - text: "} [" - - link "five" + - link "five": + - /url: "#" - text: "]" - text: "[Select all]" `); @@ -521,7 +537,8 @@ it('should normalize whitespace', async ({ page }) => { await checkAndMatchSnapshot(page.locator('body'), ` - group: - text: one two - - link "link 1" + - link "link 1": + - /url: "#" - textbox: hello world - button "helloworld" `); @@ -532,7 +549,8 @@ it('should normalize whitespace', async ({ page }) => { - text: | one two - - link " link 1 " + - link " link 1 ": + - /url: "#" - textbox: hello world - button "he\u00adlloworld\u200b" `); @@ -548,6 +566,7 @@ it('should handle long strings', async ({ page }) => { await checkAndMatchSnapshot(page.locator('body'), ` - link: + - /url: about:blank - region: ${s} `); }); @@ -562,15 +581,20 @@ it('should escape special yaml characters', async ({ page }) => { `); await checkAndMatchSnapshot(page.locator('body'), ` - - link "@hello" + - link "@hello": + - /url: "#" - text: "@hello" - - link "]hello" + - link "]hello": + - /url: "#" - text: "]hello" - - link "hello" + - link "hello": + - /url: "#" - text: hello - - link "hello" + - link "hello": + - /url: "#" - text: hello - - link "#hello" + - link "#hello": + - /url: "#" - text: "#hello" `); }); @@ -589,21 +613,29 @@ it('should escape special yaml values', async ({ page }) => { `); await checkAndMatchSnapshot(page.locator('body'), ` - - link "true" + - link "true": + - /url: "#" - text: "False" - - link "NO" + - link "NO": + - /url: "#" - text: "yes" - - link "y" + - link "y": + - /url: "#" - text: "N" - - link "on" + - link "on": + - /url: "#" - text: "Off" - - link "null" + - link "null": + - /url: "#" - text: "NULL" - - link "123" + - link "123": + - /url: "#" - text: "123" - - link "-1.2" + - link "-1.2": + - /url: "#" - text: "-1.2" - - link "-" + - link "-": + - /url: "#" - text: "-" - textbox: "555" `); diff --git a/tests/page/to-match-aria-snapshot.spec.ts b/tests/page/to-match-aria-snapshot.spec.ts index 2f7ff203a5..f57a01cab4 100644 --- a/tests/page/to-match-aria-snapshot.spec.ts +++ b/tests/page/to-match-aria-snapshot.spec.ts @@ -682,3 +682,14 @@ test('should not match what is not matched', async ({ page }) => { - - button "bogus" + - paragraph: Text`); }); + +test('should match url', async ({ page }) => { + await page.setContent(` + Link + `); + + await expect(page.locator('body')).toMatchAriaSnapshot(` + - link: + - /url: /.*example.com/ + `); +}); diff --git a/tests/playwright-test/update-aria-snapshot.spec.ts b/tests/playwright-test/update-aria-snapshot.spec.ts index 12457ba8e0..78cdab3bab 100644 --- a/tests/playwright-test/update-aria-snapshot.spec.ts +++ b/tests/playwright-test/update-aria-snapshot.spec.ts @@ -256,7 +256,7 @@ test('should generate baseline with special characters', async ({ runInlineTest expect(trimPatch(data)).toBe(`diff --git a/a.spec.ts b/a.spec.ts --- a/a.spec.ts +++ b/a.spec.ts -@@ -17,6 +17,27 @@ +@@ -17,6 +17,30 @@
  • Item: 1
  • Item {a: b}
  • \`); @@ -265,11 +265,14 @@ test('should generate baseline with special characters', async ({ runInlineTest + - list: + - group: + - text: "one:" -+ - link "link1" ++ - link "link1": ++ - /url: "#" + - text: "\\\\\"two" -+ - link "link2" ++ - link "link2": ++ - /url: "#" + - text: "'three" -+ - link "link3" ++ - link "link3": ++ - /url: "#" + - text: "\\\`four" + - heading "heading \\\\"name\\\\" [level=1]" [level=1] + - 'button "Click: me"'