diff --git a/packages/vitest/src/node/cli-api.ts b/packages/vitest/src/node/cli-api.ts index 2eb7f719d87a..29ba2b5d1776 100644 --- a/packages/vitest/src/node/cli-api.ts +++ b/packages/vitest/src/node/cli-api.ts @@ -58,8 +58,8 @@ export async function startVitest(cliFilters: string[], options: CliOptions, vit } catch (e) { process.exitCode = 1 - await ctx.printError(e, true, 'Unhandled Error') - ctx.error('\n\n') + await ctx.logger.printError(e, true, 'Unhandled Error') + ctx.logger.error('\n\n') return false } diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index 2e62091d8fc1..088d32eccbd6 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -1,5 +1,4 @@ import { existsSync, promises as fs } from 'fs' -import readline from 'readline' import type { ViteDevServer } from 'vite' import { relative, toNamespacedPath } from 'pathe' import fg from 'fast-glob' @@ -16,8 +15,8 @@ import type { WorkerPool } from './pool' import { createReporters } from './reporters/utils' import { StateManager } from './state' import { resolveConfig } from './config' -import { printError } from './error' import { VitestGit } from './git' +import { Logger } from './logger' import { VitestCache } from './cache' const WATCHER_DEBOUNCE = 100 @@ -32,12 +31,9 @@ export class Vitest { snapshot: SnapshotManager = undefined! cache: VitestCache = undefined! reporters: Reporter[] = undefined! - console: Console + logger: Logger pool: WorkerPool | undefined - outputStream = process.stdout - errorStream = process.stderr - vitenode: ViteNodeServer = undefined! invalidates: Set = new Set() @@ -49,12 +45,12 @@ export class Vitest { restartsCount = 0 runner: ViteNodeRunner = undefined! - private _onRestartListeners: Array<() => void> = [] - constructor() { - this.console = globalThis.console + this.logger = new Logger(this) } + private _onRestartListeners: Array<() => void> = [] + async setServer(options: UserConfig, server: ViteDevServer) { this.unregisterWatcher?.() clearTimeout(this._rerunTimer) @@ -100,7 +96,7 @@ export class Vitest { await this.cache.results.readFromCache() } catch (err) { - this.error(`[vitest] Error, while trying to parse cache in ${this.cache.results.getCachePath()}:`, err) + this.logger.error(`[vitest] Error, while trying to parse cache in ${this.cache.results.getCachePath()}:`, err) } } @@ -132,20 +128,7 @@ export class Vitest { if (!files.length) { const exitCode = this.config.passWithNoTests ? 0 : 1 - const comma = c.dim(', ') - if (filters?.length) - this.console.error(c.dim('filter: ') + c.yellow(filters.join(comma))) - if (this.config.include) - this.console.error(c.dim('include: ') + c.yellow(this.config.include.join(comma))) - if (this.config.exclude) - this.console.error(c.dim('exclude: ') + c.yellow(this.config.exclude.join(comma))) - if (this.config.watchExclude) - this.console.error(c.dim('watch exclude: ') + c.yellow(this.config.watchExclude.join(comma))) - - if (this.config.passWithNoTests) - this.log('No test files found, exiting with code 0\n') - else - this.error(c.red('\nNo test files found, exiting with code 1')) + this.logger.printNoTestFound(filters) process.exit(exitCode) } @@ -193,7 +176,7 @@ export class Vitest { changedSince: this.config.changed, }) if (!related) { - this.error(c.red('Could not find Git root. Have you initialized git with `git init`?\n')) + this.logger.error(c.red('Could not find Git root. Have you initialized git with `git init`?\n')) process.exit(1) } this.config.related = Array.from(new Set(related)) @@ -302,25 +285,6 @@ export class Vitest { } } - log(...args: any[]) { - this.console.log(...args) - } - - error(...args: any[]) { - this.console.error(...args) - } - - clearScreen() { - if (this.server.config.clearScreen === false) - return - - const repeatCount = (process.stdout?.rows ?? 0) - 2 - const blank = repeatCount > 0 ? '\n'.repeat(repeatCount) : '' - this.console.log(blank) - readline.cursorTo(process.stdout, 0, 0) - readline.clearScreenDown(process.stdout) - } - private _rerunTimer: any private async scheduleRerun(triggerId: string) { const currentCount = this.restartsCount @@ -459,7 +423,7 @@ export class Vitest { this.server.close(), ].filter(Boolean)).then((results) => { results.filter(r => r.status === 'rejected').forEach((err) => { - this.error('error during close', (err as PromiseRejectedResult).reason) + this.logger.error('error during close', (err as PromiseRejectedResult).reason) }) }) } @@ -536,14 +500,6 @@ export class Vitest { return code.includes('import.meta.vitest') } - printError(err: unknown, fullStack = false, type?: string) { - return printError(err, this, { - fullStack, - type, - showCodeFrame: true, - }) - } - onServerRestarted(fn: () => void) { this._onRestartListeners.push(fn) } diff --git a/packages/vitest/src/node/error.ts b/packages/vitest/src/node/error.ts index afb1d4123994..824eada57796 100644 --- a/packages/vitest/src/node/error.ts +++ b/packages/vitest/src/node/error.ts @@ -10,6 +10,7 @@ import { stringify } from '../integrations/chai/jest-matcher-utils' import type { Vitest } from './core' import { type DiffOptions, unifiedDiff } from './diff' import { divider } from './reporters/renderers/utils' +import type { Logger } from './logger' export function fileFromParsedStack(stack: ParsedStack) { if (stack?.sourcePos?.source?.startsWith('..')) @@ -47,7 +48,8 @@ export async function printError(error: unknown, ctx: Vitest, options: PrintErro if (type) printErrorType(type, ctx) - printErrorMessage(e, ctx.console) + + printErrorMessage(e, ctx.logger) printStack(ctx, stacks, nearest, errorProperties, (s, pos) => { if (showCodeFrame && s === nearest && nearest) { const file = fileFromParsedStack(nearest) @@ -55,7 +57,7 @@ export async function printError(error: unknown, ctx: Vitest, options: PrintErro // for example, when there is a source map file, but no source in node_modules if (existsSync(file)) { const sourceCode = readFileSync(file, 'utf-8') - ctx.log(c.yellow(generateCodeFrame(sourceCode, 4, pos))) + ctx.logger.log(c.yellow(generateCodeFrame(sourceCode, 4, pos))) } } }) @@ -68,7 +70,7 @@ export async function printError(error: unknown, ctx: Vitest, options: PrintErro handleImportOutsideModuleError(e.stack || e.stackStr || '', ctx) if (e.showDiff) { - displayDiff(stringify(e.actual), stringify(e.expected), ctx.console, { + displayDiff(stringify(e.actual), stringify(e.expected), ctx.logger.console, { outputTruncateLength: ctx.config.outputTruncateLength, outputDiffLines: ctx.config.outputDiffLines, }) @@ -76,7 +78,7 @@ export async function printError(error: unknown, ctx: Vitest, options: PrintErro } function printErrorType(type: string, ctx: Vitest) { - ctx.error(`\n${c.red(divider(c.bold(c.inverse(` ${type} `))))}`) + ctx.logger.error(`\n${c.red(divider(c.bold(c.inverse(` ${type} `))))}`) } const skipErrorProperties = [ @@ -124,7 +126,7 @@ function handleImportOutsideModuleError(stack: string, ctx: Vitest) { else name = name.split('/')[0] - ctx.error(c.yellow( + ctx.logger.error(c.yellow( `Module ${path} seems to be an ES Module but shipped in a CommonJS package. ` + `You might want to create an issue to the package ${c.bold(`"${name}"`)} asking ` + 'them to ship the file in .mjs extension or add "type": "module" in their package.json.' @@ -148,9 +150,9 @@ function displayDiff(actual: string, expected: string, console: Console, options console.error(c.gray(unifiedDiff(actual, expected, options)) + '\n') } -function printErrorMessage(error: ErrorWithDiff, console: Console) { +function printErrorMessage(error: ErrorWithDiff, logger: Logger) { const errorName = error.name || error.nameStr || 'Unknown Error' - console.error(c.red(`${c.bold(errorName)}: ${error.message}`)) + logger.error(c.red(`${c.bold(errorName)}: ${error.message}`)) } function printStack( @@ -163,25 +165,27 @@ function printStack( if (!stack.length) return + const logger = ctx.logger + for (const frame of stack) { const pos = frame.sourcePos || frame const color = frame === highlight ? c.yellow : c.gray const file = fileFromParsedStack(frame) const path = relative(ctx.config.root, file) - ctx.log(color(` ${c.dim(F_POINTER)} ${[frame.method, c.dim(`${path}:${pos.line}:${pos.column}`)].filter(Boolean).join(' ')}`)) + logger.log(color(` ${c.dim(F_POINTER)} ${[frame.method, c.dim(`${path}:${pos.line}:${pos.column}`)].filter(Boolean).join(' ')}`)) onStack?.(frame, pos) // reached at test file, skip the follow stack if (frame.file in ctx.state.filesMap) break } - ctx.log() + logger.log() const hasProperties = Object.keys(errorProperties).length > 0 if (hasProperties) { - ctx.log(c.red(c.dim(divider()))) + logger.log(c.red(c.dim(divider()))) const propertiesString = stringify(errorProperties, 10, { printBasicPrototype: false }) - ctx.log(c.red(c.bold('Serialized Error:')), c.gray(propertiesString)) + logger.log(c.red(c.bold('Serialized Error:')), c.gray(propertiesString)) } } diff --git a/packages/vitest/src/node/logger.ts b/packages/vitest/src/node/logger.ts new file mode 100644 index 000000000000..37e6c24dc59b --- /dev/null +++ b/packages/vitest/src/node/logger.ts @@ -0,0 +1,118 @@ +import { createLogUpdate } from 'log-update' +import c from 'picocolors' +import { version } from '../../../../package.json' +import type { ErrorWithDiff } from '../types' +import { divider } from './reporters/renderers/utils' +import type { Vitest } from './core' +import { printError } from './error' + +export class Logger { + outputStream = process.stdout + errorStream = process.stderr + logUpdate = createLogUpdate(process.stdout) + + private _clearScreenPending: string | undefined + + constructor( + public ctx: Vitest, + public console = globalThis.console, + ) { + + } + + log(...args: any[]) { + this._clearScreen() + this.console.log(...args) + } + + error(...args: any[]) { + this._clearScreen() + this.console.error(...args) + } + + warn(...args: any[]) { + this._clearScreen() + this.console.warn(...args) + } + + clearScreen(message: string, force = false) { + if (this.ctx.server.config.clearScreen === false) { + this.console.log(message) + return + } + + this._clearScreenPending = message + if (force) + this._clearScreen() + } + + private _clearScreen() { + if (!this._clearScreenPending) + return + + const log = this._clearScreenPending + this._clearScreenPending = undefined + // equivalent to ansi-escapes: + // stdout.write(ansiEscapes.cursorTo(0, 0) + ansiEscapes.eraseDown + log) + this.console.log(`\u001B[1;1H\u001B[J${log}`) + } + + printError(err: unknown, fullStack = false, type?: string) { + return printError(err, this.ctx, { + fullStack, + type, + showCodeFrame: true, + }) + } + + printNoTestFound(filters?: string[]) { + const config = this.ctx.config + const comma = c.dim(', ') + if (filters?.length) + this.console.error(c.dim('filter: ') + c.yellow(filters.join(comma))) + if (config.include) + this.console.error(c.dim('include: ') + c.yellow(config.include.join(comma))) + if (config.exclude) + this.console.error(c.dim('exclude: ') + c.yellow(config.exclude.join(comma))) + if (config.watchExclude) + this.console.error(c.dim('watch exclude: ') + c.yellow(config.watchExclude.join(comma))) + + if (config.passWithNoTests) + this.log('No test files found, exiting with code 0\n') + else + this.error(c.red('\nNo test files found, exiting with code 1')) + } + + printBanner() { + this.log() + + const versionTest = this.ctx.config.watch + ? c.blue(`v${version}`) + : c.cyan(`v${version}`) + const mode = this.ctx.config.watch + ? c.blue(' DEV ') + : c.cyan(' RUN ') + + this.log(`${c.inverse(c.bold(mode))} ${versionTest} ${c.gray(this.ctx.config.root)}`) + + 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}`))) + else if (this.ctx.config.api) + this.log(c.dim(c.green(` API started at http://${this.ctx.config.api?.host || 'localhost'}:${c.bold(`${this.ctx.config.api.port}`)}`))) + + this.log() + } + + async printUnhandledErrors(errors: unknown[]) { + const errorMessage = c.red(c.bold( + `\nVitest caught ${errors.length} unhandled error${errors.length > 1 ? 's' : ''} during the test run. This might cause false positive tests.` + + '\nPlease, resolve all the errors to make sure your tests are not affected.', + )) + this.log(c.red(divider(c.bold(c.inverse(' Unhandled Errors '))))) + this.log(errorMessage) + await Promise.all(errors.map(async (err) => { + await this.printError(err, true, (err as ErrorWithDiff).type || 'Unhandled Error') + })) + this.log(c.red(divider())) + } +} diff --git a/packages/vitest/src/node/plugins/globalSetup.ts b/packages/vitest/src/node/plugins/globalSetup.ts index 3d71d78c953f..7d69c449f01a 100644 --- a/packages/vitest/src/node/plugins/globalSetup.ts +++ b/packages/vitest/src/node/plugins/globalSetup.ts @@ -65,8 +65,8 @@ export const GlobalSetupPlugin = (ctx: Vitest): Plugin => { } } catch (e) { - ctx.error(`\n${c.red(divider(c.bold(c.inverse(' Error during global setup '))))}`) - await ctx.printError(e) + ctx.logger.error(`\n${c.red(divider(c.bold(c.inverse(' Error during global setup '))))}`) + await ctx.logger.printError(e) process.exit(1) } }, @@ -78,7 +78,7 @@ export const GlobalSetupPlugin = (ctx: Vitest): Plugin => { await globalSetupFile.teardown?.() } catch (error) { - console.error(`error during global teardown of ${globalSetupFile.file}`, error) + ctx.logger.error(`error during global teardown of ${globalSetupFile.file}`, error) } } } diff --git a/packages/vitest/src/node/plugins/index.ts b/packages/vitest/src/node/plugins/index.ts index 5a66f5de5d09..3ee8a8513507 100644 --- a/packages/vitest/src/node/plugins/index.ts +++ b/packages/vitest/src/node/plugins/index.ts @@ -133,7 +133,7 @@ export async function VitestPlugin(options: UserConfig = {}, ctx = new Vitest()) (await import('../../api/setup')).setup(ctx) } catch (err) { - ctx.printError(err, true) + ctx.logger.printError(err, true) process.exit(1) } diff --git a/packages/vitest/src/node/reporters/base.ts b/packages/vitest/src/node/reporters/base.ts index 129fa8ebdfc7..a92247b0f057 100644 --- a/packages/vitest/src/node/reporters/base.ts +++ b/packages/vitest/src/node/reporters/base.ts @@ -2,9 +2,8 @@ import { performance } from 'perf_hooks' import { relative } from 'pathe' import c from 'picocolors' import type { ErrorWithDiff, File, Reporter, Task, TaskResultPack, UserConsoleLog } from '../../types' -import { getFullName, getSuites, getTests, hasFailed, hasFailedSnapshot, isNode } from '../../utils' +import { clearInterval, getFullName, getSuites, getTests, hasFailed, hasFailedSnapshot, isNode, setInterval } from '../../utils' import type { Vitest } from '../../node' -import { version } from '../../../package.json' import { F_RIGHT } from '../../utils/figures' import { divider, getStateString, getStateSymbol, pointer, renderSnapshotSummary } from './renderers/utils' @@ -17,6 +16,7 @@ const WAIT_FOR_CHANGE_PASS = `\n${c.bold(c.inverse(c.green(' PASS ')))}${c.green const WAIT_FOR_CHANGE_FAIL = `\n${c.bold(c.inverse(c.red(' FAIL ')))}${c.red(' Tests failed. Watching for file changes...')}` const DURATION_LONG = 300 +const LAST_RUN_LOG_TIMEOUT = 1_500 export abstract class BaseReporter implements Reporter { start = 0 @@ -25,6 +25,11 @@ export abstract class BaseReporter implements Reporter { isTTY = isNode && process.stdout?.isTTY && !process.env.CI ctx: Vitest = undefined! + private _filesInWatchMode = new Map() + private _lastRunTimeout = 0 + private _lastRunTimer: NodeJS.Timer | undefined + private _lastRunCount = 0 + constructor() { this.registerUnhandledRejection() } @@ -32,22 +37,7 @@ export abstract class BaseReporter implements Reporter { onInit(ctx: Vitest) { this.ctx = ctx - this.ctx.log() - - const versionTest = this.ctx.config.watch - ? c.blue(`v${version}`) - : c.cyan(`v${version}`) - const mode = this.ctx.config.watch - ? c.blue(' DEV ') - : c.cyan(' RUN ') - this.ctx.log(`${c.inverse(c.bold(mode))} ${versionTest} ${c.gray(this.ctx.config.root)}`) - - if (this.ctx.config.ui) - this.ctx.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}`))) - else if (this.ctx.config.api) - this.ctx.log(c.dim(c.green(` API started at http://${this.ctx.config.api?.host || 'localhost'}:${c.bold(`${this.ctx.config.api.port}`)}`))) - - this.ctx.log() + ctx.logger.printBanner() this.start = performance.now() } @@ -60,22 +50,14 @@ export abstract class BaseReporter implements Reporter { await this.reportSummary(files) if (errors.length) { process.exitCode = 1 - const errorMessage = c.red(c.bold( - `\nVitest caught ${errors.length} unhandled error${errors.length > 1 ? 's' : ''} during the test run. This might cause false positive tests.` - + '\nPlease, resolve all the errors to make sure your tests are not affected.', - )) - this.ctx.log(c.red(divider(c.bold(c.inverse(' Unhandled Errors '))))) - this.ctx.log(errorMessage) - await Promise.all(errors.map(async (err) => { - await this.ctx.printError(err, true, (err as ErrorWithDiff).type || 'Unhandled Error') - })) - this.ctx.log(c.red(divider())) + this.ctx.logger.printUnhandledErrors(errors) } } onTaskUpdate(packs: TaskResultPack[]) { if (this.isTTY) return + const logger = this.ctx.logger for (const pack of packs) { const task = this.ctx.state.idMap.get(pack[0]) if (task && 'filepath' in task && task.result?.state && task.result?.state !== 'run') { @@ -95,26 +77,28 @@ export abstract class BaseReporter implements Reporter { if (this.ctx.config.logHeapUsage && task.result.heap != null) suffix += c.magenta(` ${Math.floor(task.result.heap / 1024 / 1024)} MB heap used`) - this.ctx.log(` ${getStateSymbol(task)} ${task.name} ${suffix}`) + logger.log(` ${getStateSymbol(task)} ${task.name} ${suffix}`) // print short errors, full errors will be at the end in summary for (const test of failed) { - this.ctx.log(c.red(` ${pointer} ${getFullName(test)}`)) - this.ctx.log(c.red(` ${F_RIGHT} ${(test.result!.error as any)?.message}`)) + logger.log(c.red(` ${pointer} ${getFullName(test)}`)) + logger.log(c.red(` ${F_RIGHT} ${(test.result!.error as any)?.message}`)) } } } } async onWatcherStart() { + this.resetLastRunLog() + const files = this.ctx.state.getFiles() const errors = this.ctx.state.getUnhandledErrors() const failed = errors.length > 0 || hasFailed(files) const failedSnap = hasFailedSnapshot(files) if (failed) - this.ctx.log(WAIT_FOR_CHANGE_FAIL) + this.ctx.logger.log(WAIT_FOR_CHANGE_FAIL) else - this.ctx.log(WAIT_FOR_CHANGE_PASS) + this.ctx.logger.log(WAIT_FOR_CHANGE_PASS) const hints = [HELP_HINT] if (failedSnap) @@ -122,14 +106,58 @@ export abstract class BaseReporter implements Reporter { else hints.push(HELP_QUITE) - this.ctx.log(BADGE_PADDING + hints.join(c.dim(', '))) + this.ctx.logger.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() } async onWatcherRerun(files: string[], trigger?: string) { + this.resetLastRunLog() this.watchFilters = files - this.ctx.clearScreen() - this.ctx.log(`\n${c.inverse(c.bold(c.blue(' RERUN ')))}${trigger ? c.dim(` ${this.relative(trigger)}\n`) : ''}`) + 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)}`) : '' + if (files.length > 1) { + // we need to figure out how to handle rerun all from stdin + this.ctx.logger.clearScreen(`\n${BADGE}${TRIGGER}\n`, true) + this._lastRunCount = 0 + } + else if (files.length === 1) { + const rerun = this._filesInWatchMode.get(files[0]) ?? 1 + this._lastRunCount = rerun + this.ctx.logger.clearScreen(`\n${BADGE}${TRIGGER} ${c.blue(`x${rerun}`)}\n`) + } + this.start = performance.now() } @@ -137,7 +165,7 @@ export abstract class BaseReporter implements Reporter { if (!this.shouldLog(log)) return const task = log.taskId ? this.ctx.state.idMap.get(log.taskId) : undefined - this.ctx.log(c.gray(log.type + c.dim(` | ${task ? getFullName(task) : 'unknown test'}`))) + this.ctx.logger.log(c.gray(log.type + c.dim(` | ${task ? getFullName(task) : 'unknown test'}`))) process[log.type].write(`${log.content}\n`) } @@ -151,10 +179,11 @@ export abstract class BaseReporter implements Reporter { } onServerRestart() { - this.ctx.log(c.cyan('Restarted due to config changes...')) + this.ctx.logger.log(c.cyan('Restarted due to config changes...')) } async reportSummary(files: File[]) { + const logger = this.ctx.logger const suites = getSuites(files) const tests = getTests(files) @@ -164,17 +193,17 @@ export abstract class BaseReporter implements Reporter { let current = 1 - const errorDivider = () => this.ctx.error(`${c.red(c.dim(divider(`[${current++}/${failedTotal}]`, undefined, 1)))}\n`) + const errorDivider = () => logger.error(`${c.red(c.dim(divider(`[${current++}/${failedTotal}]`, undefined, 1)))}\n`) if (failedSuites.length) { - this.ctx.error(c.red(divider(c.bold(c.inverse(` Failed Suites ${failedSuites.length} `))))) - this.ctx.error() + logger.error(c.red(divider(c.bold(c.inverse(` Failed Suites ${failedSuites.length} `))))) + logger.error() await this.printTaskErrors(failedSuites, errorDivider) } if (failedTests.length) { - this.ctx.error(c.red(divider(c.bold(c.inverse(` Failed Tests ${failedTests.length} `))))) - this.ctx.error() + logger.error(c.red(divider(c.bold(c.inverse(` Failed Tests ${failedTests.length} `))))) + logger.error() await this.printTaskErrors(failedTests, errorDivider) } @@ -191,23 +220,23 @@ export abstract class BaseReporter implements Reporter { const snapshotOutput = renderSnapshotSummary(this.ctx.config.root, this.ctx.snapshot.summary) if (snapshotOutput.length) { - this.ctx.log(snapshotOutput.map((t, i) => i === 0 + logger.log(snapshotOutput.map((t, i) => i === 0 ? `${padTitle('Snapshots')} ${t}` : `${padTitle('')} ${t}`, ).join('\n')) if (snapshotOutput.length > 1) - this.ctx.log() + logger.log() } - this.ctx.log(padTitle('Test Files'), getStateString(files)) - this.ctx.log(padTitle('Tests'), getStateString(tests)) + logger.log(padTitle('Test Files'), getStateString(files)) + logger.log(padTitle('Tests'), getStateString(tests)) if (this.watchFilters) - this.ctx.log(padTitle('Time'), time(threadTime)) + logger.log(padTitle('Time'), time(threadTime)) else - this.ctx.log(padTitle('Time'), time(executionTime) + c.gray(` (in thread ${time(threadTime)}, ${(executionTime / threadTime * 100).toFixed(2)}%)`)) + logger.log(padTitle('Time'), time(executionTime) + c.gray(` (in thread ${time(threadTime)}, ${(executionTime / threadTime * 100).toFixed(2)}%)`)) - this.ctx.log() + logger.log() } private async printTaskErrors(tasks: Task[], errorDivider: () => void) { @@ -228,9 +257,9 @@ export abstract class BaseReporter implements Reporter { if (filepath) name = `${name} ${c.dim(`[ ${this.relative(filepath)} ]`)}` - this.ctx.error(`${c.red(c.bold(c.inverse(' FAIL ')))} ${name}`) + this.ctx.logger.error(`${c.red(c.bold(c.inverse(' FAIL ')))} ${name}`) } - await this.ctx.printError(error) + await this.ctx.logger.printError(error) errorDivider() await Promise.resolve() } @@ -239,8 +268,8 @@ export abstract class BaseReporter implements Reporter { registerUnhandledRejection() { process.on('unhandledRejection', async (err) => { process.exitCode = 1 - await this.ctx.printError(err, true, 'Unhandled Rejection') - this.ctx.error('\n\n') + await this.ctx.logger.printError(err, true, 'Unhandled Rejection') + this.ctx.logger.error('\n\n') process.exit(1) }) } diff --git a/packages/vitest/src/node/reporters/default.ts b/packages/vitest/src/node/reporters/default.ts index 7ea15a2cb439..403610916197 100644 --- a/packages/vitest/src/node/reporters/default.ts +++ b/packages/vitest/src/node/reporters/default.ts @@ -10,18 +10,17 @@ export class DefaultReporter extends BaseReporter { async onTestRemoved(trigger?: string) { await this.stopListRender() - this.ctx.clearScreen() - this.ctx.log(c.yellow('Test removed...') + (trigger ? c.dim(` [ ${this.relative(trigger)} ]\n`) : '')) + this.ctx.logger.clearScreen(c.yellow('Test removed...') + (trigger ? c.dim(` [ ${this.relative(trigger)} ]\n`) : ''), true) const files = this.ctx.state.getFiles(this.watchFilters) createListRenderer(files, this.rendererOptions).stop() - this.ctx.log() + this.ctx.logger.log() await super.reportSummary(files) super.onWatcherStart() } onCollected() { if (this.isTTY) { - this.rendererOptions.outputStream = this.ctx.outputStream + this.rendererOptions.logger = this.ctx.logger this.rendererOptions.showHeap = this.ctx.config.logHeapUsage const files = this.ctx.state.getFiles(this.watchFilters) if (!this.renderer) @@ -33,13 +32,13 @@ export class DefaultReporter extends BaseReporter { async onFinished(files = this.ctx.state.getFiles(), errors = this.ctx.state.getUnhandledErrors()) { await this.stopListRender() - this.ctx.log() + this.ctx.logger.log() await super.onFinished(files, errors) } async onWatcherStart() { await this.stopListRender() - super.onWatcherStart() + await super.onWatcherStart() } async stopListRender() { diff --git a/packages/vitest/src/node/reporters/dot.ts b/packages/vitest/src/node/reporters/dot.ts index 3bc3e820fcd7..792e85ce13e7 100644 --- a/packages/vitest/src/node/reporters/dot.ts +++ b/packages/vitest/src/node/reporters/dot.ts @@ -11,7 +11,7 @@ export class DotReporter extends BaseReporter { if (this.isTTY) { const files = this.ctx.state.getFiles(this.watchFilters) if (!this.renderer) - this.renderer = createDotRenderer(files, { outputStream: this.ctx.outputStream }).start() + this.renderer = createDotRenderer(files, { logger: this.ctx.logger }).start() else this.renderer.update(files) } @@ -19,7 +19,7 @@ export class DotReporter extends BaseReporter { async onFinished(files = this.ctx.state.getFiles(), errors = this.ctx.state.getUnhandledErrors()) { await this.stopListRender() - this.ctx.log() + this.ctx.logger.log() await super.onFinished(files, errors) } diff --git a/packages/vitest/src/node/reporters/json.ts b/packages/vitest/src/node/reporters/json.ts index 0dacc37f63c5..86be25378632 100644 --- a/packages/vitest/src/node/reporters/json.ts +++ b/packages/vitest/src/node/reporters/json.ts @@ -112,7 +112,7 @@ export class JsonReporter implements Reporter { }) if (tests.some(t => t.result?.state === 'run')) { - this.ctx.console.warn('WARNING: Some tests are still running when generating the JSON report.' + this.ctx.logger.warn('WARNING: Some tests are still running when generating the JSON report.' + 'This is likely an internal bug in Vitest.' + 'Please report it to https://github.com/vitest-dev/vitest/issues') } @@ -170,10 +170,10 @@ export class JsonReporter implements Reporter { await fs.mkdir(outputDirectory, { recursive: true }) await fs.writeFile(reportFile, report, 'utf-8') - this.ctx.log(`JSON report written to ${reportFile}`) + this.ctx.logger.log(`JSON report written to ${reportFile}`) } else { - this.ctx.log(report) + this.ctx.logger.log(report) } } } diff --git a/packages/vitest/src/node/reporters/junit.ts b/packages/vitest/src/node/reporters/junit.ts index df3f8d5238fe..ce599fc5051f 100644 --- a/packages/vitest/src/node/reporters/junit.ts +++ b/packages/vitest/src/node/reporters/junit.ts @@ -85,7 +85,7 @@ export class JUnitReporter implements Reporter { this.baseLog = async (text: string) => await fs.writeFile(fileFd, `${text}\n`) } else { - this.baseLog = async (text: string) => this.ctx.log(text) + this.baseLog = async (text: string) => this.ctx.logger.log(text) } this.logger = new IndentedLogger(this.baseLog) @@ -219,6 +219,6 @@ export class JUnitReporter implements Reporter { }) if (this.reportFile) - this.ctx.log(`JUNIT report written to ${this.reportFile}`) + this.ctx.logger.log(`JUNIT report written to ${this.reportFile}`) } } diff --git a/packages/vitest/src/node/reporters/renderers/dotRenderer.ts b/packages/vitest/src/node/reporters/renderers/dotRenderer.ts index d85accba551c..b478d39c027e 100644 --- a/packages/vitest/src/node/reporters/renderers/dotRenderer.ts +++ b/packages/vitest/src/node/reporters/renderers/dotRenderer.ts @@ -1,10 +1,10 @@ -import { createLogUpdate } from 'log-update' import c from 'picocolors' import type { Task } from '../../../types' import { clearInterval, getTests, setInterval } from '../../../utils' +import type { Logger } from '../../logger' export interface DotRendererOptions { - outputStream: NodeJS.WritableStream + logger: Logger } const check = c.green('ยท') @@ -32,7 +32,7 @@ export const createDotRenderer = (_tasks: Task[], options: DotRendererOptions) = let tasks = _tasks let timer: any - const log = createLogUpdate(options.outputStream) + const log = options.logger.logUpdate function update() { log(render(tasks)) @@ -56,7 +56,7 @@ export const createDotRenderer = (_tasks: Task[], options: DotRendererOptions) = timer = undefined } log.clear() - options.outputStream.write(`${render(tasks)}\n`) + options.logger.log(render(tasks)) return this }, clear() { diff --git a/packages/vitest/src/node/reporters/renderers/listRenderer.ts b/packages/vitest/src/node/reporters/renderers/listRenderer.ts index 4d6f8cd6684e..01006a6103eb 100644 --- a/packages/vitest/src/node/reporters/renderers/listRenderer.ts +++ b/packages/vitest/src/node/reporters/renderers/listRenderer.ts @@ -1,15 +1,15 @@ -import { createLogUpdate } from 'log-update' import c from 'picocolors' import cliTruncate from 'cli-truncate' import stripAnsi from 'strip-ansi' import type { SuiteHooks, Task } from '../../../types' import { clearInterval, getTests, setInterval } from '../../../utils' import { F_RIGHT } from '../../../utils/figures' +import type { Logger } from '../../logger' import { getCols, getHookStateSymbol, getStateSymbol } from './utils' export interface ListRendererOptions { renderSucceed?: boolean - outputStream: NodeJS.WritableStream + logger: Logger showHeap: boolean } @@ -94,7 +94,7 @@ export const createListRenderer = (_tasks: Task[], options: ListRendererOptions) let tasks = _tasks let timer: any - const log = createLogUpdate(options.outputStream) + const log = options.logger.logUpdate function update() { log(renderTree(tasks, options)) @@ -118,7 +118,7 @@ export const createListRenderer = (_tasks: Task[], options: ListRendererOptions) timer = undefined } log.clear() - options.outputStream.write(`${renderTree(tasks, options)}\n`) + options.logger.log(renderTree(tasks, options)) return this }, clear() { diff --git a/packages/vitest/src/node/reporters/tap-flat.ts b/packages/vitest/src/node/reporters/tap-flat.ts index b18cbcfc837b..86ac59bbcaf3 100644 --- a/packages/vitest/src/node/reporters/tap-flat.ts +++ b/packages/vitest/src/node/reporters/tap-flat.ts @@ -22,7 +22,7 @@ export class TapFlatReporter extends TapReporter { } async onFinished(files = this.ctx.state.getFiles()) { - this.ctx.log('TAP version 13') + this.ctx.logger.log('TAP version 13') const flatTasks = files.flatMap(task => flattenTasks(task)) diff --git a/packages/vitest/src/node/reporters/tap.ts b/packages/vitest/src/node/reporters/tap.ts index c8589206e603..1bee279ccfd4 100644 --- a/packages/vitest/src/node/reporters/tap.ts +++ b/packages/vitest/src/node/reporters/tap.ts @@ -20,7 +20,7 @@ export class TapReporter implements Reporter { onInit(ctx: Vitest): void { this.ctx = ctx - this.logger = new IndentedLogger(this.ctx.log.bind(this.ctx)) + this.logger = new IndentedLogger(this.ctx.logger.log.bind(this.ctx)) } static getComment(task: Task): string { diff --git a/packages/vitest/src/node/reporters/verbose.ts b/packages/vitest/src/node/reporters/verbose.ts index 25a7e736651b..cd89aefe53ba 100644 --- a/packages/vitest/src/node/reporters/verbose.ts +++ b/packages/vitest/src/node/reporters/verbose.ts @@ -20,9 +20,9 @@ export class VerboseReporter extends DefaultReporter { let title = ` ${getStateSymbol(task)} ${getFullName(task)}` if (this.ctx.config.logHeapUsage && task.result.heap != null) title += c.magenta(` ${Math.floor(task.result.heap / 1024 / 1024)} MB heap used`) - this.ctx.log(title) + this.ctx.logger.log(title) if (task.result.state === 'fail') - this.ctx.log(c.red(` ${F_RIGHT} ${(task.result.error as any)?.message}`)) + this.ctx.logger.log(c.red(` ${F_RIGHT} ${(task.result.error as any)?.message}`)) } } } diff --git a/packages/vitest/src/node/stdin.ts b/packages/vitest/src/node/stdin.ts index 9b6698ff56ae..2c065405039e 100644 --- a/packages/vitest/src/node/stdin.ts +++ b/packages/vitest/src/node/stdin.ts @@ -41,7 +41,7 @@ export function registerConsoleShortcuts(ctx: Vitest) { return ctx.updateSnapshot() // rerun all tests if (name === 'a' || name === 'return') - return ctx.rerunFiles(undefined, 'rerun all') + return ctx.rerunFiles(undefined) // rerun only failed tests if (name === 'f') return ctx.rerunFailed() diff --git a/packages/vitest/src/utils/base.ts b/packages/vitest/src/utils/base.ts index 9633c670cfe0..21a706a0837f 100644 --- a/packages/vitest/src/utils/base.ts +++ b/packages/vitest/src/utils/base.ts @@ -7,10 +7,10 @@ function isFinalObj(obj: any) { function collectOwnProperties(obj: any, collector: Set) { const props = Object.getOwnPropertyNames(obj) - const symbs = Object.getOwnPropertySymbols(obj) + const symbols = Object.getOwnPropertySymbols(obj) props.forEach(prop => collector.add(prop)) - symbs.forEach(symb => collector.add(symb)) + symbols.forEach(symbol => collector.add(symbol)) } export function getAllProperties(obj: any) { diff --git a/test/core/test/mocked-no-mocks-same.test.ts b/test/core/test/mocked-no-mocks-same.test.ts index 2db34c1b1187..5c6a77db273b 100644 --- a/test/core/test/mocked-no-mocks-same.test.ts +++ b/test/core/test/mocked-no-mocks-same.test.ts @@ -3,7 +3,7 @@ import { mockedA } from '../src/mockedA' vi.mock('../src/mockedA.ts') -test('testing mocking module without __mocks__ - suites dont conflict', () => { +test('testing mocking module without __mocks__ - suites don\'t conflict', () => { mockedA() expect(mockedA).toHaveBeenCalledTimes(1) diff --git a/test/reporters/src/context.ts b/test/reporters/src/context.ts index 854b99bb524b..363f98b8e69f 100644 --- a/test/reporters/src/context.ts +++ b/test/reporters/src/context.ts @@ -1,3 +1,4 @@ +import type { Logger } from '../../../packages/vitest/src/node/logger' import type { Vitest } from '../../../packages/vitest/src/node' import type { StateManager } from '../../../packages/vitest/src/node/state' import type { File, ResolvedConfig } from '../../../packages/vitest/src/types' @@ -9,7 +10,6 @@ interface Context { export function getContext(): Context { let output = '' - const log = (text: string) => output += `${text}\n` const config: Partial = { root: '/', @@ -20,11 +20,15 @@ export function getContext(): Context { } const context: Partial = { - log, state: state as StateManager, config: config as ResolvedConfig, } + context.logger = { + ctx: context as Vitest, + log: (text: string) => output += `${text}\n`, + } as unknown as Logger + return { vitest: context as Vitest, get output() { diff --git a/test/reporters/src/custom-reporter.js b/test/reporters/src/custom-reporter.js index 6b86abbf4440..5c98cf7faded 100644 --- a/test/reporters/src/custom-reporter.js +++ b/test/reporters/src/custom-reporter.js @@ -4,6 +4,6 @@ export default class TestReporter { } onFinished() { - this.ctx.log('hello from custom reporter') + this.ctx.logger.log('hello from custom reporter') } } diff --git a/test/reporters/src/custom-reporter.ts b/test/reporters/src/custom-reporter.ts index 3826005e5edf..81f53f421db2 100644 --- a/test/reporters/src/custom-reporter.ts +++ b/test/reporters/src/custom-reporter.ts @@ -8,6 +8,6 @@ export default class TestReporter implements Reporter { } onFinished() { - this.ctx.log('hello from custom reporter') + this.ctx.logger.log('hello from custom reporter') } }