chrome: expose link/url in aria (#35134)

This commit is contained in:
Pavel Feldman 2025-03-11 10:04:52 -07:00 committed by GitHub
parent e2f95747e1
commit 63e5257a4c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 109 additions and 36 deletions

View File

@ -27,6 +27,7 @@ export type AriaNode = AriaProps & {
name: string;
children: (AriaNode | string)[];
element: Element;
props: Record<string, string>;
};
export type AriaSnapshot = {
@ -40,7 +41,7 @@ export function generateAriaTree(rootElement: Element, generation: number): Aria
const visited = new Set<Node>();
const snapshot: AriaSnapshot = {
root: { role: 'fragment', name: '', children: [], element: rootElement },
root: { role: 'fragment', name: '', children: [], element: rootElement, props: {} },
elements: new Map<number, Element>(),
generation,
ids: new Map<Element, number>(),
@ -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 + ' ');
}

View File

@ -46,6 +46,7 @@ export type AriaTemplateRoleNode = AriaProps & {
role: AriaRole | 'fragment';
name?: AriaRegex | string;
children?: AriaTemplateNode[];
props?: Record<string, string | AriaRegex>;
};
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)

View File

@ -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"
`);

View File

@ -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(`
<a href='https://example.com'>Link</a>
`);
await expect(page.locator('body')).toMatchAriaSnapshot(`
- link:
- /url: /.*example.com/
`);
});

View File

@ -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 @@
<li>Item: 1</li>
<li>Item {a: b}</li>
</ul>\`);
@ -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"'