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

View file

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

View file

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

View file

@ -459,3 +459,36 @@ it('should be ok with circular ownership', async ({ page }) => {
- region: Hello - 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 }) => { test('should match regex', async ({ page }) => {
await page.setContent(`<h1>Issues 12</h1>`); {
await expect(page.locator('body')).toMatchAriaSnapshot(` await page.setContent(`<h1>Issues 12</h1>`);
- heading ${/Issues \d+/} 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 }) => { test('should allow text nodes', async ({ page }) => {
@ -472,6 +492,26 @@ test('should unpack escaped names', async ({ page }) => {
- 'button "Click '' me"' - '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 }) => { 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]"`); 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' }); 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) => { test('should update snapshot with the update-snapshots flag', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({ const result = await runInlineTest({
'a.spec.ts': ` 'a.spec.ts': `
@ -36,7 +40,7 @@ test('should update snapshot with the update-snapshots flag', async ({ runInline
expect(result.exitCode).toBe(0); expect(result.exitCode).toBe(0);
const patchPath = testInfo.outputPath('test-results/rebaselines.patch'); const patchPath = testInfo.outputPath('test-results/rebaselines.patch');
const data = fs.readFileSync(patchPath, 'utf-8'); 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 +++ b/a.spec.ts
@@ -3,7 +3,7 @@ @@ -3,7 +3,7 @@
test('test', async ({ page }) => { test('test', async ({ page }) => {
@ -46,7 +50,7 @@ test('should update snapshot with the update-snapshots flag', async ({ runInline
+ - heading "hello" [level=1] + - heading "hello" [level=1]
\`); \`);
}); });
`); `);
expect(stripAnsi(result.output).replace(/\\/g, '/')).toContain(`New baselines created for: 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 patchPath = testInfo.outputPath('test-results/rebaselines.patch');
const data = fs.readFileSync(patchPath, 'utf-8'); 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 +++ b/a.spec.ts
@@ -2,6 +2,8 @@ @@ -2,6 +2,8 @@
import { test, expect } from '@playwright/test'; import { test, expect } from '@playwright/test';
@ -94,7 +98,7 @@ test('should update missing snapshots', async ({ runInlineTest }, testInfo) => {
+ - heading "hello" [level=1] + - heading "hello" [level=1]
+ \`); + \`);
}); });
`); `);
execSync(`patch -p1 < ${patchPath}`, { cwd: testInfo.outputPath() }); 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); expect(result.exitCode).toBe(0);
const patchPath = testInfo.outputPath('test-results/rebaselines.patch'); const patchPath = testInfo.outputPath('test-results/rebaselines.patch');
const data = fs.readFileSync(patchPath, 'utf-8'); 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 +++ b/a.spec.ts
@@ -13,6 +13,18 @@ @@ -13,6 +13,18 @@
<li>/Regex 1/</li> <li>/Regex 1/</li>
@ -148,7 +152,7 @@ test('should generate baseline with regex', async ({ runInlineTest }, testInfo)
+ - listitem: /\\\\/Regex \\\\d+[hmsp]+\\\\// + - listitem: /\\\\/Regex \\\\d+[hmsp]+\\\\//
+ \`); + \`);
}); });
`); `);
execSync(`patch -p1 < ${patchPath}`, { cwd: testInfo.outputPath() }); 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'; import { test, expect } from '@playwright/test';
test('test', async ({ page }) => { test('test', async ({ page }) => {
await page.setContent(\`<ul> 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: me</button>
<button>Click: 123</button> <button>Click: 123</button>
<button>Click ' me</button> <button>Click ' me</button>
@ -181,15 +189,24 @@ test('should generate baseline with special characters', async ({ runInlineTest
expect(result.exitCode).toBe(0); expect(result.exitCode).toBe(0);
const patchPath = testInfo.outputPath('test-results/rebaselines.patch'); const patchPath = testInfo.outputPath('test-results/rebaselines.patch');
const data = fs.readFileSync(patchPath, 'utf-8'); 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 +++ b/a.spec.ts
@@ -13,6 +13,18 @@ @@ -17,6 +17,27 @@
<li>Item: 1</li> <li>Item: 1</li>
<li>Item {a: b}</li> <li>Item {a: b}</li>
</ul>\`); </ul>\`);
- await expect(page.locator('body')).toMatchAriaSnapshot(\`\`); - await expect(page.locator('body')).toMatchAriaSnapshot(\`\`);
+ await expect(page.locator('body')).toMatchAriaSnapshot(\` + await expect(page.locator('body')).toMatchAriaSnapshot(\`
+ - list: + - 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: me"'
+ - 'button /Click: \\\\d+/' + - 'button /Click: \\\\d+/'
+ - button "Click ' me" + - button "Click ' me"
@ -202,7 +219,7 @@ test('should generate baseline with special characters', async ({ runInlineTest
+ - listitem: \"Item {a: b}\" + - listitem: \"Item {a: b}\"
+ \`); + \`);
}); });
`); `);
execSync(`patch -p1 < ${patchPath}`, { cwd: testInfo.outputPath() }); 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); expect(result.exitCode).toBe(0);
const patchPath = testInfo.outputPath('test-results/rebaselines.patch'); const patchPath = testInfo.outputPath('test-results/rebaselines.patch');
const data = fs.readFileSync(patchPath, 'utf-8'); 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 +++ b/src/button.test.tsx
@@ -4,6 +4,8 @@ @@ -4,6 +4,8 @@
test('pass', async ({ mount }) => { test('pass', async ({ mount }) => {
const component = await mount(<Button></Button>); const component = await mount(<Button></Button>);
- await expect(component).toMatchAriaSnapshot(\`\`); - await expect(component).toMatchAriaSnapshot(\`\`);
@ -245,7 +262,7 @@ test('should update missing snapshots in tsx', async ({ runInlineTest }, testInf
+ - button \"Button\" + - button \"Button\"
+ \`); + \`);
}); });
`); `);
execSync(`patch -p1 < ${patchPath}`, { cwd: testInfo.outputPath() }); 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 patchPath = testInfo.outputPath('test-results/rebaselines.patch');
const data = fs.readFileSync(patchPath, 'utf-8'); 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 +++ b/src/button-1.test.tsx
@@ -4,6 +4,8 @@ @@ -4,6 +4,8 @@
test('pass 1', async ({ mount }) => { test('pass 1', async ({ mount }) => {
const component = await mount(<Button></Button>); const component = await mount(<Button></Button>);
- await expect(component).toMatchAriaSnapshot(\`\`); - await expect(component).toMatchAriaSnapshot(\`\`);
@ -307,12 +324,12 @@ test('should update multiple files', async ({ runInlineTest }, testInfo) => {
+ - button \"Button\" + - button \"Button\"
+ \`); + \`);
}); });
--- a/src/button-2.test.tsx --- a/src/button-2.test.tsx
+++ b/src/button-2.test.tsx +++ b/src/button-2.test.tsx
@@ -4,6 +4,8 @@ @@ -4,6 +4,8 @@
test('pass 2', async ({ mount }) => { test('pass 2', async ({ mount }) => {
const component = await mount(<Button></Button>); const component = await mount(<Button></Button>);
- await expect(component).toMatchAriaSnapshot(\`\`); - await expect(component).toMatchAriaSnapshot(\`\`);
@ -320,7 +337,7 @@ test('should update multiple files', async ({ runInlineTest }, testInfo) => {
+ - button \"Button\" + - button \"Button\"
+ \`); + \`);
}); });
`); `);
execSync(`patch -p1 < ${patchPath}`, { cwd: testInfo.outputPath() }); 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); expect(result.exitCode).toBe(0);
const patchPath = testInfo.outputPath('test-results/rebaselines.patch'); const patchPath = testInfo.outputPath('test-results/rebaselines.patch');
const data = fs.readFileSync(patchPath, 'utf-8'); 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 +++ b/a.spec.ts
@@ -2,6 +2,8 @@ @@ -2,6 +2,8 @@
import { test, expect } from '@playwright/test'; import { test, expect } from '@playwright/test';
@ -353,7 +370,7 @@ test('should generate baseline for input values', async ({ runInlineTest }, test
+ - textbox: hello world + - textbox: hello world
+ \`); + \`);
}); });
`); `);
execSync(`patch -p1 < ${patchPath}`, { cwd: testInfo.outputPath() }); execSync(`patch -p1 < ${patchPath}`, { cwd: testInfo.outputPath() });