@epicat/toon-reporter 0.0.7 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -5,8 +5,10 @@ import { Reporter, TestRunEndReason, Vitest } from "vitest/node";
5
5
  interface ToonReporterOptions {
6
6
  outputFile?: string;
7
7
  color?: boolean;
8
- /** Include per-file coverage percentages (lines, stmts, branch, funcs) */
8
+ /** When true, shows all files and all coverage metrics. When false (default), only shows files with gaps and only metrics < 100% */
9
9
  verbose?: boolean;
10
+ /** When true, shows per-test timing in passing[N]{at,name,ms} format */
11
+ timing?: boolean;
10
12
  /** @internal Used for testing to capture output */
11
13
  _captureOutput?: (output: string) => void;
12
14
  }
package/dist/index.mjs CHANGED
@@ -38,13 +38,26 @@ const colors = {
38
38
  red: (s) => `\x1b[31m${s}\x1b[0m`,
39
39
  yellow: (s) => `\x1b[33m${s}\x1b[0m`,
40
40
  gray: (s) => `\x1b[90m${s}\x1b[0m`,
41
- cyan: (s) => `\x1b[36m${s}\x1b[0m`
41
+ cyan: (s) => `\x1b[36m${s}\x1b[0m`,
42
+ purple: (s) => `\x1b[35m${s}\x1b[0m`
42
43
  };
43
44
  function shouldUseColor(option) {
44
45
  if (process.env.CI) return false;
45
46
  if (option !== void 0) return option;
46
47
  return !!process.env.COLOR;
47
48
  }
49
+ function formatDuration(ms) {
50
+ const h = Math.floor(ms / 36e5);
51
+ const m = Math.floor(ms % 36e5 / 6e4);
52
+ const s = Math.floor(ms % 6e4 / 1e3);
53
+ const remaining = Math.round(ms % 1e3);
54
+ const parts = [];
55
+ if (h) parts.push(`${h}h`);
56
+ if (m) parts.push(`${m}m`);
57
+ if (s) parts.push(`${s}s`);
58
+ if (remaining || parts.length === 0) parts.push(`${remaining}ms`);
59
+ return parts.join("");
60
+ }
48
61
  var ToonReporter = class {
49
62
  constructor(options = {}) {
50
63
  this.options = options;
@@ -76,19 +89,27 @@ var ToonReporter = class {
76
89
  const verbose = this.options.verbose;
77
90
  for (const file of map.files()) {
78
91
  const fc = map.fileCoverageFor(file);
92
+ const fileSummary = fc.toSummary().toJSON();
79
93
  const uncoveredLines = fc.getUncoveredLines();
80
- const hasGaps = uncoveredLines.length > 0;
81
- if (!verbose && !hasGaps) continue;
94
+ const lines = fileSummary.lines?.pct ?? 100;
95
+ const stmts = fileSummary.statements?.pct ?? 100;
96
+ const branch = fileSummary.branches?.pct ?? 100;
97
+ const funcs = fileSummary.functions?.pct ?? 100;
98
+ if (!verbose && lines === 100 && stmts === 100 && branch === 100 && funcs === 100) continue;
82
99
  const entry = {
83
100
  file: relative(rootDir, file),
84
101
  uncoveredLines: this.formatLineRanges(uncoveredLines)
85
102
  };
86
103
  if (verbose) {
87
- const fileSummary = fc.toSummary().toJSON();
88
- entry["lines%"] = fileSummary.lines?.pct ?? 0;
89
- entry["stmts%"] = fileSummary.statements?.pct ?? 0;
90
- entry["branch%"] = fileSummary.branches?.pct ?? 0;
91
- entry["funcs%"] = fileSummary.functions?.pct ?? 0;
104
+ entry["lines%"] = lines;
105
+ entry["stmts%"] = stmts;
106
+ entry["branch%"] = branch;
107
+ entry["funcs%"] = funcs;
108
+ } else {
109
+ if (lines < 100) entry["lines%"] = lines;
110
+ if (stmts < 100) entry["stmts%"] = stmts;
111
+ if (branch < 100) entry["branch%"] = branch;
112
+ if (funcs < 100) entry["funcs%"] = funcs;
92
113
  }
93
114
  entries.push(entry);
94
115
  }
@@ -149,10 +170,12 @@ var ToonReporter = class {
149
170
  return message;
150
171
  }
151
172
  buildReportForModules(modules) {
152
- const tests = getTests(modules.map((m) => m.task));
173
+ const files = modules.map((m) => m.task);
174
+ const tests = getTests(files);
153
175
  const rootDir = this.ctx.config.root;
154
176
  const failedTests = tests.filter((t) => t.result?.state === "fail");
155
- const passedCount = tests.filter((t) => t.result?.state === "pass").length;
177
+ const passedTests = tests.filter((t) => t.result?.state === "pass");
178
+ const passedCount = passedTests.length;
156
179
  const skippedTests = tests.filter((t) => t.mode === "skip");
157
180
  const todoTests = tests.filter((t) => t.mode === "todo");
158
181
  const grouped = /* @__PURE__ */ new Map();
@@ -183,13 +206,30 @@ var ToonReporter = class {
183
206
  at: this.formatLocation(relative(rootDir, t.file.filepath), t.location?.line, t.location?.column),
184
207
  name: t.name
185
208
  });
186
- const report = { passing: passedCount };
209
+ const report = {};
210
+ if (this.options.timing) {
211
+ report.duration = formatDuration(files.reduce((sum, f) => sum + (f.result?.duration ?? 0), 0));
212
+ report.passing = passedTests.map((t) => ({
213
+ at: this.formatLocation(relative(rootDir, t.file.filepath), t.location?.line, t.location?.column),
214
+ name: t.name,
215
+ ms: Math.round(t.result?.duration ?? 0)
216
+ }));
217
+ } else report.passing = passedCount;
187
218
  if (failures.length > 0) report.failing = failures;
188
- if (todoTests.length > 0) report.todo = todoTests.map(mapToSkipped);
189
- if (skippedTests.length > 0) report.skipped = skippedTests.map(mapToSkipped);
219
+ if (passedCount === 0 && failedTests.length === 0) {
220
+ if (todoTests.length > 0) report.todo = todoTests.length;
221
+ if (skippedTests.length > 0) report.skipped = skippedTests.length;
222
+ } else {
223
+ if (todoTests.length > 0) report.todo = todoTests.map(mapToSkipped);
224
+ if (skippedTests.length > 0) report.skipped = skippedTests.map(mapToSkipped);
225
+ }
190
226
  return report;
191
227
  }
192
228
  async onTestRunEnd(testModules, _unhandledErrors, _reason) {
229
+ if (testModules.length === 0) {
230
+ await this.writeReport(encode({ error: "No test files found" }));
231
+ return;
232
+ }
193
233
  const projectNames = /* @__PURE__ */ new Set();
194
234
  const modulesByProject = /* @__PURE__ */ new Map();
195
235
  for (const m of testModules) {
@@ -209,14 +249,17 @@ var ToonReporter = class {
209
249
  else await this.writeReport(encode(projectReports));
210
250
  return;
211
251
  }
252
+ const testNamePattern = this.ctx.config.testNamePattern;
212
253
  const report = this.buildReportForModules([...testModules]);
213
254
  const coverage = this.getCoverageSummary();
214
255
  if (coverage) report.coverage = coverage;
215
- await this.writeReport(encode(report));
256
+ let output = encode(report);
257
+ if (testNamePattern) output = output.replace(/^(passing: .+)$/m, "$1 (filtered)");
258
+ await this.writeReport(output);
216
259
  }
217
260
  colorize(report) {
218
261
  if (!this.useColor) return report;
219
- return report.replace(/^(passing:)/m, colors.green("$1")).replace(/^(failing\[.*?\]:)/m, colors.red("$1")).replace(/^(todo\[.*?\]:)/m, colors.cyan("$1")).replace(/^(skipped\[.*?\]:)/m, colors.gray("$1")).replace(/at: "([^"]+)"/g, `at: "${colors.yellow("$1")}"`).replace(/("at",)([^,\n]+)/g, `$1${colors.yellow("$2")}`);
262
+ return report.replace(/^(passing:)/m, colors.green("$1")).replace(/(\(filtered\))/g, colors.purple("$1")).replace(/^(failing\[.*?\]:)/m, colors.red("$1")).replace(/^(todo\[.*?\]:)/m, colors.cyan("$1")).replace(/^(todo:)/m, colors.cyan("$1")).replace(/^(skipped\[.*?\]:)/m, colors.gray("$1")).replace(/^(skipped:)/m, colors.yellow("$1")).replace(/at: "([^"]+)"/g, `at: "${colors.yellow("$1")}"`).replace(/("at",)([^,\n]+)/g, `$1${colors.yellow("$2")}`);
220
263
  }
221
264
  async writeReport(report) {
222
265
  if (this.options._captureOutput) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@epicat/toon-reporter",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "A minimal Vitest reporter optimized for LLM consumption",
5
5
  "repository": {
6
6
  "type": "git",