vitest
Version:
Next generation testing framework powered by Vite
1,355 lines (1,332 loc) • 129 kB
JavaScript
import { existsSync, readFileSync, promises } from 'node:fs';
import { mkdir, writeFile, readdir, stat, readFile } from 'node:fs/promises';
import { resolve as resolve$1, dirname, isAbsolute, relative, basename, join, normalize } from 'pathe';
import { performance as performance$1 } from 'node:perf_hooks';
import { getTests, getTestName, hasFailed, getSuites, generateHash, calculateSuiteHash, someTasksAreOnly, interpretTaskModes, getTasks, getFullName } from '@vitest/runner/utils';
import { slash, toArray, isPrimitive } from '@vitest/utils/helpers';
import { parseStacktrace, defaultStackIgnorePatterns, parseErrorStacktrace } from '@vitest/utils/source-map';
import c from 'tinyrainbow';
import { i as isTTY } from './env.D4Lgay0q.js';
import { stripVTControlCharacters } from 'node:util';
import { Console } from 'node:console';
import { Writable } from 'node:stream';
import { inspect } from '@vitest/utils/display';
import nodeos__default, { hostname } from 'node:os';
import { x } from 'tinyexec';
import { distDir } from '../path.js';
import { parseAstAsync } from 'vite';
import { positionToOffset, lineSplitRE } from '@vitest/utils/offset';
import { createRequire } from 'node:module';
/// <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$1 = {};
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$1;
lazy.push({k, a: [input, parsed, tmp, $]});
}
else
output[k] = $.call(output, k, tmp);
}
else if (output[k] !== ignore$1)
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$1 = (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 getOutputFile(config, reporter) {
if (!config?.outputFile) return;
if (typeof config.outputFile === "string") return config.outputFile;
return config.outputFile[reporter];
}
function createDefinesScript(define) {
if (!define) return "";
if (serializeDefine(define) === "{}") return "";
return `
const defines = ${serializeDefine(define)}
Object.keys(defines).forEach((key) => {
const segments = key.split('.')
let target = globalThis
for (let i = 0; i < segments.length; i++) {
const segment = segments[i]
if (i === segments.length - 1) {
target[segment] = defines[key]
} else {
target = target[segment] || (target[segment] = {})
}
}
})
`;
}
/**
* Like `JSON.stringify` but keeps raw string values as a literal
* in the generated code. For example: `"window"` would refer to
* the global `window` object directly.
*/
function serializeDefine(define) {
const userDefine = {};
for (const key in define) {
// vitest sets this to avoid vite:client-inject plugin
if (key === "process.env.NODE_ENV" && define[key] === "process.env.NODE_ENV") continue;
// import.meta.env.* is handled in `importAnalysis` plugin
if (!key.startsWith("import.meta.env.")) userDefine[key] = define[key];
}
let res = `{`;
const keys = Object.keys(userDefine).sort();
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const val = userDefine[key];
res += `${JSON.stringify(key)}: ${handleDefineValue(val)}`;
if (i !== keys.length - 1) res += `, `;
}
return `${res}}`;
}
function handleDefineValue(value) {
if (typeof value === "undefined") return "undefined";
if (typeof value === "string") return value;
return JSON.stringify(value);
}
class BlobReporter {
start = 0;
ctx;
options;
coverage;
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;
this.start = performance.now();
this.coverage = void 0;
}
onCoverage(coverage) {
this.coverage = coverage;
}
async onTestRunEnd(testModules, unhandledErrors) {
const executionTime = performance.now() - this.start;
const files = testModules.map((testModule) => testModule.task);
const errors = [...unhandledErrors];
const coverage = this.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 = [
this.ctx.version,
files,
errors,
modules,
coverage,
executionTime
];
const reportFile = resolve$1(this.ctx.config.root, outputFile);
await writeBlob(report, reportFile);
this.ctx.logger.log("blob report written to", reportFile);
}
}
async function writeBlob(content, filename) {
const report = stringify(content);
const dir = dirname(filename);
if (!existsSync(dir)) await mkdir(dir, { recursive: true });
await writeFile(filename, report, "utf-8");
}
async function readBlobs(currentVersion, blobsDirectory, projectsArray) {
// using process.cwd() because --merge-reports can only be used in CLI
const resolvedDir = resolve$1(process.cwd(), blobsDirectory);
const promises = (await readdir(resolvedDir)).map(async (filename) => {
const fullPath = resolve$1(resolvedDir, filename);
if (!(await stat(fullPath)).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 [version, files, errors, moduleKeys, coverage, executionTime] = parse$1(await readFile(fullPath, "utf-8"));
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,
executionTime
};
});
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}`);
// fake module graph - it is used to check if module is imported, but we don't use values inside
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;
moduleNode.transformResult = {
code: " ",
map: null
};
project.vite.moduleGraph.idToModuleMap.set(moduleId, moduleNode);
});
});
});
return {
files: blobs.flatMap((blob) => blob.files).sort((f1, f2) => {
return (f1.result?.startTime || 0) - (f2.result?.startTime || 0);
}),
errors: blobs.flatMap((blob) => blob.errors),
coverages: blobs.map((blob) => blob.coverage),
executionTimes: blobs.map((blob) => blob.executionTime)
};
}
function hasFailedSnapshot(suite) {
return getTests(suite).some((s) => {
return s.result?.errors?.some((e) => typeof e?.message === "string" && e.message.match(/Snapshot .* mismatched/));
});
}
function convertTasksToEvents(file, onTask) {
const packs = [];
const events = [];
function visit(suite) {
onTask?.(suite);
packs.push([
suite.id,
suite.result,
suite.meta
]);
events.push([
suite.id,
"suite-prepare",
void 0
]);
suite.tasks.forEach((task) => {
if (task.type === "suite") visit(task);
else {
onTask?.(task);
if (suite.mode !== "skip" && suite.mode !== "todo") {
packs.push([
task.id,
task.result,
task.meta
]);
events.push([
task.id,
"test-prepare",
void 0
]);
task.annotations.forEach((annotation) => {
events.push([
task.id,
"test-annotation",
{ annotation }
]);
});
task.artifacts.forEach((artifact) => {
events.push([
task.id,
"test-artifact",
{ artifact }
]);
});
events.push([
task.id,
"test-finished",
void 0
]);
}
}
});
events.push([
suite.id,
"suite-finished",
void 0
]);
}
visit(file);
return {
packs,
events
};
}
const F_RIGHT = "→";
const F_DOWN = "↓";
const F_DOWN_RIGHT = "↳";
const F_POINTER = "❯";
const F_DOT = "·";
const F_CHECK = "✓";
const F_CROSS = "×";
const F_LONG_DASH = "⎯";
const F_TREE_NODE_MIDDLE = "├──";
const F_TREE_NODE_END = "└──";
const pointer = c.yellow(F_POINTER);
const skipped = c.dim(c.gray(F_DOWN));
const benchmarkPass = c.green(F_DOT);
const testPass = c.green(F_CHECK);
const taskFail = c.red(F_CROSS);
const suiteFail = c.red(F_POINTER);
const pending$1 = c.gray("·");
const separator = c.dim(" > ");
const labelDefaultColors = [
c.bgYellow,
c.bgCyan,
c.bgGreen,
c.bgMagenta
];
function getCols(delta = 0) {
let length = process.stdout?.columns;
if (!length || Number.isNaN(length)) length = 30;
return Math.max(length + delta, 0);
}
function errorBanner(message) {
return divider(c.bold(c.bgRed(` ${message} `)), null, null, c.red);
}
function divider(text, left, right, color) {
const cols = getCols();
const c = color || ((text) => text);
if (text) {
const textLength = stripVTControlCharacters(text).length;
if (left == null && right != null) left = cols - textLength - right;
else {
left = left ?? Math.floor((cols - textLength) / 2);
right = cols - textLength - left;
}
left = Math.max(0, left);
right = Math.max(0, right);
return `${c(F_LONG_DASH.repeat(left))}${text}${c(F_LONG_DASH.repeat(right))}`;
}
return F_LONG_DASH.repeat(cols);
}
function formatTestPath(root, path) {
if (isAbsolute(path)) path = relative(root, path);
const dir = dirname(path);
const ext = path.match(/(\.(spec|test)\.[cm]?[tj]sx?)$/)?.[0] || "";
const base = basename(path, ext);
return slash(c.dim(`${dir}/`) + c.bold(base)) + c.dim(ext);
}
function renderSnapshotSummary(rootDir, snapshots) {
const summary = [];
if (snapshots.added) summary.push(c.bold(c.green(`${snapshots.added} written`)));
if (snapshots.unmatched) summary.push(c.bold(c.red(`${snapshots.unmatched} failed`)));
if (snapshots.updated) summary.push(c.bold(c.green(`${snapshots.updated} updated `)));
if (snapshots.filesRemoved) if (snapshots.didUpdate) summary.push(c.bold(c.green(`${snapshots.filesRemoved} files removed `)));
else summary.push(c.bold(c.yellow(`${snapshots.filesRemoved} files obsolete `)));
if (snapshots.filesRemovedList && snapshots.filesRemovedList.length) {
const [head, ...tail] = snapshots.filesRemovedList;
summary.push(`${c.gray(F_DOWN_RIGHT)} ${formatTestPath(rootDir, head)}`);
tail.forEach((key) => {
summary.push(` ${c.gray(F_DOT)} ${formatTestPath(rootDir, key)}`);
});
}
if (snapshots.unchecked) {
if (snapshots.didUpdate) summary.push(c.bold(c.green(`${snapshots.unchecked} removed`)));
else summary.push(c.bold(c.yellow(`${snapshots.unchecked} obsolete`)));
snapshots.uncheckedKeysByFile.forEach((uncheckedFile) => {
summary.push(`${c.gray(F_DOWN_RIGHT)} ${formatTestPath(rootDir, uncheckedFile.filePath)}`);
uncheckedFile.keys.forEach((key) => summary.push(` ${c.gray(F_DOT)} ${key}`));
});
}
return summary;
}
function countTestErrors(tasks) {
return tasks.reduce((c, i) => c + (i.result?.errors?.length || 0), 0);
}
function getStateString$1(tasks, name = "tests", showTotal = true) {
if (tasks.length === 0) return c.dim(`no ${name}`);
const passed = tasks.reduce((acc, i) => i.result?.state === "pass" ? acc + 1 : acc, 0);
const failed = tasks.reduce((acc, i) => i.result?.state === "fail" ? acc + 1 : acc, 0);
const skipped = tasks.reduce((acc, i) => i.mode === "skip" ? acc + 1 : acc, 0);
const todo = tasks.reduce((acc, i) => i.mode === "todo" ? acc + 1 : acc, 0);
return [
failed ? c.bold(c.red(`${failed} failed`)) : null,
passed ? c.bold(c.green(`${passed} passed`)) : null,
skipped ? c.yellow(`${skipped} skipped`) : null,
todo ? c.gray(`${todo} todo`) : null
].filter(Boolean).join(c.dim(" | ")) + (showTotal ? c.gray(` (${tasks.length})`) : "");
}
function getStateSymbol(task) {
if (task.mode === "skip" || task.mode === "todo") return skipped;
if (!task.result) return pending$1;
if (task.result.state === "run" || task.result.state === "queued") {
if (task.type === "suite") return pointer;
}
if (task.result.state === "pass") return task.meta?.benchmark ? benchmarkPass : testPass;
if (task.result.state === "fail") return task.type === "suite" ? suiteFail : taskFail;
return " ";
}
function formatTimeString(date) {
return date.toTimeString().split(" ")[0];
}
function formatTime(time) {
if (time > 1e3) return `${(time / 1e3).toFixed(2)}s`;
return `${Math.round(time)}ms`;
}
function formatProjectName(project, suffix = " ") {
if (!project?.name) return "";
if (!c.isColorSupported) return `|${project.name}|${suffix}`;
let background = project.color && c[`bg${capitalize(project.color)}`];
if (!background) background = labelDefaultColors[project.name.split("").reduce((acc, v, idx) => acc + v.charCodeAt(0) + idx, 0) % labelDefaultColors.length];
return c.black(background(` ${project.name} `)) + suffix;
}
function withLabel(color, label, message) {
const bgColor = `bg${color.charAt(0).toUpperCase()}${color.slice(1)}`;
return `${c.bold(c[bgColor](` ${label} `))} ${message ? c[color](message) : ""}`;
}
function padSummaryTitle(str) {
return c.dim(`${str.padStart(11)} `);
}
function truncateString(text, maxLength) {
const plainText = stripVTControlCharacters(text);
if (plainText.length <= maxLength) return text;
return `${plainText.slice(0, maxLength - 1)}…`;
}
function capitalize(text) {
return `${text[0].toUpperCase()}${text.slice(1)}`;
}
var utils = /*#__PURE__*/Object.freeze({
__proto__: null,
benchmarkPass: benchmarkPass,
countTestErrors: countTestErrors,
divider: divider,
errorBanner: errorBanner,
formatProjectName: formatProjectName,
formatTestPath: formatTestPath,
formatTime: formatTime,
formatTimeString: formatTimeString,
getStateString: getStateString$1,
getStateSymbol: getStateSymbol,
padSummaryTitle: padSummaryTitle,
pending: pending$1,
pointer: pointer,
renderSnapshotSummary: renderSnapshotSummary,
separator: separator,
skipped: skipped,
suiteFail: suiteFail,
taskFail: taskFail,
testPass: testPass,
truncateString: truncateString,
withLabel: withLabel
});
const BADGE_PADDING = " ";
class BaseReporter {
start = 0;
end = 0;
watchFilters;
failedUnwatchedFiles = [];
isTTY;
ctx = void 0;
renderSucceed = false;
verbose = false;
_filesInWatchMode = /* @__PURE__ */ new Map();
_timeStart = formatTimeString(/* @__PURE__ */ new Date());
constructor(options = {}) {
this.isTTY = options.isTTY ?? isTTY;
}
onInit(ctx) {
this.ctx = ctx;
this.ctx.logger.printBanner();
}
log(...messages) {
this.ctx.logger.log(...messages);
}
error(...messages) {
this.ctx.logger.error(...messages);
}
relative(path) {
return relative(this.ctx.config.root, path);
}
onTestRunStart(_specifications) {
this.start = performance$1.now();
this._timeStart = formatTimeString(/* @__PURE__ */ new Date());
}
onTestRunEnd(testModules, unhandledErrors, _reason) {
const files = testModules.map((testModule) => testModule.task);
const errors = [...unhandledErrors];
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;
// delaying logs to calculate the test stats first
// which minimizes the amount of for loops
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();
// Skipped suites are hidden when --hideSkippedTests, print otherwise
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")
// Skipped suites are hidden when --hideSkippedTests
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 = 0 } = test.diagnostic() || {};
const padding = this.getTestIndentation(test.task);
const suffix = this.getTestCaseSuffix(test);
if (testResult.state === "failed") this.log(c.red(` ${padding}${taskFail} ${this.getTestName(test.task, separator)}`) + suffix);
else if (duration > this.ctx.config.slowTestThreshold) this.log(` ${padding}${c.yellow(c.dim(F_CHECK))} ${this.getTestName(test.task, separator)} ${suffix}`);
else if (this.ctx.config.hideSkippedTests && testResult.state === "skipped") ; else if (this.renderSucceed || moduleState === "failed") this.log(` ${padding}${this.getStateSymbol(test)} ${this.getTestName(test.task, separator)}${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`);
return ` ${this.getEntityPrefix(testModule)} ${testModule.task.name} ${suffix}`;
}
printTestSuite(testSuite) {
if (!this.renderSucceed) return;
const indentation = " ".repeat(getIndentation(testSuite.task));
const tests = Array.from(testSuite.children.allTests());
const state = this.getStateSymbol(testSuite);
this.log(` ${indentation}${state} ${testSuite.name} ${c.dim(`(${tests.length})`)}`);
}
getTestName(test, _separator) {
return test.name;
}
getFullName(test, separator) {
if (test === test.file) return test.name;
let name = test.file.name;
if (test.location) name += c.dim(`:${test.location.line}:${test.location.column}`);
name += separator;
name += getTestName(test, separator);
return name;
}
getTestIndentation(test) {
return " ".repeat(getIndentation(test));
}
printAnnotations(test, console, padding = 0) {
const annotations = test.annotations();
if (!annotations.length) return;
const PADDING = " ".repeat(padding);
const groupedAnnotations = {};
annotations.forEach((annotation) => {
const { location, type } = annotation;
let group;
if (location) {
const file = relative(test.project.config.root, location.file);
group = `${c.gray(`${file}:${location.line}:${location.column}`)} ${c.bold(type)}`;
} else group = c.bold(type);
groupedAnnotations[group] ??= [];
groupedAnnotations[group].push(annotation);
});
for (const group in groupedAnnotations) {
this[console](`${PADDING}${c.blue(F_POINTER)} ${group}`);
groupedAnnotations[group].forEach(({ message }) => {
this[console](`${PADDING} ${c.blue(F_DOWN_RIGHT)} ${message}`);
});
}
}
getEntityPrefix(entity) {
let title = this.getStateSymbol(entity);
if (entity.project.name) title += ` ${formatProjectName(entity.project, "")}`;
if (entity.meta().typecheck) title += ` ${c.bgBlue(c.bold(" TS "))}`;
return title;
}
getTestCaseSuffix(testCase) {
const { heap, retryCount, repeatCount } = testCase.diagnostic() || {};
const testResult = testCase.result();
let suffix = this.getDurationPrefix(testCase.task);
if (retryCount != null && retryCount > 0) suffix += c.yellow(` (retry x${retryCount})`);
if (repeatCount != null && repeatCount > 0) suffix += c.yellow(` (repeat x${repeatCount})`);
if (heap != null) suffix += c.magenta(` ${Math.floor(heap / 1024 / 1024)} MB heap used`);
if (testResult.state === "skipped" && testResult.note) suffix += c.dim(c.gray(` [${testResult.note}]`));
return suffix;
}
getStateSymbol(test) {
return getStateSymbol(test.task);
}
getDurationPrefix(task) {
const duration = task.result?.duration && Math.round(task.result?.duration);
if (duration == null) return "";
return (duration > this.ctx.config.slowTestThreshold ? c.yellow : c.green)(` ${duration}${c.dim("ms")}`);
}
onWatcherStart(files = this.ctx.state.getFiles(), errors = this.ctx.state.getUnhandledErrors()) {
if (errors.length > 0 || hasFailed(files)) 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");
// Update re-run count for each file
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);
}
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) : void 0;
if (task) headerText = this.getFullName(task, separator);
else if (log.taskId && log.taskId !== "__vitest__unknown_test__") headerText = log.taskId;
write(c.gray(log.type + c.dim(` | ${headerText}\n`)) + log.content);
if (log.origin) {
// browser logs don't have an extra end of line at the end like Node.js does
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;
if (this.ctx.config.onConsoleLog) {
const task = log.taskId ? this.ctx.state.idMap.get(log.taskId) : void 0;
const entity = task && this.ctx.state.getReportedEntity(task);
if (this.ctx.config.onConsoleLog(log.content, log.type, entity) === false) return false;
}
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 blobs = this.ctx.state.blobs;
// Execution time is either sum of all runs of `--merge-reports` or the current run's time
const executionTime = blobs?.executionTimes ? sum(blobs.executionTimes, (time) => time) : this.end - this.start;
const environmentTime = sum(files, (file) => file.environmentLoad);
const prepareTime = sum(files, (file) => file.prepareDuration);
const transformTime = this.ctx.state.transformTime;
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})`));
if (blobs?.executionTimes) this.log(padSummaryTitle("Per blob") + blobs.executionTimes.map((time) => ` ${formatTime(time)}`).join(""));
}
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);
// TODO: error divider should take into account merged errors for counting
let current = 1;
const errorDivider = () => this.error(`${c.red(c.dim(divider(`[${current++}/${failedTotal}]`, void 0, 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 topBenches = getTests(files).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 = this.getFullName(group, separator);
const project = this.ctx.projects.find((p) => p.name === bench.file.projectName);
this.log(` ${formatProjectName(project)}${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)
// Merge identical errors
task.result?.errors?.forEach((error) => {
let previous;
if (error?.stack) previous = errorsQueue.find((i) => {
if (i[0]?.stack !== error.stack || i[0]?.diff !== error.diff) return false;
const currentProjectName = task?.projectName || task.file?.projectName || "";
const projectName = i[1][0]?.projectName || i[1][0].file?.projectName || "";
const currentAnnotations = task.type === "test" && task.annotations;
const itemAnnotations = i[1][0].type === "test" && i[1][0].annotations;
return projectName === currentProjectName && deepEqual(currentAnnotations, itemAnnotations);
});
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 || "";
const project = this.ctx.projects.find((p) => p.name === projectName);
let name = this.getFullName(task, separator);
if (filepath) name += c.dim(` [ ${this.relative(filepath)} ]`);
this.ctx.logger.error(`${c.bgRed(c.bold(" FAIL "))} ${formatProjectName(project)}${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]
});
if (tasks[0].type === "test" && tasks[0].annotations.length) {
const test = this.ctx.state.getReportedEntity(tasks[0]);
this.printAnnotations(test, "error", 1);
this.error();
}
errorDivider();
}
}
}
function deepEqual(a, b) {
if (a === b) return true;
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
for (const key of keysA) if (!keysB.includes(key) || !deepEqual(a[key], b[key])) return false;
return true;
}
function sum(items, cb) {
return items.reduce((total, next) => {
return total + Math.max(cb(next) || 0, 0);
}, 0);
}
function getIndentation(suite, level = 1) {
if (suite.suite && !("filepath" in suite.suite)) return getIndentation(suite.suite, level + 1);
return level;
}
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 = void 0;
renderScheduled = false;
windowHeight = 0;
started = false;
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"));
// Write buffered content on unexpected exits, e.g. direct `process.exit()` calls
this.options.logger.onTerminalCleanup(() => {
this.flushBuffer();
this.stop();
});
}
start() {
this.started = true;
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;
// Concatenate same types into a single render
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;
// @ts-expect-error -- not sure how 2 overloads should be typed
stream.write = (chunk, _, callback) => {
if (chunk) if (this.finished || !this.started) 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 = /* @__PURE__ */ new Map();
/** ID of finished `this.runningModules` that are currently being shown */
finishedModules = /* @__PURE__ */ new Map();
startTime = "";
currentTime = 0;
duration = 0;
durationInterval = void 0;
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) {
// When new test module starts, take the place of previously finished test module, if any
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) {
// Track slow running tests only on verbose mode
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++;
// Keep finished tests visible in summary for a while if there are more tests left.
// When a new test starts in onTestModuleQueued it will take this ones place.
// This reduces flickering by making summary more stable.
if (this.modules.total - this.modules.completed > this.maxParallelTests) this.finishedModules.set(module.id, setTimeout(() => {
this.removeTestModule(module.id);
}, FINISHED_TEST_CLEANUP_TIME_MS).unref());
else
// Run is about to end as there are less tests left than whole run had parallel at max.
// Remove finished test immediately.
this.removeTestModule(module.id);
this.renderer.schedule();
}
getHookStats({ entity }) {
// Track slow running hooks only on verbose mode
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({
name: testFile.projectName,
color: testFile.projectColor
}) + typecheck + testFile.filename + c.dim(!testFile.completed && !testFile.total ? " [queued]" : ` ${testFile.completed}/${testFile.total}`));
const slowTasks = [testFile.hook, ...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(/* @__PURE__ */ 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,
projectColor: module.project.color,
tests: /* @__PURE__ */ 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) {
if (this.isTTY) {
if (this.renderSucceed === void 0) this.renderSucceed = !!this.renderSucceed;
if (this.renderSucceed !== true) this.renderSucceed = specifications.length <= 1;
}
super.onTestRunStart(specifications);
this.summary?.onTestRunStart(specifications);
}
onTestRunEnd(testModules, unhandledErrors, reason) {
super.onTestRunEnd(testModules, unhandledErrors, reason);
this.summary?.onTestRunEnd();
}
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 });
}
}
class DotReporter extends BaseReporter {
renderer;
tests = /* @__PURE__ */ new Map();
finishedTests = /* @__PURE__ */ 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());
}
}
// Ignore default logging of base reporter
printTestModule() {}
onWatcherRerun(files, trigger) {
this.tests.clear();
this.renderer?.start();
super.onWatcherRerun(files, trigger);
}
onTestRunEnd(testModules, unhandledErrors, reaso