[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
12

Configure Feed

Select the types of activity you want to include in your feed.

refactor(reporters): base reporter readability improvements (#6889)

authored by

Ari Perkkiö and committed by
GitHub
(Nov 11, 2024, 9:56 AM +0200) 00ebea64 9b3c3de2

+252 -342
+3 -3
test/config/test/console-color.test.ts
··· 1 1 import { x } from 'tinyexec' 2 2 import { expect, test } from 'vitest' 3 3 4 - // use "x" directly since "runVitestCli" strips color 4 + // use "tinyexec" directly since "runVitestCli" strips color 5 5 6 6 test('with color', async () => { 7 7 const proc = await x('vitest', ['run', '--root=./fixtures/console-color'], { ··· 14 14 }, 15 15 }, 16 16 }) 17 - expect(proc.stdout).toContain('\n\x1B[33mtrue\x1B[39m\n') 17 + expect(proc.stdout).toContain('\x1B[33mtrue\x1B[39m\n') 18 18 }) 19 19 20 20 test('without color', async () => { ··· 28 28 }, 29 29 }, 30 30 }) 31 - expect(proc.stdout).toContain('\ntrue\n') 31 + expect(proc.stdout).toContain('true\n') 32 32 })
+2 -2
test/reporters/tests/merge-reports.test.ts
··· 88 88 beforeEach 89 89 test 1-2 90 90 91 - ❯ first.test.ts (2 tests | 1 failed) <time> 91 + ❯ first.test.ts (2 tests | 1 failed) <time> 92 92 × test 1-2 <time> 93 93 → expected 1 to be 2 // Object.is equality 94 94 stdout | second.test.ts > test 2-1 95 95 test 2-1 96 96 97 - ❯ second.test.ts (3 tests | 1 failed) <time> 97 + ❯ second.test.ts (3 tests | 1 failed) <time> 98 98 × test 2-1 <time> 99 99 → expected 1 to be 2 // Object.is equality 100 100
+27 -50
packages/vitest/src/node/logger.ts
··· 12 12 import c from 'tinyrainbow' 13 13 import { highlightCode } from '../utils/colors' 14 14 import { printError } from './error' 15 - import { divider } from './reporters/renderers/utils' 15 + import { divider, withLabel } from './reporters/renderers/utils' 16 16 import { RandomSequencer } from './sequencers/RandomSequencer' 17 17 18 18 export interface ErrorOptions { ··· 24 24 task?: Task 25 25 showCodeFrame?: boolean 26 26 } 27 + 28 + const PAD = ' ' 27 29 28 30 const ESC = '\x1B[' 29 31 const ERASE_DOWN = `${ESC}J` ··· 64 66 this.console.warn(...args) 65 67 } 66 68 67 - clearFullScreen(message: string) { 69 + clearFullScreen(message = '') { 68 70 if (!this.ctx.config.clearScreen) { 69 71 this.console.log(message) 70 72 return 71 73 } 72 74 73 - this.console.log(`${CLEAR_SCREEN}${ERASE_SCROLLBACK}${message}`) 75 + if (message) { 76 + this.console.log(`${CLEAR_SCREEN}${ERASE_SCROLLBACK}${message}`) 77 + } 78 + else { 79 + (this.outputStream as Writable).write(`${CLEAR_SCREEN}${ERASE_SCROLLBACK}`) 80 + } 74 81 } 75 82 76 83 clearScreen(message: string, force = false) { ··· 201 208 printBanner() { 202 209 this.log() 203 210 204 - const versionTest = this.ctx.config.watch 205 - ? c.blue(`v${this.ctx.version}`) 206 - : c.cyan(`v${this.ctx.version}`) 207 - const mode = this.ctx.config.watch ? c.blue(' DEV ') : c.cyan(' RUN ') 211 + const color = this.ctx.config.watch ? 'blue' : 'cyan' 212 + const mode = this.ctx.config.watch ? 'DEV' : 'RUN' 208 213 209 - this.log( 210 - `${c.inverse(c.bold(mode))} ${versionTest} ${c.gray( 211 - this.ctx.config.root, 212 - )}`, 213 - ) 214 + this.log(withLabel(color, mode, `v${this.ctx.version} `) + c.gray(this.ctx.config.root)) 214 215 215 216 if (this.ctx.config.sequence.sequencer === RandomSequencer) { 216 - this.log( 217 - c.gray( 218 - ` Running tests with seed "${this.ctx.config.sequence.seed}"`, 219 - ), 220 - ) 217 + this.log(PAD + c.gray(`Running tests with seed "${this.ctx.config.sequence.seed}"`)) 221 218 } 222 219 223 220 this.ctx.projects.forEach((project) => { ··· 231 228 const origin = resolvedUrls?.local[0] ?? resolvedUrls?.network[0] 232 229 const provider = project.browser.provider.name 233 230 const providerString = provider === 'preview' ? '' : ` by ${provider}` 234 - this.log( 235 - c.dim( 236 - c.green( 237 - ` ${output} Browser runner started${providerString} at ${new URL('/', origin)}`, 238 - ), 239 - ), 240 - ) 231 + 232 + this.log(PAD + c.dim(c.green(`${output} Browser runner started${providerString} at ${new URL('/', origin)}`))) 241 233 }) 242 234 243 235 if (this.ctx.config.ui) { 244 - this.log( 245 - c.dim( 246 - c.green( 247 - ` UI started at http://${ 248 - this.ctx.config.api?.host || 'localhost' 249 - }:${c.bold(`${this.ctx.server.config.server.port}`)}${ 250 - this.ctx.config.uiBase 251 - }`, 252 - ), 253 - ), 254 - ) 236 + const host = this.ctx.config.api?.host || 'localhost' 237 + const port = this.ctx.server.config.server.port 238 + const base = this.ctx.config.uiBase 239 + 240 + this.log(PAD + c.dim(c.green(`UI started at http://${host}:${c.bold(port)}${base}`))) 255 241 } 256 242 else if (this.ctx.config.api?.port) { 257 243 const resolvedUrls = this.ctx.server.resolvedUrls 258 244 // workaround for https://github.com/vitejs/vite/issues/15438, it was fixed in vite 5.1 259 - const fallbackUrl = `http://${this.ctx.config.api.host || 'localhost'}:${ 260 - this.ctx.config.api.port 261 - }` 262 - const origin 263 - = resolvedUrls?.local[0] ?? resolvedUrls?.network[0] ?? fallbackUrl 264 - this.log(c.dim(c.green(` API started at ${new URL('/', origin)}`))) 245 + const fallbackUrl = `http://${this.ctx.config.api.host || 'localhost'}:${this.ctx.config.api.port}` 246 + const origin = resolvedUrls?.local[0] ?? resolvedUrls?.network[0] ?? fallbackUrl 247 + 248 + this.log(PAD + c.dim(c.green(`API started at ${new URL('/', origin)}`))) 265 249 } 266 250 267 251 if (this.ctx.coverageProvider) { 268 - this.log( 269 - c.dim(' Coverage enabled with ') 270 - + c.yellow(this.ctx.coverageProvider.name), 271 - ) 252 + this.log(PAD + c.dim('Coverage enabled with ') + c.yellow(this.ctx.coverageProvider.name)) 272 253 } 273 254 274 255 if (this.ctx.config.standalone) { 275 - this.log( 276 - c.yellow( 277 - `\nVitest is running in standalone mode. Edit a test file to rerun tests.`, 278 - ), 279 - ) 256 + this.log(c.yellow(`\nVitest is running in standalone mode. Edit a test file to rerun tests.`)) 280 257 } 281 258 else { 282 259 this.log()
+213 -286
packages/vitest/src/node/reporters/base.ts
··· 11 11 import { isCI, isDeno, isNode } from '../../utils/env' 12 12 import { hasFailedSnapshot } from '../../utils/tasks' 13 13 import { F_CHECK, F_POINTER, F_RIGHT } from './renderers/figures' 14 - import { 15 - countTestErrors, 16 - divider, 17 - formatProjectName, 18 - formatTimeString, 19 - getStateString, 20 - getStateSymbol, 21 - renderSnapshotSummary, 22 - taskFail, 23 - } from './renderers/utils' 14 + import { countTestErrors, divider, formatProjectName, formatTimeString, getStateString, getStateSymbol, renderSnapshotSummary, taskFail, withLabel } from './renderers/utils' 24 15 25 16 const BADGE_PADDING = ' ' 26 - const HELP_HINT = `${c.dim('press ')}${c.bold('h')}${c.dim(' to show help')}` 27 - const HELP_UPDATE_SNAP 28 - = c.dim('press ') + c.bold(c.yellow('u')) + c.dim(' to update snapshot') 29 - const HELP_QUITE = `${c.dim('press ')}${c.bold('q')}${c.dim(' to quit')}` 30 - 31 - const WAIT_FOR_CHANGE_PASS = `\n${c.bold( 32 - c.inverse(c.green(' PASS ')), 33 - )}${c.green(' Waiting for file changes...')}` 34 - const WAIT_FOR_CHANGE_FAIL = `\n${c.bold(c.inverse(c.red(' FAIL ')))}${c.red( 35 - ' Tests failed. Watching for file changes...', 36 - )}` 37 - const WAIT_FOR_CHANGE_CANCELLED = `\n${c.bold( 38 - c.inverse(c.red(' CANCELLED ')), 39 - )}${c.red(' Test run cancelled. Watching for file changes...')}` 40 - 41 17 const LAST_RUN_LOG_TIMEOUT = 1_500 42 18 43 19 export interface BaseOptions { ··· 55 31 protected verbose = false 56 32 57 33 private _filesInWatchMode = new Map<string, number>() 34 + private _timeStart = formatTimeString(new Date()) 58 35 private _lastRunTimeout = 0 59 36 private _lastRunTimer: NodeJS.Timeout | undefined 60 37 private _lastRunCount = 0 61 - private _timeStart = new Date() 62 38 63 39 constructor(options: BaseOptions = {}) { 64 40 this.isTTY = options.isTTY ?? ((isNode || isDeno) && process.stdout?.isTTY && !isCI) 65 41 } 66 42 67 - get mode() { 68 - return this.ctx.config.mode 69 - } 70 - 71 43 onInit(ctx: Vitest) { 72 44 this.ctx = ctx 73 - ctx.logger.printBanner() 45 + 46 + this.ctx.logger.printBanner() 74 47 this.start = performance.now() 48 + } 49 + 50 + log(...messages: any) { 51 + this.ctx.logger.log(...messages) 52 + } 53 + 54 + error(...messages: any) { 55 + this.ctx.logger.error(...messages) 75 56 } 76 57 77 58 relative(path: string) { 78 59 return relative(this.ctx.config.root, path) 79 60 } 80 61 81 - onFinished( 82 - files = this.ctx.state.getFiles(), 83 - errors = this.ctx.state.getUnhandledErrors(), 84 - ) { 62 + onFinished(files = this.ctx.state.getFiles(), errors = this.ctx.state.getUnhandledErrors()) { 85 63 this.end = performance.now() 86 - 87 64 this.reportSummary(files, errors) 88 65 } 89 66 ··· 93 70 } 94 71 for (const pack of packs) { 95 72 const task = this.ctx.state.idMap.get(pack[0]) 73 + 96 74 if (task) { 97 75 this.printTask(task) 98 76 } ··· 106 84 || task.result?.state === 'run') { 107 85 return 108 86 } 109 - const logger = this.ctx.logger 110 87 111 88 const tests = getTests(task) 112 89 const failed = tests.filter(t => t.result?.state === 'fail') 113 - const skipped = tests.filter( 114 - t => t.mode === 'skip' || t.mode === 'todo', 115 - ) 90 + const skipped = tests.filter(t => t.mode === 'skip' || t.mode === 'todo') 91 + 116 92 let state = c.dim(`${tests.length} test${tests.length > 1 ? 's' : ''}`) 93 + 117 94 if (failed.length) { 118 - state += ` ${c.dim('|')} ${c.red(`${failed.length} failed`)}` 119 - } 120 - if (skipped.length) { 121 - state += ` ${c.dim('|')} ${c.yellow(`${skipped.length} skipped`)}` 122 - } 123 - let suffix = c.dim(' (') + state + c.dim(')') 124 - suffix += this.getDurationPrefix(task) 125 - if (this.ctx.config.logHeapUsage && task.result.heap != null) { 126 - suffix += c.magenta( 127 - ` ${Math.floor(task.result.heap / 1024 / 1024)} MB heap used`, 128 - ) 95 + state += c.dim(' | ') + c.red(`${failed.length} failed`) 129 96 } 130 97 131 - let title = ` ${getStateSymbol(task)} ` 98 + if (skipped.length) { 99 + state += c.dim(' | ') + c.yellow(`${skipped.length} skipped`) 100 + } 101 + 102 + let suffix = c.dim('(') + state + c.dim(')') + this.getDurationPrefix(task) 103 + 104 + if (this.ctx.config.logHeapUsage && task.result.heap != null) { 105 + suffix += c.magenta(` ${Math.floor(task.result.heap / 1024 / 1024)} MB heap used`) 106 + } 107 + 108 + let title = getStateSymbol(task) 109 + 132 110 if (task.meta.typecheck) { 133 - title += `${c.bgBlue(c.bold(' TS '))} ` 111 + title += ` ${c.bgBlue(c.bold(' TS '))}` 134 112 } 113 + 135 114 if (task.projectName) { 136 - title += formatProjectName(task.projectName) 115 + title += ` ${formatProjectName(task.projectName, '')}` 137 116 } 138 - title += `${task.name} ${suffix}` 139 - logger.log(title) 117 + 118 + this.log(` ${title} ${task.name} ${suffix}`) 140 119 141 120 for (const test of tests) { 142 121 const duration = test.result?.duration 122 + 143 123 if (test.result?.state === 'fail') { 144 124 const suffix = this.getDurationPrefix(test) 145 - logger.log(c.red(` ${taskFail} ${getTestName(test, c.dim(' > '))}${suffix}`)) 125 + this.log(c.red(` ${taskFail} ${getTestName(test, c.dim(' > '))}${suffix}`)) 146 126 147 127 test.result?.errors?.forEach((e) => { 148 128 // print short errors, full errors will be at the end in summary 149 - logger.log(c.red(` ${F_RIGHT} ${(e as any)?.message}`)) 129 + this.log(c.red(` ${F_RIGHT} ${e?.message}`)) 150 130 }) 151 131 } 132 + 152 133 // also print slow tests 153 134 else if (duration && duration > this.ctx.config.slowTestThreshold) { 154 - logger.log( 155 - ` ${c.yellow(c.dim(F_CHECK))} ${getTestName(test, c.dim(' > '))}${c.yellow( 156 - ` ${Math.round(duration)}${c.dim('ms')}`, 157 - )}`, 135 + this.log( 136 + ` ${c.yellow(c.dim(F_CHECK))} ${getTestName(test, c.dim(' > '))}` 137 + + ` ${c.yellow(Math.round(duration) + c.dim('ms'))}`, 158 138 ) 159 139 } 160 140 } ··· 164 144 if (!task.result?.duration) { 165 145 return '' 166 146 } 147 + 167 148 const color = task.result.duration > this.ctx.config.slowTestThreshold 168 149 ? c.yellow 169 150 : c.gray 151 + 170 152 return color(` ${Math.round(task.result.duration)}${c.dim('ms')}`) 171 153 } 172 154 173 - onWatcherStart( 174 - files = this.ctx.state.getFiles(), 175 - errors = this.ctx.state.getUnhandledErrors(), 176 - ) { 155 + onWatcherStart(files = this.ctx.state.getFiles(), errors = this.ctx.state.getUnhandledErrors()) { 177 156 this.resetLastRunLog() 178 157 179 158 const failed = errors.length > 0 || hasFailed(files) 180 - const failedSnap = hasFailedSnapshot(files) 181 - const cancelled = this.ctx.isCancelling 182 159 183 160 if (failed) { 184 - this.ctx.logger.log(WAIT_FOR_CHANGE_FAIL) 161 + this.log(withLabel('red', 'FAIL', 'Tests failed. Watching for file changes...')) 185 162 } 186 - else if (cancelled) { 187 - this.ctx.logger.log(WAIT_FOR_CHANGE_CANCELLED) 188 - } 189 - else { 190 - this.ctx.logger.log(WAIT_FOR_CHANGE_PASS) 191 - } 192 - 193 - const hints: string[] = [] 194 - hints.push(HELP_HINT) 195 - if (failedSnap) { 196 - hints.unshift(HELP_UPDATE_SNAP) 163 + else if (this.ctx.isCancelling) { 164 + this.log(withLabel('red', 'CANCELLED', 'Test run cancelled. Watching for file changes...')) 197 165 } 198 166 else { 199 - hints.push(HELP_QUITE) 167 + this.log(withLabel('green', 'PASS', 'Waiting for file changes...')) 200 168 } 201 169 202 - this.ctx.logger.log(BADGE_PADDING + hints.join(c.dim(', '))) 170 + const hints = [c.dim('press ') + c.bold('h') + c.dim(' to show help')] 171 + 172 + if (hasFailedSnapshot(files)) { 173 + hints.unshift(c.dim('press ') + c.bold(c.yellow('u')) + c.dim(' to update snapshot')) 174 + } 175 + else { 176 + hints.push(c.dim('press ') + c.bold('q') + c.dim(' to quit')) 177 + } 178 + 179 + this.log(BADGE_PADDING + hints.join(c.dim(', '))) 203 180 204 181 if (this._lastRunCount) { 205 182 const LAST_RUN_TEXT = `rerun x${this._lastRunCount}` ··· 233 210 onWatcherRerun(files: string[], trigger?: string) { 234 211 this.resetLastRunLog() 235 212 this.watchFilters = files 236 - this.failedUnwatchedFiles = this.ctx.state.getFiles().filter((file) => { 237 - return !files.includes(file.filepath) && hasFailed(file) 238 - }) 213 + this.failedUnwatchedFiles = this.ctx.state.getFiles().filter(file => 214 + !files.includes(file.filepath) && hasFailed(file), 215 + ) 239 216 217 + // Update re-run count for each file 240 218 files.forEach((filepath) => { 241 219 let reruns = this._filesInWatchMode.get(filepath) ?? 0 242 220 this._filesInWatchMode.set(filepath, ++reruns) 243 221 }) 244 222 245 - const BADGE = c.inverse(c.bold(c.blue(' RERUN '))) 246 - const TRIGGER = trigger ? c.dim(` ${this.relative(trigger)}`) : '' 247 - const FILENAME_PATTERN = this.ctx.filenamePattern 248 - ? `${BADGE_PADDING} ${c.dim('Filename pattern: ')}${c.blue( 249 - this.ctx.filenamePattern, 250 - )}\n` 251 - : '' 252 - const TESTNAME_PATTERN = this.ctx.configOverride.testNamePattern 253 - ? `${BADGE_PADDING} ${c.dim('Test name pattern: ')}${c.blue( 254 - String(this.ctx.configOverride.testNamePattern), 255 - )}\n` 256 - : '' 257 - const PROJECT_FILTER = this.ctx.configOverride.project 258 - ? `${BADGE_PADDING} ${c.dim('Project name: ')}${c.blue( 259 - toArray(this.ctx.configOverride.project).join(', '), 260 - )}\n` 261 - : '' 223 + let banner = trigger ? c.dim(`${this.relative(trigger)} `) : '' 262 224 263 225 if (files.length > 1 || !files.length) { 264 226 // we need to figure out how to handle rerun all from stdin 265 - this.ctx.logger.clearFullScreen( 266 - `\n${BADGE}${TRIGGER}\n${PROJECT_FILTER}${FILENAME_PATTERN}${TESTNAME_PATTERN}`, 267 - ) 268 227 this._lastRunCount = 0 269 228 } 270 229 else if (files.length === 1) { 271 230 const rerun = this._filesInWatchMode.get(files[0]) ?? 1 272 - this._lastRunCount = rerun 273 - this.ctx.logger.clearFullScreen( 274 - `\n${BADGE}${TRIGGER} ${c.blue( 275 - `x${rerun}`, 276 - )}\n${PROJECT_FILTER}${FILENAME_PATTERN}${TESTNAME_PATTERN}`, 277 - ) 231 + banner += c.blue(`x${rerun} `) 278 232 } 233 + 234 + this.ctx.logger.clearFullScreen() 235 + this.log(withLabel('blue', 'RERUN', banner)) 236 + 237 + if (this.ctx.configOverride.project) { 238 + this.log(BADGE_PADDING + c.dim(' Project name: ') + c.blue(toArray(this.ctx.configOverride.project).join(', '))) 239 + } 240 + 241 + if (this.ctx.filenamePattern) { 242 + this.log(BADGE_PADDING + c.dim(' Filename pattern: ') + c.blue(this.ctx.filenamePattern)) 243 + } 244 + 245 + if (this.ctx.configOverride.testNamePattern) { 246 + this.log(BADGE_PADDING + c.dim(' Test name pattern: ') + c.blue(String(this.ctx.configOverride.testNamePattern))) 247 + } 248 + 249 + this.log('') 279 250 280 251 if (!this.isTTY) { 281 252 for (const task of this.failedUnwatchedFiles) { ··· 283 254 } 284 255 } 285 256 286 - this._timeStart = new Date() 257 + this._timeStart = formatTimeString(new Date()) 287 258 this.start = performance.now() 288 259 } 289 260 ··· 291 262 if (!this.shouldLog(log)) { 292 263 return 293 264 } 294 - const task = log.taskId ? this.ctx.state.idMap.get(log.taskId) : undefined 295 - const header = c.gray( 296 - log.type 297 - + c.dim( 298 - ` | ${ 299 - task 300 - ? getFullName(task, c.dim(' > ')) 301 - : log.taskId !== '__vitest__unknown_test__' 302 - ? log.taskId 303 - : 'unknown test' 304 - }`, 305 - ), 306 - ) 307 265 308 266 const output 309 267 = log.type === 'stdout' 310 268 ? this.ctx.logger.outputStream 311 269 : this.ctx.logger.errorStream 270 + 312 271 const write = (msg: string) => (output as any).write(msg) 313 272 314 - write(`${header}\n${log.content}`) 273 + let headerText = 'unknown test' 274 + const task = log.taskId ? this.ctx.state.idMap.get(log.taskId) : undefined 275 + 276 + if (task) { 277 + headerText = getFullName(task, c.dim(' > ')) 278 + } 279 + else if (log.taskId && log.taskId !== '__vitest__unknown_test__') { 280 + headerText = log.taskId 281 + } 282 + 283 + write(c.gray(log.type + c.dim(` | ${headerText}\n`)) + log.content) 315 284 316 285 if (log.origin) { 317 286 // browser logs don't have an extra end of line at the end like Node.js does ··· 327 296 ? (project.browser?.parseStacktrace(log.origin) || []) 328 297 : parseStacktrace(log.origin) 329 298 330 - const highlight = task 331 - ? stack.find(i => i.file === task.file.filepath) 332 - : null 299 + const highlight = task && stack.find(i => i.file === task.file.filepath) 300 + 333 301 for (const frame of stack) { 334 302 const color = frame === highlight ? c.cyan : c.gray 335 303 const path = relative(project.config.root, frame.file) 336 304 337 - write( 338 - color( 339 - ` ${c.dim(F_POINTER)} ${[ 340 - frame.method, 341 - `${path}:${c.dim(`${frame.line}:${frame.column}`)}`, 342 - ] 343 - .filter(Boolean) 344 - .join(' ')}\n`, 345 - ), 346 - ) 305 + const positions = [ 306 + frame.method, 307 + `${path}:${c.dim(`${frame.line}:${frame.column}`)}`, 308 + ] 309 + .filter(Boolean) 310 + .join(' ') 311 + 312 + write(color(` ${c.dim(F_POINTER)} ${positions}\n`)) 347 313 } 348 314 } 349 315 350 316 write('\n') 317 + } 318 + 319 + onTestRemoved(trigger?: string) { 320 + this.log(c.yellow('Test removed...') + (trigger ? c.dim(` [ ${this.relative(trigger)} ]\n`) : '')) 351 321 } 352 322 353 323 shouldLog(log: UserConsoleLog) { ··· 362 332 } 363 333 364 334 onServerRestart(reason?: string) { 365 - this.ctx.logger.log( 366 - c.bold( 367 - c.magenta( 368 - reason === 'config' 369 - ? '\nRestarting due to config changes...' 370 - : '\nRestarting Vitest...', 371 - ), 372 - ), 373 - ) 335 + this.log(c.bold(c.magenta( 336 + reason === 'config' 337 + ? '\nRestarting due to config changes...' 338 + : '\nRestarting Vitest...', 339 + ))) 374 340 } 375 341 376 342 reportSummary(files: File[], errors: unknown[]) { 377 343 this.printErrorsSummary(files, errors) 378 - if (this.mode === 'benchmark') { 344 + 345 + if (this.ctx.config.mode === 'benchmark') { 379 346 this.reportBenchmarkSummary(files) 380 347 } 381 348 else { ··· 389 356 ...files, 390 357 ] 391 358 const tests = getTests(affectedFiles) 392 - const logger = this.ctx.logger 393 - 394 - const executionTime = this.end - this.start 395 - const collectTime = files.reduce( 396 - (acc, test) => acc + Math.max(0, test.collectDuration || 0), 397 - 0, 398 - ) 399 - const setupTime = files.reduce( 400 - (acc, test) => acc + Math.max(0, test.setupDuration || 0), 401 - 0, 402 - ) 403 - const testsTime = files.reduce( 404 - (acc, test) => acc + Math.max(0, test.result?.duration || 0), 405 - 0, 406 - ) 407 - const transformTime = this.ctx.projects 408 - .flatMap(w => w.vitenode.getTotalDuration()) 409 - .reduce((a, b) => a + b, 0) 410 - const environmentTime = files.reduce( 411 - (acc, file) => acc + Math.max(0, file.environmentLoad || 0), 412 - 0, 413 - ) 414 - const prepareTime = files.reduce( 415 - (acc, file) => acc + Math.max(0, file.prepareDuration || 0), 416 - 0, 417 - ) 418 - const threadTime = collectTime + testsTime + setupTime 419 - 420 - // show top 10 costly transform module 421 - // console.log(Array.from(this.ctx.vitenode.fetchCache.entries()).filter(i => i[1].duration) 422 - // .sort((a, b) => b[1].duration! - a[1].duration!) 423 - // .map(i => `${time(i[1].duration!)} ${i[0]}`) 424 - // .slice(0, 10) 425 - // .join('\n'), 426 - // ) 427 359 428 360 const snapshotOutput = renderSnapshotSummary( 429 361 this.ctx.config.root, 430 362 this.ctx.snapshot.summary, 431 363 ) 432 - if (snapshotOutput.length) { 433 - logger.log( 434 - snapshotOutput 435 - .map((t, i) => 436 - i === 0 ? `${padTitle('Snapshots')} ${t}` : `${padTitle('')} ${t}`, 437 - ) 438 - .join('\n'), 439 - ) 440 - if (snapshotOutput.length > 1) { 441 - logger.log() 442 - } 364 + 365 + for (const [index, snapshot] of snapshotOutput.entries()) { 366 + const title = index === 0 ? 'Snapshots' : '' 367 + this.log(`${padTitle(title)} ${snapshot}`) 443 368 } 444 369 445 - logger.log(padTitle('Test Files'), getStateString(affectedFiles)) 446 - logger.log(padTitle('Tests'), getStateString(tests)) 370 + if (snapshotOutput.length > 1) { 371 + this.log() 372 + } 373 + 374 + this.log(padTitle('Test Files'), getStateString(affectedFiles)) 375 + this.log(padTitle('Tests'), getStateString(tests)) 376 + 447 377 if (this.ctx.projects.some(c => c.config.typecheck.enabled)) { 448 - const failed = tests.filter( 449 - t => t.meta?.typecheck && t.result?.errors?.length, 450 - ) 451 - logger.log( 378 + const failed = tests.filter(t => t.meta?.typecheck && t.result?.errors?.length) 379 + 380 + this.log( 452 381 padTitle('Type Errors'), 453 382 failed.length 454 383 ? c.bold(c.red(`${failed.length} failed`)) 455 384 : c.dim('no errors'), 456 385 ) 457 386 } 387 + 458 388 if (errors.length) { 459 - logger.log( 389 + this.log( 460 390 padTitle('Errors'), 461 391 c.bold(c.red(`${errors.length} error${errors.length > 1 ? 's' : ''}`)), 462 392 ) 463 393 } 464 - logger.log(padTitle('Start at'), formatTimeString(this._timeStart)) 394 + 395 + this.log(padTitle('Start at'), this._timeStart) 396 + 397 + const collectTime = sum(files, file => file.collectDuration) 398 + const testsTime = sum(files, file => file.result?.duration) 399 + const setupTime = sum(files, file => file.setupDuration) 400 + 465 401 if (this.watchFilters) { 466 - logger.log(padTitle('Duration'), time(threadTime)) 402 + this.log(padTitle('Duration'), time(collectTime + testsTime + setupTime)) 467 403 } 468 404 else { 469 - let timers = `transform ${time(transformTime)}, setup ${time( 470 - setupTime, 471 - )}, collect ${time(collectTime)}, tests ${time( 472 - testsTime, 473 - )}, environment ${time(environmentTime)}, prepare ${time(prepareTime)}` 474 - const typecheck = this.ctx.projects.reduce( 475 - (acc, c) => acc + (c.typechecker?.getResult().time || 0), 476 - 0, 477 - ) 478 - if (typecheck) { 479 - timers += `, typecheck ${time(typecheck)}` 480 - } 481 - logger.log( 482 - padTitle('Duration'), 483 - time(executionTime) + c.dim(` (${timers})`), 484 - ) 405 + const executionTime = this.end - this.start 406 + const environmentTime = sum(files, file => file.environmentLoad) 407 + const prepareTime = sum(files, file => file.prepareDuration) 408 + const transformTime = sum(this.ctx.projects, project => project.vitenode.getTotalDuration()) 409 + const typecheck = sum(this.ctx.projects, project => project.typechecker?.getResult().time) 410 + 411 + const timers = [ 412 + `transform ${time(transformTime)}`, 413 + `setup ${time(setupTime)}`, 414 + `collect ${time(collectTime)}`, 415 + `tests ${time(testsTime)}`, 416 + `environment ${time(environmentTime)}`, 417 + `prepare ${time(prepareTime)}`, 418 + typecheck && `typecheck ${time(typecheck)}`, 419 + ].filter(Boolean).join(', ') 420 + 421 + this.log(padTitle('Duration'), time(executionTime) + c.dim(` (${timers})`)) 485 422 } 486 423 487 - logger.log() 424 + this.log() 488 425 } 489 426 490 427 private printErrorsSummary(files: File[], errors: unknown[]) { 491 - const logger = this.ctx.logger 492 428 const suites = getSuites(files) 493 429 const tests = getTests(files) 494 430 495 431 const failedSuites = suites.filter(i => i.result?.errors) 496 432 const failedTests = tests.filter(i => i.result?.state === 'fail') 497 - const failedTotal 498 - = countTestErrors(failedSuites) + countTestErrors(failedTests) 433 + const failedTotal = countTestErrors(failedSuites) + countTestErrors(failedTests) 499 434 500 435 let current = 1 501 - 502 - const errorDivider = () => 503 - logger.error( 504 - `${c.red( 505 - c.dim(divider(`[${current++}/${failedTotal}]`, undefined, 1)), 506 - )}\n`, 507 - ) 436 + const errorDivider = () => this.error(`${c.red(c.dim(divider(`[${current++}/${failedTotal}]`, undefined, 1)))}\n`) 508 437 509 438 if (failedSuites.length) { 510 - logger.error( 511 - c.red( 512 - divider(c.bold(c.inverse(` Failed Suites ${failedSuites.length} `))), 513 - ), 514 - ) 515 - logger.error() 439 + this.error(`${errorBanner(`Failed Suites ${failedSuites.length}`)}\n`) 516 440 this.printTaskErrors(failedSuites, errorDivider) 517 441 } 518 442 519 443 if (failedTests.length) { 520 - logger.error( 521 - c.red( 522 - divider(c.bold(c.inverse(` Failed Tests ${failedTests.length} `))), 523 - ), 524 - ) 525 - logger.error() 526 - 444 + this.error(`${errorBanner(`Failed Tests ${failedTests.length}`)}\n`) 527 445 this.printTaskErrors(failedTests, errorDivider) 528 446 } 447 + 529 448 if (errors.length) { 530 - logger.printUnhandledErrors(errors) 531 - logger.error() 449 + this.ctx.logger.printUnhandledErrors(errors) 450 + this.error() 532 451 } 533 - return tests 534 452 } 535 453 536 454 reportBenchmarkSummary(files: File[]) { 537 - const logger = this.ctx.logger 538 455 const benches = getTests(files) 539 - 540 456 const topBenches = benches.filter(i => i.result?.benchmark?.rank === 1) 541 457 542 - logger.log( 543 - `\n${c.cyan(c.inverse(c.bold(' BENCH ')))} ${c.cyan('Summary')}\n`, 544 - ) 458 + this.log(withLabel('cyan', 'BENCH', 'Summary\n')) 459 + 545 460 for (const bench of topBenches) { 546 461 const group = bench.suite || bench.file 462 + 547 463 if (!group) { 548 464 continue 549 465 } 466 + 550 467 const groupName = getFullName(group, c.dim(' > ')) 551 - logger.log(` ${bench.name}${c.dim(` - ${groupName}`)}`) 468 + this.log(` ${bench.name}${c.dim(` - ${groupName}`)}`) 469 + 552 470 const siblings = group.tasks 553 471 .filter(i => i.meta.benchmark && i.result?.benchmark && i !== bench) 554 472 .sort((a, b) => a.result!.benchmark!.rank - b.result!.benchmark!.rank) 555 - if (siblings.length === 0) { 556 - logger.log('') 557 - continue 558 - } 473 + 559 474 for (const sibling of siblings) { 560 - const number = `${( 561 - sibling.result!.benchmark!.mean / bench.result!.benchmark!.mean 562 - ).toFixed(2)}x` 563 - logger.log( 564 - ` ${c.green(number)} ${c.gray('faster than')} ${sibling.name}`, 565 - ) 475 + const number = (sibling.result!.benchmark!.mean / bench.result!.benchmark!.mean).toFixed(2) 476 + this.log(c.green(` ${number}x `) + c.gray('faster than ') + sibling.name) 566 477 } 567 - logger.log('') 478 + 479 + this.log('') 568 480 } 569 481 } 570 482 571 483 private printTaskErrors(tasks: Task[], errorDivider: () => void) { 572 484 const errorsQueue: [error: ErrorWithDiff | undefined, tests: Task[]][] = [] 485 + 573 486 for (const task of tasks) { 574 - // merge identical errors 487 + // Merge identical errors 575 488 task.result?.errors?.forEach((error) => { 576 - const errorItem 577 - = error?.stackStr 578 - && errorsQueue.find((i) => { 579 - const hasStr = i[0]?.stackStr === error.stackStr 580 - if (!hasStr) { 489 + let previous 490 + 491 + if (error?.stackStr) { 492 + previous = errorsQueue.find((i) => { 493 + if (i[0]?.stackStr !== error.stackStr) { 581 494 return false 582 495 } 583 - const currentProjectName 584 - = (task as File)?.projectName || task.file?.projectName || '' 585 - const projectName 586 - = (i[1][0] as File)?.projectName || i[1][0].file?.projectName || '' 496 + 497 + const currentProjectName = (task as File)?.projectName || task.file?.projectName || '' 498 + const projectName = (i[1][0] as File)?.projectName || i[1][0].file?.projectName || '' 499 + 587 500 return projectName === currentProjectName 588 501 }) 589 - if (errorItem) { 590 - errorItem[1].push(task) 502 + } 503 + 504 + if (previous) { 505 + previous[1].push(task) 591 506 } 592 507 else { 593 508 errorsQueue.push([error, [task]]) 594 509 } 595 510 }) 596 511 } 512 + 597 513 for (const [error, tasks] of errorsQueue) { 598 514 for (const task of tasks) { 599 515 const filepath = (task as File)?.filepath || '' 600 - const projectName 601 - = (task as File)?.projectName || task.file?.projectName || '' 516 + const projectName = (task as File)?.projectName || task.file?.projectName || '' 517 + 602 518 let name = getFullName(task, c.dim(' > ')) 519 + 603 520 if (filepath) { 604 - name = `${name} ${c.dim(`[ ${this.relative(filepath)} ]`)}` 521 + name += c.dim(` [ ${this.relative(filepath)} ]`) 605 522 } 606 523 607 524 this.ctx.logger.error( 608 - `${c.red(c.bold(c.inverse(' FAIL ')))} ${formatProjectName( 609 - projectName, 610 - )}${name}`, 525 + `${c.red(c.bold(c.inverse(' FAIL ')))}${formatProjectName(projectName)} ${name}`, 611 526 ) 612 527 } 613 - const screenshots = tasks.filter(t => t.meta?.failScreenshotPath).map(t => t.meta?.failScreenshotPath as string) 614 - const project = this.ctx.getProjectByTaskId(tasks[0].id) 528 + 529 + const screenshotPaths = tasks.map(t => t.meta?.failScreenshotPath).filter(screenshot => screenshot != null) 530 + 615 531 this.ctx.logger.printError(error, { 616 - project, 532 + project: this.ctx.getProjectByTaskId(tasks[0].id), 617 533 verbose: this.verbose, 618 - screenshotPaths: screenshots, 534 + screenshotPaths, 619 535 task: tasks[0], 620 536 }) 537 + 621 538 errorDivider() 622 539 } 623 540 } 541 + } 542 + 543 + function errorBanner(message: string) { 544 + return c.red(divider(c.bold(c.inverse(` ${message} `)))) 624 545 } 625 546 626 547 function padTitle(str: string) { ··· 632 553 return `${(time / 1000).toFixed(2)}s` 633 554 } 634 555 return `${Math.round(time)}ms` 556 + } 557 + 558 + function sum<T>(items: T[], cb: (_next: T) => number | undefined) { 559 + return items.reduce((total, next) => { 560 + return total + Math.max(cb(next) || 0, 0) 561 + }, 0) 635 562 }
+1 -1
packages/vitest/src/node/reporters/default.ts
··· 41 41 this.rendererOptions.showHeap = this.ctx.config.logHeapUsage 42 42 this.rendererOptions.slowTestThreshold 43 43 = this.ctx.config.slowTestThreshold 44 - this.rendererOptions.mode = this.mode 44 + this.rendererOptions.mode = this.ctx.config.mode 45 45 const files = this.ctx.state.getFiles(this.watchFilters) 46 46 if (!this.renderer) { 47 47 this.renderer = createListRenderer(files, this.rendererOptions).start()
+6
packages/vitest/src/node/reporters/renderers/utils.ts
··· 254 254 const index = name 255 255 .split('') 256 256 .reduce((acc, v, idx) => acc + v.charCodeAt(0) + idx, 0) 257 + 257 258 const colors = [c.blue, c.yellow, c.cyan, c.green, c.magenta] 259 + 258 260 return colors[index % colors.length](`|${name}|`) + suffix 261 + } 262 + 263 + export function withLabel(color: 'red' | 'green' | 'blue' | 'cyan', label: string, message: string) { 264 + return `${c.bold(c.inverse(c[color](` ${label} `)))} ${c[color](message)}` 259 265 }