diff --git a/packages/playwright-core/src/server/injected/ariaSnapshot.ts b/packages/playwright-core/src/server/injected/ariaSnapshot.ts index 56044a2027..682f48365a 100644 --- a/packages/playwright-core/src/server/injected/ariaSnapshot.ts +++ b/packages/playwright-core/src/server/injected/ariaSnapshot.ts @@ -208,7 +208,7 @@ function matchesText(text: string | undefined, template: RegExp | string | undef export function matchesAriaTree(rootElement: Element, template: AriaTemplateNode): { matches: boolean, received: string } { const root = generateAriaTree(rootElement); const matches = matchesNodeDeep(root, template); - return { matches, received: renderAriaTree(root) }; + return { matches, received: renderAriaTree(root, { noText: true }) }; } function matchesNode(node: AriaNode | string, template: AriaTemplateNode | RegExp | string, depth: number): boolean { @@ -276,11 +276,12 @@ function matchesNodeDeep(root: AriaNode, template: AriaTemplateNode): boolean { return !!results.length; } -export function renderAriaTree(ariaNode: AriaNode): string { +export function renderAriaTree(ariaNode: AriaNode, options?: { noText?: boolean }): string { const lines: string[] = []; const visit = (ariaNode: AriaNode | string, indent: string) => { if (typeof ariaNode === 'string') { - lines.push(indent + '- text: ' + escapeYamlString(ariaNode)); + if (!options?.noText) + lines.push(indent + '- text: ' + escapeYamlString(ariaNode)); return; } let line = `${indent}- ${ariaNode.role}`; @@ -301,10 +302,12 @@ export function renderAriaTree(ariaNode: AriaNode): string { line += ` [pressed=mixed]`; if (ariaNode.pressed === true) line += ` [pressed]`; + if (ariaNode.selected === true) + line += ` [selected]`; const stringValue = !ariaNode.children.length || (ariaNode.children?.length === 1 && typeof ariaNode.children[0] === 'string'); if (stringValue) { - if (ariaNode.children.length) + if (!options?.noText && ariaNode.children.length) line += ': ' + escapeYamlString(ariaNode.children?.[0] as string); lines.push(line); return; diff --git a/packages/playwright-core/src/utils/sequence.ts b/packages/playwright-core/src/utils/sequence.ts new file mode 100644 index 0000000000..2af5429bd6 --- /dev/null +++ b/packages/playwright-core/src/utils/sequence.ts @@ -0,0 +1,63 @@ +/** + * 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 findRepeatedSubsequences(s: string[]): { sequence: string[]; count: number }[] { + const n = s.length; + const result = []; + let i = 0; + + const arraysEqual = (a1: string[], a2: string[]) => { + if (a1.length !== a2.length) return false; + for (let j = 0; j < a1.length; j++) { + if (a1[j] !== a2[j]) return false; + } + return true; + }; + + while (i < n) { + let maxRepeatCount = 1; + let maxRepeatSubstr = [s[i]]; // Initialize with the element at index i + let maxRepeatLength = 1; + + // Try substrings of length from 1 to the remaining length of the array + for (let p = 1; p <= n - i; p++) { + const substr = s.slice(i, i + p); // Extract substring as array + let k = 1; + + // Count how many times the substring repeats consecutively + while ( + i + p * k <= n && + arraysEqual(s.slice(i + p * (k - 1), i + p * k), substr) + ) { + k += 1; + } + k -= 1; // Adjust k since it increments one extra time in the loop + + // Update the maximal repeating substring if necessary + if (k > 1 && (k * p) > (maxRepeatCount * maxRepeatLength)) { + maxRepeatCount = k; + maxRepeatSubstr = substr; + maxRepeatLength = p; + } + } + + // Record the substring and its count + result.push({ sequence: maxRepeatSubstr, count: maxRepeatCount }); + i += maxRepeatLength * maxRepeatCount; // Move index forward + } + + return result; +} \ No newline at end of file diff --git a/packages/playwright-core/src/utils/stackTrace.ts b/packages/playwright-core/src/utils/stackTrace.ts index 77e1365b3f..6f9a87578b 100644 --- a/packages/playwright-core/src/utils/stackTrace.ts +++ b/packages/playwright-core/src/utils/stackTrace.ts @@ -19,6 +19,7 @@ import { parseStackTraceLine } from '../utilsBundle'; import { isUnderTest } from './'; import type { StackFrame } from '@protocol/channels'; import { colors } from '../utilsBundle'; +import { findRepeatedSubsequences } from './sequence'; export function rewriteErrorMessage(e: E, newMessage: string): E { const lines: string[] = (e.stack?.split('\n') || []).filter(l => l.startsWith(' at ')); @@ -132,9 +133,26 @@ export function splitErrorMessage(message: string): { name: string, message: str export function formatCallLog(log: string[] | undefined): string { if (!log || !log.some(l => !!l)) return ''; + + const lines: string[] = []; + + for (const block of findRepeatedSubsequences(log)) { + for (let i = 0; i < block.sequence.length; i++) { + const line = block.sequence[i]; + const leadingWhitespace = line.match(/^\s*/); + const whitespacePrefix = ' ' + leadingWhitespace?.[0] || ''; + const countPrefix = `${block.count} × `; + if (block.count > 1 && i === 0) + lines.push(whitespacePrefix + countPrefix + line.trim()); + else if (block.count > 1) + lines.push(whitespacePrefix + ' '.repeat(countPrefix.length - 2) + '- ' + line.trim()); + else + lines.push(whitespacePrefix + '- ' + line.trim()); + } + } return ` Call log: - ${colors.dim('- ' + (log || []).join('\n - '))} +${colors.dim(lines.join('\n'))} `; } diff --git a/packages/trace-viewer/src/ui/uiModeTestListView.tsx b/packages/trace-viewer/src/ui/uiModeTestListView.tsx index e0ef2a8bca..a6cb82fb8a 100644 --- a/packages/trace-viewer/src/ui/uiModeTestListView.tsx +++ b/packages/trace-viewer/src/ui/uiModeTestListView.tsx @@ -159,12 +159,15 @@ export const TestListView: React.FC<{ rootItem={testTree.rootItem} dataTestId='test-tree' render={treeItem => { - return
-
+ const prefixId = treeItem.id.replace(/[^\w\d-_]/g, '-'); + const labelId = prefixId + '-label'; + const timeId = prefixId + '-time'; + return
+
{treeItem.title} {treeItem.kind === 'case' ? treeItem.tags.map(tag => handleTagClick(e, tag)} />) : null}
- {!!treeItem.duration && treeItem.status !== 'skipped' &&
{msToString(treeItem.duration)}
} + {!!treeItem.duration && treeItem.status !== 'skipped' &&
{msToString(treeItem.duration)}
} runTreeItem(treeItem)} disabled={!!runningState && !runningState.completed}> diff --git a/packages/web/src/components/treeView.tsx b/packages/web/src/components/treeView.tsx index 9af8609f3b..cb7ab7150d 100644 --- a/packages/web/src/components/treeView.tsx +++ b/packages/web/src/components/treeView.tsx @@ -249,8 +249,9 @@ export function TreeItemHeader({ const rendered = render(item); const children = expanded && item.children.length ? item.children as T[] : []; const titled = title?.(item); + const iconed = icon?.(item) || 'codicon-blank'; - return
+ return
onAccepted?.(item)} className={clsx( @@ -277,10 +278,10 @@ export function TreeItemHeader({ toggleExpanded(item); }} /> - {icon && } + {icon &&
} {typeof rendered === 'string' ?
{rendered}
: rendered}
- {!!children.length &&
+ {!!children.length &&
{children.map(child => { const itemData = treeItems.get(child); return itemData && { + const input = []; + const expectedOutput = []; + expect(findRepeatedSubsequences(input)).toEqual(expectedOutput); +}); + +it('should handle a single-element array', () => { + const input = ['a']; + const expectedOutput = [{ sequence: ['a'], count: 1 }]; + expect(findRepeatedSubsequences(input)).toEqual(expectedOutput); +}); + +it('should handle an array with no repeats', () => { + const input = ['a', 'b', 'c']; + const expectedOutput = [ + { sequence: ['a'], count: 1 }, + { sequence: ['b'], count: 1 }, + { sequence: ['c'], count: 1 }, + ]; + expect(findRepeatedSubsequences(input)).toEqual(expectedOutput); +}); + +it('should handle contiguous repeats of single elements', () => { + const input = ['a', 'a', 'a', 'b', 'b', 'c']; + const expectedOutput = [ + { sequence: ['a'], count: 3 }, + { sequence: ['b'], count: 2 }, + { sequence: ['c'], count: 1 }, + ]; + expect(findRepeatedSubsequences(input)).toEqual(expectedOutput); +}); + +it('should detect longer repeating substrings', () => { + const input = ['a', 'b', 'a', 'b', 'a', 'b']; + const expectedOutput = [{ sequence: ['a', 'b'], count: 3 }]; + expect(findRepeatedSubsequences(input)).toEqual(expectedOutput); +}); + +it('should handle multiple repeating substrings', () => { + const input = ['a', 'a', 'b', 'b', 'a', 'a', 'b', 'b']; + const expectedOutput = [ + { sequence: ['a', 'a', 'b', 'b'], count: 2 }, + ]; + expect(findRepeatedSubsequences(input)).toEqual(expectedOutput); +}); + +it('should handle complex cases with overlapping repeats', () => { + const input = ['a', 'a', 'a', 'a']; + const expectedOutput = [{ sequence: ['a'], count: 4 }]; + expect(findRepeatedSubsequences(input)).toEqual(expectedOutput); +}); + +it('should handle complex acceptance cases with multiple possible repeats', () => { + const input = ['a', 'a', 'b', 'b', 'a', 'a', 'b', 'b', 'c', 'c', 'c', 'c']; + const expectedOutput = [ + { sequence: ['a', 'a', 'b', 'b'], count: 2 }, + { sequence: ['c'], count: 4 }, + ]; + expect(findRepeatedSubsequences(input)).toEqual(expectedOutput); +}); + +it('should handle non-repeating sequences correctly', () => { + const input = ['a', 'b', 'c', 'd', 'e']; + const expectedOutput = [ + { sequence: ['a'], count: 1 }, + { sequence: ['b'], count: 1 }, + { sequence: ['c'], count: 1 }, + { sequence: ['d'], count: 1 }, + { sequence: ['e'], count: 1 }, + ]; + expect(findRepeatedSubsequences(input)).toEqual(expectedOutput); +}); + +it('should handle a case where the entire array is a repeating sequence', () => { + const input = ['x', 'y', 'x', 'y', 'x', 'y']; + const expectedOutput = [{ sequence: ['x', 'y'], count: 3 }]; + expect(findRepeatedSubsequences(input)).toEqual(expectedOutput); +}); + +it('should correctly identify the maximal repeating substring', () => { + const input = ['a', 'b', 'a', 'b', 'a', 'b', 'c', 'c', 'c', 'c']; + const expectedOutput = [ + { sequence: ['a', 'b'], count: 3 }, + { sequence: ['c'], count: 4 }, + ]; + expect(findRepeatedSubsequences(input)).toEqual(expectedOutput); +}); + +it('should handle repeats with varying lengths', () => { + const input = ['a', 'a', 'b', 'b', 'b', 'b', 'a', 'a']; + const expectedOutput = [ + { sequence: ['a'], count: 2 }, + { sequence: ['b'], count: 4 }, + { sequence: ['a'], count: 2 }, + ]; + expect(findRepeatedSubsequences(input)).toEqual(expectedOutput); +}); + +it('should correctly handle a repeat count of one (k adjustment to zero)', () => { + const input = ['a', 'b', 'a', 'b', 'c']; + const expectedOutput = [ + { sequence: ['a', 'b'], count: 2 }, + { sequence: ['c'], count: 1 }, + ]; + expect(findRepeatedSubsequences(input)).toEqual(expectedOutput); +}); + +it('should correctly handle repeats at the end of the array', () => { + const input = ['x', 'y', 'x', 'y', 'x', 'y', 'z']; + const expectedOutput = [ + { sequence: ['x', 'y'], count: 3 }, + { sequence: ['z'], count: 1 }, + ]; + expect(findRepeatedSubsequences(input)).toEqual(expectedOutput); +}); + +it('should not overcount repeats when the last potential repeat is incomplete', () => { + const input = ['m', 'n', 'm', 'n', 'm']; + const expectedOutput = [ + { sequence: ['m', 'n'], count: 2 }, + { sequence: ['m'], count: 1 }, + ]; + expect(findRepeatedSubsequences(input)).toEqual(expectedOutput); +}); + +it('should handle single repeats correctly when the substring length is greater than one', () => { + const input = ['a', 'b', 'c', 'a', 'b', 'd']; + const expectedOutput = [ + { sequence: ['a'], count: 1 }, + { sequence: ['b'], count: 1 }, + { sequence: ['c'], count: 1 }, + { sequence: ['a'], count: 1 }, + { sequence: ['b'], count: 1 }, + { sequence: ['d'], count: 1 }, + ]; + expect(findRepeatedSubsequences(input)).toEqual(expectedOutput); +}); diff --git a/tests/playwright-test/stable-test-runner/package-lock.json b/tests/playwright-test/stable-test-runner/package-lock.json index df6792d59d..1ebdfb52cc 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-17" + "@playwright/test": "1.49.0-alpha-2024-10-20" } }, "node_modules/@playwright/test": { - "version": "1.49.0-alpha-2024-10-17", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0-alpha-2024-10-17.tgz", - "integrity": "sha512-HLZY3sM6xt9Wi8K09zPwjJQtcUBZNBcNSIVoMZhtJM3+TikCKx4SiJ3P8vbSlk7Tm3s2oqlS+wA181IxhbTGBA==", + "version": "1.49.0-alpha-2024-10-20", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0-alpha-2024-10-20.tgz", + "integrity": "sha512-lSagJ8KSD636T/TNfSJRh+vuBBssCL5xJgYmsvsF37cDMATTdVf2OVozVK91V9MAL7CxP4F5sQFVq/8rqu23WA==", "dependencies": { - "playwright": "1.49.0-alpha-2024-10-17" + "playwright": "1.49.0-alpha-2024-10-20" }, "bin": { "playwright": "cli.js" @@ -36,11 +36,11 @@ } }, "node_modules/playwright": { - "version": "1.49.0-alpha-2024-10-17", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0-alpha-2024-10-17.tgz", - "integrity": "sha512-IgcLunnpocVS/AEq2lcftVOu0DGQzFm1Qt25SCJsrVvKVe83ElKXZYskPz7yA0HeuOVxQyN69EDWI09ph7lfoQ==", + "version": "1.49.0-alpha-2024-10-20", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0-alpha-2024-10-20.tgz", + "integrity": "sha512-lkZXCaLoVKaa3eVu8qJJiLym6SkjXD+ilE4XZJx3AIE0o4vqMEYVB8tjLzAcl4UZx8wVcCps/WcCvTWhOSIXRA==", "dependencies": { - "playwright-core": "1.49.0-alpha-2024-10-17" + "playwright-core": "1.49.0-alpha-2024-10-20" }, "bin": { "playwright": "cli.js" @@ -53,9 +53,9 @@ } }, "node_modules/playwright-core": { - "version": "1.49.0-alpha-2024-10-17", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0-alpha-2024-10-17.tgz", - "integrity": "sha512-XLTKmPBm2ZIOXBckXtiimSOIjQsYy8MqEP9CsHSgytsP0E+j/44v1BuwHOOMaG8sfjcuZLZ1QdFidnl07A9wSg==", + "version": "1.49.0-alpha-2024-10-20", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0-alpha-2024-10-20.tgz", + "integrity": "sha512-TeQNA7vsGVrHaArr+giPyiWPAV27+wIcuMLrAJXzUB0leVA9bkXbNQ5lA5+G4OhqlmYAbMOpJMtN+TREDv4nXA==", "bin": { "playwright-core": "cli.js" }, @@ -66,11 +66,11 @@ }, "dependencies": { "@playwright/test": { - "version": "1.49.0-alpha-2024-10-17", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0-alpha-2024-10-17.tgz", - "integrity": "sha512-HLZY3sM6xt9Wi8K09zPwjJQtcUBZNBcNSIVoMZhtJM3+TikCKx4SiJ3P8vbSlk7Tm3s2oqlS+wA181IxhbTGBA==", + "version": "1.49.0-alpha-2024-10-20", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0-alpha-2024-10-20.tgz", + "integrity": "sha512-lSagJ8KSD636T/TNfSJRh+vuBBssCL5xJgYmsvsF37cDMATTdVf2OVozVK91V9MAL7CxP4F5sQFVq/8rqu23WA==", "requires": { - "playwright": "1.49.0-alpha-2024-10-17" + "playwright": "1.49.0-alpha-2024-10-20" } }, "fsevents": { @@ -80,18 +80,18 @@ "optional": true }, "playwright": { - "version": "1.49.0-alpha-2024-10-17", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0-alpha-2024-10-17.tgz", - "integrity": "sha512-IgcLunnpocVS/AEq2lcftVOu0DGQzFm1Qt25SCJsrVvKVe83ElKXZYskPz7yA0HeuOVxQyN69EDWI09ph7lfoQ==", + "version": "1.49.0-alpha-2024-10-20", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0-alpha-2024-10-20.tgz", + "integrity": "sha512-lkZXCaLoVKaa3eVu8qJJiLym6SkjXD+ilE4XZJx3AIE0o4vqMEYVB8tjLzAcl4UZx8wVcCps/WcCvTWhOSIXRA==", "requires": { "fsevents": "2.3.2", - "playwright-core": "1.49.0-alpha-2024-10-17" + "playwright-core": "1.49.0-alpha-2024-10-20" } }, "playwright-core": { - "version": "1.49.0-alpha-2024-10-17", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0-alpha-2024-10-17.tgz", - "integrity": "sha512-XLTKmPBm2ZIOXBckXtiimSOIjQsYy8MqEP9CsHSgytsP0E+j/44v1BuwHOOMaG8sfjcuZLZ1QdFidnl07A9wSg==" + "version": "1.49.0-alpha-2024-10-20", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0-alpha-2024-10-20.tgz", + "integrity": "sha512-TeQNA7vsGVrHaArr+giPyiWPAV27+wIcuMLrAJXzUB0leVA9bkXbNQ5lA5+G4OhqlmYAbMOpJMtN+TREDv4nXA==" } } } diff --git a/tests/playwright-test/stable-test-runner/package.json b/tests/playwright-test/stable-test-runner/package.json index 14625ebe6d..dbe21acd15 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-17" + "@playwright/test": "1.49.0-alpha-2024-10-20" } } diff --git a/tests/playwright-test/ui-mode-test-run.spec.ts b/tests/playwright-test/ui-mode-test-run.spec.ts index f60a6dde86..3673faab45 100644 --- a/tests/playwright-test/ui-mode-test-run.spec.ts +++ b/tests/playwright-test/ui-mode-test-run.spec.ts @@ -61,22 +61,25 @@ test('should run visible', async ({ runUITest }) => { ⊘ skipped `); - // await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` - // - tree: - // - treeitem "a.test.ts" [expanded]: - // - treeitem "passes" - // - treeitem "fails" [selected]: - // - button "Run" - // - button "Show source" - // - button "Watch" - // - treeitem "suite" - // - treeitem "b.test.ts" [expanded]: - // - treeitem "passes" - // - treeitem "fails" - // - treeitem "c.test.ts" [expanded]: - // - treeitem "passes" - // - treeitem "skipped" - // `); + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-error] a.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] passes \d+ms/} + - treeitem ${/\[icon-error\] fails \d+ms/} [selected]: + - button "Run" + - button "Show source" + - button "Watch" + - treeitem "[icon-error] suite" + - treeitem "[icon-error] b.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] passes \d+ms/} + - treeitem ${/\[icon-error\] fails \d+ms/} + - treeitem "[icon-check] c.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] passes \d+ms/} + - treeitem "[icon-circle-slash] skipped" + `); await expect(page.getByTestId('status-line')).toHaveText('4/8 passed (50%)'); }); @@ -117,6 +120,17 @@ test('should run on hover', async ({ runUITest }) => { ✅ passes <= ◯ fails `); + + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-circle-outline] a.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] passes \d+ms/}: + - button "Run" + - button "Show source" + - button "Watch" + - treeitem "[icon-circle-outline] fails" + `); }); test('should run on double click', async ({ runUITest }) => { @@ -135,6 +149,17 @@ test('should run on double click', async ({ runUITest }) => { ✅ passes <= ◯ fails `); + + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-circle-outline] a.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] passes/} [selected]: + - button "Run" + - button "Show source" + - button "Watch" + - treeitem "[icon-circle-outline] fails" + `); }); test('should run on Enter', async ({ runUITest }) => { @@ -154,6 +179,17 @@ test('should run on Enter', async ({ runUITest }) => { ◯ passes ❌ fails <= `); + + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-error] a.test.ts" [expanded]: + - group: + - treeitem "[icon-circle-outline] passes" + - treeitem ${/\[icon-error\] fails \d+ms/} [selected]: + - button "Run" + - button "Show source" + - button "Watch" + `); }); test('should run by project', async ({ runUITest }) => { @@ -185,6 +221,26 @@ test('should run by project', async ({ runUITest }) => { ⊘ skipped `); + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-error] a.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] passes \d+ms/} + - treeitem ${/\[icon-error\] fails \d+ms/} [selected]: + - button "Run" + - button "Show source" + - button "Watch" + - treeitem "[icon-error] suite" + - treeitem "[icon-error] b.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] passes \d+ms/} + - treeitem ${/\[icon-error\] fails \d+ms/} + - treeitem "[icon-check] c.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] passes \d+ms/} + - treeitem "[icon-circle-slash] skipped" + `); + await page.getByText('Status:').click(); await page.getByLabel('bar').setChecked(true); @@ -203,6 +259,29 @@ test('should run by project', async ({ runUITest }) => { ► ◯ skipped `); + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-error] a.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-circle-outline\] passes/} + - treeitem ${/\[icon-error\] fails/}: + - group: + - treeitem ${/\[icon-error\] foo/} [selected]: + - button "Run" + - button "Show source" + - button "Watch" + - treeitem "[icon-circle-outline] bar" + - treeitem "[icon-error] suite" + - treeitem "[icon-error] b.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-circle-outline\] passes/} + - treeitem ${/\[icon-error\] fails/} + - treeitem "[icon-circle-outline] c.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-circle-outline\] passes/} + - treeitem ${/\[icon-circle-outline\] skipped/} + `); + await page.getByText('Status:').click(); await page.getByTestId('test-tree').getByText('passes').first().click(); @@ -216,6 +295,20 @@ test('should run by project', async ({ runUITest }) => { ► ❌ fails `); + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-error] a.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-circle-outline\] passes \d+ms/} [expanded] [selected]: + - button "Run" + - button "Show source" + - button "Watch" + - group: + - treeitem ${/\[icon-check\] foo \d+ms/} + - treeitem ${/\[icon-circle-outline\] bar/} + - treeitem ${/\[icon-error\] fails \d+ms/} + `); + await expect(page.getByText('Projects: foo bar')).toBeVisible(); await page.getByTitle('Run all').click(); @@ -235,6 +328,32 @@ test('should run by project', async ({ runUITest }) => { ► ✅ passes ► ⊘ skipped `); + + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-error] a.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] passes \d+ms/} [expanded]: + - group: + - treeitem ${/\[icon-check\] foo \d+ms/} + - treeitem ${/\[icon-check\] bar \d+ms/} + - treeitem ${/\[icon-error\] fails \d+ms/} [expanded]: + - group: + - treeitem ${/\[icon-error\] foo \d+ms/} [selected]: + - button "Run" + - button "Show source" + - button "Watch" + - treeitem ${/\[icon-error\] bar \d+ms/} + - treeitem ${/\[icon-error\] suite/} + - treeitem "[icon-error] b.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] passes/} + - treeitem ${/\[icon-error\] fails/} + - treeitem "[icon-check] c.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] passes/} + - treeitem ${/\[icon-circle-slash\] skipped/} + `); }); test('should stop', async ({ runUITest }) => { @@ -261,6 +380,16 @@ test('should stop', async ({ runUITest }) => { 🕦 test 3 `); + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-loading] a.test.ts" [expanded]: + - group: + - treeitem "[icon-circle-slash] test 0" + - treeitem ${/\[icon-check\] test 1 \d+ms/} + - treeitem ${/\[icon-loading\] test 2/} + - treeitem ${/\[icon-clock\] test 3/} + `); + await expect(page.getByTitle('Run all')).toBeDisabled(); await expect(page.getByTitle('Stop')).toBeEnabled(); @@ -273,6 +402,16 @@ test('should stop', async ({ runUITest }) => { ◯ test 2 ◯ test 3 `); + + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-circle-outline] a.test.ts" [expanded]: + - group: + - treeitem "[icon-circle-slash] test 0" + - treeitem ${/\[icon-check\] test 1 \d+ms/} + - treeitem ${/\[icon-circle-outline\] test 2/} + - treeitem ${/\[icon-circle-outline\] test 3/} + `); }); test('should run folder', async ({ runUITest }) => { @@ -301,6 +440,17 @@ test('should run folder', async ({ runUITest }) => { ▼ ◯ in-a.test.ts ◯ passes `); + + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-check] folder-b" [expanded] [selected]: + - group: + - treeitem "[icon-check] folder-c" + - treeitem "[icon-check] in-b.test.ts" + - treeitem "[icon-circle-outline] in-a.test.ts" [expanded]: + - group: + - treeitem "[icon-circle-outline] passes" + `); }); test('should show time', async ({ runUITest }) => { @@ -324,6 +474,26 @@ test('should show time', async ({ runUITest }) => { ⊘ skipped `); + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-error] a.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] passes \d+ms/} + - treeitem ${/\[icon-error\] fails \d+ms/} [selected]: + - button "Run" + - button "Show source" + - button "Watch" + - treeitem "[icon-error] suite" + - treeitem "[icon-error] b.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] passes \d+ms/} + - treeitem ${/\[icon-error\] fails \d+ms/} + - treeitem "[icon-check] c.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] passes \d+ms/} + - treeitem "[icon-circle-slash] skipped" + `); + await expect(page.getByTestId('status-line')).toHaveText('4/8 passed (50%)'); }); @@ -348,6 +518,13 @@ test('should show test.fail as passing', async ({ runUITest }) => { ✅ should fail XXms `); + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-check] a.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] should fail \d+ms/} + `); + await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)'); }); @@ -377,6 +554,13 @@ test('should ignore repeatEach', async ({ runUITest }) => { ✅ should pass `); + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-check] a.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] should pass \d+ms/} + `); + await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)'); }); @@ -404,6 +588,14 @@ test('should remove output folder before test run', async ({ runUITest }) => { ▼ ✅ a.test.ts ✅ should pass `); + + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-check] a.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] should pass \d+ms/} + `); + await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)'); await page.getByTitle('Run all').click(); @@ -411,6 +603,14 @@ test('should remove output folder before test run', async ({ runUITest }) => { ▼ ✅ a.test.ts ✅ should pass `); + + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-check] a.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] should pass \d+ms/} + `); + await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)'); }); @@ -451,6 +651,18 @@ test('should show proper total when using deps', async ({ runUITest }) => { ✅ run @setup <= ◯ run @chromium `); + + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-circle-outline] a.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] run @setup setup \d+ms/} [selected]: + - button "Run" + - button "Show source" + - button "Watch" + - treeitem "[icon-circle-outline] run @chromium chromium" + `); + await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)'); await page.getByTitle('run @chromium').dblclick(); @@ -459,6 +671,18 @@ test('should show proper total when using deps', async ({ runUITest }) => { ✅ run @setup ✅ run @chromium <= `); + + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-check] a.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] run @setup setup \d+ms/} + - treeitem ${/\[icon-check\] run @chromium chromium \d+ms/} [selected]: + - button "Run" + - button "Show source" + - button "Watch" + `); + await expect(page.getByTestId('status-line')).toHaveText('2/2 passed (100%)'); }); @@ -518,6 +742,13 @@ test('should respect --tsconfig option', { ✅ test `); + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-check] a.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] test \d+ms/} + `); + await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)'); }); @@ -539,4 +770,11 @@ test('should respect --ignore-snapshots option', { ▼ ✅ a.test.ts ✅ snapshot `); + + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-check] a.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] snapshot \d+ms/} + `); });