diff --git a/docs/guide/reporters.md b/docs/guide/reporters.md index e960e2363dea9..0fef317566407 100644 --- a/docs/guide/reporters.md +++ b/docs/guide/reporters.md @@ -172,7 +172,7 @@ Example output using basic reporter: ### Verbose Reporter -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. +Verbose reporter is same as `default` reporter, but it also displays each individual test after the suite has finished. It also displays currently running tests that are taking longer than [`slowTestThreshold`](/config/#slowtestthreshold). Similar to `default` reporter, you can disable the summary by configuring the reporter. :::code-group ```bash [CLI] @@ -190,6 +190,24 @@ export default defineConfig({ ``` ::: +Example output for tests in progress with default `slowTestThreshold: 300`: + +```bash + ✓ __tests__/example-1.test.ts (2) 725ms + ✓ first test file (2) 725ms + ✓ 2 + 2 should equal 4 + ✓ 4 - 2 should equal 2 + + ❯ test/example-2.test.ts 3/5 + ↳ should run longer than three seconds 1.57s + ❯ test/example-3.test.ts 1/5 + + Test Files 2 passed (4) + Tests 10 passed | 3 skipped (65) + Start at 11:01:36 + Duration 2.00s +``` + Example of final terminal output for a passing test suite: ```bash diff --git a/packages/vitest/src/node/reporters/default.ts b/packages/vitest/src/node/reporters/default.ts index f147bdeafcf83..5ac02191fcab1 100644 --- a/packages/vitest/src/node/reporters/default.ts +++ b/packages/vitest/src/node/reporters/default.ts @@ -30,7 +30,7 @@ export class DefaultReporter extends BaseReporter { onInit(ctx: Vitest) { super.onInit(ctx) - this.summary?.onInit(ctx) + this.summary?.onInit(ctx, { verbose: this.verbose }) } onPathsCollected(paths: string[] = []) { diff --git a/packages/vitest/src/node/reporters/renderers/figures.ts b/packages/vitest/src/node/reporters/renderers/figures.ts index b5dc259d3bc14..99543e0788612 100644 --- a/packages/vitest/src/node/reporters/renderers/figures.ts +++ b/packages/vitest/src/node/reporters/renderers/figures.ts @@ -8,3 +8,5 @@ export const F_CHECK = '✓' export const F_CROSS = '×' export const F_LONG_DASH = '⎯' export const F_RIGHT_TRI = '▶' +export const F_TREE_NODE_MIDDLE = '├──' +export const F_TREE_NODE_END = '└──' diff --git a/packages/vitest/src/node/reporters/summary.ts b/packages/vitest/src/node/reporters/summary.ts index a2f7d5968c157..d61cfc96d2bb8 100644 --- a/packages/vitest/src/node/reporters/summary.ts +++ b/packages/vitest/src/node/reporters/summary.ts @@ -3,13 +3,17 @@ import type { Vitest } from '../core' import type { Reporter } from '../types/reporter' import { getTests } from '@vitest/runner/utils' import c from 'tinyrainbow' -import { F_POINTER } from './renderers/figures' +import { F_POINTER, F_TREE_NODE_END, F_TREE_NODE_MIDDLE } from './renderers/figures' import { formatProjectName, formatTime, formatTimeString, padSummaryTitle } from './renderers/utils' import { WindowRenderer } from './renderers/windowedRenderer' -const DURATION_UPDATE_INTERVAL_MS = 500 +const DURATION_UPDATE_INTERVAL_MS = 100 const FINISHED_TEST_CLEANUP_TIME_MS = 1_000 +interface Options { + verbose?: boolean +} + interface Counter { total: number completed: number @@ -19,9 +23,17 @@ interface Counter { todo: number } +interface SlowTest { + name?: string + startTime: number + timeout: NodeJS.Timeout + +} + interface RunningTest extends Pick { filename: File['name'] projectName: File['projectName'] + slowTests: Map } /** @@ -30,6 +42,7 @@ interface RunningTest extends Pick { */ export class SummaryReporter implements Reporter { private ctx!: Vitest + private options!: Options private renderer!: WindowRenderer private suites = emptyCounters() @@ -46,12 +59,18 @@ export class SummaryReporter implements Reporter { private allFinishedTests = new Set() private startTime = '' + private currentTime = 0 private duration = 0 private durationInterval: NodeJS.Timeout | undefined = undefined - onInit(ctx: Vitest) { + onInit(ctx: Vitest, options: Options = {}) { this.ctx = ctx + this.options = { + verbose: false, + ...options, + } + this.renderer = new WindowRenderer({ logger: ctx.logger, getWindow: () => this.createSummary(), @@ -88,7 +107,10 @@ export class SummaryReporter implements Reporter { } if (task?.type === 'test' || task?.type === 'custom') { - if (task.result?.state !== 'run') { + if (task.result?.state === 'run') { + this.onTestStart(task) + } + else { this.onTestFinished(task) } } @@ -127,9 +149,12 @@ export class SummaryReporter implements Reporter { const finished = this.finishedTests.entries().next().value if (finished) { + const slowTests = this.runningTests.get(finished[0])?.slowTests + slowTests?.forEach(slowTest => clearTimeout(slowTest.timeout)) + clearTimeout(finished[1]) - this.finishedTests.delete(finished[0]) this.runningTests.delete(finished[0]) + this.finishedTests.delete(finished[0]) } } @@ -138,12 +163,13 @@ export class SummaryReporter implements Reporter { completed: 0, filename: file.name, projectName: file.projectName, + slowTests: new Map(), }) this.maxParallelTests = Math.max(this.maxParallelTests, this.runningTests.size) } - private onTestFinished(test: Test | Custom) { + private getTestStats(test: Test | Custom) { const file = test.file let stats = this.runningTests.get(file.id) @@ -158,6 +184,43 @@ export class SummaryReporter implements Reporter { } } + return stats + } + + private onTestStart(test: Test | Custom) { + // Track slow running tests only on verbose mode + if (!this.options.verbose) { + return + } + + const stats = this.getTestStats(test) + + if (!stats) { + return + } + + const slowTest: SlowTest = { + name: undefined, + startTime: performance.now(), + timeout: setTimeout(() => { + slowTest.name = test.name + }, this.ctx.config.slowTestThreshold).unref(), + } + + stats.slowTests.set(test.id, slowTest) + } + + private onTestFinished(test: Test | Custom) { + const file = test.file + const stats = this.getTestStats(test) + + if (!stats) { + return + } + + clearTimeout(stats.slowTests.get(test.id)?.timeout) + stats.slowTests.delete(test.id) + stats.completed++ const result = test.result @@ -225,6 +288,19 @@ export class SummaryReporter implements Reporter { + test.filename + c.dim(` ${test.completed}/${test.total}`), ) + + const slowTests = Array.from(test.slowTests.values()).filter(t => t.name) + + for (const [index, slowTest] of slowTests.entries()) { + const elapsed = this.currentTime - slowTest.startTime + const icon = index === slowTests.length - 1 ? F_TREE_NODE_END : F_TREE_NODE_MIDDLE + + summary.push( + c.bold(c.yellow(` ${icon} `)) + + slowTest.name + + c.bold(c.yellow(` ${formatTime(Math.max(0, elapsed))}`)), + ) + } } if (this.runningTests.size > 0) { @@ -246,8 +322,9 @@ export class SummaryReporter implements Reporter { this.startTime = formatTimeString(new Date()) this.durationInterval = setInterval(() => { - this.duration = performance.now() - start - }, DURATION_UPDATE_INTERVAL_MS) + this.currentTime = performance.now() + this.duration = this.currentTime - start + }, DURATION_UPDATE_INTERVAL_MS).unref() } }