chore: suggest aria snapshots w/ regex (#33334)
This commit is contained in:
parent
3b1883444d
commit
9ce401d44a
|
|
@ -17,6 +17,8 @@
|
|||
import * as roleUtils from './roleUtils';
|
||||
import { getElementComputedStyle } from './domUtils';
|
||||
import type { AriaRole } from './roleUtils';
|
||||
import { escapeRegExp, longestCommonSubstring } from '@isomorphic/stringUtils';
|
||||
import { yamlEscapeStringIfNeeded, yamlQuoteFragment } from './yaml';
|
||||
|
||||
type AriaProps = {
|
||||
checked?: boolean | 'mixed';
|
||||
|
|
@ -89,7 +91,7 @@ export function generateAriaTree(rootElement: Element): AriaNode {
|
|||
if (treatAsBlock)
|
||||
ariaNode.children.push(treatAsBlock);
|
||||
|
||||
if (ariaNode.children.length === 1 && ariaNode.name === ariaNode.children[0])
|
||||
if (ariaNode.children.length === 1 && ariaNode.name === ariaNode.children[0])
|
||||
ariaNode.children = [];
|
||||
}
|
||||
|
||||
|
|
@ -180,10 +182,19 @@ function matchesText(text: string | undefined, template: RegExp | string | undef
|
|||
return !!text.match(template);
|
||||
}
|
||||
|
||||
export function matchesAriaTree(rootElement: Element, template: AriaTemplateNode): { matches: boolean, received: string } {
|
||||
export function matchesAriaTree(rootElement: Element, template: AriaTemplateNode): { matches: boolean, received: { raw: string, regex: string } } {
|
||||
const root = generateAriaTree(rootElement);
|
||||
const matches = matchesNodeDeep(root, template);
|
||||
return { matches, received: renderAriaTree(root) };
|
||||
return {
|
||||
matches,
|
||||
received: {
|
||||
raw: renderAriaTree(root),
|
||||
regex: renderAriaTree(root, {
|
||||
includeText,
|
||||
renderString: convertToBestGuessRegex
|
||||
}),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function matchesNode(node: AriaNode | string, template: AriaTemplateNode | RegExp | string, depth: number): boolean {
|
||||
|
|
@ -251,62 +262,111 @@ function matchesNodeDeep(root: AriaNode, template: AriaTemplateNode): boolean {
|
|||
return !!results.length;
|
||||
}
|
||||
|
||||
export function renderAriaTree(ariaNode: AriaNode, options?: { noText?: boolean }): string {
|
||||
type RenderAriaTreeOptions = {
|
||||
includeText?: (node: AriaNode, text: string) => boolean;
|
||||
renderString?: (text: string) => string | null;
|
||||
};
|
||||
|
||||
export function renderAriaTree(ariaNode: AriaNode, options?: RenderAriaTreeOptions): string {
|
||||
const lines: string[] = [];
|
||||
const visit = (ariaNode: AriaNode | string, indent: string) => {
|
||||
const includeText = options?.includeText || (() => true);
|
||||
const renderString = options?.renderString || (str => str);
|
||||
const visit = (ariaNode: AriaNode | string, parentAriaNode: AriaNode | null, indent: string) => {
|
||||
if (typeof ariaNode === 'string') {
|
||||
if (!options?.noText)
|
||||
lines.push(indent + '- text: ' + quoteYamlString(ariaNode));
|
||||
if (parentAriaNode && !includeText(parentAriaNode, ariaNode))
|
||||
return;
|
||||
const text = renderString(ariaNode);
|
||||
if (text)
|
||||
lines.push(indent + '- text: ' + text);
|
||||
return;
|
||||
}
|
||||
let line = `${indent}- ${ariaNode.role}`;
|
||||
if (ariaNode.name)
|
||||
line += ` ${quoteYamlString(ariaNode.name)}`;
|
||||
|
||||
let key = ariaNode.role;
|
||||
if (ariaNode.name) {
|
||||
const name = renderString(ariaNode.name);
|
||||
if (name)
|
||||
key += ' ' + yamlQuoteFragment(name);
|
||||
}
|
||||
if (ariaNode.checked === 'mixed')
|
||||
line += ` [checked=mixed]`;
|
||||
key += ` [checked=mixed]`;
|
||||
if (ariaNode.checked === true)
|
||||
line += ` [checked]`;
|
||||
key += ` [checked]`;
|
||||
if (ariaNode.disabled)
|
||||
line += ` [disabled]`;
|
||||
key += ` [disabled]`;
|
||||
if (ariaNode.expanded)
|
||||
line += ` [expanded]`;
|
||||
key += ` [expanded]`;
|
||||
if (ariaNode.level)
|
||||
line += ` [level=${ariaNode.level}]`;
|
||||
key += ` [level=${ariaNode.level}]`;
|
||||
if (ariaNode.pressed === 'mixed')
|
||||
line += ` [pressed=mixed]`;
|
||||
key += ` [pressed=mixed]`;
|
||||
if (ariaNode.pressed === true)
|
||||
line += ` [pressed]`;
|
||||
key += ` [pressed]`;
|
||||
if (ariaNode.selected === true)
|
||||
line += ` [selected]`;
|
||||
key += ` [selected]`;
|
||||
|
||||
const escapedKey = indent + '- ' + yamlEscapeStringIfNeeded(key, '\'');
|
||||
if (!ariaNode.children.length) {
|
||||
lines.push(line);
|
||||
lines.push(escapedKey);
|
||||
} else if (ariaNode.children.length === 1 && typeof ariaNode.children[0] === 'string') {
|
||||
if (!options?.noText)
|
||||
line += ': ' + quoteYamlString(ariaNode.children[0]);
|
||||
lines.push(line);
|
||||
const text = includeText(ariaNode, ariaNode.children[0]) ? renderString(ariaNode.children[0] as string) : null;
|
||||
if (text)
|
||||
lines.push(escapedKey + ': ' + yamlEscapeStringIfNeeded(text, '"'));
|
||||
else
|
||||
lines.push(escapedKey);
|
||||
} else {
|
||||
lines.push(line + ':');
|
||||
lines.push(escapedKey + ':');
|
||||
for (const child of ariaNode.children || [])
|
||||
visit(child, indent + ' ');
|
||||
visit(child, ariaNode, indent + ' ');
|
||||
}
|
||||
};
|
||||
|
||||
if (ariaNode.role === 'fragment') {
|
||||
// Render fragment.
|
||||
for (const child of ariaNode.children || [])
|
||||
visit(child, '');
|
||||
visit(child, ariaNode, '');
|
||||
} else {
|
||||
visit(ariaNode, '');
|
||||
visit(ariaNode, null, '');
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function quoteYamlString(str: string) {
|
||||
return `"${str
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/"/g, '\\"')
|
||||
.replace(/\n/g, '\\n')
|
||||
.replace(/\r/g, '\\r')}"`;
|
||||
function convertToBestGuessRegex(text: string): string {
|
||||
const dynamicContent = [
|
||||
// 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]+' },
|
||||
];
|
||||
|
||||
let result = escapeRegExp(text);
|
||||
let hasDynamicContent = false;
|
||||
|
||||
for (const { regex, replacement } of dynamicContent) {
|
||||
if (regex.test(result)) {
|
||||
result = result.replace(regex, replacement);
|
||||
hasDynamicContent = true;
|
||||
}
|
||||
}
|
||||
|
||||
return hasDynamicContent ? String(new RegExp(result)) : text;
|
||||
}
|
||||
|
||||
function includeText(node: AriaNode, text: string): boolean {
|
||||
if (!text.length)
|
||||
return false;
|
||||
|
||||
if (!node.name)
|
||||
return true;
|
||||
|
||||
// Figure out if text adds any value.
|
||||
const substr = longestCommonSubstring(text, node.name);
|
||||
let filtered = text;
|
||||
while (substr && filtered.includes(substr))
|
||||
filtered = filtered.replace(substr, '');
|
||||
return filtered.trim().length / text.length > 0.1;
|
||||
}
|
||||
|
|
|
|||
107
packages/playwright-core/src/server/injected/yaml.ts
Normal file
107
packages/playwright-core/src/server/injected/yaml.ts
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export function yamlEscapeStringIfNeeded(str: string, quote = '"'): string {
|
||||
if (!yamlStringNeedsQuotes(str))
|
||||
return str;
|
||||
return yamlEscapeString(str, quote);
|
||||
}
|
||||
|
||||
export function yamlEscapeString(str: string, quote = '"'): string {
|
||||
return quote + str.replace(/[\\"\x00-\x1f\x7f-\x9f]/g, c => {
|
||||
switch (c) {
|
||||
case '\\':
|
||||
return '\\\\';
|
||||
case '"':
|
||||
return quote === '"' ? '\\"' : '"';
|
||||
case '\'':
|
||||
return quote === '\'' ? '\\\'' : '\'';
|
||||
case '\b':
|
||||
return '\\b';
|
||||
case '\f':
|
||||
return '\\f';
|
||||
case '\n':
|
||||
return '\\n';
|
||||
case '\r':
|
||||
return '\\r';
|
||||
case '\t':
|
||||
return '\\t';
|
||||
default:
|
||||
const code = c.charCodeAt(0);
|
||||
return '\\x' + code.toString(16).padStart(2, '0');
|
||||
}
|
||||
}) + quote;
|
||||
}
|
||||
|
||||
export function yamlQuoteFragment(str: string, quote = '"'): string {
|
||||
return quote + str.replace(/['"]/g, c => {
|
||||
switch (c) {
|
||||
case '"':
|
||||
return quote === '"' ? '\\"' : '"';
|
||||
case '\'':
|
||||
return quote === '\'' ? '\\\'' : '\'';
|
||||
default:
|
||||
return c;
|
||||
}
|
||||
}) + quote;
|
||||
}
|
||||
|
||||
function yamlStringNeedsQuotes(str: string): boolean {
|
||||
if (str.length === 0)
|
||||
return true;
|
||||
|
||||
// Strings with leading or trailing whitespace need quotes
|
||||
if (/^\s|\s$/.test(str))
|
||||
return true;
|
||||
|
||||
// Strings containing control characters need quotes
|
||||
if (/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]/.test(str))
|
||||
return true;
|
||||
|
||||
// Strings starting with '-' followed by a space need quotes
|
||||
if (/^-\s/.test(str))
|
||||
return true;
|
||||
|
||||
// Strings that start with a special indicator character need quotes
|
||||
if (/^[&*].*/.test(str))
|
||||
return true;
|
||||
|
||||
// Strings containing ':' followed by a space or at the end need quotes
|
||||
if (/:(\s|$)/.test(str))
|
||||
return true;
|
||||
|
||||
// Strings containing '#' preceded by a space need quotes (comment indicator)
|
||||
if (/\s#/.test(str))
|
||||
return true;
|
||||
|
||||
// Strings that contain line breaks need quotes
|
||||
if (/[\n\r]/.test(str))
|
||||
return true;
|
||||
|
||||
// Strings starting with '?' or '!' (directives) need quotes
|
||||
if (/^[?!]/.test(str))
|
||||
return true;
|
||||
|
||||
// Strings starting with '>' or '|' (block scalar indicators) need quotes
|
||||
if (/^[>|]/.test(str))
|
||||
return true;
|
||||
|
||||
// Strings containing special characters that could cause ambiguity
|
||||
if (/[{}`]/.test(str))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
|
@ -140,3 +140,32 @@ export function escapeHTMLAttribute(s: string): string {
|
|||
export function escapeHTML(s: string): string {
|
||||
return s.replace(/[&<]/ug, char => (escaped as any)[char]);
|
||||
}
|
||||
|
||||
export function longestCommonSubstring(s1: string, s2: string): string {
|
||||
const n = s1.length;
|
||||
const m = s2.length;
|
||||
let maxLen = 0;
|
||||
let endingIndex = 0;
|
||||
|
||||
// Initialize a 2D array with zeros
|
||||
const dp = Array(n + 1)
|
||||
.fill(null)
|
||||
.map(() => Array(m + 1).fill(0));
|
||||
|
||||
// Build the dp table
|
||||
for (let i = 1; i <= n; i++) {
|
||||
for (let j = 1; j <= m; j++) {
|
||||
if (s1[i - 1] === s2[j - 1]) {
|
||||
dp[i][j] = dp[i - 1][j - 1] + 1;
|
||||
|
||||
if (dp[i][j] > maxLen) {
|
||||
maxLen = dp[i][j];
|
||||
endingIndex = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract the longest common substring
|
||||
return s1.slice(endingIndex - maxLen, endingIndex);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,11 +70,25 @@ export async function toMatchAriaSnapshot(
|
|||
const timeout = options.timeout ?? this.timeout;
|
||||
expected = unshift(expected);
|
||||
const { matches: pass, received, log, timedOut } = await receiver._expect('to.match.aria', { expectedValue: expected, isNot: this.isNot, timeout });
|
||||
const typedReceived = received as {
|
||||
raw: string;
|
||||
noText: string;
|
||||
regex: string;
|
||||
} | typeof kNoElementsFoundError;
|
||||
|
||||
const messagePrefix = matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined);
|
||||
const notFound = received === kNoElementsFoundError;
|
||||
const notFound = typedReceived === kNoElementsFoundError;
|
||||
if (notFound) {
|
||||
return {
|
||||
pass: this.isNot,
|
||||
message: () => messagePrefix + `Expected: ${this.utils.printExpected(expected)}\nReceived: ${EXPECTED_COLOR('not found')}` + callLogText(log),
|
||||
name: 'toMatchAriaSnapshot',
|
||||
expected,
|
||||
};
|
||||
}
|
||||
|
||||
const escapedExpected = escapePrivateUsePoints(expected);
|
||||
const escapedReceived = escapePrivateUsePoints(received);
|
||||
const escapedReceived = escapePrivateUsePoints(typedReceived.raw);
|
||||
const message = () => {
|
||||
if (pass) {
|
||||
if (notFound)
|
||||
|
|
@ -91,7 +105,7 @@ export async function toMatchAriaSnapshot(
|
|||
|
||||
if (!this.isNot && pass === this.isNot && generateNewBaseline) {
|
||||
// Only rebaseline failed snapshots.
|
||||
const suggestedRebaseline = `toMatchAriaSnapshot(\`\n${indent(received, '${indent} ')}\n\${indent}\`)`;
|
||||
const suggestedRebaseline = `toMatchAriaSnapshot(\`\n${indent(typedReceived.regex, '${indent} ')}\n\${indent}\`)`;
|
||||
return { pass: this.isNot, message: () => '', name: 'toMatchAriaSnapshot', suggestedRebaseline };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -64,8 +64,8 @@ it('should snapshot list with accessible name', async ({ page }) => {
|
|||
`);
|
||||
await checkAndMatchSnapshot(page.locator('body'), `
|
||||
- list "my list":
|
||||
- listitem: "one"
|
||||
- listitem: "two"
|
||||
- listitem: one
|
||||
- listitem: two
|
||||
`);
|
||||
});
|
||||
|
||||
|
|
@ -92,7 +92,7 @@ it('should allow text nodes', async ({ page }) => {
|
|||
|
||||
await checkAndMatchSnapshot(page.locator('body'), `
|
||||
- heading "Microsoft" [level=1]
|
||||
- text: "Open source projects and samples from Microsoft"
|
||||
- text: Open source projects and samples from Microsoft
|
||||
`);
|
||||
});
|
||||
|
||||
|
|
@ -105,7 +105,7 @@ it('should snapshot details visibility', async ({ page }) => {
|
|||
`);
|
||||
|
||||
await checkAndMatchSnapshot(page.locator('body'), `
|
||||
- group: "Summary"
|
||||
- group: Summary
|
||||
`);
|
||||
});
|
||||
|
||||
|
|
@ -145,10 +145,10 @@ it('should snapshot integration', async ({ page }) => {
|
|||
|
||||
await checkAndMatchSnapshot(page.locator('body'), `
|
||||
- heading "Microsoft" [level=1]
|
||||
- text: "Open source projects and samples from Microsoft"
|
||||
- text: Open source projects and samples from Microsoft
|
||||
- list:
|
||||
- listitem:
|
||||
- group: "Verified"
|
||||
- group: Verified
|
||||
- listitem:
|
||||
- link "Sponsor"
|
||||
`);
|
||||
|
|
@ -164,7 +164,7 @@ it('should support multiline text', async ({ page }) => {
|
|||
`);
|
||||
|
||||
await checkAndMatchSnapshot(page.locator('body'), `
|
||||
- paragraph: "Line 1 Line 2 Line 3"
|
||||
- paragraph: Line 1 Line 2 Line 3
|
||||
`);
|
||||
await expect(page.locator('body')).toMatchAriaSnapshot(`
|
||||
- paragraph: |
|
||||
|
|
@ -180,7 +180,7 @@ it('should concatenate span text', async ({ page }) => {
|
|||
`);
|
||||
|
||||
await checkAndMatchSnapshot(page.locator('body'), `
|
||||
- text: "One Two Three"
|
||||
- text: One Two Three
|
||||
`);
|
||||
});
|
||||
|
||||
|
|
@ -190,7 +190,7 @@ it('should concatenate span text 2', async ({ page }) => {
|
|||
`);
|
||||
|
||||
await checkAndMatchSnapshot(page.locator('body'), `
|
||||
- text: "One Two Three"
|
||||
- text: One Two Three
|
||||
`);
|
||||
});
|
||||
|
||||
|
|
@ -200,7 +200,7 @@ it('should concatenate div text with spaces', async ({ page }) => {
|
|||
`);
|
||||
|
||||
await checkAndMatchSnapshot(page.locator('body'), `
|
||||
- text: "One Two Three"
|
||||
- text: One Two Three
|
||||
`);
|
||||
});
|
||||
|
||||
|
|
@ -362,12 +362,12 @@ it('should snapshot inner text', async ({ page }) => {
|
|||
|
||||
await checkAndMatchSnapshot(page.locator('body'), `
|
||||
- listitem:
|
||||
- text: "a.test.ts"
|
||||
- text: a.test.ts
|
||||
- button "Run"
|
||||
- button "Show source"
|
||||
- button "Watch"
|
||||
- listitem:
|
||||
- text: "snapshot 30ms"
|
||||
- text: snapshot 30ms
|
||||
- button "Run"
|
||||
- button "Show source"
|
||||
- button "Watch"
|
||||
|
|
@ -382,7 +382,7 @@ it('should include pseudo codepoints', async ({ page, server }) => {
|
|||
`);
|
||||
|
||||
await checkAndMatchSnapshot(page.locator('body'), `
|
||||
- paragraph: "\ueab2hello"
|
||||
- paragraph: \ueab2hello
|
||||
`);
|
||||
});
|
||||
|
||||
|
|
@ -396,7 +396,7 @@ it('check aria-hidden text', async ({ page, server }) => {
|
|||
`);
|
||||
|
||||
await checkAndMatchSnapshot(page.locator('body'), `
|
||||
- paragraph: "hello"
|
||||
- paragraph: hello
|
||||
`);
|
||||
});
|
||||
|
||||
|
|
@ -410,6 +410,6 @@ it('should ignore presentation and none roles', async ({ page, server }) => {
|
|||
`);
|
||||
|
||||
await checkAndMatchSnapshot(page.locator('body'), `
|
||||
- list: "hello world"
|
||||
- list: hello world
|
||||
`);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -145,6 +145,17 @@ test('should format console messages in page', async ({ runUITest }, testInfo) =
|
|||
'Failed to load resource: net::ERR_CONNECTION_REFUSED',
|
||||
]);
|
||||
|
||||
await expect(page.locator('.console-tab')).toMatchAriaSnapshot(`
|
||||
- list:
|
||||
- listitem: "/<anonymous>:1 Object {a: 1}/"
|
||||
- listitem: "/<anonymous>:4 Date/"
|
||||
- listitem: "/<anonymous>:5 Regex \/a\//"
|
||||
- listitem: "/<anonymous>:6 Number 0 one 2/"
|
||||
- listitem: "/<anonymous>:7 Download the React DevTools for a better development experience: https:\/\/fb\.me\/react-devtools/"
|
||||
- listitem: "/<anonymous>:8 Array of values/"
|
||||
- listitem: "/Failed to load resource: net::ERR_CONNECTION_REFUSED/"
|
||||
`);
|
||||
|
||||
const label = page.getByText('React DevTools');
|
||||
await expect(label).toHaveCSS('color', 'rgb(255, 0, 0)');
|
||||
await expect(label).toHaveCSS('font-weight', '700');
|
||||
|
|
|
|||
|
|
@ -77,3 +77,89 @@ test('should update missing snapshots', async ({ runInlineTest }, testInfo) => {
|
|||
|
||||
`);
|
||||
});
|
||||
|
||||
test('should generate baseline with regex', async ({ runInlineTest }, testInfo) => {
|
||||
const result = await runInlineTest({
|
||||
'a.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('test', async ({ page }) => {
|
||||
await page.setContent(\`<ul>
|
||||
<li>Item 1</li>
|
||||
<li>Item 2</li>
|
||||
<li>Time 15:30</li>
|
||||
<li>Year 2022</li>
|
||||
<li>Duration 12ms</li>
|
||||
<li>22,333</li>
|
||||
<li>2,333.79</li>
|
||||
<li>Total 22</li>
|
||||
<li>/Regex 1/</li>
|
||||
<li>/Regex 22ms/</li>
|
||||
</ul>\`);
|
||||
await expect(page.locator('body')).toMatchAriaSnapshot(\`\`);
|
||||
});
|
||||
`
|
||||
});
|
||||
|
||||
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
|
||||
+++ b/a.spec.ts
|
||||
@@ -13,6 +13,18 @@
|
||||
<li>/Regex 1/</li>
|
||||
<li>/Regex 22ms/</li>
|
||||
</ul>\`);
|
||||
- await expect(page.locator('body')).toMatchAriaSnapshot(\`\`);
|
||||
+ await expect(page.locator('body')).toMatchAriaSnapshot(\`
|
||||
+ - list:
|
||||
+ - listitem: Item 1
|
||||
+ - listitem: Item 2
|
||||
+ - listitem: /Time \\d+:\\d+/
|
||||
+ - listitem: /Year \\d+/
|
||||
+ - listitem: /Duration \\d+[hms]+/
|
||||
+ - listitem: /\\d+,\\d+/
|
||||
+ - listitem: /2,\\d+\\.\\d+/
|
||||
+ - listitem: /Total \\d+/
|
||||
+ - listitem: /Regex 1/
|
||||
+ - listitem: /\\/Regex \\d+[hms]+\\//
|
||||
+ \`);
|
||||
});
|
||||
|
||||
`);
|
||||
});
|
||||
|
||||
test('should generate baseline with special characters', async ({ runInlineTest }, testInfo) => {
|
||||
const result = await runInlineTest({
|
||||
'a.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('test', async ({ page }) => {
|
||||
await page.setContent(\`<ul>
|
||||
<button>Click: me</button>
|
||||
<li>Item: 1</li>
|
||||
<li>Item {a: b}</li>
|
||||
</ul>\`);
|
||||
await expect(page.locator('body')).toMatchAriaSnapshot(\`\`);
|
||||
});
|
||||
`
|
||||
});
|
||||
|
||||
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
|
||||
+++ b/a.spec.ts
|
||||
@@ -6,6 +6,11 @@
|
||||
<li>Item: 1</li>
|
||||
<li>Item {a: b}</li>
|
||||
</ul>\`);
|
||||
- await expect(page.locator('body')).toMatchAriaSnapshot(\`\`);
|
||||
+ await expect(page.locator('body')).toMatchAriaSnapshot(\`
|
||||
+ - list:
|
||||
+ - 'button "Click: me"'
|
||||
+ - listitem: \"Item: 1\"
|
||||
+ - listitem: \"Item {a: b}\"
|
||||
+ \`);
|
||||
});
|
||||
|
||||
`);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue