chore: generate aria name regex when possible (#33373)
This commit is contained in:
parent
676f014b5f
commit
a2e901e080
|
@ -275,10 +275,11 @@ ${body}
|
|||
}
|
||||
|
||||
export function quoteMultiline(text: string, indent = ' ') {
|
||||
const escape = (text: string) => text.replace(/`/g, '\\`').replace(/\\/g, '\\\\');
|
||||
const lines = text.split('\n');
|
||||
if (lines.length === 1)
|
||||
return '`' + text.replace(/`/g, '\\`').replace(/\${/g, '\\${') + '`';
|
||||
return '`\n' + lines.map(line => indent + line.replace(/`/g, '\\`').replace(/\${/g, '\\${')).join('\n') + `\n${indent}\``;
|
||||
return '`' + escape(text) + '`';
|
||||
return '`\n' + lines.map(line => indent + escape(line).replace(/\${/g, '\\${')).join('\n') + `\n${indent}\``;
|
||||
}
|
||||
|
||||
function isMultilineString(text: string) {
|
||||
|
|
|
@ -144,8 +144,8 @@ function toAriaNode(element: Element): AriaNode | null {
|
|||
return result;
|
||||
}
|
||||
|
||||
export function renderedAriaTree(rootElement: Element): string {
|
||||
return renderAriaTree(generateAriaTree(rootElement));
|
||||
export function renderedAriaTree(rootElement: Element, options?: { mode?: 'raw' | 'regex' }): string {
|
||||
return renderAriaTree(generateAriaTree(rootElement), options);
|
||||
}
|
||||
|
||||
function normalizeStringChildren(rootA11yNode: AriaNode) {
|
||||
|
@ -209,11 +209,8 @@ export function matchesAriaTree(rootElement: Element, template: AriaTemplateNode
|
|||
return {
|
||||
matches,
|
||||
received: {
|
||||
raw: renderAriaTree(root),
|
||||
regex: renderAriaTree(root, {
|
||||
includeText,
|
||||
renderString: convertToBestGuessRegex
|
||||
}),
|
||||
raw: renderAriaTree(root, { mode: 'raw' }),
|
||||
regex: renderAriaTree(root, { mode: 'regex' }),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -283,15 +280,10 @@ function matchesNodeDeep(root: AriaNode, template: AriaTemplateNode): boolean {
|
|||
return !!results.length;
|
||||
}
|
||||
|
||||
type RenderAriaTreeOptions = {
|
||||
includeText?: (node: AriaNode, text: string) => boolean;
|
||||
renderString?: (text: string) => string | null;
|
||||
};
|
||||
|
||||
export function renderAriaTree(ariaNode: AriaNode, options?: RenderAriaTreeOptions): string {
|
||||
export function renderAriaTree(ariaNode: AriaNode, options?: { mode?: 'raw' | 'regex' }): string {
|
||||
const lines: string[] = [];
|
||||
const includeText = options?.includeText || (() => true);
|
||||
const renderString = options?.renderString || (str => str);
|
||||
const includeText = options?.mode === 'regex' ? textContributesInfo : () => true;
|
||||
const renderString = options?.mode === 'regex' ? convertToBestGuessRegex : (str: string) => str;
|
||||
const visit = (ariaNode: AriaNode | string, parentAriaNode: AriaNode | null, indent: string) => {
|
||||
if (typeof ariaNode === 'string') {
|
||||
if (parentAriaNode && !includeText(parentAriaNode, ariaNode))
|
||||
|
@ -306,7 +298,7 @@ export function renderAriaTree(ariaNode: AriaNode, options?: RenderAriaTreeOptio
|
|||
if (ariaNode.name) {
|
||||
const name = renderString(ariaNode.name);
|
||||
if (name)
|
||||
key += ' ' + yamlQuoteFragment(name);
|
||||
key += ' ' + (name.startsWith('/') && name.endsWith('/') ? name : yamlQuoteFragment(name));
|
||||
}
|
||||
if (ariaNode.checked === 'mixed')
|
||||
key += ` [checked=mixed]`;
|
||||
|
@ -353,31 +345,45 @@ export function renderAriaTree(ariaNode: AriaNode, options?: RenderAriaTreeOptio
|
|||
|
||||
function convertToBestGuessRegex(text: string): string {
|
||||
const dynamicContent = [
|
||||
// 2mb
|
||||
{ regex: /\b[\d,.]+[bkmBKM]+\b/, replacement: '[\\d,.]+[bkmBKM]+' },
|
||||
// 2ms, 20s
|
||||
{ regex: /\b\d+[hmsp]+\b/, replacement: '\\d+[hmsp]+' },
|
||||
{ regex: /\b[\d,.]+[hmsp]+\b/, replacement: '[\\d,.]+[hmsp]+' },
|
||||
// Do not replace single digits with regex by default.
|
||||
// 2+ digits: [Issue 22, 22.3, 2.33, 2,333]
|
||||
{ regex: /\b\d{2,}\b/g, replacement: '\\d+' },
|
||||
{ regex: /\b\{2,}\.\d+\b/g, replacement: '\\d+\\.\\d+' },
|
||||
{ regex: /\b\d+\.\d{2,}\b/g, replacement: '\\d+\\.\\d+' },
|
||||
{ regex: /\b\d+,\d+\b/g, replacement: '\\d+,\\d+' },
|
||||
// 2ms, 20s
|
||||
{ regex: /\b\d+[hms]+\b/g, replacement: '\\d+[hms]+' },
|
||||
{ regex: /\b[\d,.]+[hms]+\b/g, replacement: '[\\d,.]+[hms]+' },
|
||||
{ regex: /\b\d+,\d+\b/, replacement: '\\d+,\\d+' },
|
||||
{ regex: /\b\d+\.\d{2,}\b/, replacement: '\\d+\\.\\d+' },
|
||||
{ regex: /\b\d{2,}\.\d+\b/, replacement: '\\d+\\.\\d+' },
|
||||
{ regex: /\b\d{2,}\b/, replacement: '\\d+' },
|
||||
];
|
||||
|
||||
let result = escapeRegExp(text);
|
||||
let hasDynamicContent = false;
|
||||
let pattern = '';
|
||||
let lastIndex = 0;
|
||||
|
||||
for (const { regex, replacement } of dynamicContent) {
|
||||
if (regex.test(result)) {
|
||||
result = result.replace(regex, replacement);
|
||||
hasDynamicContent = true;
|
||||
const combinedRegex = new RegExp(dynamicContent.map(r => '(' + r.regex.source + ')').join('|'), 'g');
|
||||
text.replace(combinedRegex, (match, ...args) => {
|
||||
const offset = args[args.length - 2];
|
||||
const groups = args.slice(0, -2);
|
||||
pattern += escapeRegExp(text.slice(lastIndex, offset));
|
||||
for (let i = 0; i < groups.length; i++) {
|
||||
if (groups[i]) {
|
||||
const { replacement } = dynamicContent[i];
|
||||
pattern += replacement;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
lastIndex = offset + match.length;
|
||||
return match;
|
||||
});
|
||||
if (!pattern)
|
||||
return text;
|
||||
|
||||
return hasDynamicContent ? String(new RegExp(result)) : text;
|
||||
pattern += escapeRegExp(text.slice(lastIndex));
|
||||
return String(new RegExp(pattern));
|
||||
}
|
||||
|
||||
function includeText(node: AriaNode, text: string): boolean {
|
||||
function textContributesInfo(node: AriaNode, text: string): boolean {
|
||||
if (!text.length)
|
||||
return false;
|
||||
|
||||
|
|
|
@ -212,10 +212,10 @@ export class InjectedScript {
|
|||
return new Set<Element>(result.map(r => r.element));
|
||||
}
|
||||
|
||||
ariaSnapshot(node: Node): string {
|
||||
ariaSnapshot(node: Node, options?: { mode?: 'raw' | 'regex' }): string {
|
||||
if (node.nodeType !== Node.ELEMENT_NODE)
|
||||
throw this.createStacklessError('Can only capture aria snapshot of Element nodes.');
|
||||
return renderedAriaTree(node as Element);
|
||||
return renderedAriaTree(node as Element, options);
|
||||
}
|
||||
|
||||
querySelectorAll(selector: ParsedSelector, root: Node): Element[] {
|
||||
|
|
|
@ -715,7 +715,7 @@ class TextAssertionTool implements RecorderTool {
|
|||
name: 'assertSnapshot',
|
||||
selector: this._hoverHighlight.selector,
|
||||
signals: [],
|
||||
snapshot: this._recorder.injectedScript.ariaSnapshot(target),
|
||||
snapshot: this._recorder.injectedScript.ariaSnapshot(target, { mode: 'regex' }),
|
||||
};
|
||||
} else {
|
||||
this._hoverHighlight = this._recorder.injectedScript.generateSelector(target, { testIdAttributeName: this._recorder.state.testIdAttributeName, forTextExpect: true });
|
||||
|
|
|
@ -86,7 +86,7 @@ export async function applySuggestedRebaselines(config: FullConfigInternal) {
|
|||
for (const range of ranges)
|
||||
result = result.substring(0, range.start) + range.newText + result.substring(range.end);
|
||||
|
||||
const relativeName = path.relative(process.cwd(), fileName);
|
||||
const relativeName = path.relative(process.cwd(), fileName).replace(/\\/g, '/');
|
||||
|
||||
const patchFile = path.join(project.project.outputDir, 'rebaselines.patch');
|
||||
await fs.promises.mkdir(path.dirname(patchFile), { recursive: true });
|
||||
|
|
|
@ -39,4 +39,24 @@ test.describe(() => {
|
|||
await expect.poll(() =>
|
||||
recorder.text('C#')).toContain(`await Expect(page.GetByRole(AriaRole.Button)).ToMatchAriaSnapshotAsync("- button \\"Submit\\"");`);
|
||||
});
|
||||
|
||||
test('should generate regex in aria snapshot', async ({ openRecorder }) => {
|
||||
const { recorder } = await openRecorder();
|
||||
await recorder.setContentAndWait(`<main><button>Submit 123</button></main>`);
|
||||
|
||||
await recorder.page.click('x-pw-tool-item.snapshot');
|
||||
await recorder.page.hover('button');
|
||||
await recorder.trustedClick();
|
||||
|
||||
await expect.poll(() =>
|
||||
recorder.text('JavaScript')).toContain(`await expect(page.getByRole('button')).toMatchAriaSnapshot(\`- button /Submit \\\\d+/\`);`);
|
||||
await expect.poll(() =>
|
||||
recorder.text('Python')).toContain(`expect(page.get_by_role("button")).to_match_aria_snapshot("- button /Submit \\\\d+/")`);
|
||||
await expect.poll(() =>
|
||||
recorder.text('Python Async')).toContain(`await expect(page.get_by_role(\"button\")).to_match_aria_snapshot("- button /Submit \\\\d+/")`);
|
||||
await expect.poll(() =>
|
||||
recorder.text('Java')).toContain(`assertThat(page.getByRole(AriaRole.BUTTON)).matchesAriaSnapshot("- button /Submit \\\\d+/");`);
|
||||
await expect.poll(() =>
|
||||
recorder.text('C#')).toContain(`await Expect(page.GetByRole(AriaRole.Button)).ToMatchAriaSnapshotAsync("- button /Submit \\\\d+/");`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -116,12 +116,12 @@ test('should generate baseline with regex', async ({ runInlineTest }, testInfo)
|
|||
+ - listitem: Item 2
|
||||
+ - listitem: /Time \\d+:\\d+/
|
||||
+ - listitem: /Year \\d+/
|
||||
+ - listitem: /Duration \\d+[hms]+/
|
||||
+ - listitem: /Duration \\d+[hmsp]+/
|
||||
+ - listitem: /\\d+,\\d+/
|
||||
+ - listitem: /2,\\d+\\.\\d+/
|
||||
+ - listitem: /\\d+,\\d+\\.\\d+/
|
||||
+ - listitem: /Total \\d+/
|
||||
+ - listitem: /Regex 1/
|
||||
+ - listitem: /\\/Regex \\d+[hms]+\\//
|
||||
+ - listitem: /\\/Regex \\d+[hmsp]+\\//
|
||||
+ \`);
|
||||
});
|
||||
|
||||
|
@ -135,6 +135,7 @@ test('should generate baseline with special characters', async ({ runInlineTest
|
|||
test('test', async ({ page }) => {
|
||||
await page.setContent(\`<ul>
|
||||
<button>Click: me</button>
|
||||
<button>Click: 123</button>
|
||||
<li>Item: 1</li>
|
||||
<li>Item {a: b}</li>
|
||||
</ul>\`);
|
||||
|
@ -148,7 +149,7 @@ test('should generate baseline with special characters', async ({ runInlineTest
|
|||
const data = fs.readFileSync(patchPath, 'utf-8');
|
||||
expect(data).toBe(`--- a/a.spec.ts
|
||||
+++ b/a.spec.ts
|
||||
@@ -6,6 +6,11 @@
|
||||
@@ -7,6 +7,12 @@
|
||||
<li>Item: 1</li>
|
||||
<li>Item {a: b}</li>
|
||||
</ul>\`);
|
||||
|
@ -156,6 +157,7 @@ test('should generate baseline with special characters', async ({ runInlineTest
|
|||
+ await expect(page.locator('body')).toMatchAriaSnapshot(\`
|
||||
+ - list:
|
||||
+ - 'button "Click: me"'
|
||||
+ - 'button /Click: \\\\d+/'
|
||||
+ - listitem: \"Item: 1\"
|
||||
+ - listitem: \"Item {a: b}\"
|
||||
+ \`);
|
||||
|
|
Loading…
Reference in New Issue