diff --git a/packages/playwright-core/src/server/ariaSnapshot.ts b/packages/playwright-core/src/server/ariaSnapshot.ts index 03b5a05e92..7a17b49752 100644 --- a/packages/playwright-core/src/server/ariaSnapshot.ts +++ b/packages/playwright-core/src/server/ariaSnapshot.ts @@ -14,19 +14,19 @@ * limitations under the License. */ -import type { AriaTemplateNode } from './injected/ariaSnapshot'; +import type { AriaTemplateNode, AriaTemplateRoleNode } from './injected/ariaSnapshot'; import { yaml } from '../utilsBundle'; import type { AriaRole } from '@injected/roleUtils'; import { assert } from '../utils'; export function parseAriaSnapshot(text: string): AriaTemplateNode { const fragment = yaml.parse(text) as any[]; - const result: AriaTemplateNode = { role: 'fragment' }; + const result: AriaTemplateNode = { kind: 'role', role: 'fragment' }; populateNode(result, fragment); return result; } -function populateNode(node: AriaTemplateNode, container: any[]) { +function populateNode(node: AriaTemplateRoleNode, container: any[]) { for (const object of container) { if (typeof object === 'string') { const childNode = parseKey(object); @@ -36,17 +36,33 @@ function populateNode(node: AriaTemplateNode, container: any[]) { } for (const key of Object.keys(object)) { - const childNode = parseKey(key); - const value = object[key]; node.children = node.children || []; + const value = object[key]; - if (childNode.role === 'text') { - node.children.push(valueOrRegex(value)); + if (key === 'text') { + node.children.push({ + kind: 'text', + text: valueOrRegex(value) + }); + continue; + } + + const childNode = parseKey(key); + if (childNode.kind === 'text') { + node.children.push({ + kind: 'text', + text: valueOrRegex(value) + }); continue; } if (typeof value === 'string') { - node.children.push({ ...childNode, children: [valueOrRegex(value)] }); + node.children.push({ + ...childNode, children: [{ + kind: 'text', + text: valueOrRegex(value) + }] + }); continue; } @@ -56,7 +72,7 @@ function populateNode(node: AriaTemplateNode, container: any[]) { } } -function applyAttribute(node: AriaTemplateNode, key: string, value: string) { +function applyAttribute(node: AriaTemplateRoleNode, key: string, value: string) { if (key === 'checked') { assert(value === 'true' || value === 'false' || value === 'mixed', 'Value of "disabled" attribute must be a boolean or "mixed"'); node.checked = value === 'true' ? true : value === 'false' ? false : 'mixed'; @@ -100,7 +116,7 @@ function parseKey(key: string): AriaTemplateNode { if (tokens.length === 0) throw new Error(`Invalid key ${key}`); - const role = tokens[0] as AriaRole | 'text'; + const role = tokens[0] as AriaRole; let name: string | RegExp = ''; let index = 1; @@ -115,7 +131,7 @@ function parseKey(key: string): AriaTemplateNode { index = 2; } - const result: AriaTemplateNode = { role, name }; + const result: AriaTemplateRoleNode = { kind: 'role', role, name }; for (; index < tokens.length; index++) { const attrToken = tokens[index]; if (attrToken.startsWith('[') && attrToken.endsWith(']')) { diff --git a/packages/playwright-core/src/server/injected/ariaSnapshot.ts b/packages/playwright-core/src/server/injected/ariaSnapshot.ts index d2a4df8e07..7e2a6bd309 100644 --- a/packages/playwright-core/src/server/injected/ariaSnapshot.ts +++ b/packages/playwright-core/src/server/injected/ariaSnapshot.ts @@ -35,12 +35,20 @@ type AriaNode = AriaProps & { children: (AriaNode | string)[]; }; -export type AriaTemplateNode = AriaProps & { - role: AriaRole | 'fragment' | 'text'; - name?: RegExp | string; - children?: (AriaTemplateNode | string | RegExp)[]; +export type AriaTemplateTextNode = { + kind: 'text'; + text: RegExp | string; }; +export type AriaTemplateRoleNode = AriaProps & { + kind: 'role'; + role: AriaRole | 'fragment'; + name?: RegExp | string; + children?: AriaTemplateNode[]; +}; + +export type AriaTemplateNode = AriaTemplateRoleNode | AriaTemplateTextNode; + export function generateAriaTree(rootElement: Element): AriaNode { const visit = (ariaNode: AriaNode, node: Node) => { if (node.nodeType === Node.TEXT_NODE && node.nodeValue) { @@ -172,7 +180,7 @@ function normalizeStringChildren(rootA11yNode: AriaNode) { const normalizeWhitespaceWithin = (text: string) => text.replace(/[\u200b\s\t\r\n]+/g, ' '); -function matchesText(text: string | undefined, template: RegExp | string | undefined) { +function matchesText(text: string, template: RegExp | string | undefined): boolean { if (!template) return true; if (!text) @@ -182,7 +190,20 @@ function matchesText(text: string | undefined, template: RegExp | string | undef return !!text.match(template); } -export function matchesAriaTree(rootElement: Element, template: AriaTemplateNode): { matches: boolean, received: { raw: string, regex: string } } { +function matchesTextNode(text: string, template: AriaTemplateTextNode) { + return matchesText(text, template.text); +} + +function matchesName(text: string, template: AriaTemplateRoleNode) { + return matchesText(text, template.name); +} + +export type MatcherReceived = { + raw: string; + regex: string; +}; + +export function matchesAriaTree(rootElement: Element, template: AriaTemplateNode): { matches: boolean, received: MatcherReceived } { const root = generateAriaTree(rootElement); const matches = matchesNodeDeep(root, template); return { @@ -197,11 +218,11 @@ export function matchesAriaTree(rootElement: Element, template: AriaTemplateNode }; } -function matchesNode(node: AriaNode | string, template: AriaTemplateNode | RegExp | string, depth: number): boolean { - if (typeof node === 'string' && (typeof template === 'string' || template instanceof RegExp)) - return matchesText(node, template); +function matchesNode(node: AriaNode | string, template: AriaTemplateNode, depth: number): boolean { + if (typeof node === 'string' && template.kind === 'text') + return matchesTextNode(node, template); - if (typeof node === 'object' && typeof template === 'object' && !(template instanceof RegExp)) { + if (typeof node === 'object' && template.kind === 'role') { if (template.role !== 'fragment' && template.role !== node.role) return false; if (template.checked !== undefined && template.checked !== node.checked) @@ -216,7 +237,7 @@ function matchesNode(node: AriaNode | string, template: AriaTemplateNode | RegEx return false; if (template.selected !== undefined && template.selected !== node.selected) return false; - if (!matchesText(node.name, template.name)) + if (!matchesName(node.name, template)) return false; if (!containsList(node.children || [], template.children || [], depth)) return false; @@ -225,7 +246,7 @@ function matchesNode(node: AriaNode | string, template: AriaTemplateNode | RegEx return false; } -function containsList(children: (AriaNode | string)[], template: (AriaTemplateNode | RegExp | string)[], depth: number): boolean { +function containsList(children: (AriaNode | string)[], template: AriaTemplateNode[], depth: number): boolean { if (template.length > children.length) return false; const cc = children.slice(); @@ -363,6 +384,9 @@ function includeText(node: AriaNode, text: string): boolean { if (!node.name) return true; + if (node.name.length > text.length) + return false; + // Figure out if text adds any value. const substr = longestCommonSubstring(text, node.name); let filtered = text; diff --git a/packages/playwright/src/matchers/toMatchAriaSnapshot.ts b/packages/playwright/src/matchers/toMatchAriaSnapshot.ts index 73e206e62f..b21677da5a 100644 --- a/packages/playwright/src/matchers/toMatchAriaSnapshot.ts +++ b/packages/playwright/src/matchers/toMatchAriaSnapshot.ts @@ -23,6 +23,7 @@ import { EXPECTED_COLOR } from '../common/expectBundle'; import { callLogText } from '../util'; import { printReceivedStringContainExpectedSubstring } from './expect'; import { currentTestInfo } from '../common/globals'; +import type { MatcherReceived } from '@injected/ariaSnapshot'; export async function toMatchAriaSnapshot( this: ExpectMatcherState, @@ -70,11 +71,7 @@ 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 typedReceived = received as MatcherReceived | typeof kNoElementsFoundError; const messagePrefix = matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined); const notFound = typedReceived === kNoElementsFoundError; @@ -94,12 +91,12 @@ export async function toMatchAriaSnapshot( if (notFound) return messagePrefix + `Expected: not ${this.utils.printExpected(escapedExpected)}\nReceived: ${escapedReceived}` + callLogText(log); const printedReceived = printReceivedStringContainExpectedSubstring(escapedReceived, escapedReceived.indexOf(escapedExpected), escapedExpected.length); - return messagePrefix + `Expected: not ${this.utils.printExpected(escapedExpected)}\nReceived string: ${printedReceived}` + callLogText(log); + return messagePrefix + `Expected: not ${this.utils.printExpected(escapedExpected)}\nReceived: ${printedReceived}` + callLogText(log); } else { const labelExpected = `Expected`; if (notFound) return messagePrefix + `${labelExpected}: ${this.utils.printExpected(escapedExpected)}\nReceived: ${escapedReceived}` + callLogText(log); - return messagePrefix + this.utils.printDiffOrStringify(escapedExpected, escapedReceived, labelExpected, 'Received string', false) + callLogText(log); + return messagePrefix + this.utils.printDiffOrStringify(escapedExpected, escapedReceived, labelExpected, 'Received', false) + callLogText(log); } }; diff --git a/tests/page/to-match-aria-snapshot.spec.ts b/tests/page/to-match-aria-snapshot.spec.ts index 8050c3b569..e9f79c09e3 100644 --- a/tests/page/to-match-aria-snapshot.spec.ts +++ b/tests/page/to-match-aria-snapshot.spec.ts @@ -396,8 +396,8 @@ test('expected formatter', async ({ page }) => { expect(stripAnsi(error.message)).toContain(` Locator: locator('body') -- Expected - 2 -+ Received string + 3 +- Expected - 2 ++ Received + 3 - - heading "todos" - - textbox "Wrong text" diff --git a/tests/playwright-test/stable-test-runner/package-lock.json b/tests/playwright-test/stable-test-runner/package-lock.json index 9b14a59c90..2aefdb7d5f 100644 --- a/tests/playwright-test/stable-test-runner/package-lock.json +++ b/tests/playwright-test/stable-test-runner/package-lock.json @@ -5,15 +5,15 @@ "packages": { "": { "dependencies": { - "@playwright/test": "1.49.0-alpha-2024-10-26" + "@playwright/test": "1.49.0-alpha-2024-10-29" } }, "node_modules/@playwright/test": { - "version": "1.49.0-alpha-2024-10-26", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0-alpha-2024-10-26.tgz", - "integrity": "sha512-EUl8wIsAWVJJlX2ynKdY1KxRs44Yz9MPDmN8AH6HIdwazSRe1ML46kaM3V49gQvMVMo5JZfuXnRzbtYDMFpKYA==", + "version": "1.49.0-alpha-2024-10-29", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0-alpha-2024-10-29.tgz", + "integrity": "sha512-JyT6BHjuJl5Iv91PvaYa1RXRQfSwHk1Abq/hzYFpebQQuKKNr3pck55qmih39+S/bGsuYW6XdzqAX+CfknR3sA==", "dependencies": { - "playwright": "1.49.0-alpha-2024-10-26" + "playwright": "1.49.0-alpha-2024-10-29" }, "bin": { "playwright": "cli.js" @@ -36,11 +36,11 @@ } }, "node_modules/playwright": { - "version": "1.49.0-alpha-2024-10-26", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0-alpha-2024-10-26.tgz", - "integrity": "sha512-1qh/6z4UdWv7qMocNQmUMbvZAXzzS93jckUzjGr0mWMn9rs4QavHhuK0s2HIS0hLB+t5T1+NBUpHudWzeasudA==", + "version": "1.49.0-alpha-2024-10-29", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0-alpha-2024-10-29.tgz", + "integrity": "sha512-ypwaWQwpxAiB5JEz4ACrztZsII4BdD5zOuAnjPtiXZtemSZNwxxY7phKlX8nLUlGwWDpb8aGe9tBcxoyrcFIww==", "dependencies": { - "playwright-core": "1.49.0-alpha-2024-10-26" + "playwright-core": "1.49.0-alpha-2024-10-29" }, "bin": { "playwright": "cli.js" @@ -53,9 +53,9 @@ } }, "node_modules/playwright-core": { - "version": "1.49.0-alpha-2024-10-26", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0-alpha-2024-10-26.tgz", - "integrity": "sha512-ELIdRRHkdzkHP7siPcFSE9jBLRnDHE1l3UigIgEzVN9o34yGBgH8TAkC2uK1M8Jrkomc3jKQm5faiBsimu0XEQ==", + "version": "1.49.0-alpha-2024-10-29", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0-alpha-2024-10-29.tgz", + "integrity": "sha512-pJmBdOnVFzBzA6Jo1q7FtJferyLK0a2cNZGbuOMO0LOPWY7FOT91225TYZ9a1qgaYMav+uudmYw6im/qjEwmIQ==", "bin": { "playwright-core": "cli.js" }, @@ -66,11 +66,11 @@ }, "dependencies": { "@playwright/test": { - "version": "1.49.0-alpha-2024-10-26", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0-alpha-2024-10-26.tgz", - "integrity": "sha512-EUl8wIsAWVJJlX2ynKdY1KxRs44Yz9MPDmN8AH6HIdwazSRe1ML46kaM3V49gQvMVMo5JZfuXnRzbtYDMFpKYA==", + "version": "1.49.0-alpha-2024-10-29", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0-alpha-2024-10-29.tgz", + "integrity": "sha512-JyT6BHjuJl5Iv91PvaYa1RXRQfSwHk1Abq/hzYFpebQQuKKNr3pck55qmih39+S/bGsuYW6XdzqAX+CfknR3sA==", "requires": { - "playwright": "1.49.0-alpha-2024-10-26" + "playwright": "1.49.0-alpha-2024-10-29" } }, "fsevents": { @@ -80,18 +80,18 @@ "optional": true }, "playwright": { - "version": "1.49.0-alpha-2024-10-26", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0-alpha-2024-10-26.tgz", - "integrity": "sha512-1qh/6z4UdWv7qMocNQmUMbvZAXzzS93jckUzjGr0mWMn9rs4QavHhuK0s2HIS0hLB+t5T1+NBUpHudWzeasudA==", + "version": "1.49.0-alpha-2024-10-29", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0-alpha-2024-10-29.tgz", + "integrity": "sha512-ypwaWQwpxAiB5JEz4ACrztZsII4BdD5zOuAnjPtiXZtemSZNwxxY7phKlX8nLUlGwWDpb8aGe9tBcxoyrcFIww==", "requires": { "fsevents": "2.3.2", - "playwright-core": "1.49.0-alpha-2024-10-26" + "playwright-core": "1.49.0-alpha-2024-10-29" } }, "playwright-core": { - "version": "1.49.0-alpha-2024-10-26", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0-alpha-2024-10-26.tgz", - "integrity": "sha512-ELIdRRHkdzkHP7siPcFSE9jBLRnDHE1l3UigIgEzVN9o34yGBgH8TAkC2uK1M8Jrkomc3jKQm5faiBsimu0XEQ==" + "version": "1.49.0-alpha-2024-10-29", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0-alpha-2024-10-29.tgz", + "integrity": "sha512-pJmBdOnVFzBzA6Jo1q7FtJferyLK0a2cNZGbuOMO0LOPWY7FOT91225TYZ9a1qgaYMav+uudmYw6im/qjEwmIQ==" } } } diff --git a/tests/playwright-test/stable-test-runner/package.json b/tests/playwright-test/stable-test-runner/package.json index 93ee2dba85..2e185ba7e3 100644 --- a/tests/playwright-test/stable-test-runner/package.json +++ b/tests/playwright-test/stable-test-runner/package.json @@ -1,6 +1,6 @@ { "private": true, "dependencies": { - "@playwright/test": "1.49.0-alpha-2024-10-26" + "@playwright/test": "1.49.0-alpha-2024-10-29" } }