Skip to content

Commit be969cf

Browse files
authored
fix(reporters): rewrite dot reporter without log-update (#6943)
1 parent 80cde2a commit be969cf

File tree

5 files changed

+288
-167
lines changed

5 files changed

+288
-167
lines changed

packages/vitest/LICENSE.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1245,6 +1245,30 @@ Repository: git://github.com/feross/run-parallel.git
12451245
12461246
---------------------------------------
12471247

1248+
## signal-exit
1249+
License: ISC
1250+
By: Ben Coe
1251+
Repository: https://github.com/tapjs/signal-exit.git
1252+
1253+
> The ISC License
1254+
>
1255+
> Copyright (c) 2015, Contributors
1256+
>
1257+
> Permission to use, copy, modify, and/or distribute this software
1258+
> for any purpose with or without fee is hereby granted, provided
1259+
> that the above copyright notice and this permission notice
1260+
> appear in all copies.
1261+
>
1262+
> THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
1263+
> WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
1264+
> OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE
1265+
> LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES
1266+
> OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
1267+
> WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
1268+
> ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
1269+
1270+
---------------------------------------
1271+
12481272
## sisteransi
12491273
License: MIT
12501274
By: Terkel Gjervig
Lines changed: 162 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,182 @@
1-
import type { UserConsoleLog } from '../../types/general'
1+
import type { Custom, File, TaskResultPack, TaskState, Test } from '@vitest/runner'
2+
import type { Vitest } from '../core'
3+
import { getTests } from '@vitest/runner/utils'
4+
import c from 'tinyrainbow'
25
import { BaseReporter } from './base'
3-
import { createDotRenderer } from './renderers/dotRenderer'
6+
import { WindowRenderer } from './renderers/windowedRenderer'
7+
import { TaskParser } from './task-parser'
8+
9+
interface Icon {
10+
char: string
11+
color: (char: string) => string
12+
}
413

514
export class DotReporter extends BaseReporter {
6-
renderer?: ReturnType<typeof createDotRenderer>
15+
private summary?: DotSummary
716

8-
onTaskUpdate() {}
17+
onInit(ctx: Vitest) {
18+
super.onInit(ctx)
919

10-
onCollected() {
1120
if (this.isTTY) {
12-
const files = this.ctx.state.getFiles(this.watchFilters)
13-
if (!this.renderer) {
14-
this.renderer = createDotRenderer(files, {
15-
logger: this.ctx.logger,
16-
}).start()
17-
}
18-
else {
19-
this.renderer.update(files)
20-
}
21+
this.summary = new DotSummary()
22+
this.summary.onInit(ctx)
23+
}
24+
}
25+
26+
onTaskUpdate(packs: TaskResultPack[]) {
27+
this.summary?.onTaskUpdate(packs)
28+
29+
if (!this.isTTY) {
30+
super.onTaskUpdate(packs)
2131
}
2232
}
2333

24-
async onFinished(
25-
files = this.ctx.state.getFiles(),
26-
errors = this.ctx.state.getUnhandledErrors(),
27-
) {
28-
await this.stopListRender()
29-
this.ctx.logger.log()
34+
onWatcherRerun(files: string[], trigger?: string) {
35+
this.summary?.onWatcherRerun()
36+
super.onWatcherRerun(files, trigger)
37+
}
38+
39+
onFinished(files?: File[], errors?: unknown[]) {
40+
this.summary?.onFinished()
3041
super.onFinished(files, errors)
3142
}
43+
}
44+
45+
class DotSummary extends TaskParser {
46+
private renderer!: WindowRenderer
47+
private tests = new Map<Test['id'], TaskState>()
48+
private finishedTests = new Set<Test['id']>()
49+
50+
onInit(ctx: Vitest): void {
51+
this.ctx = ctx
52+
53+
this.renderer = new WindowRenderer({
54+
logger: ctx.logger,
55+
getWindow: () => this.createSummary(),
56+
})
57+
58+
this.ctx.onClose(() => this.renderer.stop())
59+
}
3260

33-
async onWatcherStart() {
34-
await this.stopListRender()
35-
super.onWatcherStart()
61+
onWatcherRerun() {
62+
this.tests.clear()
63+
this.renderer.start()
3664
}
3765

38-
async stopListRender() {
39-
this.renderer?.stop()
40-
this.renderer = undefined
41-
await new Promise(resolve => setTimeout(resolve, 10))
66+
onFinished() {
67+
const finalLog = formatTests(Array.from(this.tests.values()))
68+
this.ctx.logger.log(finalLog)
69+
70+
this.tests.clear()
71+
this.renderer.finish()
4272
}
4373

44-
async onWatcherRerun(files: string[], trigger?: string) {
45-
await this.stopListRender()
46-
super.onWatcherRerun(files, trigger)
74+
onTestFilePrepare(file: File): void {
75+
for (const test of getTests(file)) {
76+
// Dot reporter marks pending tests as running
77+
this.onTestStart(test)
78+
}
79+
}
80+
81+
onTestStart(test: Test | Custom) {
82+
if (this.finishedTests.has(test.id)) {
83+
return
84+
}
85+
86+
this.tests.set(test.id, test.mode || 'run')
4787
}
4888

49-
onUserConsoleLog(log: UserConsoleLog) {
50-
this.renderer?.clear()
51-
super.onUserConsoleLog(log)
89+
onTestFinished(test: Test | Custom) {
90+
if (this.finishedTests.has(test.id)) {
91+
return
92+
}
93+
94+
this.finishedTests.add(test.id)
95+
this.tests.set(test.id, test.result?.state || 'skip')
5296
}
97+
98+
onTestFileFinished() {
99+
const columns = this.renderer.getColumns()
100+
101+
if (this.tests.size < columns) {
102+
return
103+
}
104+
105+
const finishedTests = Array.from(this.tests).filter(entry => entry[1] !== 'run')
106+
107+
if (finishedTests.length < columns) {
108+
return
109+
}
110+
111+
// Remove finished tests from state and render them in static output
112+
const states: TaskState[] = []
113+
let count = 0
114+
115+
for (const [id, state] of finishedTests) {
116+
if (count++ >= columns) {
117+
break
118+
}
119+
120+
this.tests.delete(id)
121+
states.push(state)
122+
}
123+
124+
this.ctx.logger.log(formatTests(states))
125+
}
126+
127+
private createSummary() {
128+
return [
129+
formatTests(Array.from(this.tests.values())),
130+
'',
131+
]
132+
}
133+
}
134+
135+
// These are compared with reference equality in formatTests
136+
const pass: Icon = { char: '·', color: c.green }
137+
const fail: Icon = { char: 'x', color: c.red }
138+
const pending: Icon = { char: '*', color: c.yellow }
139+
const skip: Icon = { char: '-', color: (char: string) => c.dim(c.gray(char)) }
140+
141+
function getIcon(state: TaskState): Icon {
142+
switch (state) {
143+
case 'pass':
144+
return pass
145+
case 'fail':
146+
return fail
147+
case 'skip':
148+
case 'todo':
149+
return skip
150+
default:
151+
return pending
152+
}
153+
}
154+
155+
/**
156+
* Format test states into string while keeping ANSI escapes at minimal.
157+
* Sibling icons with same color are merged into a single c.color() call.
158+
*/
159+
function formatTests(states: TaskState[]): string {
160+
let currentIcon = pending
161+
let count = 0
162+
let output = ''
163+
164+
for (const state of states) {
165+
const icon = getIcon(state)
166+
167+
if (currentIcon === icon) {
168+
count++
169+
continue
170+
}
171+
172+
output += currentIcon.color(currentIcon.char.repeat(count))
173+
174+
// Start tracking new group
175+
count = 1
176+
currentIcon = icon
177+
}
178+
179+
output += currentIcon.color(currentIcon.char.repeat(count))
180+
181+
return output
53182
}

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

Lines changed: 0 additions & 130 deletions
This file was deleted.

0 commit comments

Comments
 (0)