Skip to content

Commit be969cf

Browse files
authoredNov 26, 2024··
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

+24
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
+162-33
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

-130
This file was deleted.

‎packages/vitest/src/node/reporters/renderers/windowedRenderer.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ export class WindowRenderer {
7777
clearInterval(this.renderInterval)
7878
}
7979

80+
getColumns() {
81+
return 'columns' in this.options.logger.outputStream ? this.options.logger.outputStream.columns : 80
82+
}
83+
8084
private flushBuffer() {
8185
if (this.buffer.length === 0) {
8286
return this.render()
@@ -112,11 +116,11 @@ export class WindowRenderer {
112116
}
113117

114118
const windowContent = this.options.getWindow()
115-
const rowCount = getRenderedRowCount(windowContent, this.options.logger.outputStream)
119+
const rowCount = getRenderedRowCount(windowContent, this.getColumns())
116120
let padding = this.windowHeight - rowCount
117121

118122
if (padding > 0 && message) {
119-
padding -= getRenderedRowCount([message], this.options.logger.outputStream)
123+
padding -= getRenderedRowCount([message], this.getColumns())
120124
}
121125

122126
this.write(SYNC_START)
@@ -203,9 +207,8 @@ export class WindowRenderer {
203207
}
204208

205209
/** Calculate the actual row count needed to render `rows` into `stream` */
206-
function getRenderedRowCount(rows: string[], stream: Options['logger']['outputStream']) {
210+
function getRenderedRowCount(rows: string[], columns: number) {
207211
let count = 0
208-
const columns = 'columns' in stream ? stream.columns : 80
209212

210213
for (const row of rows) {
211214
const text = stripVTControlCharacters(row)

‎test/reporters/tests/dot.test.ts

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { describe, expect, test } from 'vitest'
2+
import { runVitest } from '../../test-utils'
3+
4+
describe('{ isTTY: true }', () => {
5+
const isTTY = true
6+
7+
test('renders successful tests', async () => {
8+
const { stdout, stderr } = await runVitest({
9+
include: ['./fixtures/ok.test.ts'],
10+
reporters: [['dot', { isTTY }]],
11+
typecheck: undefined,
12+
})
13+
14+
expect(stdout).toContain('\n·\n')
15+
expect(stdout).toContain('Test Files 1 passed (1)')
16+
17+
expect(stderr).toBe('')
18+
})
19+
20+
test('renders failing tests', async () => {
21+
const { stdout, stderr } = await runVitest({
22+
include: ['./fixtures/some-failing.test.ts'],
23+
reporters: [['dot', { isTTY }]],
24+
typecheck: undefined,
25+
})
26+
27+
expect(stdout).toContain('\n·x\n')
28+
expect(stdout).toContain('Test Files 1 failed (1)')
29+
expect(stdout).toContain('Tests 1 failed | 1 passed')
30+
31+
expect(stderr).toContain('AssertionError: expected 6 to be 7 // Object.is equality')
32+
})
33+
34+
test('renders skipped tests', async () => {
35+
const { stdout, stderr } = await runVitest({
36+
include: ['./fixtures/all-skipped.test.ts'],
37+
reporters: [['dot', { isTTY }]],
38+
typecheck: undefined,
39+
})
40+
41+
expect(stdout).toContain('\n--\n')
42+
expect(stdout).toContain('Test Files 1 skipped (1)')
43+
expect(stdout).toContain('Tests 1 skipped | 1 todo')
44+
45+
expect(stderr).toContain('')
46+
})
47+
})
48+
49+
describe('{ isTTY: false }', () => {
50+
const isTTY = false
51+
52+
test('renders successful tests', async () => {
53+
const { stdout, stderr } = await runVitest({
54+
include: ['./fixtures/ok.test.ts'],
55+
reporters: [['dot', { isTTY }]],
56+
typecheck: undefined,
57+
})
58+
59+
expect(stdout).toContain('✓ fixtures/ok.test.ts')
60+
expect(stdout).toContain('Test Files 1 passed (1)')
61+
62+
expect(stderr).toBe('')
63+
})
64+
65+
test('renders failing tests', async () => {
66+
const { stdout, stderr } = await runVitest({
67+
include: ['./fixtures/some-failing.test.ts'],
68+
reporters: [['dot', { isTTY }]],
69+
typecheck: undefined,
70+
})
71+
72+
expect(stdout).toContain('❯ fixtures/some-failing.test.ts (2 tests | 1 failed)')
73+
expect(stdout).toContain('✓ 2 + 3 = 5')
74+
expect(stdout).toContain('× 3 + 3 = 7')
75+
76+
expect(stdout).toContain('Test Files 1 failed (1)')
77+
expect(stdout).toContain('Tests 1 failed | 1 passed')
78+
79+
expect(stderr).toContain('AssertionError: expected 6 to be 7 // Object.is equality')
80+
})
81+
82+
test('renders skipped tests', async () => {
83+
const { stdout, stderr } = await runVitest({
84+
include: ['./fixtures/all-skipped.test.ts'],
85+
reporters: [['dot', { isTTY }]],
86+
typecheck: undefined,
87+
})
88+
89+
expect(stdout).toContain('↓ fixtures/all-skipped.test.ts (2 tests | 2 skipped)')
90+
expect(stdout).toContain('Test Files 1 skipped (1)')
91+
expect(stdout).toContain('Tests 1 skipped | 1 todo')
92+
93+
expect(stderr).toContain('')
94+
})
95+
})

0 commit comments

Comments
 (0)
Please sign in to comment.