fix(aria snapshot): assorted fixes (#33512)

This commit is contained in:
Dmitry Gozman 2024-11-08 10:25:05 -08:00 committed by GitHub
parent 9c6c58f8ce
commit c29f573243
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 169 additions and 33 deletions

View File

@ -292,7 +292,7 @@ export function renderAriaTree(ariaNode: AriaNode, options?: { mode?: 'raw' | 'r
if (typeof ariaNode === 'string') {
if (parentAriaNode && !includeText(parentAriaNode, ariaNode))
return;
const text = renderString(ariaNode);
const text = yamlEscapeValueIfNeeded(renderString(ariaNode));
if (text)
lines.push(indent + '- text: ' + text);
return;
@ -399,8 +399,8 @@ function textContributesInfo(node: AriaNode, text: string): boolean {
if (node.name.length > text.length)
return false;
// Figure out if text adds any value.
const substr = longestCommonSubstring(text, node.name);
// Figure out if text adds any value. "longestCommonSubstring" is expensive, so limit strings length.
const substr = (text.length <= 200 && node.name.length <= 200) ? longestCommonSubstring(text, node.name) : '';
let filtered = text;
while (substr && filtered.includes(substr))
filtered = filtered.replace(substr, '');

View File

@ -86,6 +86,10 @@ function yamlStringNeedsQuotes(str: string): boolean {
if (/^[>|]/.test(str))
return true;
// Strings starting with quotes need quotes
if (/^["']/.test(str))
return true;
// Strings containing special characters that could cause ambiguity
if (/[{}`]/.test(str))
return true;

View File

@ -138,14 +138,18 @@ class KeyParser {
return this._pos >= this._length;
}
private _isWhitespace() {
return !this._eof() && /\s/.test(this._peek());
}
private _skipWhitespace() {
while (!this._eof() && /\s/.test(this._peek()))
while (this._isWhitespace())
this._pos++;
}
private _readIdentifier(): string {
private _readIdentifier(type: 'role' | 'attribute'): string {
if (this._eof())
this._throwError('Unexpected end of input when expecting identifier');
this._throwError(`Unexpected end of input when expecting ${type}`);
const start = this._pos;
while (!this._eof() && /[a-zA-Z]/.test(this._peek()))
this._pos++;
@ -178,6 +182,7 @@ class KeyParser {
private _readRegex(): string {
let result = '';
let escaped = false;
let insideClass = false;
while (!this._eof()) {
const ch = this._next();
if (escaped) {
@ -186,8 +191,14 @@ class KeyParser {
} else if (ch === '\\') {
escaped = true;
result += ch;
} else if (ch === '/') {
} else if (ch === '/' && !insideClass) {
return result;
} else if (ch === '[') {
insideClass = true;
result += ch;
} else if (ch === ']' && insideClass) {
result += ch;
insideClass = false;
} else {
result += ch;
}
@ -218,14 +229,14 @@ class KeyParser {
this._next();
this._skipWhitespace();
errorPos = this._pos;
const flagName = this._readIdentifier();
const flagName = this._readIdentifier('attribute');
this._skipWhitespace();
let flagValue = '';
if (this._peek() === '=') {
this._next();
this._skipWhitespace();
errorPos = this._pos;
while (this._peek() !== ']' && !this._eof())
while (this._peek() !== ']' && !this._isWhitespace() && !this._eof())
flagValue += this._next();
}
this._skipWhitespace();
@ -243,7 +254,7 @@ class KeyParser {
_parse(): AriaTemplateNode {
this._skipWhitespace();
const role = this._readIdentifier() as AriaTemplateRoleNode['role'];
const role = this._readIdentifier('role') as AriaTemplateRoleNode['role'];
this._skipWhitespace();
const name = this._readStringOrRegex() || '';
const result: AriaTemplateRoleNode = { kind: 'role', role, name };

View File

@ -459,3 +459,36 @@ it('should be ok with circular ownership', async ({ page }) => {
- region: Hello
`);
});
it('should escape yaml text in text nodes', async ({ page }) => {
await page.setContent(`
<details>
<summary>one: <a href="#">link1</a> "two <a href="#">link2</a> 'three <a href="#">link3</a> \`four</summary>
</details>
`);
await checkAndMatchSnapshot(page.locator('body'), `
- group:
- text: "one:"
- link "link1"
- text: "\\\"two"
- link "link2"
- text: "'three"
- link "link3"
- text: "\`four"
`);
});
it.fixme('should handle long strings', async ({ page }) => {
await page.setContent(`
<a href='about:blank'>
<div role='region'>${'a'.repeat(100000)}</div>
</a>
`);
const trimmed = 'a'.repeat(1000);
await checkAndMatchSnapshot(page.locator('body'), `
- link "${trimmed}":
- region: "${trimmed}"
`);
});

View File

@ -76,10 +76,30 @@ test('should match complex', async ({ page }) => {
});
test('should match regex', async ({ page }) => {
await page.setContent(`<h1>Issues 12</h1>`);
await expect(page.locator('body')).toMatchAriaSnapshot(`
- heading ${/Issues \d+/}
`);
{
await page.setContent(`<h1>Issues 12</h1>`);
await expect(page.locator('body')).toMatchAriaSnapshot(`
- heading ${/Issues \d+/}
`);
}
{
await page.setContent(`<h1>Issues 1/2</h1>`);
await expect(page.locator('body')).toMatchAriaSnapshot(`
- heading ${/Issues 1[/]2/}
`);
}
{
await page.setContent(`<h1>Issues 1[</h1>`);
await expect(page.locator('body')).toMatchAriaSnapshot(`
- heading ${/Issues 1\[/}
`);
}
{
await page.setContent(`<h1>Issues 1]]2</h1>`);
await expect(page.locator('body')).toMatchAriaSnapshot(`
- heading ${/Issues 1[\]]]2/}
`);
}
});
test('should allow text nodes', async ({ page }) => {
@ -472,6 +492,26 @@ test('should unpack escaped names', async ({ page }) => {
- 'button "Click '' me"'
`);
}
{
await page.setContent(`
<h1>heading "name" [level=1]</h1>
`);
await expect(page.locator('body')).toMatchAriaSnapshot(`
- heading "heading \\"name\\" [level=1]" [level=1]
`);
}
{
await page.setContent(`
<h1>heading \\" [level=2]</h1>
`);
await expect(page.locator('body')).toMatchAriaSnapshot(`
- |
heading "heading \\\\\\" [level=2]" [
level = 1 ]
`);
}
});
test('should report error in YAML', async ({ page }) => {
@ -599,3 +639,34 @@ test('call log should contain actual snapshot', async ({ page }) => {
expect(stripAnsi(error.message)).toContain(`- unexpected value "- heading "todos" [level=1]"`);
});
test.fixme('should normalize whitespace when matching accessible name', async ({ page }) => {
await page.setContent(`
<button>hello world</button>
`);
await expect(page.locator('body')).toMatchAriaSnapshot(`
- |
button "hello
world"
`);
});
test('should parse attributes', async ({ page }) => {
{
await page.setContent(`
<button aria-pressed="mixed">hello world</button>
`);
await expect(page.locator('body')).toMatchAriaSnapshot(`
- button [pressed=mixed ]
`);
}
{
await page.setContent(`
<h2>hello world</h2>
`);
await expect(page.locator('body')).not.toMatchAriaSnapshot(`
- heading [level = -3 ]
`);
}
});

View File

@ -20,6 +20,10 @@ import { execSync } from 'child_process';
test.describe.configure({ mode: 'parallel' });
function trimPatch(patch: string) {
return patch.split('\n').map(line => line.trimEnd()).join('\n');
}
test('should update snapshot with the update-snapshots flag', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
'a.spec.ts': `
@ -36,7 +40,7 @@ test('should update snapshot with the update-snapshots flag', async ({ runInline
expect(result.exitCode).toBe(0);
const patchPath = testInfo.outputPath('test-results/rebaselines.patch');
const data = fs.readFileSync(patchPath, 'utf-8');
expect(data).toBe(`--- a/a.spec.ts
expect(trimPatch(data)).toBe(`--- a/a.spec.ts
+++ b/a.spec.ts
@@ -3,7 +3,7 @@
test('test', async ({ page }) => {
@ -46,7 +50,7 @@ test('should update snapshot with the update-snapshots flag', async ({ runInline
+ - heading "hello" [level=1]
\`);
});
`);
expect(stripAnsi(result.output).replace(/\\/g, '/')).toContain(`New baselines created for:
@ -83,7 +87,7 @@ test('should update missing snapshots', async ({ runInlineTest }, testInfo) => {
const patchPath = testInfo.outputPath('test-results/rebaselines.patch');
const data = fs.readFileSync(patchPath, 'utf-8');
expect(data).toBe(`--- a/a.spec.ts
expect(trimPatch(data)).toBe(`--- a/a.spec.ts
+++ b/a.spec.ts
@@ -2,6 +2,8 @@
import { test, expect } from '@playwright/test';
@ -94,7 +98,7 @@ test('should update missing snapshots', async ({ runInlineTest }, testInfo) => {
+ - heading "hello" [level=1]
+ \`);
});
`);
execSync(`patch -p1 < ${patchPath}`, { cwd: testInfo.outputPath() });
@ -127,7 +131,7 @@ test('should generate baseline with regex', async ({ runInlineTest }, testInfo)
expect(result.exitCode).toBe(0);
const patchPath = testInfo.outputPath('test-results/rebaselines.patch');
const data = fs.readFileSync(patchPath, 'utf-8');
expect(data).toBe(`--- a/a.spec.ts
expect(trimPatch(data)).toBe(`--- a/a.spec.ts
+++ b/a.spec.ts
@@ -13,6 +13,18 @@
<li>/Regex 1/</li>
@ -148,7 +152,7 @@ test('should generate baseline with regex', async ({ runInlineTest }, testInfo)
+ - listitem: /\\\\/Regex \\\\d+[hmsp]+\\\\//
+ \`);
});
`);
execSync(`patch -p1 < ${patchPath}`, { cwd: testInfo.outputPath() });
@ -162,6 +166,10 @@ test('should generate baseline with special characters', async ({ runInlineTest
import { test, expect } from '@playwright/test';
test('test', async ({ page }) => {
await page.setContent(\`<ul>
<details>
<summary>one: <a href="#">link1</a> "two <a href="#">link2</a> 'three <a href="#">link3</a> \\\`four</summary>
</details>
<h1>heading "name" [level=1]</h1>
<button>Click: me</button>
<button>Click: 123</button>
<button>Click ' me</button>
@ -181,15 +189,24 @@ test('should generate baseline with special characters', async ({ runInlineTest
expect(result.exitCode).toBe(0);
const patchPath = testInfo.outputPath('test-results/rebaselines.patch');
const data = fs.readFileSync(patchPath, 'utf-8');
expect(data).toBe(`--- a/a.spec.ts
expect(trimPatch(data)).toBe(`--- a/a.spec.ts
+++ b/a.spec.ts
@@ -13,6 +13,18 @@
@@ -17,6 +17,27 @@
<li>Item: 1</li>
<li>Item {a: b}</li>
</ul>\`);
- await expect(page.locator('body')).toMatchAriaSnapshot(\`\`);
+ await expect(page.locator('body')).toMatchAriaSnapshot(\`
+ - list:
+ - group:
+ - text: "one:"
+ - link "link1"
+ - text: "\\\\\"two"
+ - link "link2"
+ - text: "'three"
+ - link "link3"
+ - text: "\\\`four"
+ - heading "heading \\\\"name\\\\" [level=1]" [level=1]
+ - 'button "Click: me"'
+ - 'button /Click: \\\\d+/'
+ - button "Click ' me"
@ -202,7 +219,7 @@ test('should generate baseline with special characters', async ({ runInlineTest
+ - listitem: \"Item {a: b}\"
+ \`);
});
`);
execSync(`patch -p1 < ${patchPath}`, { cwd: testInfo.outputPath() });
@ -234,10 +251,10 @@ test('should update missing snapshots in tsx', async ({ runInlineTest }, testInf
expect(result.exitCode).toBe(0);
const patchPath = testInfo.outputPath('test-results/rebaselines.patch');
const data = fs.readFileSync(patchPath, 'utf-8');
expect(data).toBe(`--- a/src/button.test.tsx
expect(trimPatch(data)).toBe(`--- a/src/button.test.tsx
+++ b/src/button.test.tsx
@@ -4,6 +4,8 @@
test('pass', async ({ mount }) => {
const component = await mount(<Button></Button>);
- await expect(component).toMatchAriaSnapshot(\`\`);
@ -245,7 +262,7 @@ test('should update missing snapshots in tsx', async ({ runInlineTest }, testInf
+ - button \"Button\"
+ \`);
});
`);
execSync(`patch -p1 < ${patchPath}`, { cwd: testInfo.outputPath() });
@ -296,10 +313,10 @@ test('should update multiple files', async ({ runInlineTest }, testInfo) => {
const patchPath = testInfo.outputPath('test-results/rebaselines.patch');
const data = fs.readFileSync(patchPath, 'utf-8');
expect(data).toBe(`--- a/src/button-1.test.tsx
expect(trimPatch(data)).toBe(`--- a/src/button-1.test.tsx
+++ b/src/button-1.test.tsx
@@ -4,6 +4,8 @@
test('pass 1', async ({ mount }) => {
const component = await mount(<Button></Button>);
- await expect(component).toMatchAriaSnapshot(\`\`);
@ -307,12 +324,12 @@ test('should update multiple files', async ({ runInlineTest }, testInfo) => {
+ - button \"Button\"
+ \`);
});
--- a/src/button-2.test.tsx
+++ b/src/button-2.test.tsx
@@ -4,6 +4,8 @@
test('pass 2', async ({ mount }) => {
const component = await mount(<Button></Button>);
- await expect(component).toMatchAriaSnapshot(\`\`);
@ -320,7 +337,7 @@ test('should update multiple files', async ({ runInlineTest }, testInfo) => {
+ - button \"Button\"
+ \`);
});
`);
execSync(`patch -p1 < ${patchPath}`, { cwd: testInfo.outputPath() });
@ -342,7 +359,7 @@ test('should generate baseline for input values', async ({ runInlineTest }, test
expect(result.exitCode).toBe(0);
const patchPath = testInfo.outputPath('test-results/rebaselines.patch');
const data = fs.readFileSync(patchPath, 'utf-8');
expect(data).toBe(`--- a/a.spec.ts
expect(trimPatch(data)).toBe(`--- a/a.spec.ts
+++ b/a.spec.ts
@@ -2,6 +2,8 @@
import { test, expect } from '@playwright/test';
@ -353,7 +370,7 @@ test('should generate baseline for input values', async ({ runInlineTest }, test
+ - textbox: hello world
+ \`);
});
`);
execSync(`patch -p1 < ${patchPath}`, { cwd: testInfo.outputPath() });