Skip to content

Commit

Permalink
feat(reporters): summary option for verbose and default reporters
Browse files Browse the repository at this point in the history
  • Loading branch information
AriPerkkio committed Nov 11, 2024
1 parent 07c19b8 commit 9f5803e
Show file tree
Hide file tree
Showing 15 changed files with 604 additions and 522 deletions.
49 changes: 36 additions & 13 deletions docs/guide/reporters.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,33 +96,54 @@ This example will write separate JSON and XML reports as well as printing a verb

### Default Reporter

By default (i.e. if no reporter is specified), Vitest will display results for each test suite hierarchically as they run, and then collapse after a suite passes. When all tests have finished running, the final terminal output will display a summary of results and details of any failed tests.
By default (i.e. if no reporter is specified), Vitest will display summary of running tests and their status at the bottom. Once a suite passes, its status will be reported on top of the summary.

You can disable the summary by configuring the reporter:

:::code-group
```ts [vitest.config.ts]
export default defineConfig({
test: {
reporters: [
['default', { summary: false }]
]
},
})
```
:::

Example output for tests in progress:

```bash
✓ __tests__/file1.test.ts (2) 725ms
✓ __tests__/file2.test.ts (5) 746ms
✓ second test file (2) 746ms
✓ 1 + 1 should equal 2
✓ 2 - 1 should equal 1
✓ test/example-1.test.ts (5 tests | 1 skipped) 306ms
✓ test/example-2.test.ts (5 tests | 1 skipped) 307ms

❯ test/example-3.test.ts 3/5
❯ test/example-4.test.ts 1/5

Test Files 2 passed (4)
Tests 10 passed | 3 skipped (65)
Start at 11:01:36
Duration 2.00s
```

Final output after tests have finished:

```bash
✓ __tests__/file1.test.ts (2) 725ms
✓ __tests__/file2.test.ts (2) 746ms
✓ test/example-1.test.ts (5 tests | 1 skipped) 306ms
✓ test/example-2.test.ts (5 tests | 1 skipped) 307ms
✓ test/example-3.test.ts (5 tests | 1 skipped) 307ms
✓ test/example-4.test.ts (5 tests | 1 skipped) 307ms

Test Files 2 passed (2)
Tests 4 passed (4)
Test Files 4 passed (4)
Tests 16 passed | 4 skipped (20)
Start at 12:34:32
Duration 1.26s (transform 35ms, setup 1ms, collect 90ms, tests 1.47s, environment 0ms, prepare 267ms)
```

### Basic Reporter

The `basic` reporter displays the test files that have run and a summary of results after the entire suite has finished running. Individual tests are not included in the report unless they fail.
The `basic` reporter is equivalent to `default` reporter without `summary`.

:::code-group
```bash [CLI]
Expand Down Expand Up @@ -151,7 +172,7 @@ Example output using basic reporter:

### Verbose Reporter

Follows the same hierarchical structure as the `default` reporter, but does not collapse sub-trees for passed test suites. The final terminal output displays all tests that have run, including those that have passed.
Verbose reporter is same as `default` reporter, but it also displays each individual test after the suite has finished. Similar to `default` reporter, you can disable the summary by configuring the reporter.

:::code-group
```bash [CLI]
Expand All @@ -161,7 +182,9 @@ npx vitest --reporter=verbose
```ts [vitest.config.ts]
export default defineConfig({
test: {
reporters: ['verbose']
reporters: [
['verbose', { summary: false }]
]
},
})
```
Expand Down
2 changes: 1 addition & 1 deletion packages/vitest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@
"std-env": "^3.7.0",
"tinybench": "^2.9.0",
"tinyexec": "^0.3.1",
"tinypool": "^1.0.1",
"tinypool": "https://pkg.pr.new/tinypool@67f3129",
"tinyrainbow": "^1.2.0",
"vite": "^5.0.0",
"vite-node": "workspace:*",
Expand Down
102 changes: 28 additions & 74 deletions packages/vitest/src/node/reporters/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,9 @@ import c from 'tinyrainbow'
import { isCI, isDeno, isNode } from '../../utils/env'
import { hasFailedSnapshot } from '../../utils/tasks'
import { F_CHECK, F_POINTER, F_RIGHT } from './renderers/figures'
import { countTestErrors, divider, formatProjectName, formatTimeString, getStateString, getStateSymbol, renderSnapshotSummary, taskFail, withLabel } from './renderers/utils'
import { countTestErrors, divider, formatProjectName, formatTime, formatTimeString, getStateString, getStateSymbol, padSummaryTitle, renderSnapshotSummary, taskFail, withLabel } from './renderers/utils'

const BADGE_PADDING = ' '
const LAST_RUN_LOG_TIMEOUT = 1_500

export interface BaseOptions {
isTTY?: boolean
Expand All @@ -27,14 +26,12 @@ export abstract class BaseReporter implements Reporter {
failedUnwatchedFiles: Task[] = []
isTTY: boolean
ctx: Vitest = undefined!
renderSucceed = false

protected verbose = false

private _filesInWatchMode = new Map<string, number>()
private _timeStart = formatTimeString(new Date())
private _lastRunTimeout = 0
private _lastRunTimer: NodeJS.Timeout | undefined
private _lastRunCount = 0

constructor(options: BaseOptions = {}) {
this.isTTY = options.isTTY ?? ((isNode || isDeno) && process.stdout?.isTTY && !isCI)
Expand Down Expand Up @@ -65,9 +62,6 @@ export abstract class BaseReporter implements Reporter {
}

onTaskUpdate(packs: TaskResultPack[]) {
if (this.isTTY) {
return
}
for (const pack of packs) {
const task = this.ctx.state.idMap.get(pack[0])

Expand Down Expand Up @@ -117,6 +111,8 @@ export abstract class BaseReporter implements Reporter {

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

const anyFailed = tests.some(test => test.result?.state === 'fail')

for (const test of tests) {
const duration = test.result?.duration

Expand All @@ -137,6 +133,10 @@ export abstract class BaseReporter implements Reporter {
+ ` ${c.yellow(Math.round(duration) + c.dim('ms'))}`,
)
}

else if (this.renderSucceed || anyFailed) {
this.log(` ${c.green(c.dim(F_CHECK))} ${getTestName(test, c.dim(' > '))}`)
}
}
}

Expand All @@ -153,8 +153,6 @@ export abstract class BaseReporter implements Reporter {
}

onWatcherStart(files = this.ctx.state.getFiles(), errors = this.ctx.state.getUnhandledErrors()) {
this.resetLastRunLog()

const failed = errors.length > 0 || hasFailed(files)

if (failed) {
Expand All @@ -177,38 +175,9 @@ export abstract class BaseReporter implements Reporter {
}

this.log(BADGE_PADDING + hints.join(c.dim(', ')))

if (this._lastRunCount) {
const LAST_RUN_TEXT = `rerun x${this._lastRunCount}`
const LAST_RUN_TEXTS = [
c.blue(LAST_RUN_TEXT),
c.gray(LAST_RUN_TEXT),
c.dim(c.gray(LAST_RUN_TEXT)),
]
this.ctx.logger.logUpdate(BADGE_PADDING + LAST_RUN_TEXTS[0])
this._lastRunTimeout = 0
this._lastRunTimer = setInterval(() => {
this._lastRunTimeout += 1
if (this._lastRunTimeout >= LAST_RUN_TEXTS.length) {
this.resetLastRunLog()
}
else {
this.ctx.logger.logUpdate(
BADGE_PADDING + LAST_RUN_TEXTS[this._lastRunTimeout],
)
}
}, LAST_RUN_LOG_TIMEOUT / LAST_RUN_TEXTS.length)
}
}

private resetLastRunLog() {
clearInterval(this._lastRunTimer)
this._lastRunTimer = undefined
this.ctx.logger.logUpdate.clear()
}

onWatcherRerun(files: string[], trigger?: string) {
this.resetLastRunLog()
this.watchFilters = files
this.failedUnwatchedFiles = this.ctx.state.getFiles().filter(file =>
!files.includes(file.filepath) && hasFailed(file),
Expand All @@ -222,11 +191,7 @@ export abstract class BaseReporter implements Reporter {

let banner = trigger ? c.dim(`${this.relative(trigger)} `) : ''

if (files.length > 1 || !files.length) {
// we need to figure out how to handle rerun all from stdin
this._lastRunCount = 0
}
else if (files.length === 1) {
if (files.length === 1) {
const rerun = this._filesInWatchMode.get(files[0]) ?? 1
banner += c.blue(`x${rerun} `)
}
Expand All @@ -248,10 +213,8 @@ export abstract class BaseReporter implements Reporter {

this.log('')

if (!this.isTTY) {
for (const task of this.failedUnwatchedFiles) {
this.printTask(task)
}
for (const task of this.failedUnwatchedFiles) {
this.printTask(task)
}

this._timeStart = formatTimeString(new Date())
Expand Down Expand Up @@ -351,6 +314,8 @@ export abstract class BaseReporter implements Reporter {
}

reportTestSummary(files: File[], errors: unknown[]) {
this.log()

const affectedFiles = [
...this.failedUnwatchedFiles,
...files,
Expand All @@ -364,21 +329,21 @@ export abstract class BaseReporter implements Reporter {

for (const [index, snapshot] of snapshotOutput.entries()) {
const title = index === 0 ? 'Snapshots' : ''
this.log(`${padTitle(title)} ${snapshot}`)
this.log(`${padSummaryTitle(title)} ${snapshot}`)
}

if (snapshotOutput.length > 1) {
this.log()
}

this.log(padTitle('Test Files'), getStateString(affectedFiles))
this.log(padTitle('Tests'), getStateString(tests))
this.log(padSummaryTitle('Test Files'), getStateString(affectedFiles))
this.log(padSummaryTitle('Tests'), getStateString(tests))

if (this.ctx.projects.some(c => c.config.typecheck.enabled)) {
const failed = tests.filter(t => t.meta?.typecheck && t.result?.errors?.length)

this.log(
padTitle('Type Errors'),
padSummaryTitle('Type Errors'),
failed.length
? c.bold(c.red(`${failed.length} failed`))
: c.dim('no errors'),
Expand All @@ -387,19 +352,19 @@ export abstract class BaseReporter implements Reporter {

if (errors.length) {
this.log(
padTitle('Errors'),
padSummaryTitle('Errors'),
c.bold(c.red(`${errors.length} error${errors.length > 1 ? 's' : ''}`)),
)
}

this.log(padTitle('Start at'), this._timeStart)
this.log(padSummaryTitle('Start at'), this._timeStart)

const collectTime = sum(files, file => file.collectDuration)
const testsTime = sum(files, file => file.result?.duration)
const setupTime = sum(files, file => file.setupDuration)

if (this.watchFilters) {
this.log(padTitle('Duration'), time(collectTime + testsTime + setupTime))
this.log(padSummaryTitle('Duration'), formatTime(collectTime + testsTime + setupTime))
}
else {
const executionTime = this.end - this.start
Expand All @@ -409,16 +374,16 @@ export abstract class BaseReporter implements Reporter {
const typecheck = sum(this.ctx.projects, project => project.typechecker?.getResult().time)

const timers = [
`transform ${time(transformTime)}`,
`setup ${time(setupTime)}`,
`collect ${time(collectTime)}`,
`tests ${time(testsTime)}`,
`environment ${time(environmentTime)}`,
`prepare ${time(prepareTime)}`,
typecheck && `typecheck ${time(typecheck)}`,
`transform ${formatTime(transformTime)}`,
`setup ${formatTime(setupTime)}`,
`collect ${formatTime(collectTime)}`,
`tests ${formatTime(testsTime)}`,
`environment ${formatTime(environmentTime)}`,
`prepare ${formatTime(prepareTime)}`,
typecheck && `typecheck ${formatTime(typecheck)}`,
].filter(Boolean).join(', ')

this.log(padTitle('Duration'), time(executionTime) + c.dim(` (${timers})`))
this.log(padSummaryTitle('Duration'), formatTime(executionTime) + c.dim(` (${timers})`))
}

this.log()
Expand Down Expand Up @@ -544,17 +509,6 @@ function errorBanner(message: string) {
return c.red(divider(c.bold(c.inverse(` ${message} `))))
}

function padTitle(str: string) {
return c.dim(`${str.padStart(11)} `)
}

function time(time: number) {
if (time > 1000) {
return `${(time / 1000).toFixed(2)}s`
}
return `${Math.round(time)}ms`
}

function sum<T>(items: T[], cb: (_next: T) => number | undefined) {
return items.reduce((total, next) => {
return total + Math.max(cb(next) || 0, 0)
Expand Down
Loading

0 comments on commit 9f5803e

Please sign in to comment.