mirror of
https://github.com/vitest-dev/vitest.git
synced 2025-12-08 18:26:03 +00:00
refactor(reporters): base reporter readability improvements (#6889)
This commit is contained in:
parent
9b3c3de27b
commit
00ebea6414
@ -12,7 +12,7 @@ import { createLogUpdate } from 'log-update'
|
||||
import c from 'tinyrainbow'
|
||||
import { highlightCode } from '../utils/colors'
|
||||
import { printError } from './error'
|
||||
import { divider } from './reporters/renderers/utils'
|
||||
import { divider, withLabel } from './reporters/renderers/utils'
|
||||
import { RandomSequencer } from './sequencers/RandomSequencer'
|
||||
|
||||
export interface ErrorOptions {
|
||||
@ -25,6 +25,8 @@ export interface ErrorOptions {
|
||||
showCodeFrame?: boolean
|
||||
}
|
||||
|
||||
const PAD = ' '
|
||||
|
||||
const ESC = '\x1B['
|
||||
const ERASE_DOWN = `${ESC}J`
|
||||
const ERASE_SCROLLBACK = `${ESC}3J`
|
||||
@ -64,13 +66,18 @@ export class Logger {
|
||||
this.console.warn(...args)
|
||||
}
|
||||
|
||||
clearFullScreen(message: string) {
|
||||
clearFullScreen(message = '') {
|
||||
if (!this.ctx.config.clearScreen) {
|
||||
this.console.log(message)
|
||||
return
|
||||
}
|
||||
|
||||
this.console.log(`${CLEAR_SCREEN}${ERASE_SCROLLBACK}${message}`)
|
||||
if (message) {
|
||||
this.console.log(`${CLEAR_SCREEN}${ERASE_SCROLLBACK}${message}`)
|
||||
}
|
||||
else {
|
||||
(this.outputStream as Writable).write(`${CLEAR_SCREEN}${ERASE_SCROLLBACK}`)
|
||||
}
|
||||
}
|
||||
|
||||
clearScreen(message: string, force = false) {
|
||||
@ -201,23 +208,13 @@ export class Logger {
|
||||
printBanner() {
|
||||
this.log()
|
||||
|
||||
const versionTest = this.ctx.config.watch
|
||||
? c.blue(`v${this.ctx.version}`)
|
||||
: c.cyan(`v${this.ctx.version}`)
|
||||
const mode = this.ctx.config.watch ? c.blue(' DEV ') : c.cyan(' RUN ')
|
||||
const color = this.ctx.config.watch ? 'blue' : 'cyan'
|
||||
const mode = this.ctx.config.watch ? 'DEV' : 'RUN'
|
||||
|
||||
this.log(
|
||||
`${c.inverse(c.bold(mode))} ${versionTest} ${c.gray(
|
||||
this.ctx.config.root,
|
||||
)}`,
|
||||
)
|
||||
this.log(withLabel(color, mode, `v${this.ctx.version} `) + c.gray(this.ctx.config.root))
|
||||
|
||||
if (this.ctx.config.sequence.sequencer === RandomSequencer) {
|
||||
this.log(
|
||||
c.gray(
|
||||
` Running tests with seed "${this.ctx.config.sequence.seed}"`,
|
||||
),
|
||||
)
|
||||
this.log(PAD + c.gray(`Running tests with seed "${this.ctx.config.sequence.seed}"`))
|
||||
}
|
||||
|
||||
this.ctx.projects.forEach((project) => {
|
||||
@ -231,52 +228,32 @@ export class Logger {
|
||||
const origin = resolvedUrls?.local[0] ?? resolvedUrls?.network[0]
|
||||
const provider = project.browser.provider.name
|
||||
const providerString = provider === 'preview' ? '' : ` by ${provider}`
|
||||
this.log(
|
||||
c.dim(
|
||||
c.green(
|
||||
` ${output} Browser runner started${providerString} at ${new URL('/', origin)}`,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
this.log(PAD + c.dim(c.green(`${output} Browser runner started${providerString} at ${new URL('/', origin)}`)))
|
||||
})
|
||||
|
||||
if (this.ctx.config.ui) {
|
||||
this.log(
|
||||
c.dim(
|
||||
c.green(
|
||||
` UI started at http://${
|
||||
this.ctx.config.api?.host || 'localhost'
|
||||
}:${c.bold(`${this.ctx.server.config.server.port}`)}${
|
||||
this.ctx.config.uiBase
|
||||
}`,
|
||||
),
|
||||
),
|
||||
)
|
||||
const host = this.ctx.config.api?.host || 'localhost'
|
||||
const port = this.ctx.server.config.server.port
|
||||
const base = this.ctx.config.uiBase
|
||||
|
||||
this.log(PAD + c.dim(c.green(`UI started at http://${host}:${c.bold(port)}${base}`)))
|
||||
}
|
||||
else if (this.ctx.config.api?.port) {
|
||||
const resolvedUrls = this.ctx.server.resolvedUrls
|
||||
// workaround for https://github.com/vitejs/vite/issues/15438, it was fixed in vite 5.1
|
||||
const fallbackUrl = `http://${this.ctx.config.api.host || 'localhost'}:${
|
||||
this.ctx.config.api.port
|
||||
}`
|
||||
const origin
|
||||
= resolvedUrls?.local[0] ?? resolvedUrls?.network[0] ?? fallbackUrl
|
||||
this.log(c.dim(c.green(` API started at ${new URL('/', origin)}`)))
|
||||
const fallbackUrl = `http://${this.ctx.config.api.host || 'localhost'}:${this.ctx.config.api.port}`
|
||||
const origin = resolvedUrls?.local[0] ?? resolvedUrls?.network[0] ?? fallbackUrl
|
||||
|
||||
this.log(PAD + c.dim(c.green(`API started at ${new URL('/', origin)}`)))
|
||||
}
|
||||
|
||||
if (this.ctx.coverageProvider) {
|
||||
this.log(
|
||||
c.dim(' Coverage enabled with ')
|
||||
+ c.yellow(this.ctx.coverageProvider.name),
|
||||
)
|
||||
this.log(PAD + c.dim('Coverage enabled with ') + c.yellow(this.ctx.coverageProvider.name))
|
||||
}
|
||||
|
||||
if (this.ctx.config.standalone) {
|
||||
this.log(
|
||||
c.yellow(
|
||||
`\nVitest is running in standalone mode. Edit a test file to rerun tests.`,
|
||||
),
|
||||
)
|
||||
this.log(c.yellow(`\nVitest is running in standalone mode. Edit a test file to rerun tests.`))
|
||||
}
|
||||
else {
|
||||
this.log()
|
||||
|
||||
@ -11,33 +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,
|
||||
} from './renderers/utils'
|
||||
import { countTestErrors, divider, formatProjectName, formatTimeString, getStateString, getStateSymbol, renderSnapshotSummary, taskFail, withLabel } from './renderers/utils'
|
||||
|
||||
const BADGE_PADDING = ' '
|
||||
const HELP_HINT = `${c.dim('press ')}${c.bold('h')}${c.dim(' to show help')}`
|
||||
const HELP_UPDATE_SNAP
|
||||
= c.dim('press ') + c.bold(c.yellow('u')) + c.dim(' to update snapshot')
|
||||
const HELP_QUITE = `${c.dim('press ')}${c.bold('q')}${c.dim(' to quit')}`
|
||||
|
||||
const WAIT_FOR_CHANGE_PASS = `\n${c.bold(
|
||||
c.inverse(c.green(' PASS ')),
|
||||
)}${c.green(' Waiting for file changes...')}`
|
||||
const WAIT_FOR_CHANGE_FAIL = `\n${c.bold(c.inverse(c.red(' FAIL ')))}${c.red(
|
||||
' Tests failed. Watching for file changes...',
|
||||
)}`
|
||||
const WAIT_FOR_CHANGE_CANCELLED = `\n${c.bold(
|
||||
c.inverse(c.red(' CANCELLED ')),
|
||||
)}${c.red(' Test run cancelled. Watching for file changes...')}`
|
||||
|
||||
const LAST_RUN_LOG_TIMEOUT = 1_500
|
||||
|
||||
export interface BaseOptions {
|
||||
@ -55,35 +31,36 @@ export abstract class BaseReporter implements Reporter {
|
||||
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
|
||||
private _timeStart = new Date()
|
||||
|
||||
constructor(options: BaseOptions = {}) {
|
||||
this.isTTY = options.isTTY ?? ((isNode || isDeno) && process.stdout?.isTTY && !isCI)
|
||||
}
|
||||
|
||||
get mode() {
|
||||
return this.ctx.config.mode
|
||||
}
|
||||
|
||||
onInit(ctx: Vitest) {
|
||||
this.ctx = ctx
|
||||
ctx.logger.printBanner()
|
||||
|
||||
this.ctx.logger.printBanner()
|
||||
this.start = performance.now()
|
||||
}
|
||||
|
||||
log(...messages: any) {
|
||||
this.ctx.logger.log(...messages)
|
||||
}
|
||||
|
||||
error(...messages: any) {
|
||||
this.ctx.logger.error(...messages)
|
||||
}
|
||||
|
||||
relative(path: string) {
|
||||
return relative(this.ctx.config.root, path)
|
||||
}
|
||||
|
||||
onFinished(
|
||||
files = this.ctx.state.getFiles(),
|
||||
errors = this.ctx.state.getUnhandledErrors(),
|
||||
) {
|
||||
onFinished(files = this.ctx.state.getFiles(), errors = this.ctx.state.getUnhandledErrors()) {
|
||||
this.end = performance.now()
|
||||
|
||||
this.reportSummary(files, errors)
|
||||
}
|
||||
|
||||
@ -93,6 +70,7 @@ export abstract class BaseReporter implements Reporter {
|
||||
}
|
||||
for (const pack of packs) {
|
||||
const task = this.ctx.state.idMap.get(pack[0])
|
||||
|
||||
if (task) {
|
||||
this.printTask(task)
|
||||
}
|
||||
@ -106,55 +84,57 @@ export abstract class BaseReporter implements Reporter {
|
||||
|| task.result?.state === 'run') {
|
||||
return
|
||||
}
|
||||
const logger = this.ctx.logger
|
||||
|
||||
const tests = getTests(task)
|
||||
const failed = tests.filter(t => t.result?.state === 'fail')
|
||||
const skipped = tests.filter(
|
||||
t => t.mode === 'skip' || t.mode === 'todo',
|
||||
)
|
||||
const skipped = tests.filter(t => t.mode === 'skip' || t.mode === 'todo')
|
||||
|
||||
let state = c.dim(`${tests.length} test${tests.length > 1 ? 's' : ''}`)
|
||||
|
||||
if (failed.length) {
|
||||
state += ` ${c.dim('|')} ${c.red(`${failed.length} failed`)}`
|
||||
}
|
||||
if (skipped.length) {
|
||||
state += ` ${c.dim('|')} ${c.yellow(`${skipped.length} skipped`)}`
|
||||
}
|
||||
let suffix = c.dim(' (') + state + c.dim(')')
|
||||
suffix += this.getDurationPrefix(task)
|
||||
if (this.ctx.config.logHeapUsage && task.result.heap != null) {
|
||||
suffix += c.magenta(
|
||||
` ${Math.floor(task.result.heap / 1024 / 1024)} MB heap used`,
|
||||
)
|
||||
state += c.dim(' | ') + c.red(`${failed.length} failed`)
|
||||
}
|
||||
|
||||
let title = ` ${getStateSymbol(task)} `
|
||||
if (skipped.length) {
|
||||
state += c.dim(' | ') + c.yellow(`${skipped.length} skipped`)
|
||||
}
|
||||
|
||||
let suffix = c.dim('(') + state + c.dim(')') + this.getDurationPrefix(task)
|
||||
|
||||
if (this.ctx.config.logHeapUsage && task.result.heap != null) {
|
||||
suffix += c.magenta(` ${Math.floor(task.result.heap / 1024 / 1024)} MB heap used`)
|
||||
}
|
||||
|
||||
let title = getStateSymbol(task)
|
||||
|
||||
if (task.meta.typecheck) {
|
||||
title += `${c.bgBlue(c.bold(' TS '))} `
|
||||
title += ` ${c.bgBlue(c.bold(' TS '))}`
|
||||
}
|
||||
|
||||
if (task.projectName) {
|
||||
title += formatProjectName(task.projectName)
|
||||
title += ` ${formatProjectName(task.projectName, '')}`
|
||||
}
|
||||
title += `${task.name} ${suffix}`
|
||||
logger.log(title)
|
||||
|
||||
this.log(` ${title} ${task.name} ${suffix}`)
|
||||
|
||||
for (const test of tests) {
|
||||
const duration = test.result?.duration
|
||||
|
||||
if (test.result?.state === 'fail') {
|
||||
const suffix = this.getDurationPrefix(test)
|
||||
logger.log(c.red(` ${taskFail} ${getTestName(test, c.dim(' > '))}${suffix}`))
|
||||
this.log(c.red(` ${taskFail} ${getTestName(test, c.dim(' > '))}${suffix}`))
|
||||
|
||||
test.result?.errors?.forEach((e) => {
|
||||
// print short errors, full errors will be at the end in summary
|
||||
logger.log(c.red(` ${F_RIGHT} ${(e as any)?.message}`))
|
||||
this.log(c.red(` ${F_RIGHT} ${e?.message}`))
|
||||
})
|
||||
}
|
||||
|
||||
// also print slow tests
|
||||
else if (duration && duration > this.ctx.config.slowTestThreshold) {
|
||||
logger.log(
|
||||
` ${c.yellow(c.dim(F_CHECK))} ${getTestName(test, c.dim(' > '))}${c.yellow(
|
||||
` ${Math.round(duration)}${c.dim('ms')}`,
|
||||
)}`,
|
||||
this.log(
|
||||
` ${c.yellow(c.dim(F_CHECK))} ${getTestName(test, c.dim(' > '))}`
|
||||
+ ` ${c.yellow(Math.round(duration) + c.dim('ms'))}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -164,42 +144,39 @@ export abstract class BaseReporter implements Reporter {
|
||||
if (!task.result?.duration) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const color = task.result.duration > this.ctx.config.slowTestThreshold
|
||||
? c.yellow
|
||||
: c.gray
|
||||
|
||||
return color(` ${Math.round(task.result.duration)}${c.dim('ms')}`)
|
||||
}
|
||||
|
||||
onWatcherStart(
|
||||
files = this.ctx.state.getFiles(),
|
||||
errors = this.ctx.state.getUnhandledErrors(),
|
||||
) {
|
||||
onWatcherStart(files = this.ctx.state.getFiles(), errors = this.ctx.state.getUnhandledErrors()) {
|
||||
this.resetLastRunLog()
|
||||
|
||||
const failed = errors.length > 0 || hasFailed(files)
|
||||
const failedSnap = hasFailedSnapshot(files)
|
||||
const cancelled = this.ctx.isCancelling
|
||||
|
||||
if (failed) {
|
||||
this.ctx.logger.log(WAIT_FOR_CHANGE_FAIL)
|
||||
this.log(withLabel('red', 'FAIL', 'Tests failed. Watching for file changes...'))
|
||||
}
|
||||
else if (cancelled) {
|
||||
this.ctx.logger.log(WAIT_FOR_CHANGE_CANCELLED)
|
||||
else if (this.ctx.isCancelling) {
|
||||
this.log(withLabel('red', 'CANCELLED', 'Test run cancelled. Watching for file changes...'))
|
||||
}
|
||||
else {
|
||||
this.ctx.logger.log(WAIT_FOR_CHANGE_PASS)
|
||||
this.log(withLabel('green', 'PASS', 'Waiting for file changes...'))
|
||||
}
|
||||
|
||||
const hints: string[] = []
|
||||
hints.push(HELP_HINT)
|
||||
if (failedSnap) {
|
||||
hints.unshift(HELP_UPDATE_SNAP)
|
||||
const hints = [c.dim('press ') + c.bold('h') + c.dim(' to show help')]
|
||||
|
||||
if (hasFailedSnapshot(files)) {
|
||||
hints.unshift(c.dim('press ') + c.bold(c.yellow('u')) + c.dim(' to update snapshot'))
|
||||
}
|
||||
else {
|
||||
hints.push(HELP_QUITE)
|
||||
hints.push(c.dim('press ') + c.bold('q') + c.dim(' to quit'))
|
||||
}
|
||||
|
||||
this.ctx.logger.log(BADGE_PADDING + hints.join(c.dim(', ')))
|
||||
this.log(BADGE_PADDING + hints.join(c.dim(', ')))
|
||||
|
||||
if (this._lastRunCount) {
|
||||
const LAST_RUN_TEXT = `rerun x${this._lastRunCount}`
|
||||
@ -233,57 +210,51 @@ export abstract class BaseReporter implements Reporter {
|
||||
onWatcherRerun(files: string[], trigger?: string) {
|
||||
this.resetLastRunLog()
|
||||
this.watchFilters = files
|
||||
this.failedUnwatchedFiles = this.ctx.state.getFiles().filter((file) => {
|
||||
return !files.includes(file.filepath) && hasFailed(file)
|
||||
})
|
||||
this.failedUnwatchedFiles = this.ctx.state.getFiles().filter(file =>
|
||||
!files.includes(file.filepath) && hasFailed(file),
|
||||
)
|
||||
|
||||
// Update re-run count for each file
|
||||
files.forEach((filepath) => {
|
||||
let reruns = this._filesInWatchMode.get(filepath) ?? 0
|
||||
this._filesInWatchMode.set(filepath, ++reruns)
|
||||
})
|
||||
|
||||
const BADGE = c.inverse(c.bold(c.blue(' RERUN ')))
|
||||
const TRIGGER = trigger ? c.dim(` ${this.relative(trigger)}`) : ''
|
||||
const FILENAME_PATTERN = this.ctx.filenamePattern
|
||||
? `${BADGE_PADDING} ${c.dim('Filename pattern: ')}${c.blue(
|
||||
this.ctx.filenamePattern,
|
||||
)}\n`
|
||||
: ''
|
||||
const TESTNAME_PATTERN = this.ctx.configOverride.testNamePattern
|
||||
? `${BADGE_PADDING} ${c.dim('Test name pattern: ')}${c.blue(
|
||||
String(this.ctx.configOverride.testNamePattern),
|
||||
)}\n`
|
||||
: ''
|
||||
const PROJECT_FILTER = this.ctx.configOverride.project
|
||||
? `${BADGE_PADDING} ${c.dim('Project name: ')}${c.blue(
|
||||
toArray(this.ctx.configOverride.project).join(', '),
|
||||
)}\n`
|
||||
: ''
|
||||
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.ctx.logger.clearFullScreen(
|
||||
`\n${BADGE}${TRIGGER}\n${PROJECT_FILTER}${FILENAME_PATTERN}${TESTNAME_PATTERN}`,
|
||||
)
|
||||
this._lastRunCount = 0
|
||||
}
|
||||
else if (files.length === 1) {
|
||||
const rerun = this._filesInWatchMode.get(files[0]) ?? 1
|
||||
this._lastRunCount = rerun
|
||||
this.ctx.logger.clearFullScreen(
|
||||
`\n${BADGE}${TRIGGER} ${c.blue(
|
||||
`x${rerun}`,
|
||||
)}\n${PROJECT_FILTER}${FILENAME_PATTERN}${TESTNAME_PATTERN}`,
|
||||
)
|
||||
banner += c.blue(`x${rerun} `)
|
||||
}
|
||||
|
||||
this.ctx.logger.clearFullScreen()
|
||||
this.log(withLabel('blue', 'RERUN', banner))
|
||||
|
||||
if (this.ctx.configOverride.project) {
|
||||
this.log(BADGE_PADDING + c.dim(' Project name: ') + c.blue(toArray(this.ctx.configOverride.project).join(', ')))
|
||||
}
|
||||
|
||||
if (this.ctx.filenamePattern) {
|
||||
this.log(BADGE_PADDING + c.dim(' Filename pattern: ') + c.blue(this.ctx.filenamePattern))
|
||||
}
|
||||
|
||||
if (this.ctx.configOverride.testNamePattern) {
|
||||
this.log(BADGE_PADDING + c.dim(' Test name pattern: ') + c.blue(String(this.ctx.configOverride.testNamePattern)))
|
||||
}
|
||||
|
||||
this.log('')
|
||||
|
||||
if (!this.isTTY) {
|
||||
for (const task of this.failedUnwatchedFiles) {
|
||||
this.printTask(task)
|
||||
}
|
||||
}
|
||||
|
||||
this._timeStart = new Date()
|
||||
this._timeStart = formatTimeString(new Date())
|
||||
this.start = performance.now()
|
||||
}
|
||||
|
||||
@ -291,27 +262,25 @@ export abstract class BaseReporter implements Reporter {
|
||||
if (!this.shouldLog(log)) {
|
||||
return
|
||||
}
|
||||
const task = log.taskId ? this.ctx.state.idMap.get(log.taskId) : undefined
|
||||
const header = c.gray(
|
||||
log.type
|
||||
+ c.dim(
|
||||
` | ${
|
||||
task
|
||||
? getFullName(task, c.dim(' > '))
|
||||
: log.taskId !== '__vitest__unknown_test__'
|
||||
? log.taskId
|
||||
: 'unknown test'
|
||||
}`,
|
||||
),
|
||||
)
|
||||
|
||||
const output
|
||||
= log.type === 'stdout'
|
||||
? this.ctx.logger.outputStream
|
||||
: this.ctx.logger.errorStream
|
||||
|
||||
const write = (msg: string) => (output as any).write(msg)
|
||||
|
||||
write(`${header}\n${log.content}`)
|
||||
let headerText = 'unknown test'
|
||||
const task = log.taskId ? this.ctx.state.idMap.get(log.taskId) : undefined
|
||||
|
||||
if (task) {
|
||||
headerText = getFullName(task, c.dim(' > '))
|
||||
}
|
||||
else if (log.taskId && log.taskId !== '__vitest__unknown_test__') {
|
||||
headerText = log.taskId
|
||||
}
|
||||
|
||||
write(c.gray(log.type + c.dim(` | ${headerText}\n`)) + log.content)
|
||||
|
||||
if (log.origin) {
|
||||
// browser logs don't have an extra end of line at the end like Node.js does
|
||||
@ -327,29 +296,30 @@ export abstract class BaseReporter implements Reporter {
|
||||
? (project.browser?.parseStacktrace(log.origin) || [])
|
||||
: parseStacktrace(log.origin)
|
||||
|
||||
const highlight = task
|
||||
? stack.find(i => i.file === task.file.filepath)
|
||||
: null
|
||||
const highlight = task && stack.find(i => i.file === task.file.filepath)
|
||||
|
||||
for (const frame of stack) {
|
||||
const color = frame === highlight ? c.cyan : c.gray
|
||||
const path = relative(project.config.root, frame.file)
|
||||
|
||||
write(
|
||||
color(
|
||||
` ${c.dim(F_POINTER)} ${[
|
||||
frame.method,
|
||||
`${path}:${c.dim(`${frame.line}:${frame.column}`)}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}\n`,
|
||||
),
|
||||
)
|
||||
const positions = [
|
||||
frame.method,
|
||||
`${path}:${c.dim(`${frame.line}:${frame.column}`)}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
|
||||
write(color(` ${c.dim(F_POINTER)} ${positions}\n`))
|
||||
}
|
||||
}
|
||||
|
||||
write('\n')
|
||||
}
|
||||
|
||||
onTestRemoved(trigger?: string) {
|
||||
this.log(c.yellow('Test removed...') + (trigger ? c.dim(` [ ${this.relative(trigger)} ]\n`) : ''))
|
||||
}
|
||||
|
||||
shouldLog(log: UserConsoleLog) {
|
||||
if (this.ctx.config.silent) {
|
||||
return false
|
||||
@ -362,20 +332,17 @@ export abstract class BaseReporter implements Reporter {
|
||||
}
|
||||
|
||||
onServerRestart(reason?: string) {
|
||||
this.ctx.logger.log(
|
||||
c.bold(
|
||||
c.magenta(
|
||||
reason === 'config'
|
||||
? '\nRestarting due to config changes...'
|
||||
: '\nRestarting Vitest...',
|
||||
),
|
||||
),
|
||||
)
|
||||
this.log(c.bold(c.magenta(
|
||||
reason === 'config'
|
||||
? '\nRestarting due to config changes...'
|
||||
: '\nRestarting Vitest...',
|
||||
)))
|
||||
}
|
||||
|
||||
reportSummary(files: File[], errors: unknown[]) {
|
||||
this.printErrorsSummary(files, errors)
|
||||
if (this.mode === 'benchmark') {
|
||||
|
||||
if (this.ctx.config.mode === 'benchmark') {
|
||||
this.reportBenchmarkSummary(files)
|
||||
}
|
||||
else {
|
||||
@ -389,240 +356,194 @@ export abstract class BaseReporter implements Reporter {
|
||||
...files,
|
||||
]
|
||||
const tests = getTests(affectedFiles)
|
||||
const logger = this.ctx.logger
|
||||
|
||||
const executionTime = this.end - this.start
|
||||
const collectTime = files.reduce(
|
||||
(acc, test) => acc + Math.max(0, test.collectDuration || 0),
|
||||
0,
|
||||
)
|
||||
const setupTime = files.reduce(
|
||||
(acc, test) => acc + Math.max(0, test.setupDuration || 0),
|
||||
0,
|
||||
)
|
||||
const testsTime = files.reduce(
|
||||
(acc, test) => acc + Math.max(0, test.result?.duration || 0),
|
||||
0,
|
||||
)
|
||||
const transformTime = this.ctx.projects
|
||||
.flatMap(w => w.vitenode.getTotalDuration())
|
||||
.reduce((a, b) => a + b, 0)
|
||||
const environmentTime = files.reduce(
|
||||
(acc, file) => acc + Math.max(0, file.environmentLoad || 0),
|
||||
0,
|
||||
)
|
||||
const prepareTime = files.reduce(
|
||||
(acc, file) => acc + Math.max(0, file.prepareDuration || 0),
|
||||
0,
|
||||
)
|
||||
const threadTime = collectTime + testsTime + setupTime
|
||||
|
||||
// show top 10 costly transform module
|
||||
// console.log(Array.from(this.ctx.vitenode.fetchCache.entries()).filter(i => i[1].duration)
|
||||
// .sort((a, b) => b[1].duration! - a[1].duration!)
|
||||
// .map(i => `${time(i[1].duration!)} ${i[0]}`)
|
||||
// .slice(0, 10)
|
||||
// .join('\n'),
|
||||
// )
|
||||
|
||||
const snapshotOutput = renderSnapshotSummary(
|
||||
this.ctx.config.root,
|
||||
this.ctx.snapshot.summary,
|
||||
)
|
||||
if (snapshotOutput.length) {
|
||||
logger.log(
|
||||
snapshotOutput
|
||||
.map((t, i) =>
|
||||
i === 0 ? `${padTitle('Snapshots')} ${t}` : `${padTitle('')} ${t}`,
|
||||
)
|
||||
.join('\n'),
|
||||
)
|
||||
if (snapshotOutput.length > 1) {
|
||||
logger.log()
|
||||
}
|
||||
|
||||
for (const [index, snapshot] of snapshotOutput.entries()) {
|
||||
const title = index === 0 ? 'Snapshots' : ''
|
||||
this.log(`${padTitle(title)} ${snapshot}`)
|
||||
}
|
||||
|
||||
logger.log(padTitle('Test Files'), getStateString(affectedFiles))
|
||||
logger.log(padTitle('Tests'), getStateString(tests))
|
||||
if (snapshotOutput.length > 1) {
|
||||
this.log()
|
||||
}
|
||||
|
||||
this.log(padTitle('Test Files'), getStateString(affectedFiles))
|
||||
this.log(padTitle('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,
|
||||
)
|
||||
logger.log(
|
||||
const failed = tests.filter(t => t.meta?.typecheck && t.result?.errors?.length)
|
||||
|
||||
this.log(
|
||||
padTitle('Type Errors'),
|
||||
failed.length
|
||||
? c.bold(c.red(`${failed.length} failed`))
|
||||
: c.dim('no errors'),
|
||||
)
|
||||
}
|
||||
|
||||
if (errors.length) {
|
||||
logger.log(
|
||||
this.log(
|
||||
padTitle('Errors'),
|
||||
c.bold(c.red(`${errors.length} error${errors.length > 1 ? 's' : ''}`)),
|
||||
)
|
||||
}
|
||||
logger.log(padTitle('Start at'), formatTimeString(this._timeStart))
|
||||
|
||||
this.log(padTitle('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) {
|
||||
logger.log(padTitle('Duration'), time(threadTime))
|
||||
this.log(padTitle('Duration'), time(collectTime + testsTime + setupTime))
|
||||
}
|
||||
else {
|
||||
let timers = `transform ${time(transformTime)}, setup ${time(
|
||||
setupTime,
|
||||
)}, collect ${time(collectTime)}, tests ${time(
|
||||
testsTime,
|
||||
)}, environment ${time(environmentTime)}, prepare ${time(prepareTime)}`
|
||||
const typecheck = this.ctx.projects.reduce(
|
||||
(acc, c) => acc + (c.typechecker?.getResult().time || 0),
|
||||
0,
|
||||
)
|
||||
if (typecheck) {
|
||||
timers += `, typecheck ${time(typecheck)}`
|
||||
}
|
||||
logger.log(
|
||||
padTitle('Duration'),
|
||||
time(executionTime) + c.dim(` (${timers})`),
|
||||
)
|
||||
const executionTime = this.end - this.start
|
||||
const environmentTime = sum(files, file => file.environmentLoad)
|
||||
const prepareTime = sum(files, file => file.prepareDuration)
|
||||
const transformTime = sum(this.ctx.projects, project => project.vitenode.getTotalDuration())
|
||||
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)}`,
|
||||
].filter(Boolean).join(', ')
|
||||
|
||||
this.log(padTitle('Duration'), time(executionTime) + c.dim(` (${timers})`))
|
||||
}
|
||||
|
||||
logger.log()
|
||||
this.log()
|
||||
}
|
||||
|
||||
private printErrorsSummary(files: File[], errors: unknown[]) {
|
||||
const logger = this.ctx.logger
|
||||
const suites = getSuites(files)
|
||||
const tests = getTests(files)
|
||||
|
||||
const failedSuites = suites.filter(i => i.result?.errors)
|
||||
const failedTests = tests.filter(i => i.result?.state === 'fail')
|
||||
const failedTotal
|
||||
= countTestErrors(failedSuites) + countTestErrors(failedTests)
|
||||
const failedTotal = countTestErrors(failedSuites) + countTestErrors(failedTests)
|
||||
|
||||
let current = 1
|
||||
|
||||
const errorDivider = () =>
|
||||
logger.error(
|
||||
`${c.red(
|
||||
c.dim(divider(`[${current++}/${failedTotal}]`, undefined, 1)),
|
||||
)}\n`,
|
||||
)
|
||||
const errorDivider = () => this.error(`${c.red(c.dim(divider(`[${current++}/${failedTotal}]`, undefined, 1)))}\n`)
|
||||
|
||||
if (failedSuites.length) {
|
||||
logger.error(
|
||||
c.red(
|
||||
divider(c.bold(c.inverse(` Failed Suites ${failedSuites.length} `))),
|
||||
),
|
||||
)
|
||||
logger.error()
|
||||
this.error(`${errorBanner(`Failed Suites ${failedSuites.length}`)}\n`)
|
||||
this.printTaskErrors(failedSuites, errorDivider)
|
||||
}
|
||||
|
||||
if (failedTests.length) {
|
||||
logger.error(
|
||||
c.red(
|
||||
divider(c.bold(c.inverse(` Failed Tests ${failedTests.length} `))),
|
||||
),
|
||||
)
|
||||
logger.error()
|
||||
|
||||
this.error(`${errorBanner(`Failed Tests ${failedTests.length}`)}\n`)
|
||||
this.printTaskErrors(failedTests, errorDivider)
|
||||
}
|
||||
|
||||
if (errors.length) {
|
||||
logger.printUnhandledErrors(errors)
|
||||
logger.error()
|
||||
this.ctx.logger.printUnhandledErrors(errors)
|
||||
this.error()
|
||||
}
|
||||
return tests
|
||||
}
|
||||
|
||||
reportBenchmarkSummary(files: File[]) {
|
||||
const logger = this.ctx.logger
|
||||
const benches = getTests(files)
|
||||
|
||||
const topBenches = benches.filter(i => i.result?.benchmark?.rank === 1)
|
||||
|
||||
logger.log(
|
||||
`\n${c.cyan(c.inverse(c.bold(' BENCH ')))} ${c.cyan('Summary')}\n`,
|
||||
)
|
||||
this.log(withLabel('cyan', 'BENCH', 'Summary\n'))
|
||||
|
||||
for (const bench of topBenches) {
|
||||
const group = bench.suite || bench.file
|
||||
|
||||
if (!group) {
|
||||
continue
|
||||
}
|
||||
|
||||
const groupName = getFullName(group, c.dim(' > '))
|
||||
logger.log(` ${bench.name}${c.dim(` - ${groupName}`)}`)
|
||||
this.log(` ${bench.name}${c.dim(` - ${groupName}`)}`)
|
||||
|
||||
const siblings = group.tasks
|
||||
.filter(i => i.meta.benchmark && i.result?.benchmark && i !== bench)
|
||||
.sort((a, b) => a.result!.benchmark!.rank - b.result!.benchmark!.rank)
|
||||
if (siblings.length === 0) {
|
||||
logger.log('')
|
||||
continue
|
||||
}
|
||||
|
||||
for (const sibling of siblings) {
|
||||
const number = `${(
|
||||
sibling.result!.benchmark!.mean / bench.result!.benchmark!.mean
|
||||
).toFixed(2)}x`
|
||||
logger.log(
|
||||
` ${c.green(number)} ${c.gray('faster than')} ${sibling.name}`,
|
||||
)
|
||||
const number = (sibling.result!.benchmark!.mean / bench.result!.benchmark!.mean).toFixed(2)
|
||||
this.log(c.green(` ${number}x `) + c.gray('faster than ') + sibling.name)
|
||||
}
|
||||
logger.log('')
|
||||
|
||||
this.log('')
|
||||
}
|
||||
}
|
||||
|
||||
private printTaskErrors(tasks: Task[], errorDivider: () => void) {
|
||||
const errorsQueue: [error: ErrorWithDiff | undefined, tests: Task[]][] = []
|
||||
|
||||
for (const task of tasks) {
|
||||
// merge identical errors
|
||||
// Merge identical errors
|
||||
task.result?.errors?.forEach((error) => {
|
||||
const errorItem
|
||||
= error?.stackStr
|
||||
&& errorsQueue.find((i) => {
|
||||
const hasStr = i[0]?.stackStr === error.stackStr
|
||||
if (!hasStr) {
|
||||
let previous
|
||||
|
||||
if (error?.stackStr) {
|
||||
previous = errorsQueue.find((i) => {
|
||||
if (i[0]?.stackStr !== error.stackStr) {
|
||||
return false
|
||||
}
|
||||
const currentProjectName
|
||||
= (task as File)?.projectName || task.file?.projectName || ''
|
||||
const projectName
|
||||
= (i[1][0] as File)?.projectName || i[1][0].file?.projectName || ''
|
||||
|
||||
const currentProjectName = (task as File)?.projectName || task.file?.projectName || ''
|
||||
const projectName = (i[1][0] as File)?.projectName || i[1][0].file?.projectName || ''
|
||||
|
||||
return projectName === currentProjectName
|
||||
})
|
||||
if (errorItem) {
|
||||
errorItem[1].push(task)
|
||||
}
|
||||
|
||||
if (previous) {
|
||||
previous[1].push(task)
|
||||
}
|
||||
else {
|
||||
errorsQueue.push([error, [task]])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
for (const [error, tasks] of errorsQueue) {
|
||||
for (const task of tasks) {
|
||||
const filepath = (task as File)?.filepath || ''
|
||||
const projectName
|
||||
= (task as File)?.projectName || task.file?.projectName || ''
|
||||
const projectName = (task as File)?.projectName || task.file?.projectName || ''
|
||||
|
||||
let name = getFullName(task, c.dim(' > '))
|
||||
|
||||
if (filepath) {
|
||||
name = `${name} ${c.dim(`[ ${this.relative(filepath)} ]`)}`
|
||||
name += c.dim(` [ ${this.relative(filepath)} ]`)
|
||||
}
|
||||
|
||||
this.ctx.logger.error(
|
||||
`${c.red(c.bold(c.inverse(' FAIL ')))} ${formatProjectName(
|
||||
projectName,
|
||||
)}${name}`,
|
||||
`${c.red(c.bold(c.inverse(' FAIL ')))}${formatProjectName(projectName)} ${name}`,
|
||||
)
|
||||
}
|
||||
const screenshots = tasks.filter(t => t.meta?.failScreenshotPath).map(t => t.meta?.failScreenshotPath as string)
|
||||
const project = this.ctx.getProjectByTaskId(tasks[0].id)
|
||||
|
||||
const screenshotPaths = tasks.map(t => t.meta?.failScreenshotPath).filter(screenshot => screenshot != null)
|
||||
|
||||
this.ctx.logger.printError(error, {
|
||||
project,
|
||||
project: this.ctx.getProjectByTaskId(tasks[0].id),
|
||||
verbose: this.verbose,
|
||||
screenshotPaths: screenshots,
|
||||
screenshotPaths,
|
||||
task: tasks[0],
|
||||
})
|
||||
|
||||
errorDivider()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function errorBanner(message: string) {
|
||||
return c.red(divider(c.bold(c.inverse(` ${message} `))))
|
||||
}
|
||||
|
||||
function padTitle(str: string) {
|
||||
return c.dim(`${str.padStart(11)} `)
|
||||
}
|
||||
@ -633,3 +554,9 @@ function time(time: number) {
|
||||
}
|
||||
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)
|
||||
}, 0)
|
||||
}
|
||||
|
||||
@ -41,7 +41,7 @@ export class DefaultReporter extends BaseReporter {
|
||||
this.rendererOptions.showHeap = this.ctx.config.logHeapUsage
|
||||
this.rendererOptions.slowTestThreshold
|
||||
= this.ctx.config.slowTestThreshold
|
||||
this.rendererOptions.mode = this.mode
|
||||
this.rendererOptions.mode = this.ctx.config.mode
|
||||
const files = this.ctx.state.getFiles(this.watchFilters)
|
||||
if (!this.renderer) {
|
||||
this.renderer = createListRenderer(files, this.rendererOptions).start()
|
||||
|
||||
@ -254,6 +254,12 @@ export function formatProjectName(name: string | undefined, suffix = ' ') {
|
||||
const index = name
|
||||
.split('')
|
||||
.reduce((acc, v, idx) => acc + v.charCodeAt(0) + idx, 0)
|
||||
|
||||
const colors = [c.blue, c.yellow, c.cyan, c.green, c.magenta]
|
||||
|
||||
return colors[index % colors.length](`|${name}|`) + suffix
|
||||
}
|
||||
|
||||
export function withLabel(color: 'red' | 'green' | 'blue' | 'cyan', label: string, message: string) {
|
||||
return `${c.bold(c.inverse(c[color](` ${label} `)))} ${c[color](message)}`
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { x } from 'tinyexec'
|
||||
import { expect, test } from 'vitest'
|
||||
|
||||
// use "x" directly since "runVitestCli" strips color
|
||||
// use "tinyexec" directly since "runVitestCli" strips color
|
||||
|
||||
test('with color', async () => {
|
||||
const proc = await x('vitest', ['run', '--root=./fixtures/console-color'], {
|
||||
@ -14,7 +14,7 @@ test('with color', async () => {
|
||||
},
|
||||
},
|
||||
})
|
||||
expect(proc.stdout).toContain('\n\x1B[33mtrue\x1B[39m\n')
|
||||
expect(proc.stdout).toContain('\x1B[33mtrue\x1B[39m\n')
|
||||
})
|
||||
|
||||
test('without color', async () => {
|
||||
@ -28,5 +28,5 @@ test('without color', async () => {
|
||||
},
|
||||
},
|
||||
})
|
||||
expect(proc.stdout).toContain('\ntrue\n')
|
||||
expect(proc.stdout).toContain('true\n')
|
||||
})
|
||||
|
||||
@ -88,13 +88,13 @@ test('merge reports', async () => {
|
||||
beforeEach
|
||||
test 1-2
|
||||
|
||||
❯ first.test.ts (2 tests | 1 failed) <time>
|
||||
❯ first.test.ts (2 tests | 1 failed) <time>
|
||||
× test 1-2 <time>
|
||||
→ expected 1 to be 2 // Object.is equality
|
||||
stdout | second.test.ts > test 2-1
|
||||
test 2-1
|
||||
|
||||
❯ second.test.ts (3 tests | 1 failed) <time>
|
||||
❯ second.test.ts (3 tests | 1 failed) <time>
|
||||
× test 2-1 <time>
|
||||
→ expected 1 to be 2 // Object.is equality
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user