Skip to content

Commit 91ba6f9

Browse files
authored
fix(vitest): show all failed tests when rerunning a test (#6022)
1 parent a820b15 commit 91ba6f9

File tree

14 files changed

+194
-63
lines changed

14 files changed

+194
-63
lines changed

packages/vitest/src/api/setup.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ export class WebSocketReporter implements Reporter {
201201
})
202202
}
203203

204-
onFinished(files?: File[], errors?: unknown[]) {
204+
onFinished(files: File[], errors: unknown[]) {
205205
this.clients.forEach((client) => {
206206
client.onFinished?.(files, errors)?.catch?.(noop)
207207
})

packages/vitest/src/node/pools/typecheck.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export function createTypecheckPool(ctx: Vitest): ProcessPool {
4040

4141
// triggered by TSC watcher, not Vitest watcher, so we need to emulate what Vitest does in this case
4242
if (ctx.config.watch && !ctx.runningPromise) {
43-
await ctx.report('onFinished', files)
43+
await ctx.report('onFinished', files, [])
4444
await ctx.report('onWatcherStart', files, [
4545
...(project.config.typecheck.ignoreSourceErrors ? [] : sourceErrors),
4646
...ctx.state.getUnhandledErrors(),

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

+71-50
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
getFullName,
1515
getSafeTimers,
1616
getSuites,
17+
getTestName,
1718
getTests,
1819
hasFailed,
1920
hasFailedSnapshot,
@@ -33,8 +34,8 @@ import {
3334
formatTimeString,
3435
getStateString,
3536
getStateSymbol,
36-
pointer,
3737
renderSnapshotSummary,
38+
taskFail,
3839
} from './renderers/utils'
3940

4041
const BADGE_PADDING = ' '
@@ -63,6 +64,7 @@ export abstract class BaseReporter implements Reporter {
6364
start = 0
6465
end = 0
6566
watchFilters?: string[]
67+
failedUnwatchedFiles: Task[] = []
6668
isTTY: boolean
6769
ctx: Vitest = undefined!
6870

@@ -115,59 +117,65 @@ export abstract class BaseReporter implements Reporter {
115117
if (this.isTTY) {
116118
return
117119
}
118-
const logger = this.ctx.logger
119120
for (const pack of packs) {
120121
const task = this.ctx.state.idMap.get(pack[0])
121-
if (
122-
task
123-
&& 'filepath' in task
124-
&& task.result?.state
125-
&& task.result?.state !== 'run'
126-
) {
127-
const tests = getTests(task)
128-
const failed = tests.filter(t => t.result?.state === 'fail')
129-
const skipped = tests.filter(
130-
t => t.mode === 'skip' || t.mode === 'todo',
131-
)
132-
let state = c.dim(`${tests.length} test${tests.length > 1 ? 's' : ''}`)
133-
if (failed.length) {
134-
state += ` ${c.dim('|')} ${c.red(`${failed.length} failed`)}`
135-
}
136-
if (skipped.length) {
137-
state += ` ${c.dim('|')} ${c.yellow(`${skipped.length} skipped`)}`
138-
}
139-
let suffix = c.dim(' (') + state + c.dim(')')
140-
if (task.result.duration) {
141-
const color
142-
= task.result.duration > this.ctx.config.slowTestThreshold
143-
? c.yellow
144-
: c.gray
145-
suffix += color(` ${Math.round(task.result.duration)}${c.dim('ms')}`)
146-
}
147-
if (this.ctx.config.logHeapUsage && task.result.heap != null) {
148-
suffix += c.magenta(
149-
` ${Math.floor(task.result.heap / 1024 / 1024)} MB heap used`,
150-
)
151-
}
152-
153-
let title = ` ${getStateSymbol(task)} `
154-
if (task.projectName) {
155-
title += formatProjectName(task.projectName)
156-
}
157-
title += `${task.name} ${suffix}`
158-
logger.log(title)
159-
160-
// print short errors, full errors will be at the end in summary
161-
for (const test of failed) {
162-
logger.log(c.red(` ${pointer} ${getFullName(test, c.dim(' > '))}`))
163-
test.result?.errors?.forEach((e) => {
164-
logger.log(c.red(` ${F_RIGHT} ${(e as any)?.message}`))
165-
})
166-
}
122+
if (task) {
123+
this.printTask(task)
167124
}
168125
}
169126
}
170127

128+
protected printTask(task: Task) {
129+
if (
130+
!('filepath' in task)
131+
|| !task.result?.state
132+
|| task.result?.state === 'run') {
133+
return
134+
}
135+
const logger = this.ctx.logger
136+
137+
const tests = getTests(task)
138+
const failed = tests.filter(t => t.result?.state === 'fail')
139+
const skipped = tests.filter(
140+
t => t.mode === 'skip' || t.mode === 'todo',
141+
)
142+
let state = c.dim(`${tests.length} test${tests.length > 1 ? 's' : ''}`)
143+
if (failed.length) {
144+
state += ` ${c.dim('|')} ${c.red(`${failed.length} failed`)}`
145+
}
146+
if (skipped.length) {
147+
state += ` ${c.dim('|')} ${c.yellow(`${skipped.length} skipped`)}`
148+
}
149+
let suffix = c.dim(' (') + state + c.dim(')')
150+
if (task.result.duration) {
151+
const color
152+
= task.result.duration > this.ctx.config.slowTestThreshold
153+
? c.yellow
154+
: c.gray
155+
suffix += color(` ${Math.round(task.result.duration)}${c.dim('ms')}`)
156+
}
157+
if (this.ctx.config.logHeapUsage && task.result.heap != null) {
158+
suffix += c.magenta(
159+
` ${Math.floor(task.result.heap / 1024 / 1024)} MB heap used`,
160+
)
161+
}
162+
163+
let title = ` ${getStateSymbol(task)} `
164+
if (task.projectName) {
165+
title += formatProjectName(task.projectName)
166+
}
167+
title += `${task.name} ${suffix}`
168+
logger.log(title)
169+
170+
// print short errors, full errors will be at the end in summary
171+
for (const test of failed) {
172+
logger.log(c.red(` ${taskFail} ${getTestName(test, c.dim(' > '))}`))
173+
test.result?.errors?.forEach((e) => {
174+
logger.log(c.red(` ${F_RIGHT} ${(e as any)?.message}`))
175+
})
176+
}
177+
}
178+
171179
onWatcherStart(
172180
files = this.ctx.state.getFiles(),
173181
errors = this.ctx.state.getUnhandledErrors(),
@@ -233,6 +241,9 @@ export abstract class BaseReporter implements Reporter {
233241
onWatcherRerun(files: string[], trigger?: string) {
234242
this.resetLastRunLog()
235243
this.watchFilters = files
244+
this.failedUnwatchedFiles = this.ctx.state.getFiles().filter((file) => {
245+
return !files.includes(file.filepath) && hasFailed(file)
246+
})
236247

237248
files.forEach((filepath) => {
238249
let reruns = this._filesInWatchMode.get(filepath) ?? 0
@@ -274,6 +285,12 @@ export abstract class BaseReporter implements Reporter {
274285
)
275286
}
276287

288+
if (!this.isTTY) {
289+
for (const task of this.failedUnwatchedFiles) {
290+
this.printTask(task)
291+
}
292+
}
293+
277294
this._timeStart = new Date()
278295
this.start = performance.now()
279296
}
@@ -375,7 +392,11 @@ export abstract class BaseReporter implements Reporter {
375392
}
376393

377394
reportTestSummary(files: File[], errors: unknown[]) {
378-
const tests = getTests(files)
395+
const affectedFiles = [
396+
...this.failedUnwatchedFiles,
397+
...files,
398+
]
399+
const tests = getTests(affectedFiles)
379400
const logger = this.ctx.logger
380401

381402
const executionTime = this.end - this.start
@@ -437,7 +458,7 @@ export abstract class BaseReporter implements Reporter {
437458
}
438459
}
439460

440-
logger.log(padTitle('Test Files'), getStateString(files))
461+
logger.log(padTitle('Test Files'), getStateString(affectedFiles))
441462
logger.log(padTitle('Tests'), getStateString(tests))
442463
if (this.ctx.projects.some(c => c.config.typecheck.enabled)) {
443464
const failed = tests.filter(

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

+4-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import type { File } from '../../types/tasks'
22
import { BaseReporter } from './base'
33

44
export class BasicReporter extends BaseReporter {
5-
isTTY = false
5+
constructor() {
6+
super()
7+
this.isTTY = false
8+
}
69

710
reportSummary(files: File[], errors: unknown[]) {
811
// non-tty mode doesn't add a new line

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

+10
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,16 @@ export class DefaultReporter extends BaseReporter {
5656
files = this.ctx.state.getFiles(),
5757
errors = this.ctx.state.getUnhandledErrors(),
5858
) {
59+
// print failed tests without their errors to keep track of previously failed tests
60+
// this can happen if there are multiple test errors, and user changed a file
61+
// that triggered a rerun of unrelated tests - in that case they want to see
62+
// the error for the test they are currently working on, but still keep track of
63+
// the other failed tests
64+
this.renderer?.update([
65+
...this.failedUnwatchedFiles,
66+
...files,
67+
])
68+
5969
this.stopListRender()
6070
this.ctx.logger.log()
6171
super.onFinished(files, errors)

packages/vitest/src/node/reporters/renderers/utils.ts

+9-4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ export const hookSpinnerMap = new WeakMap<Task, Map<string, () => string>>()
1919
export const pointer = c.yellow(F_POINTER)
2020
export const skipped = c.dim(c.gray(F_DOWN))
2121

22+
export const benchmarkPass = c.green(F_DOT)
23+
export const testPass = c.green(F_CHECK)
24+
export const taskFail = c.red(F_CROSS)
25+
export const suiteFail = c.red(F_POINTER)
26+
export const pending = c.gray('·')
27+
2228
export function getCols(delta = 0) {
2329
let length = process.stdout?.columns
2430
if (!length || Number.isNaN(length)) {
@@ -154,10 +160,9 @@ export function getStateSymbol(task: Task) {
154160
}
155161

156162
if (!task.result) {
157-
return c.gray('·')
163+
return pending
158164
}
159165

160-
// pending
161166
if (task.result.state === 'run') {
162167
if (task.type === 'suite') {
163168
return pointer
@@ -171,11 +176,11 @@ export function getStateSymbol(task: Task) {
171176
}
172177

173178
if (task.result.state === 'pass') {
174-
return task.meta?.benchmark ? c.green(F_DOT) : c.green(F_CHECK)
179+
return task.meta?.benchmark ? benchmarkPass : testPass
175180
}
176181

177182
if (task.result.state === 'fail') {
178-
return task.type === 'suite' ? pointer : c.red(F_CROSS)
183+
return task.type === 'suite' ? suiteFail : taskFail
179184
}
180185

181186
return ' '

packages/vitest/src/types/reporter.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ export interface Reporter {
88
onSpecsCollected?: (specs?: SerializableSpec[]) => Awaitable<void>
99
onCollected?: (files?: File[]) => Awaitable<void>
1010
onFinished?: (
11-
files?: File[],
12-
errors?: unknown[],
11+
files: File[],
12+
errors: unknown[],
1313
coverage?: unknown
1414
) => Awaitable<void>
1515
onTaskUpdate?: (packs: TaskResultPack[]) => Awaitable<void>

test/core/vite.config.ts

-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ export default defineConfig({
5050
port: 3022,
5151
},
5252
test: {
53-
reporters: ['dot'],
5453
api: {
5554
port: 3023,
5655
},

test/reporters/tests/merge-reports.test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,13 @@ test('merge reports', async () => {
8989
test 1-2
9090
9191
❯ first.test.ts (2 tests | 1 failed) <time>
92-
❯ first.test.ts > test 1-2
92+
× test 1-2
9393
→ expected 1 to be 2 // Object.is equality
9494
stdout | second.test.ts > test 2-1
9595
test 2-1
9696
9797
❯ second.test.ts (3 tests | 1 failed) <time>
98-
❯ second.test.ts > test 2-1
98+
× test 2-1
9999
→ expected 1 to be 2 // Object.is equality
100100
101101
Test Files 2 failed (2)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { expect, it } from 'vitest';
2+
3+
it('works correctly', () => {
4+
console.log('log basic')
5+
expect(1).toBe(1)
6+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { it } from 'vitest';
2+
3+
it('fails', () => {
4+
console.log('log fail')
5+
throw new Error('failed')
6+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { defineConfig } from 'vitest/config'
2+
3+
// Patch stdin on the process so that we can fake it to seem like a real interactive terminal and pass the TTY checks
4+
process.stdin.isTTY = true
5+
process.stdin.setRawMode = () => process.stdin
6+
7+
export default defineConfig({
8+
test: {
9+
watch: true,
10+
},
11+
})

test/watch/fixtures/vitest.config.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { defineConfig } from 'vitest/config'
1+
import { defaultExclude, defineConfig } from 'vitest/config'
22

33
// Patch stdin on the process so that we can fake it to seem like a real interactive terminal and pass the TTY checks
44
process.stdin.isTTY = true
@@ -7,6 +7,10 @@ process.stdin.setRawMode = () => process.stdin
77
export default defineConfig({
88
test: {
99
watch: true,
10+
exclude: [
11+
...defaultExclude,
12+
'**/single-failed/**',
13+
],
1014

1115
// This configuration is edited by tests
1216
reporters: 'verbose',

0 commit comments

Comments
 (0)