Skip to content

Commit 027ce9b

Browse files
authored
fix(reporters): render tasks in tree when in TTY (#7503)
1 parent dd6d685 commit 027ce9b

File tree

6 files changed

+278
-45
lines changed

6 files changed

+278
-45
lines changed

packages/vitest/src/node/reporters/base.ts

Lines changed: 64 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,12 @@ export abstract class BaseReporter implements Reporter {
8888
return
8989
}
9090

91-
const tests = getTests(task)
92-
const failed = tests.filter(t => t.result?.state === 'fail')
93-
const skipped = tests.filter(t => t.mode === 'skip' || t.mode === 'todo')
91+
const suites = getSuites(task)
92+
const allTests = getTests(task)
93+
const failed = allTests.filter(t => t.result?.state === 'fail')
94+
const skipped = allTests.filter(t => t.mode === 'skip' || t.mode === 'todo')
9495

95-
let state = c.dim(`${tests.length} test${tests.length > 1 ? 's' : ''}`)
96+
let state = c.dim(`${allTests.length} test${allTests.length > 1 ? 's' : ''}`)
9697

9798
if (failed.length) {
9899
state += c.dim(' | ') + c.red(`${failed.length} failed`)
@@ -120,52 +121,79 @@ export abstract class BaseReporter implements Reporter {
120121

121122
this.log(` ${title} ${task.name} ${suffix}`)
122123

123-
const anyFailed = tests.some(test => test.result?.state === 'fail')
124+
for (const suite of suites) {
125+
const tests = suite.tasks.filter(task => task.type === 'test')
124126

125-
for (const test of tests) {
126-
const { duration, retryCount, repeatCount } = test.result || {}
127-
let suffix = ''
128-
129-
if (retryCount != null && retryCount > 0) {
130-
suffix += c.yellow(` (retry x${retryCount})`)
127+
if (!('filepath' in suite)) {
128+
this.printSuite(suite)
131129
}
132130

133-
if (repeatCount != null && repeatCount > 0) {
134-
suffix += c.yellow(` (repeat x${repeatCount})`)
135-
}
131+
for (const test of tests) {
132+
const { duration, retryCount, repeatCount } = test.result || {}
133+
const padding = this.getTestIndentation(test)
134+
let suffix = ''
136135

137-
if (test.result?.state === 'fail') {
138-
this.log(c.red(` ${taskFail} ${getTestName(test, c.dim(' > '))}${this.getDurationPrefix(test)}`) + suffix)
136+
if (retryCount != null && retryCount > 0) {
137+
suffix += c.yellow(` (retry x${retryCount})`)
138+
}
139+
140+
if (repeatCount != null && repeatCount > 0) {
141+
suffix += c.yellow(` (repeat x${repeatCount})`)
142+
}
143+
144+
if (test.result?.state === 'fail') {
145+
this.log(c.red(` ${padding}${taskFail} ${this.getTestName(test, c.dim(' > '))}${this.getDurationPrefix(test)}`) + suffix)
139146

140-
test.result?.errors?.forEach((e) => {
141147
// print short errors, full errors will be at the end in summary
142-
this.log(c.red(` ${F_RIGHT} ${e?.message}`))
143-
})
144-
}
148+
test.result?.errors?.forEach((error) => {
149+
const message = this.formatShortError(error)
145150

146-
// also print slow tests
147-
else if (duration && duration > this.ctx.config.slowTestThreshold) {
148-
this.log(
149-
` ${c.yellow(c.dim(F_CHECK))} ${getTestName(test, c.dim(' > '))}`
150-
+ ` ${c.yellow(Math.round(duration) + c.dim('ms'))}${suffix}`,
151-
)
152-
}
151+
if (message) {
152+
this.log(c.red(` ${padding}${message}`))
153+
}
154+
})
155+
}
153156

154-
else if (this.ctx.config.hideSkippedTests && (test.mode === 'skip' || test.result?.state === 'skip')) {
155-
// Skipped tests are hidden when --hideSkippedTests
156-
}
157+
// also print slow tests
158+
else if (duration && duration > this.ctx.config.slowTestThreshold) {
159+
this.log(
160+
` ${padding}${c.yellow(c.dim(F_CHECK))} ${this.getTestName(test, c.dim(' > '))}`
161+
+ ` ${c.yellow(Math.round(duration) + c.dim('ms'))}${suffix}`,
162+
)
163+
}
157164

158-
// also print skipped tests that have notes
159-
else if (test.result?.state === 'skip' && test.result.note) {
160-
this.log(` ${getStateSymbol(test)} ${getTestName(test)}${c.dim(c.gray(` [${test.result.note}]`))}`)
161-
}
165+
else if (this.ctx.config.hideSkippedTests && (test.mode === 'skip' || test.result?.state === 'skip')) {
166+
// Skipped tests are hidden when --hideSkippedTests
167+
}
168+
169+
// also print skipped tests that have notes
170+
else if (test.result?.state === 'skip' && test.result.note) {
171+
this.log(` ${padding}${getStateSymbol(test)} ${this.getTestName(test)}${c.dim(c.gray(` [${test.result.note}]`))}`)
172+
}
162173

163-
else if (this.renderSucceed || anyFailed) {
164-
this.log(` ${getStateSymbol(test)} ${getTestName(test, c.dim(' > '))}${suffix}`)
174+
else if (this.renderSucceed || failed.length > 0) {
175+
this.log(` ${padding}${getStateSymbol(test)} ${this.getTestName(test, c.dim(' > '))}${suffix}`)
176+
}
165177
}
166178
}
167179
}
168180

181+
protected printSuite(_task: Task): void {
182+
// Suite name is included in getTestName by default
183+
}
184+
185+
protected getTestName(test: Task, separator?: string): string {
186+
return getTestName(test, separator)
187+
}
188+
189+
protected formatShortError(error: ErrorWithDiff): string {
190+
return `${F_RIGHT} ${error.message}`
191+
}
192+
193+
protected getTestIndentation(_test: Task) {
194+
return ' '
195+
}
196+
169197
private getDurationPrefix(task: Task) {
170198
if (!task.result?.duration) {
171199
return ''

packages/vitest/src/node/reporters/verbose.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Task } from '@vitest/runner'
2-
import { getFullName } from '@vitest/runner/utils'
2+
import { getFullName, getTests } from '@vitest/runner/utils'
33
import c from 'tinyrainbow'
44
import { DefaultReporter } from './default'
55
import { F_RIGHT } from './renderers/figures'
@@ -45,4 +45,33 @@ export class VerboseReporter extends DefaultReporter {
4545
task.result.errors?.forEach(error => this.log(c.red(` ${F_RIGHT} ${error?.message}`)))
4646
}
4747
}
48+
49+
protected printSuite(task: Task): void {
50+
const indentation = ' '.repeat(getIndentation(task))
51+
const tests = getTests(task)
52+
const state = getStateSymbol(task)
53+
54+
this.log(` ${indentation}${state} ${task.name} ${c.dim(`(${tests.length})`)}`)
55+
}
56+
57+
protected getTestName(test: Task): string {
58+
return test.name
59+
}
60+
61+
protected getTestIndentation(test: Task): string {
62+
return ' '.repeat(getIndentation(test))
63+
}
64+
65+
protected formatShortError(): string {
66+
// Short errors are not shown in tree-view
67+
return ''
68+
}
69+
}
70+
71+
function getIndentation(suite: Task, level = 1): number {
72+
if (suite.suite && !('filepath' in suite.suite)) {
73+
return getIndentation(suite.suite, level + 1)
74+
}
75+
76+
return level
4877
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { test, describe, expect } from "vitest";
2+
3+
test("test pass in root", () => {});
4+
5+
test.skip("test skip in root", () => {});
6+
7+
describe("suite in root", () => {
8+
test("test pass in 1. suite #1", () => {});
9+
10+
test("test pass in 1. suite #2", () => {});
11+
12+
describe("suite in suite", () => {
13+
test("test pass in nested suite #1", () => {});
14+
15+
test("test pass in nested suite #2", () => {});
16+
17+
describe("suite in nested suite", () => {
18+
test("test failure in 2x nested suite", () => {
19+
expect("should fail").toBe("as expected");
20+
});
21+
});
22+
});
23+
});
24+
25+
describe.skip("suite skip in root", () => {
26+
test("test 1.3", () => {});
27+
28+
describe("suite in suite", () => {
29+
test("test in nested suite", () => {});
30+
31+
test("test failure in nested suite of skipped suite", () => {
32+
expect("should fail").toBe("but should not run");
33+
});
34+
});
35+
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { test, describe } from "vitest";
2+
3+
test("test 0.1", () => {});
4+
5+
test.skip("test 0.2", () => {});
6+
7+
describe("suite 1.1", () => {
8+
test("test 1.1", () => {});
9+
});

test/reporters/tests/default.test.ts

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { TestSpecification } from 'vitest/node'
12
import { describe, expect, test } from 'vitest'
23
import { runVitest } from '../../test-utils'
34

@@ -7,11 +8,56 @@ describe('default reporter', async () => {
78
include: ['b1.test.ts', 'b2.test.ts'],
89
root: 'fixtures/default',
910
reporters: 'none',
11+
fileParallelism: false,
12+
sequence: {
13+
sequencer: class StableTestFileOrderSorter {
14+
sort(files: TestSpecification[]) {
15+
return files.sort((a, b) => a.moduleId.localeCompare(b.moduleId))
16+
}
17+
18+
shard(files: TestSpecification[]) {
19+
return files
20+
}
21+
},
22+
},
1023
})
1124

12-
expect(stdout).contain('✓ b2 passed > b2 test')
13-
expect(stdout).not.contain('✓ nested b1 test')
14-
expect(stdout).contain('× b1 failed > b failed test')
25+
const rows = stdout.replace(/\d+ms/g, '[...]ms').split('\n')
26+
rows.splice(0, rows.findIndex(row => row.includes('b1.test.ts')))
27+
rows.splice(rows.findIndex(row => row.includes('Test Files')))
28+
29+
expect(rows.join('\n').trim()).toMatchInlineSnapshot(`
30+
"❯ b1.test.ts (13 tests | 1 failed) [...]ms
31+
✓ b1 passed > b1 test
32+
✓ b1 passed > b2 test
33+
✓ b1 passed > b3 test
34+
✓ b1 passed > nested b > nested b1 test
35+
✓ b1 passed > nested b > nested b2 test
36+
✓ b1 passed > nested b > nested b3 test
37+
✓ b1 failed > b1 test
38+
✓ b1 failed > b2 test
39+
✓ b1 failed > b3 test
40+
× b1 failed > b failed test [...]ms
41+
→ expected 1 to be 2 // Object.is equality
42+
✓ b1 failed > nested b > nested b1 test
43+
✓ b1 failed > nested b > nested b2 test
44+
✓ b1 failed > nested b > nested b3 test
45+
❯ b2.test.ts (13 tests | 1 failed) [...]ms
46+
✓ b2 passed > b1 test
47+
✓ b2 passed > b2 test
48+
✓ b2 passed > b3 test
49+
✓ b2 passed > nested b > nested b1 test
50+
✓ b2 passed > nested b > nested b2 test
51+
✓ b2 passed > nested b > nested b3 test
52+
✓ b2 failed > b1 test
53+
✓ b2 failed > b2 test
54+
✓ b2 failed > b3 test
55+
× b2 failed > b failed test [...]ms
56+
→ expected 1 to be 2 // Object.is equality
57+
✓ b2 failed > nested b > nested b1 test
58+
✓ b2 failed > nested b > nested b2 test
59+
✓ b2 failed > nested b > nested b3 test"
60+
`)
1561
})
1662

1763
test('show full test suite when only one file', async () => {

test/reporters/tests/verbose.test.ts

Lines changed: 91 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
1+
import type { TestSpecification } from 'vitest/node'
12
import { expect, test } from 'vitest'
23
import { runVitest } from '../../test-utils'
34

45
test('duration', async () => {
5-
const result = await runVitest({
6+
const { stdout } = await runVitest({
67
root: 'fixtures/duration',
78
reporters: 'verbose',
89
env: { CI: '1' },
910
})
1011

11-
const output = result.stdout.replace(/\d+ms/g, '[...]ms')
12-
expect(output).toContain(`
12+
expect(trimReporterOutput(stdout)).toContain(`
1313
✓ basic.test.ts > fast
14-
✓ basic.test.ts > slow [...]ms
15-
`)
14+
✓ basic.test.ts > slow [...]ms`,
15+
)
1616
})
1717

1818
test('prints error properties', async () => {
@@ -72,3 +72,89 @@ test('prints repeat count', async () => {
7272
expect(stdout).toContain('1 passed')
7373
expect(stdout).toContain('✓ repeat couple of times (repeat x3)')
7474
})
75+
76+
test('renders tree when in TTY', async () => {
77+
const { stdout } = await runVitest({
78+
include: ['fixtures/verbose/*.test.ts'],
79+
reporters: [['verbose', { isTTY: true, summary: false }]],
80+
config: false,
81+
fileParallelism: false,
82+
sequence: {
83+
sequencer: class StableTestFileOrderSorter {
84+
sort(files: TestSpecification[]) {
85+
return files.sort((a, b) => a.moduleId.localeCompare(b.moduleId))
86+
}
87+
88+
shard(files: TestSpecification[]) {
89+
return files
90+
}
91+
},
92+
},
93+
})
94+
95+
expect(trimReporterOutput(stdout)).toMatchInlineSnapshot(`
96+
"❯ fixtures/verbose/example-1.test.ts (10 tests | 1 failed | 4 skipped) [...]ms
97+
✓ test pass in root
98+
↓ test skip in root
99+
❯ suite in root (5)
100+
✓ test pass in 1. suite #1
101+
✓ test pass in 1. suite #2
102+
❯ suite in suite (3)
103+
✓ test pass in nested suite #1
104+
✓ test pass in nested suite #2
105+
❯ suite in nested suite (1)
106+
× test failure in 2x nested suite [...]ms
107+
↓ suite skip in root (3)
108+
↓ test 1.3
109+
↓ suite in suite (2)
110+
↓ test in nested suite
111+
↓ test failure in nested suite of skipped suite
112+
✓ fixtures/verbose/example-2.test.ts (3 tests | 1 skipped) [...]ms
113+
✓ test 0.1
114+
↓ test 0.2
115+
✓ suite 1.1 (1)
116+
✓ test 1.1"
117+
`)
118+
})
119+
120+
test('does not render tree when in non-TTY', async () => {
121+
const { stdout } = await runVitest({
122+
include: ['fixtures/verbose/*.test.ts'],
123+
reporters: [['verbose', { isTTY: false, summary: false }]],
124+
config: false,
125+
fileParallelism: false,
126+
sequence: {
127+
sequencer: class StableTestFileOrderSorter {
128+
sort(files: TestSpecification[]) {
129+
return files.sort((a, b) => a.moduleId.localeCompare(b.moduleId))
130+
}
131+
132+
shard(files: TestSpecification[]) {
133+
return files
134+
}
135+
},
136+
},
137+
})
138+
139+
expect(trimReporterOutput(stdout)).toMatchInlineSnapshot(`
140+
"✓ fixtures/verbose/example-1.test.ts > test pass in root
141+
✓ fixtures/verbose/example-1.test.ts > suite in root > test pass in 1. suite #1
142+
✓ fixtures/verbose/example-1.test.ts > suite in root > test pass in 1. suite #2
143+
✓ fixtures/verbose/example-1.test.ts > suite in root > suite in suite > test pass in nested suite #1
144+
✓ fixtures/verbose/example-1.test.ts > suite in root > suite in suite > test pass in nested suite #2
145+
× fixtures/verbose/example-1.test.ts > suite in root > suite in suite > suite in nested suite > test failure in 2x nested suite
146+
→ expected 'should fail' to be 'as expected' // Object.is equality
147+
✓ fixtures/verbose/example-2.test.ts > test 0.1
148+
✓ fixtures/verbose/example-2.test.ts > suite 1.1 > test 1.1"
149+
`)
150+
})
151+
152+
function trimReporterOutput(report: string) {
153+
const rows = report.replace(/\d+ms/g, '[...]ms').split('\n')
154+
155+
// Trim start and end, capture just rendered tree
156+
rows.splice(0, rows.findIndex(row => row.includes('fixtures/verbose/example-')))
157+
rows.splice(rows.findIndex(row => row.includes('Test Files')))
158+
159+
return rows.join('\n').trim()
160+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy