();
+ while (true) {
+ this._skipWhitespace();
+ if (this._peek() === '[') {
+ this._next();
+ this._skipWhitespace();
+ const flagName = this._readIdentifier();
+ this._skipWhitespace();
+ let flagValue = '';
+ if (this._peek() === '=') {
+ this._next();
+ this._skipWhitespace();
+ while (this._peek() !== ']' && !this._eof())
+ flagValue += this._next();
+ }
+ this._skipWhitespace();
+ if (this._peek() !== ']')
+ throw new Error('Expected ] at position ' + this._pos);
+
+ this._next(); // Consume ']'
+ flags.set(flagName, flagValue || 'true');
+ } else {
+ break;
+ }
+ }
+ return flags;
+ }
+
+ _parse(): AriaTemplateNode {
+ this._skipWhitespace();
+
+ const role = this._readIdentifier() as AriaTemplateRoleNode['role'];
+ this._skipWhitespace();
+ const name = this._readStringOrRegex() || '';
+ const result: AriaTemplateRoleNode = { kind: 'role', role, name };
+ const flags = this._readFlags();
+ for (const [name, value] of flags)
+ applyAttribute(result, name, value);
+ this._skipWhitespace();
+ if (!this._eof())
+ throw new Error('Unexpected input at position ' + this._pos);
+
+ return result;
+ }
+}
diff --git a/packages/playwright-core/src/server/codegen/javascript.ts b/packages/playwright-core/src/server/codegen/javascript.ts
index f63dbb7c37..7a4b637a06 100644
--- a/packages/playwright-core/src/server/codegen/javascript.ts
+++ b/packages/playwright-core/src/server/codegen/javascript.ts
@@ -275,7 +275,9 @@ ${body}
}
export function quoteMultiline(text: string, indent = ' ') {
- const escape = (text: string) => text.replace(/`/g, '\\`').replace(/\\/g, '\\\\');
+ const escape = (text: string) => text.replace(/\\/g, '\\\\')
+ .replace(/`/g, '\\`')
+ .replace(/\$\{/g, '\\${');
const lines = text.split('\n');
if (lines.length === 1)
return '`' + escape(text) + '`';
diff --git a/packages/playwright-core/src/server/injected/ariaSnapshot.ts b/packages/playwright-core/src/server/injected/ariaSnapshot.ts
index b2a0de91e2..2d9f635410 100644
--- a/packages/playwright-core/src/server/injected/ariaSnapshot.ts
+++ b/packages/playwright-core/src/server/injected/ariaSnapshot.ts
@@ -18,7 +18,7 @@ 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';
+import { yamlEscapeKeyIfNeeded, yamlEscapeValueIfNeeded, yamlQuoteFragment } from './yaml';
type AriaProps = {
checked?: boolean | 'mixed';
@@ -317,13 +317,13 @@ export function renderAriaTree(ariaNode: AriaNode, options?: { mode?: 'raw' | 'r
if (ariaNode.selected === true)
key += ` [selected]`;
- const escapedKey = indent + '- ' + yamlEscapeStringIfNeeded(key, '\'');
+ const escapedKey = indent + '- ' + yamlEscapeKeyIfNeeded(key);
if (!ariaNode.children.length) {
lines.push(escapedKey);
} else if (ariaNode.children.length === 1 && typeof ariaNode.children[0] === 'string') {
const text = includeText(ariaNode, ariaNode.children[0]) ? renderString(ariaNode.children[0] as string) : null;
if (text)
- lines.push(escapedKey + ': ' + yamlEscapeStringIfNeeded(text, '"'));
+ lines.push(escapedKey + ': ' + yamlEscapeValueIfNeeded(text));
else
lines.push(escapedKey);
} else {
diff --git a/packages/playwright-core/src/server/injected/yaml.ts b/packages/playwright-core/src/server/injected/yaml.ts
index 0030ad1a26..97bf3a070d 100644
--- a/packages/playwright-core/src/server/injected/yaml.ts
+++ b/packages/playwright-core/src/server/injected/yaml.ts
@@ -14,21 +14,21 @@
* limitations under the License.
*/
-export function yamlEscapeStringIfNeeded(str: string, quote = '"'): string {
+export function yamlEscapeKeyIfNeeded(str: string): string {
if (!yamlStringNeedsQuotes(str))
return str;
- return yamlEscapeString(str, quote);
+ return `'` + str.replace(/'/g, `''`) + `'`;
}
-export function yamlEscapeString(str: string, quote = '"'): string {
- return quote + str.replace(/[\\"\x00-\x1f\x7f-\x9f]/g, c => {
+export function yamlEscapeValueIfNeeded(str: string): string {
+ if (!yamlStringNeedsQuotes(str))
+ return str;
+ return '"' + str.replace(/[\\"\x00-\x1f\x7f-\x9f]/g, c => {
switch (c) {
case '\\':
return '\\\\';
case '"':
- return quote === '"' ? '\\"' : '"';
- case '\'':
- return quote === '\'' ? '\\\'' : '\'';
+ return '\\"';
case '\b':
return '\\b';
case '\f':
@@ -43,7 +43,7 @@ export function yamlEscapeString(str: string, quote = '"'): string {
const code = c.charCodeAt(0);
return '\\x' + code.toString(16).padStart(2, '0');
}
- }) + quote;
+ }) + '"';
}
export function yamlQuoteFragment(str: string, quote = '"'): string {
diff --git a/packages/playwright-core/src/utils/isomorphic/stringUtils.ts b/packages/playwright-core/src/utils/isomorphic/stringUtils.ts
index 37db89fcc6..ed81c9a033 100644
--- a/packages/playwright-core/src/utils/isomorphic/stringUtils.ts
+++ b/packages/playwright-core/src/utils/isomorphic/stringUtils.ts
@@ -27,6 +27,13 @@ export function escapeWithQuotes(text: string, char: string = '\'') {
throw new Error('Invalid escape char');
}
+export function escapeTemplateString(text: string): string {
+ return text
+ .replace(/\\/g, '\\\\')
+ .replace(/`/g, '\\`')
+ .replace(/\$\{/g, '\\${');
+}
+
export function isString(obj: any): obj is string {
return typeof obj === 'string' || obj instanceof String;
}
diff --git a/packages/playwright/src/matchers/toMatchAriaSnapshot.ts b/packages/playwright/src/matchers/toMatchAriaSnapshot.ts
index b21677da5a..a475ec39d8 100644
--- a/packages/playwright/src/matchers/toMatchAriaSnapshot.ts
+++ b/packages/playwright/src/matchers/toMatchAriaSnapshot.ts
@@ -24,6 +24,7 @@ import { callLogText } from '../util';
import { printReceivedStringContainExpectedSubstring } from './expect';
import { currentTestInfo } from '../common/globals';
import type { MatcherReceived } from '@injected/ariaSnapshot';
+import { escapeTemplateString } from 'playwright-core/lib/utils';
export async function toMatchAriaSnapshot(
this: ExpectMatcherState,
@@ -102,7 +103,7 @@ export async function toMatchAriaSnapshot(
if (!this.isNot && pass === this.isNot && generateNewBaseline) {
// Only rebaseline failed snapshots.
- const suggestedRebaseline = `toMatchAriaSnapshot(\`\n${indent(typedReceived.regex, '${indent} ')}\n\${indent}\`)`;
+ const suggestedRebaseline = `toMatchAriaSnapshot(\`\n${escapeTemplateString(indent(typedReceived.regex, '{indent} '))}\n{indent}\`)`;
return { pass: this.isNot, message: () => '', name: 'toMatchAriaSnapshot', suggestedRebaseline };
}
@@ -118,7 +119,7 @@ export async function toMatchAriaSnapshot(
}
function escapePrivateUsePoints(str: string) {
- return str.replace(/[\uE000-\uF8FF]/g, char => `\\u${char.charCodeAt(0).toString(16).padStart(4, '0')}`);
+ return escapeTemplateString(str).replace(/[\uE000-\uF8FF]/g, char => `\\u${char.charCodeAt(0).toString(16).padStart(4, '0')}`);
}
function unshift(snapshot: string): string {
diff --git a/packages/playwright/src/runner/rebase.ts b/packages/playwright/src/runner/rebase.ts
index c75a346550..7558ea0800 100644
--- a/packages/playwright/src/runner/rebase.ts
+++ b/packages/playwright/src/runner/rebase.ts
@@ -81,7 +81,7 @@ export async function applySuggestedRebaselines(config: FullConfigInternal) {
if (matcher.loc!.start.column + 1 !== replacement.location.column)
continue;
const indent = lines[matcher.loc!.start.line - 1].match(/^\s*/)![0];
- const newText = replacement.code.replace(/\$\{indent\}/g, indent);
+ const newText = replacement.code.replace(/\{indent\}/g, indent);
ranges.push({ start: matcher.start!, end: node.end!, oldText: source.substring(matcher.start!, node.end!), newText });
}
}
diff --git a/tests/page/page-aria-snapshot.spec.ts b/tests/page/page-aria-snapshot.spec.ts
index 8881fab972..435508dab9 100644
--- a/tests/page/page-aria-snapshot.spec.ts
+++ b/tests/page/page-aria-snapshot.spec.ts
@@ -386,8 +386,7 @@ it('should include pseudo codepoints', async ({ page, server }) => {
`);
});
-it('check aria-hidden text', async ({ page, server }) => {
- await page.goto(server.EMPTY_PAGE);
+it('check aria-hidden text', async ({ page }) => {
await page.setContent(`
hello
@@ -400,8 +399,7 @@ it('check aria-hidden text', async ({ page, server }) => {
`);
});
-it('should ignore presentation and none roles', async ({ page, server }) => {
- await page.goto(server.EMPTY_PAGE);
+it('should ignore presentation and none roles', async ({ page }) => {
await page.setContent(`
- hello
diff --git a/tests/page/to-match-aria-snapshot.spec.ts b/tests/page/to-match-aria-snapshot.spec.ts
index e9f79c09e3..8c8b11525e 100644
--- a/tests/page/to-match-aria-snapshot.spec.ts
+++ b/tests/page/to-match-aria-snapshot.spec.ts
@@ -405,3 +405,56 @@ Locator: locator('body')
+ - heading "todos" [level=1]
+ - textbox "What needs to be done?"`);
});
+
+test('should unpack escaped names', async ({ page }) => {
+ {
+ await page.setContent(`
+
+ `);
+ await expect(page.locator('body')).toMatchAriaSnapshot(`
+ - 'button "Click: me"'
+ `);
+ await expect(page.locator('body')).toMatchAriaSnapshot(`
+ - 'button /Click: me/'
+ `);
+ }
+
+ {
+ await page.setContent(`
+
+ `);
+ await expect(page.locator('body')).toMatchAriaSnapshot(`
+ - button "Click / me"
+ `);
+ await expect(page.locator('body')).toMatchAriaSnapshot(`
+ - button /Click \\/ me/
+ `);
+ await expect(page.locator('body')).toMatchAriaSnapshot(`
+ - 'button /Click \\/ me/'
+ `);
+ }
+
+ {
+ await page.setContent(`
+
+ `);
+ await expect(page.locator('body')).toMatchAriaSnapshot(`
+ - button "Click \\ me"
+ `);
+ await expect(page.locator('body')).toMatchAriaSnapshot(`
+ - button /Click \\\\ me/
+ `);
+ await expect(page.locator('body')).toMatchAriaSnapshot(`
+ - 'button /Click \\\\ me/'
+ `);
+ }
+
+ {
+ await page.setContent(`
+
+ `);
+ await expect(page.locator('body')).toMatchAriaSnapshot(`
+ - 'button "Click '' me"'
+ `);
+ }
+});
diff --git a/tests/playwright-test/update-aria-snapshot.spec.ts b/tests/playwright-test/update-aria-snapshot.spec.ts
index 5bc1d28544..3c68788d4f 100644
--- a/tests/playwright-test/update-aria-snapshot.spec.ts
+++ b/tests/playwright-test/update-aria-snapshot.spec.ts
@@ -114,14 +114,14 @@ test('should generate baseline with regex', async ({ runInlineTest }, testInfo)
+ - list:
+ - listitem: Item 1
+ - listitem: Item 2
-+ - listitem: /Time \\d+:\\d+/
-+ - listitem: /Year \\d+/
-+ - listitem: /Duration \\d+[hmsp]+/
-+ - listitem: /\\d+,\\d+/
-+ - listitem: /\\d+,\\d+\\.\\d+/
-+ - listitem: /Total \\d+/
++ - listitem: /Time \\\\d+:\\\\d+/
++ - listitem: /Year \\\\d+/
++ - listitem: /Duration \\\\d+[hmsp]+/
++ - listitem: /\\\\d+,\\\\d+/
++ - listitem: /\\\\d+,\\\\d+\\\\.\\\\d+/
++ - listitem: /Total \\\\d+/
+ - listitem: /Regex 1/
-+ - listitem: /\\/Regex \\d+[hmsp]+\\//
++ - listitem: /\\\\/Regex \\\\d+[hmsp]+\\\\//
+ \`);
});
@@ -136,6 +136,8 @@ test('should generate baseline with special characters', async ({ runInlineTest
await page.setContent(\`
+
+
- Item: 1
- Item {a: b}
\`);
@@ -149,7 +151,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
-@@ -7,6 +7,12 @@
+@@ -9,6 +9,14 @@
- Item: 1
- Item {a: b}
\`);
@@ -158,6 +160,8 @@ test('should generate baseline with special characters', async ({ runInlineTest
+ - list:
+ - 'button "Click: me"'
+ - 'button /Click: \\\\d+/'
++ - button "Click ' me"
++ - 'button "Click: '' me"'
+ - listitem: \"Item: 1\"
+ - listitem: \"Item {a: b}\"
+ \`);