UNPKG

vitest

Version:

Next generation testing framework powered by Vite

1,571 lines (1,548 loc) 77 kB
import fs, { existsSync, readFileSync, promises } from 'node:fs'; import { getTestName, hasFailed, getFullName, getTests, getSuites, getTasks } from '@vitest/runner/utils'; import * as pathe from 'pathe'; import { relative, normalize, resolve, dirname } from 'pathe'; import c from 'tinyrainbow'; import { t as truncateString, d as divider, F as F_POINTER, f as formatTimeString, a as taskFail, b as F_CHECK, g as getStateSymbol, c as formatProjectName, e as F_RIGHT, w as withLabel, r as renderSnapshotSummary, p as padSummaryTitle, h as getStateString$1, i as formatTime, j as countTestErrors, k as F_TREE_NODE_END, l as F_TREE_NODE_MIDDLE } from './utils.Lot3J_8U.js'; import { stripVTControlCharacters } from 'node:util'; import { positionToOffset, lineSplitRE, isPrimitive, inspect, toArray, notNullish } from '@vitest/utils'; import { performance as performance$1 } from 'node:perf_hooks'; import { parseErrorStacktrace, parseStacktrace } from '@vitest/utils/source-map'; import { i as isTTY } from './env.Dq0hM4Xv.js'; import { T as TypeCheckError, g as getOutputFile, h as hasFailedSnapshot } from './typechecker.CG0zmr19.js'; import { mkdir, writeFile, readdir, stat, readFile } from 'node:fs/promises'; import { Console } from 'node:console'; import { Writable } from 'node:stream'; import { createRequire } from 'node:module'; import { hostname } from 'node:os'; /// <reference types="../types/index.d.ts" /> // (c) 2020-present Andrea Giammarchi const {parse: $parse, stringify: $stringify} = JSON; const {keys} = Object; const Primitive = String; // it could be Number const primitive = 'string'; // it could be 'number' const ignore = {}; const object = 'object'; const noop = (_, value) => value; const primitives = value => ( value instanceof Primitive ? Primitive(value) : value ); const Primitives = (_, value) => ( typeof value === primitive ? new Primitive(value) : value ); const revive = (input, parsed, output, $) => { const lazy = []; for (let ke = keys(output), {length} = ke, y = 0; y < length; y++) { const k = ke[y]; const value = output[k]; if (value instanceof Primitive) { const tmp = input[value]; if (typeof tmp === object && !parsed.has(tmp)) { parsed.add(tmp); output[k] = ignore; lazy.push({k, a: [input, parsed, tmp, $]}); } else output[k] = $.call(output, k, tmp); } else if (output[k] !== ignore) output[k] = $.call(output, k, value); } for (let {length} = lazy, i = 0; i < length; i++) { const {k, a} = lazy[i]; output[k] = $.call(output, k, revive.apply(null, a)); } return output; }; const set = (known, input, value) => { const index = Primitive(input.push(value) - 1); known.set(value, index); return index; }; /** * Converts a specialized flatted string into a JS value. * @param {string} text * @param {(this: any, key: string, value: any) => any} [reviver] * @returns {any} */ const parse = (text, reviver) => { const input = $parse(text, Primitives).map(primitives); const value = input[0]; const $ = reviver || noop; const tmp = typeof value === object && value ? revive(input, new Set, value, $) : value; return $.call({'': tmp}, '', tmp); }; /** * Converts a JS value into a specialized flatted string. * @param {any} value * @param {((this: any, key: string, value: any) => any) | (string | number)[] | null | undefined} [replacer] * @param {string | number | undefined} [space] * @returns {string} */ const stringify = (value, replacer, space) => { const $ = replacer && typeof replacer === object ? (k, v) => (k === '' || -1 < replacer.indexOf(k) ? v : void 0) : (replacer || noop); const known = new Map; const input = []; const output = []; let i = +set(known, input, $.call({'': value}, '', value)); let firstRun = !i; while (i < input.length) { firstRun = true; output[i] = $stringify(input[i++], replace, space); } return '[' + output.join(',') + ']'; function replace(key, value) { if (firstRun) { firstRun = !firstRun; return value; } const after = $.call(this, key, value); switch (typeof after) { case object: if (after === null) return after; case primitive: return known.get(after) || set(known, input, after); } return after; } }; function capturePrintError(error, ctx, options) { let output = ""; const writable = new Writable({ write(chunk, _encoding, callback) { output += String(chunk); callback(); } }); const console = new Console(writable); const logger = { error: console.error.bind(console), highlight: ctx.logger.highlight.bind(ctx.logger) }; const result = printError(error, ctx, logger, { showCodeFrame: false, ...options }); return { nearest: result?.nearest, output }; } function printError(error, ctx, logger, options) { const project = options.project ?? ctx.coreWorkspaceProject ?? ctx.projects[0]; return printErrorInner(error, project, { logger, type: options.type, showCodeFrame: options.showCodeFrame, screenshotPaths: options.screenshotPaths, printProperties: options.verbose, parseErrorStacktrace(error) { if (options.task?.file.pool === "browser" && project.browser) { return project.browser.parseErrorStacktrace(error, { ignoreStackEntries: options.fullStack ? [] : undefined }); } return parseErrorStacktrace(error, { frameFilter: project.config.onStackTrace, ignoreStackEntries: options.fullStack ? [] : undefined }); } }); } function printErrorInner(error, project, options) { const { showCodeFrame = true, type, printProperties = true } = options; const logger = options.logger; let e = error; if (isPrimitive(e)) { e = { message: String(error).split(/\n/g)[0], stack: String(error) }; } if (!e) { const error = new Error("unknown error"); e = { message: e ?? error.message, stack: error.stack }; } if (!project) { printErrorMessage(e, logger); return; } const stacks = options.parseErrorStacktrace(e); const nearest = error instanceof TypeCheckError ? error.stacks[0] : stacks.find((stack) => { try { return project.server && project.getModuleById(stack.file) && existsSync(stack.file); } catch { return false; } }); if (type) { printErrorType(type, project.vitest); } printErrorMessage(e, logger); if (options.screenshotPaths?.length) { const length = options.screenshotPaths.length; logger.error(`\nFailure screenshot${length > 1 ? "s" : ""}:`); logger.error(options.screenshotPaths.map((p) => ` - ${c.dim(relative(process.cwd(), p))}`).join("\n")); if (!e.diff) { logger.error(); } } if (e.codeFrame) { logger.error(`${e.codeFrame}\n`); } if ("__vitest_rollup_error__" in e) { const err = e.__vitest_rollup_error__; logger.error([ err.plugin && ` Plugin: ${c.magenta(err.plugin)}`, err.id && ` File: ${c.cyan(err.id)}${err.loc ? `:${err.loc.line}:${err.loc.column}` : ""}`, err.frame && c.yellow(err.frame.split(/\r?\n/g).map((l) => ` `.repeat(2) + l).join(`\n`)) ].filter(Boolean).join("\n")); } if (e.diff) { logger.error(`\n${e.diff}\n`); } if (e.frame) { logger.error(c.yellow(e.frame)); } else { const errorProperties = printProperties ? getErrorProperties(e) : {}; printStack(logger, project, stacks, nearest, errorProperties, (s) => { if (showCodeFrame && s === nearest && nearest) { const sourceCode = readFileSync(nearest.file, "utf-8"); logger.error(generateCodeFrame(sourceCode.length > 1e5 ? sourceCode : logger.highlight(nearest.file, sourceCode), 4, s)); } }); } const testPath = e.VITEST_TEST_PATH; const testName = e.VITEST_TEST_NAME; const afterEnvTeardown = e.VITEST_AFTER_ENV_TEARDOWN; if (testPath) { logger.error(c.red(`This error originated in "${c.bold(relative(project.config.root, testPath))}" test file. It doesn't mean the error was thrown inside the file itself, but while it was running.`)); } if (testName) { logger.error(c.red(`The latest test that might've caused the error is "${c.bold(testName)}". It might mean one of the following:` + "\n- The error was thrown, while Vitest was running this test." + "\n- If the error occurred after the test had been completed, this was the last documented test before it was thrown.")); } if (afterEnvTeardown) { logger.error(c.red("This error was caught after test environment was torn down. Make sure to cancel any running tasks before test finishes:" + "\n- cancel timeouts using clearTimeout and clearInterval" + "\n- wait for promises to resolve using the await keyword")); } if (typeof e.cause === "object" && e.cause && "name" in e.cause) { e.cause.name = `Caused by: ${e.cause.name}`; printErrorInner(e.cause, project, { showCodeFrame: false, logger: options.logger, parseErrorStacktrace: options.parseErrorStacktrace }); } handleImportOutsideModuleError(e.stack || e.stackStr || "", logger); return { nearest }; } function printErrorType(type, ctx) { ctx.logger.error(`\n${c.red(divider(c.bold(c.inverse(` ${type} `))))}`); } const skipErrorProperties = new Set([ "nameStr", "stack", "cause", "stacks", "stackStr", "type", "showDiff", "ok", "operator", "diff", "codeFrame", "actual", "expected", "diffOptions", "sourceURL", "column", "line", "fileName", "lineNumber", "columnNumber", "VITEST_TEST_NAME", "VITEST_TEST_PATH", "VITEST_AFTER_ENV_TEARDOWN", ...Object.getOwnPropertyNames(Error.prototype), ...Object.getOwnPropertyNames(Object.prototype) ]); function getErrorProperties(e) { const errorObject = Object.create(null); if (e.name === "AssertionError") { return errorObject; } for (const key of Object.getOwnPropertyNames(e)) { if (!skipErrorProperties.has(key)) { errorObject[key] = e[key]; } } return errorObject; } const esmErrors = ["Cannot use import statement outside a module", "Unexpected token 'export'"]; function handleImportOutsideModuleError(stack, logger) { if (!esmErrors.some((e) => stack.includes(e))) { return; } const path = normalize(stack.split("\n")[0].trim()); let name = path.split("/node_modules/").pop() || ""; if (name?.startsWith("@")) { name = name.split("/").slice(0, 2).join("/"); } else { name = name.split("/")[0]; } if (name) { printModuleWarningForPackage(logger, path, name); } else { printModuleWarningForSourceCode(logger, path); } } function printModuleWarningForPackage(logger, path, name) { 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." + "\n\n" + "As a temporary workaround you can try to inline the package by updating your config:" + "\n\n" + c.gray(c.dim("// vitest.config.js")) + "\n" + c.green(`export default { test: { server: { deps: { inline: [ ${c.yellow(c.bold(`"${name}"`))} ] } } } }\n`))); } function printModuleWarningForSourceCode(logger, path) { logger.error(c.yellow(`Module ${path} seems to be an ES Module but shipped in a CommonJS package. ` + "To fix this issue, change the file extension to .mjs or add \"type\": \"module\" in your package.json.")); } function printErrorMessage(error, logger) { const errorName = error.name || error.nameStr || "Unknown Error"; if (!error.message) { logger.error(error); return; } if (error.message.length > 5e3) { logger.error(`${c.red(c.bold(errorName))}: ${error.message}`); } else { logger.error(c.red(`${c.bold(errorName)}: ${error.message}`)); } } function printStack(logger, project, stack, highlight, errorProperties, onStack) { for (const frame of stack) { const color = frame === highlight ? c.cyan : c.gray; const path = relative(project.config.root, frame.file); logger.error(color(` ${c.dim(F_POINTER)} ${[frame.method, `${path}:${c.dim(`${frame.line}:${frame.column}`)}`].filter(Boolean).join(" ")}`)); onStack?.(frame); } if (stack.length) { logger.error(); } if (hasProperties(errorProperties)) { logger.error(c.red(c.dim(divider()))); const propertiesString = inspect(errorProperties); logger.error(c.red(c.bold("Serialized Error:")), c.gray(propertiesString)); } } function hasProperties(obj) { for (const _key in obj) { return true; } return false; } function generateCodeFrame(source, indent = 0, loc, range = 2) { const start = typeof loc === "object" ? positionToOffset(source, loc.line, loc.column) : loc; const end = start; const lines = source.split(lineSplitRE); const nl = /\r\n/.test(source) ? 2 : 1; let count = 0; let res = []; const columns = process.stdout?.columns || 80; for (let i = 0; i < lines.length; i++) { count += lines[i].length + nl; if (count >= start) { for (let j = i - range; j <= i + range || end > count; j++) { if (j < 0 || j >= lines.length) { continue; } const lineLength = lines[j].length; if (stripVTControlCharacters(lines[j]).length > 200) { return ""; } res.push(lineNo(j + 1) + truncateString(lines[j].replace(/\t/g, " "), columns - 5 - indent)); if (j === i) { const pad = start - (count - lineLength) + (nl - 1); const length = Math.max(1, end > count ? lineLength - pad : end - start); res.push(lineNo() + " ".repeat(pad) + c.red("^".repeat(length))); } else if (j > i) { if (end > count) { const length = Math.max(1, Math.min(end - count, lineLength)); res.push(lineNo() + c.red("^".repeat(length))); } count += lineLength + 1; } } break; } } if (indent) { res = res.map((line) => " ".repeat(indent) + line); } return res.join("\n"); } function lineNo(no = "") { return c.gray(`${String(no).padStart(3, " ")}| `); } class BlobReporter { ctx; options; constructor(options) { this.options = options; } onInit(ctx) { if (ctx.config.watch) { throw new Error("Blob reporter is not supported in watch mode"); } this.ctx = ctx; } async onFinished(files = [], errors = [], coverage) { let outputFile = this.options.outputFile ?? getOutputFile(this.ctx.config, "blob"); if (!outputFile) { const shard = this.ctx.config.shard; outputFile = shard ? `.vitest-reports/blob-${shard.index}-${shard.count}.json` : ".vitest-reports/blob.json"; } const modules = this.ctx.projects.map((project) => { return [project.name, [...project.vite.moduleGraph.idToModuleMap.entries()].map((mod) => { if (!mod[1].file) { return null; } return [ mod[0], mod[1].file, mod[1].url ]; }).filter((x) => x != null)]; }); const report = stringify([ this.ctx.version, files, errors, modules, coverage ]); const reportFile = resolve(this.ctx.config.root, outputFile); const dir = dirname(reportFile); if (!existsSync(dir)) { await mkdir(dir, { recursive: true }); } await writeFile(reportFile, report, "utf-8"); this.ctx.logger.log("blob report written to", reportFile); } } async function readBlobs(currentVersion, blobsDirectory, projectsArray) { const resolvedDir = resolve(process.cwd(), blobsDirectory); const blobsFiles = await readdir(resolvedDir); const promises = blobsFiles.map(async (filename) => { const fullPath = resolve(resolvedDir, filename); const stats = await stat(fullPath); if (!stats.isFile()) { throw new TypeError(`vitest.mergeReports() expects all paths in "${blobsDirectory}" to be files generated by the blob reporter, but "${filename}" is not a file`); } const content = await readFile(fullPath, "utf-8"); const [version, files, errors, moduleKeys, coverage] = parse(content); if (!version) { throw new TypeError(`vitest.mergeReports() expects all paths in "${blobsDirectory}" to be files generated by the blob reporter, but "${filename}" is not a valid blob file`); } return { version, files, errors, moduleKeys, coverage, file: filename }; }); const blobs = await Promise.all(promises); if (!blobs.length) { throw new Error(`vitest.mergeReports() requires at least one blob file in "${blobsDirectory}" directory, but none were found`); } const versions = new Set(blobs.map((blob) => blob.version)); if (versions.size > 1) { throw new Error(`vitest.mergeReports() requires all blob files to be generated by the same Vitest version, received\n\n${blobs.map((b) => `- "${b.file}" uses v${b.version}`).join("\n")}`); } if (!versions.has(currentVersion)) { throw new Error(`the blobs in "${blobsDirectory}" were generated by a different version of Vitest. Expected v${currentVersion}, but received v${blobs[0].version}`); } const projects = Object.fromEntries(projectsArray.map((p) => [p.name, p])); blobs.forEach((blob) => { blob.moduleKeys.forEach(([projectName, moduleIds]) => { const project = projects[projectName]; if (!project) { return; } moduleIds.forEach(([moduleId, file, url]) => { const moduleNode = project.vite.moduleGraph.createFileOnlyEntry(file); moduleNode.url = url; moduleNode.id = moduleId; project.vite.moduleGraph.idToModuleMap.set(moduleId, moduleNode); }); }); }); const files = blobs.flatMap((blob) => blob.files).sort((f1, f2) => { const time1 = f1.result?.startTime || 0; const time2 = f2.result?.startTime || 0; return time1 - time2; }); const errors = blobs.flatMap((blob) => blob.errors); const coverages = blobs.map((blob) => blob.coverage); return { files, errors, coverages }; } class HangingProcessReporter { whyRunning; onInit() { const _require = createRequire(import.meta.url); this.whyRunning = _require("why-is-node-running"); } onProcessTimeout() { this.whyRunning?.(); } } const BADGE_PADDING = " "; class BaseReporter { start = 0; end = 0; watchFilters; failedUnwatchedFiles = []; isTTY; ctx = undefined; renderSucceed = false; verbose = false; _filesInWatchMode = new Map(); _timeStart = formatTimeString(new Date()); constructor(options = {}) { this.isTTY = options.isTTY ?? isTTY; } onInit(ctx) { this.ctx = ctx; this.ctx.logger.printBanner(); this.start = performance$1.now(); } log(...messages) { this.ctx.logger.log(...messages); } error(...messages) { this.ctx.logger.error(...messages); } relative(path) { return relative(this.ctx.config.root, path); } onFinished(files = this.ctx.state.getFiles(), errors = this.ctx.state.getUnhandledErrors()) { this.end = performance$1.now(); if (!files.length && !errors.length) { this.ctx.logger.printNoTestFound(this.ctx.filenamePattern); } else { this.reportSummary(files, errors); } } onTestCaseResult(testCase) { if (testCase.result().state === "failed") { this.logFailedTask(testCase.task); } } onTestSuiteResult(testSuite) { if (testSuite.state() === "failed") { this.logFailedTask(testSuite.task); } } onTestModuleEnd(testModule) { if (testModule.state() === "failed") { this.logFailedTask(testModule.task); } this.printTestModule(testModule); } logFailedTask(task) { if (this.ctx.config.silent === "passed-only") { for (const log of task.logs || []) { this.onUserConsoleLog(log, "failed"); } } } printTestModule(testModule) { const moduleState = testModule.state(); if (moduleState === "queued" || moduleState === "pending") { return; } let testsCount = 0; let failedCount = 0; let skippedCount = 0; const logs = []; const originalLog = this.log.bind(this); this.log = (msg) => logs.push(msg); const visit = (suiteState, children) => { for (const child of children) { if (child.type === "suite") { const suiteState = child.state(); if (!this.ctx.config.hideSkippedTests || suiteState !== "skipped") { this.printTestSuite(child); } visit(suiteState, child.children); } else { const testResult = child.result(); testsCount++; if (testResult.state === "failed") { failedCount++; } else if (testResult.state === "skipped") { skippedCount++; } if (this.ctx.config.hideSkippedTests && suiteState === "skipped") { continue; } this.printTestCase(moduleState, child); } } }; try { visit(moduleState, testModule.children); } finally { this.log = originalLog; } this.log(this.getModuleLog(testModule, { tests: testsCount, failed: failedCount, skipped: skippedCount })); logs.forEach((log) => this.log(log)); } printTestCase(moduleState, test) { const testResult = test.result(); const { duration, retryCount, repeatCount } = test.diagnostic() || {}; const padding = this.getTestIndentation(test.task); let suffix = this.getDurationPrefix(test.task); if (retryCount != null && retryCount > 0) { suffix += c.yellow(` (retry x${retryCount})`); } if (repeatCount != null && repeatCount > 0) { suffix += c.yellow(` (repeat x${repeatCount})`); } if (testResult.state === "failed") { this.log(c.red(` ${padding}${taskFail} ${this.getTestName(test.task, c.dim(" > "))}`) + suffix); testResult.errors.forEach((error) => { const message = this.formatShortError(error); if (message) { this.log(c.red(` ${padding}${message}`)); } }); } else if (duration && duration > this.ctx.config.slowTestThreshold) { this.log(` ${padding}${c.yellow(c.dim(F_CHECK))} ${this.getTestName(test.task, c.dim(" > "))} ${suffix}`); } else if (this.ctx.config.hideSkippedTests && testResult.state === "skipped") ; else if (testResult.state === "skipped" && testResult.note) { this.log(` ${padding}${getStateSymbol(test.task)} ${this.getTestName(test.task, c.dim(" > "))}${c.dim(c.gray(` [${testResult.note}]`))}`); } else if (this.renderSucceed || moduleState === "failed") { this.log(` ${padding}${getStateSymbol(test.task)} ${this.getTestName(test.task, c.dim(" > "))}${suffix}`); } } getModuleLog(testModule, counts) { let state = c.dim(`${counts.tests} test${counts.tests > 1 ? "s" : ""}`); if (counts.failed) { state += c.dim(" | ") + c.red(`${counts.failed} failed`); } if (counts.skipped) { state += c.dim(" | ") + c.yellow(`${counts.skipped} skipped`); } let suffix = c.dim("(") + state + c.dim(")") + this.getDurationPrefix(testModule.task); const diagnostic = testModule.diagnostic(); if (diagnostic.heap != null) { suffix += c.magenta(` ${Math.floor(diagnostic.heap / 1024 / 1024)} MB heap used`); } let title = getStateSymbol(testModule.task); if (testModule.meta().typecheck) { title += ` ${c.bgBlue(c.bold(" TS "))}`; } if (testModule.project.name) { title += ` ${formatProjectName(testModule.project.name, "")}`; } return ` ${title} ${testModule.task.name} ${suffix}`; } printTestSuite(_suite) {} getTestName(test, separator) { return getTestName(test, separator); } formatShortError(error) { return `${F_RIGHT} ${error.message}`; } getTestIndentation(_test) { return " "; } getDurationPrefix(task) { if (!task.result?.duration) { return ""; } const color = task.result.duration > this.ctx.config.slowTestThreshold ? c.yellow : c.green; return color(` ${Math.round(task.result.duration)}${c.dim("ms")}`); } onWatcherStart(files = this.ctx.state.getFiles(), errors = this.ctx.state.getUnhandledErrors()) { const failed = errors.length > 0 || hasFailed(files); if (failed) { this.log(withLabel("red", "FAIL", "Tests failed. Watching for file changes...")); } else if (this.ctx.isCancelling) { this.log(withLabel("red", "CANCELLED", "Test run cancelled. Watching for file changes...")); } else { this.log(withLabel("green", "PASS", "Waiting for file changes...")); } 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(c.dim("press ") + c.bold("q") + c.dim(" to quit")); } this.log(BADGE_PADDING + hints.join(c.dim(", "))); } onWatcherRerun(files, trigger) { this.watchFilters = files; this.failedUnwatchedFiles = this.ctx.state.getTestModules().filter((testModule) => !files.includes(testModule.task.filepath) && testModule.state() === "failed"); files.forEach((filepath) => { let reruns = this._filesInWatchMode.get(filepath) ?? 0; this._filesInWatchMode.set(filepath, ++reruns); }); let banner = trigger ? c.dim(`${this.relative(trigger)} `) : ""; if (files.length === 1) { const rerun = this._filesInWatchMode.get(files[0]) ?? 1; 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.join(", "))); } if (this.ctx.configOverride.testNamePattern) { this.log(BADGE_PADDING + c.dim(" Test name pattern: ") + c.blue(String(this.ctx.configOverride.testNamePattern))); } this.log(""); for (const testModule of this.failedUnwatchedFiles) { this.printTestModule(testModule); } this._timeStart = formatTimeString(new Date()); this.start = performance$1.now(); } onUserConsoleLog(log, taskState) { if (!this.shouldLog(log, taskState)) { return; } const output = log.type === "stdout" ? this.ctx.logger.outputStream : this.ctx.logger.errorStream; const write = (msg) => output.write(msg); 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) { if (log.browser) { write("\n"); } const project = task ? this.ctx.getProjectByName(task.file.projectName || "") : this.ctx.getRootProject(); const stack = log.browser ? project.browser?.parseStacktrace(log.origin) || [] : parseStacktrace(log.origin); 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); 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) { this.log(c.yellow("Test removed...") + (trigger ? c.dim(` [ ${this.relative(trigger)} ]\n`) : "")); } shouldLog(log, taskState) { if (this.ctx.config.silent === true) { return false; } if (this.ctx.config.silent === "passed-only" && taskState !== "failed") { return false; } const shouldLog = this.ctx.config.onConsoleLog?.(log.content, log.type); if (shouldLog === false) { return shouldLog; } return true; } onServerRestart(reason) { this.log(c.bold(c.magenta(reason === "config" ? "\nRestarting due to config changes..." : "\nRestarting Vitest..."))); } reportSummary(files, errors) { this.printErrorsSummary(files, errors); if (this.ctx.config.mode === "benchmark") { this.reportBenchmarkSummary(files); } else { this.reportTestSummary(files, errors); } } reportTestSummary(files, errors) { this.log(); const affectedFiles = [...this.failedUnwatchedFiles.map((m) => m.task), ...files]; const tests = getTests(affectedFiles); const snapshotOutput = renderSnapshotSummary(this.ctx.config.root, this.ctx.snapshot.summary); for (const [index, snapshot] of snapshotOutput.entries()) { const title = index === 0 ? "Snapshots" : ""; this.log(`${padSummaryTitle(title)} ${snapshot}`); } if (snapshotOutput.length > 1) { this.log(); } this.log(padSummaryTitle("Test Files"), getStateString$1(affectedFiles)); this.log(padSummaryTitle("Tests"), getStateString$1(tests)); if (this.ctx.projects.some((c) => c.config.typecheck.enabled)) { const failed = tests.filter((t) => t.meta?.typecheck && t.result?.errors?.length); this.log(padSummaryTitle("Type Errors"), failed.length ? c.bold(c.red(`${failed.length} failed`)) : c.dim("no errors")); } if (errors.length) { this.log(padSummaryTitle("Errors"), c.bold(c.red(`${errors.length} error${errors.length > 1 ? "s" : ""}`))); } this.log(padSummaryTitle("Start at"), this._timeStart); const collectTime = sum(files, (file) => file.collectDuration); const testsTime = sum(files, (file) => file.result?.duration); const setupTime = sum(files, (file) => file.setupDuration); if (this.watchFilters) { this.log(padSummaryTitle("Duration"), formatTime(collectTime + testsTime + setupTime)); } else { 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 ${formatTime(transformTime)}`, `setup ${formatTime(setupTime)}`, `collect ${formatTime(collectTime)}`, `tests ${formatTime(testsTime)}`, `environment ${formatTime(environmentTime)}`, `prepare ${formatTime(prepareTime)}`, typecheck && `typecheck ${formatTime(typecheck)}` ].filter(Boolean).join(", "); this.log(padSummaryTitle("Duration"), formatTime(executionTime) + c.dim(` (${timers})`)); } this.log(); } printErrorsSummary(files, errors) { 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); let current = 1; const errorDivider = () => this.error(`${c.red(c.dim(divider(`[${current++}/${failedTotal}]`, undefined, 1)))}\n`); if (failedSuites.length) { this.error(`\n${errorBanner(`Failed Suites ${failedSuites.length}`)}\n`); this.printTaskErrors(failedSuites, errorDivider); } if (failedTests.length) { this.error(`\n${errorBanner(`Failed Tests ${failedTests.length}`)}\n`); this.printTaskErrors(failedTests, errorDivider); } if (errors.length) { this.ctx.logger.printUnhandledErrors(errors); this.error(); } } reportBenchmarkSummary(files) { const benches = getTests(files); const topBenches = benches.filter((i) => i.result?.benchmark?.rank === 1); this.log(`\n${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(" > ")); this.log(` ${formatProjectName(bench.file.projectName)}${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); for (const sibling of siblings) { const number = (sibling.result.benchmark.mean / bench.result.benchmark.mean).toFixed(2); this.log(c.green(` ${number}x `) + c.gray("faster than ") + sibling.name); } this.log(""); } } printTaskErrors(tasks, errorDivider) { const errorsQueue = []; for (const task of tasks) { task.result?.errors?.forEach((error) => { let previous; if (error?.stackStr) { previous = errorsQueue.find((i) => { if (i[0]?.stackStr !== error.stackStr) { return false; } const currentProjectName = task?.projectName || task.file?.projectName || ""; const projectName = i[1][0]?.projectName || i[1][0].file?.projectName || ""; return projectName === currentProjectName; }); } 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?.filepath || ""; const projectName = task?.projectName || task.file?.projectName || ""; let name = getFullName(task, c.dim(" > ")); if (filepath) { name += c.dim(` [ ${this.relative(filepath)} ]`); } this.ctx.logger.error(`${c.red(c.bold(c.inverse(" FAIL ")))} ${formatProjectName(projectName)}${name}`); } const screenshotPaths = tasks.map((t) => t.meta?.failScreenshotPath).filter((screenshot) => screenshot != null); this.ctx.logger.printError(error, { project: this.ctx.getProjectByName(tasks[0].file.projectName || ""), verbose: this.verbose, screenshotPaths, task: tasks[0] }); errorDivider(); } } } function errorBanner(message) { return c.red(divider(c.bold(c.inverse(` ${message} `)))); } function sum(items, cb) { return items.reduce((total, next) => { return total + Math.max(cb(next) || 0, 0); }, 0); } class BasicReporter extends BaseReporter { constructor() { super(); this.isTTY = false; } onInit(ctx) { super.onInit(ctx); ctx.logger.log(c.inverse(c.bold(c.yellow(" DEPRECATED "))), c.yellow(`'basic' reporter is deprecated and will be removed in Vitest v3.\n` + `Remove 'basic' from 'reporters' option. To match 'basic' reporter 100%, use configuration:\n${JSON.stringify({ test: { reporters: [["default", { summary: false }]] } }, null, 2)}`)); } reportSummary(files, errors) { this.ctx.logger.log(); return super.reportSummary(files, errors); } } const DEFAULT_RENDER_INTERVAL_MS = 1e3; const ESC = "\x1B["; const CLEAR_LINE = `${ESC}K`; const MOVE_CURSOR_ONE_ROW_UP = `${ESC}1A`; const SYNC_START = `${ESC}?2026h`; const SYNC_END = `${ESC}?2026l`; /** * Renders content of `getWindow` at the bottom of the terminal and * forwards all other intercepted `stdout` and `stderr` logs above it. */ class WindowRenderer { options; streams; buffer = []; renderInterval = undefined; renderScheduled = false; windowHeight = 0; finished = false; cleanups = []; constructor(options) { this.options = { interval: DEFAULT_RENDER_INTERVAL_MS, ...options }; this.streams = { output: options.logger.outputStream.write.bind(options.logger.outputStream), error: options.logger.errorStream.write.bind(options.logger.errorStream) }; this.cleanups.push(this.interceptStream(process.stdout, "output"), this.interceptStream(process.stderr, "error")); this.options.logger.onTerminalCleanup(() => { this.flushBuffer(); this.stop(); }); this.start(); } start() { this.finished = false; this.renderInterval = setInterval(() => this.schedule(), this.options.interval).unref(); } stop() { this.cleanups.splice(0).map((fn) => fn()); clearInterval(this.renderInterval); } /** * Write all buffered output and stop buffering. * All intercepted writes are forwarded to actual write after this. */ finish() { this.finished = true; this.flushBuffer(); clearInterval(this.renderInterval); } /** * Queue new render update */ schedule() { if (!this.renderScheduled) { this.renderScheduled = true; this.flushBuffer(); setTimeout(() => { this.renderScheduled = false; }, 100).unref(); } } flushBuffer() { if (this.buffer.length === 0) { return this.render(); } let current; for (const next of this.buffer.splice(0)) { if (!current) { current = next; continue; } if (current.type !== next.type) { this.render(current.message, current.type); current = next; continue; } current.message += next.message; } if (current) { this.render(current?.message, current?.type); } } render(message, type = "output") { if (this.finished) { this.clearWindow(); return this.write(message || "", type); } const windowContent = this.options.getWindow(); const rowCount = getRenderedRowCount(windowContent, this.options.logger.getColumns()); let padding = this.windowHeight - rowCount; if (padding > 0 && message) { padding -= getRenderedRowCount([message], this.options.logger.getColumns()); } this.write(SYNC_START); this.clearWindow(); if (message) { this.write(message, type); } if (padding > 0) { this.write("\n".repeat(padding)); } this.write(windowContent.join("\n")); this.write(SYNC_END); this.windowHeight = rowCount + Math.max(0, padding); } clearWindow() { if (this.windowHeight === 0) { return; } this.write(CLEAR_LINE); for (let i = 1; i < this.windowHeight; i++) { this.write(`${MOVE_CURSOR_ONE_ROW_UP}${CLEAR_LINE}`); } this.windowHeight = 0; } interceptStream(stream, type) { const original = stream.write; stream.write = (chunk, _, callback) => { if (chunk) { if (this.finished) { this.write(chunk.toString(), type); } else { this.buffer.push({ type, message: chunk.toString() }); } } callback?.(); }; return function restore() { stream.write = original; }; } write(message, type = "output") { this.streams[type](message); } } /** Calculate the actual row count needed to render `rows` into `stream` */ function getRenderedRowCount(rows, columns) { let count = 0; for (const row of rows) { const text = stripVTControlCharacters(row); count += Math.max(1, Math.ceil(text.length / columns)); } return count; } const DURATION_UPDATE_INTERVAL_MS = 100; const FINISHED_TEST_CLEANUP_TIME_MS = 1e3; /** * Reporter extension that renders summary and forwards all other logs above itself. * Intended to be used by other reporters, not as a standalone reporter. */ class SummaryReporter { ctx; options; renderer; modules = emptyCounters(); tests = emptyCounters(); maxParallelTests = 0; /** Currently running test modules, may include finished test modules too */ runningModules = new Map(); /** ID of finished `this.runningModules` that are currently being shown */ finishedModules = new Map(); startTime = ""; currentTime = 0; duration = 0; durationInterval = undefined; onInit(ctx, options = {}) { this.ctx = ctx; this.options = { verbose: false, ...options }; this.renderer = new WindowRenderer({ logger: ctx.logger, getWindow: () => this.createSummary() }); this.ctx.onClose(() => { clearInterval(this.durationInterval); this.renderer.stop(); }); } onTestRunStart(specifications) { this.runningModules.clear(); this.finishedModules.clear(); this.modules = emptyCounters(); this.tests = emptyCounters(); this.startTimers(); this.renderer.start(); this.modules.total = specifications.length; } onTestRunEnd() { this.runningModules.clear(); this.finishedModules.clear(); this.renderer.finish(); clearInterval(this.durationInterval); } onTestModuleQueued(module) { if (this.finishedModules.size) { const finished = this.finishedModules.keys().next().value; this.removeTestModule(finished); } this.runningModules.set(module.id, initializeStats(module)); this.renderer.schedule(); } onTestModuleCollected(module) { let stats = this.runningModules.get(module.id); if (!stats) { stats = initializeStats(module); this.runningModules.set(module.id, stats); } const total = Array.from(module.children.allTests()).length; this.tests.total += total; stats.total = total; this.maxParallelTests = Math.max(this.maxParallelTests, this.runningModules.size); this.renderer.schedule(); } onHookStart(options) { const stats = this.getHookStats(options); if (!stats) { return; } const hook = { name: options.name, visible: false, startTime: performance.now(), onFinish: () => {} }; stats.hook?.onFinish?.(); stats.hook = hook; const timeout = setTimeout(() => { hook.visible = true; }, this.ctx.config.slowTestThreshold).unref(); hook.onFinish = () => clearTimeout(timeout); } onHookEnd(options) { const stats = this.getHookStats(options); if (stats?.hook?.name !== options.name) { return; } stats.hook.onFinish(); stats.hook.visible = false; } onTestCaseReady(test) { if (!this.options.verbose) { return; } const stats = this.runningModules.get(test.module.id); if (!stats || stats.tests.has(test.id)) { return; } const slowTest = { name: test.name, visible: false, startTime: performance.now(), onFinish: () => {} }; const timeout = setTimeout(() => { slowTest.visible = true; }, this.ctx.config.slowTestThreshold).unref(); slowTest.onFinish = () => { slowTest.hook?.onFinish(); clearTimeout(timeout); }; stats.tests.set(test.id, slowTest); } onTestCaseResult(test) { const stats = this.runningModules.get(test.module.id); if (!stats) { return; } stats.tests.get(test.id)?.onFinish(); stats.tests.delete(test.id); stats.completed++; const result = test.result(); if (result?.state === "passed") { this.tests.passed++; } else if (result?.state === "failed") { this.tests.failed++; } else if (!result?.state || result?.state === "skipped") { this.tests.skipped++; } this.renderer.schedule(); } onTestModuleEnd(module) { const state = module.state(); this.modules.completed++; if (state === "passed") { this.modules.passed++; } else if (state === "failed") { this.modules.failed++; } else if (module.task.mode === "todo" && state === "skipped") { this.modules.todo++; } else if (state === "skipped") { this.modules.skipped++; } const left = this.modules.total - this.modules.completed; if (left > this.maxParallelTests) { this.finishedModules.set(module.id, setTimeout(() => { this.removeTestModule(module.id); }, FINISHED_TEST_CLEANUP_TIME_MS).unref()); } else { this.removeTestModule(module.id); } this.renderer.schedule(); } getHookStats({ entity }) { if (!this.options.verbose) { return; } const module = entity.type === "module" ? entity : entity.module; const stats = this.runningModules.get(module.id); if (!stats) { return; } return entity.type === "test" ? stats.tests.get(entity.id) : stats; } createSummary() { const summary = [""]; for (const testFile of Array.from(this.runningModules.values()).sort(sortRunningModules)) { const typecheck = testFile.typecheck ? `${c.bgBlue(c.bold(" TS "))} ` : ""; summary.push(c.bold(c.yellow(` ${F_POINTER} `)) + formatProjectName(testFile.projectName) + typecheck + testFile.filename + c.dim(!testFile.completed && !testFile.total ? " [queued]" : ` ${testFile.completed}/${testFile.total}`)); const slowTasks = [testFile.hook, ...Array.from(testFile.tests.values())].filter((t) => t != null && t.visible); for (const [index, task] of slowTasks.entries()) { const elapsed = this.currentTime - task.startTime; const icon = index === slowTasks.length - 1 ? F_TREE_NODE_END : F_TREE_NODE_MIDDLE; summary.push(c.bold(c.yellow(` ${icon} `)) + task.name + c.bold(c.yellow(` ${formatTime(Math.max(0, elapsed))}`))); if (task.hook?.visible) { summary.push(c.bold(c.yellow(` ${F_TREE_NODE_END} `)) + task.hook.name); } } } if (this.runningModules.size > 0) { summary.push(""); } summary.push(padSummaryTitle("Test Files") + getStateString(this.modules)); summary.push(padSummaryTitle("Tests") + getStateString(this.tests)); summary.push(padSummaryTitle("Start at") + this.startTime); summary.push(padSummaryTitle("Duration") + formatTime(this.duration)); summary.push(""); return summary; } startTimers() { const start = performance.now(); this.startTime = formatTimeString(new Date()); this.durationInterval = setInterval(() => { this.currentTime = performance.now(); this.duration = this.currentTime - start; }, DURATION_UPDATE_INTERVAL_MS).unref(); } removeTestModule(id) { if (!id) { return; } const testFile = this.runningModules.get(id); testFile?.hook?.onFinish(); testFile?.tests?.forEach((test) => test.onFinish()); this.runningModules.delete(id); clearTimeout(this.finishedModules.get(id)); this.finishedModules.delete(id); } } function emptyCounters() { return { completed: 0, passed: 0, failed: 0, skipped: 0, todo: 0, total: 0 }; } function getStateString(entry) { return [ entry.failed ? c.bold(c.red(`${entry.failed} failed`)) : null, c.bold(c.green(`${entry.passed} passed`)), entry.skipped ? c.yellow(`${entry.skipped} skipped`) : null, entry.todo ? c.gray(`${entry.todo} todo`) : null ].filter(Boolean).join(c.dim(" | ")) + c.gray(` (${entry.total})`); } function sortRunningModules(a, b) { if ((a.projectName || "") > (b.projectName || "")) { return 1; } if ((a.projectName || "") < (b.projectName || "")) { return -1; } return a.filename.localeCompare(b.filename); } function initializeStats(module) { return { total: 0, completed: 0, filename: module.task.name, projectName: module.project.name, tests: new Map(), typecheck: !!module.task.meta.typecheck }; } class DefaultReporter extends BaseReporter { options; summary; constructor(options = {}) { super(options); this.options = { summary: true, ...options }; if (!this.isTTY) { this.options.summary = false; } if (this.options.summary) { this.summary = new SummaryReporter(); } } onTestRunStart(specifications) { this.summary?.onTestRunStart(specifications); } onTestModuleQueued(file) { this.summary?.onTestModuleQueued(file); } onTestModuleCollected(module) { this.summary?.onTestModuleCollected(module); } onTestModuleEnd(module) { super.onTestModuleEnd(module); this.summary?.onTestModuleEnd(module); } onTestCaseReady(test) { this.summary?.onTestCaseReady(test); } onTestCaseResult(test) { super.onTestCaseResult(test); this.summary?.onTestCaseResult(test); } onHookStart(hook) { this.summary?.onHookStart(hook); } onHookEnd(hook) { this.summary?.onHookEnd(hook); } onInit(ctx) { super.onInit(ctx); this.summary?.onInit(ctx, { verbose: this.verbose }); } onPathsCollected(paths = []) { if (this.isTTY) { if (this.renderSucceed === undefined) { this.renderSucceed = !!this.renderSucceed; } if (this.renderSucceed !== true) { this.renderSucceed = paths.length <= 1; } } } onTestRunEnd() { this.summary?.onTestRunEnd(); } } class DotReporter extends BaseReporter { renderer; tests = new Map(); finishedTests = new Set(); onInit(ctx) { super.onInit(ctx); if (this.isTTY) { this.renderer = new WindowRenderer({ logger: ctx.logger, getWindow: () => this.createSummary() }); this.ctx.onClose(() => this.renderer?.stop()); } } printTestModule(testModule) { if (!this.isTTY) { super.printTestModule(testModule); } } onWatcherRerun(files, trigger) { this.tests.clear(); this.renderer?.start(); super.onWatcherRerun(files, trigger); } onFinished(files, errors) { if (this.isTTY) { const finalLog = formatTests(Array.from(this.tests.values())); this.ctx.logger.log(finalLog); } this.tests.clear(); this.renderer?.finish(); super.onFinished(files, errors); } onTestModuleCollected(module) { for (const test of module.children.allTests()) { this.onTestCaseReady(test); } } onTestCaseReady(test) { if (this.finishedTests.has(test.id)) { return; } this.tests.set(test.id, test.result().state || "run"); this.renderer?.schedule(); } onTestCaseResult(test) { super.onTestCaseResult(test); this.finishedTests.add(test.id); this.tests.set(test.id, test.result().state || "skipped"); this.renderer?.schedule(); } onTestModuleEnd(testModule) { super.onTestModuleEnd(testModule); if (!this.isTTY) { return; } const columns = this.ctx.logger.getColumns(); if (this.tests.size < columns) { return; } const finishedTests = Array.from(this.tests).filter((entry) => entry[1] !== "pending"); if (finishedTests.length < columns) { return; } const states = []; let count = 0; for (const [id, state] of finishedTests) { if (count++ >= columns) { break; } this.tests.delete(id); states.push(state); } this.ctx.logger.log(formatTests(states)); this.renderer?.schedule(); } createSummary() { return [formatTests(Array.from(this.tests.values())), ""]; } } const pass = { char: "·", color: c.green }; const fail = { char: "x", color: c.red }; const pending = { char: "*", color: c.yellow }; const skip = { char: "-", color: (char) => c.dim(c.gray(char)) }; function getIcon(state) { switch (state) { case "passed": return pass; case "failed": return fail; case "skipped": return skip; default: return pending; } } /** * Format test states into string while keeping ANSI escapes at minimal. * Sibling icons with same color are merged into a single c.color() call. */ function formatTests(states) { let currentIcon = pending; let count = 0; let output = ""; for (const state of states) { const icon = getIcon(state); if (currentIcon === icon) { count++; continue; } output += currentIcon.color(currentIcon.char.repeat(count)); count = 1; currentIcon = icon; } output += currentIcon.color(currentIcon.char.repeat(count)); return output; } class GithubActionsReporter { ctx = undefined; onInit(ctx) { this.ctx = ctx; } onFinished(files = [], errors = []) { const projectErrors = new Array(); for (const error of errors) { projectErrors.push({ project: this.ctx.getRootProject(), title: "Unhandled error", error }); } for (const file of files) { const tasks = getTasks(file); const project = this.ctx.getProjectByName(file.projectName || ""); for (const task of tasks) { if (task.result?.state !== "fail") { continue; } const title = getFullName(task, " > ");