From 53ff35cbc4d16e788bfd977975bfb3886c6fe14e Mon Sep 17 00:00:00 2001 From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com> Date: Tue, 29 Oct 2024 08:14:22 -0700 Subject: [PATCH 01/33] chore(driver): roll driver to recent Node.js LTS version (#33349) Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- utils/build/build-playwright-driver.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/build/build-playwright-driver.sh b/utils/build/build-playwright-driver.sh index d587cdc94d..f89e770c77 100755 --- a/utils/build/build-playwright-driver.sh +++ b/utils/build/build-playwright-driver.sh @@ -4,7 +4,7 @@ set -x trap "cd $(pwd -P)" EXIT SCRIPT_PATH="$(cd "$(dirname "$0")" ; pwd -P)" -NODE_VERSION="20.18.0" # autogenerated via ./update-playwright-driver-version.mjs +NODE_VERSION="22.11.0" # autogenerated via ./update-playwright-driver-version.mjs cd "$(dirname "$0")" PACKAGE_VERSION=$(node -p "require('../../package.json').version") From 7a1739792f91d905dd5a7afc47b224d9ece4cac5 Mon Sep 17 00:00:00 2001 From: Denis LE Date: Tue, 29 Oct 2024 16:27:14 +0100 Subject: [PATCH 02/33] docs(best-practices): improve wording (#33342) --- docs/src/best-practices-js.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/best-practices-js.md b/docs/src/best-practices-js.md index 0c4e71d3a5..20f55c61d0 100644 --- a/docs/src/best-practices-js.md +++ b/docs/src/best-practices-js.md @@ -90,7 +90,7 @@ await page #### Prefer user-facing attributes to XPath or CSS selectors -Your DOM can easily change so having your tests depend on your DOM structure can lead to failing tests. For example consider selecting this button by its CSS classes. Should the designer change something then the class might change breaking your test. +Your DOM can easily change so having your tests depend on your DOM structure can lead to failing tests. For example consider selecting this button by its CSS classes. Should the designer change something then the class might change, thus breaking your test. ```js From 3b1883444de81c93f37ff3b342b0fb91fe8403ea Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Tue, 29 Oct 2024 21:04:13 +0100 Subject: [PATCH 03/33] chore: bump Docker Node.js to 22 (#33348) --- utils/docker/Dockerfile.jammy | 2 +- utils/docker/Dockerfile.noble | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/utils/docker/Dockerfile.jammy b/utils/docker/Dockerfile.jammy index ff24c31c88..d4d0cbad8a 100644 --- a/utils/docker/Dockerfile.jammy +++ b/utils/docker/Dockerfile.jammy @@ -14,7 +14,7 @@ RUN apt-get update && \ apt-get install -y curl wget gpg ca-certificates && \ mkdir -p /etc/apt/keyrings && \ curl -sL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \ - echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" >> /etc/apt/sources.list.d/nodesource.list && \ + echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" >> /etc/apt/sources.list.d/nodesource.list && \ apt-get update && \ apt-get install -y nodejs && \ # Feature-parity with node.js base images. diff --git a/utils/docker/Dockerfile.noble b/utils/docker/Dockerfile.noble index 29ca98c4e4..62ec6232c2 100644 --- a/utils/docker/Dockerfile.noble +++ b/utils/docker/Dockerfile.noble @@ -14,7 +14,7 @@ RUN apt-get update && \ apt-get install -y curl wget gpg ca-certificates && \ mkdir -p /etc/apt/keyrings && \ curl -sL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \ - echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" >> /etc/apt/sources.list.d/nodesource.list && \ + echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" >> /etc/apt/sources.list.d/nodesource.list && \ apt-get update && \ apt-get install -y nodejs && \ # Feature-parity with node.js base images. From 9ce401d44a81bf2a0947364f10453506d459ff86 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 29 Oct 2024 16:19:08 -0700 Subject: [PATCH 04/33] chore: suggest aria snapshots w/ regex (#33334) --- .../src/server/injected/ariaSnapshot.ts | 124 +++++++++++++----- .../src/server/injected/yaml.ts | 107 +++++++++++++++ .../src/utils/isomorphic/stringUtils.ts | 29 ++++ .../src/matchers/toMatchAriaSnapshot.ts | 20 ++- tests/page/page-aria-snapshot.spec.ts | 30 ++--- .../ui-mode-test-output.spec.ts | 11 ++ .../update-aria-snapshot.spec.ts | 86 ++++++++++++ 7 files changed, 357 insertions(+), 50 deletions(-) create mode 100644 packages/playwright-core/src/server/injected/yaml.ts diff --git a/packages/playwright-core/src/server/injected/ariaSnapshot.ts b/packages/playwright-core/src/server/injected/ariaSnapshot.ts index 82f3119d9c..d2a4df8e07 100644 --- a/packages/playwright-core/src/server/injected/ariaSnapshot.ts +++ b/packages/playwright-core/src/server/injected/ariaSnapshot.ts @@ -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; } diff --git a/packages/playwright-core/src/server/injected/yaml.ts b/packages/playwright-core/src/server/injected/yaml.ts new file mode 100644 index 0000000000..0030ad1a26 --- /dev/null +++ b/packages/playwright-core/src/server/injected/yaml.ts @@ -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; +} diff --git a/packages/playwright-core/src/utils/isomorphic/stringUtils.ts b/packages/playwright-core/src/utils/isomorphic/stringUtils.ts index 23c947cc49..37db89fcc6 100644 --- a/packages/playwright-core/src/utils/isomorphic/stringUtils.ts +++ b/packages/playwright-core/src/utils/isomorphic/stringUtils.ts @@ -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); +} diff --git a/packages/playwright/src/matchers/toMatchAriaSnapshot.ts b/packages/playwright/src/matchers/toMatchAriaSnapshot.ts index 496c1b9a08..73e206e62f 100644 --- a/packages/playwright/src/matchers/toMatchAriaSnapshot.ts +++ b/packages/playwright/src/matchers/toMatchAriaSnapshot.ts @@ -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 }; } diff --git a/tests/page/page-aria-snapshot.spec.ts b/tests/page/page-aria-snapshot.spec.ts index 86843d886a..8881fab972 100644 --- a/tests/page/page-aria-snapshot.spec.ts +++ b/tests/page/page-aria-snapshot.spec.ts @@ -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 `); }); diff --git a/tests/playwright-test/ui-mode-test-output.spec.ts b/tests/playwright-test/ui-mode-test-output.spec.ts index b10c02d08a..46cdc2e478 100644 --- a/tests/playwright-test/ui-mode-test-output.spec.ts +++ b/tests/playwright-test/ui-mode-test-output.spec.ts @@ -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: "/:1 Object {a: 1}/" + - listitem: "/:4 Date/" + - listitem: "/:5 Regex \/a\//" + - listitem: "/:6 Number 0 one 2/" + - listitem: "/:7 Download the React DevTools for a better development experience: https:\/\/fb\.me\/react-devtools/" + - listitem: "/: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'); diff --git a/tests/playwright-test/update-aria-snapshot.spec.ts b/tests/playwright-test/update-aria-snapshot.spec.ts index a5f56ea28e..203fda16a5 100644 --- a/tests/playwright-test/update-aria-snapshot.spec.ts +++ b/tests/playwright-test/update-aria-snapshot.spec.ts @@ -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(\`
    +
  • Item 1
  • +
  • Item 2
  • +
  • Time 15:30
  • +
  • Year 2022
  • +
  • Duration 12ms
  • +
  • 22,333
  • +
  • 2,333.79
  • +
  • Total 22
  • +
  • /Regex 1/
  • +
  • /Regex 22ms/
  • +
\`); + 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 @@ +
  • /Regex 1/
  • +
  • /Regex 22ms/
  • + \`); +- 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(\`
      + +
    • Item: 1
    • +
    • Item {a: b}
    • +
    \`); + 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 @@ +
  • Item: 1
  • +
  • Item {a: b}
  • + \`); +- await expect(page.locator('body')).toMatchAriaSnapshot(\`\`); ++ await expect(page.locator('body')).toMatchAriaSnapshot(\` ++ - list: ++ - 'button "Click: me"' ++ - listitem: \"Item: 1\" ++ - listitem: \"Item {a: b}\" ++ \`); + }); + +`); +}); From 6f5c7b43588d2e8b95d716a21c6d2f4682042c1b Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 29 Oct 2024 18:29:07 -0700 Subject: [PATCH 05/33] feat(html): render prev/next test buttons (#33356) --- packages/html-reporter/src/headerView.tsx | 4 +- packages/html-reporter/src/index.tsx | 5 +- packages/html-reporter/src/links.tsx | 23 ++++-- packages/html-reporter/src/reportView.tsx | 73 +++++++++++++------ packages/html-reporter/src/testCaseView.css | 2 +- .../html-reporter/src/testCaseView.spec.tsx | 36 +++++++-- packages/html-reporter/src/testCaseView.tsx | 15 +++- packages/html-reporter/src/testFileView.tsx | 23 +++--- packages/html-reporter/src/testFilesView.tsx | 54 +++++++------- 9 files changed, 158 insertions(+), 77 deletions(-) diff --git a/packages/html-reporter/src/headerView.tsx b/packages/html-reporter/src/headerView.tsx index 925bc64721..01f92f1ecb 100644 --- a/packages/html-reporter/src/headerView.tsx +++ b/packages/html-reporter/src/headerView.tsx @@ -20,7 +20,7 @@ import './colors.css'; import './common.css'; import './headerView.css'; import * as icons from './icons'; -import { Link, navigate } from './links'; +import { Link, navigate, SearchParamsContext } from './links'; import { statusIcon } from './statusIcon'; import { filterWithToken } from './filter'; @@ -65,7 +65,7 @@ export const HeaderView: React.FC = ({ stats }) => { - const searchParams = new URLSearchParams(window.location.hash.slice(1)); + const searchParams = React.useContext(SearchParamsContext); const q = searchParams.get('q')?.toString() || ''; const tokens = q.split(' '); return