@epicat/toon-reporter 0.0.2 → 0.0.3

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/README.md CHANGED
@@ -42,11 +42,11 @@ passing: 42
42
42
  ```
43
43
  passing: 40
44
44
  failing[2]:
45
- - at: src/utils.test.ts:15:12
46
- expected: "7"
47
- got: "6"
48
- - at: src/api.test.ts:42:8
49
- error: TypeError: Cannot read property 'id' of undefined
45
+ - at: src/utils.test.ts:15:12
46
+ expected: "7"
47
+ got: "6"
48
+ - at: src/api.test.ts:42:8
49
+ error: TypeError: Cannot read property 'id' of undefined
50
50
  ```
51
51
 
52
52
  ### With parameterized test failures
@@ -56,10 +56,10 @@ Uses TOON tabular format for uniform parameter arrays:
56
56
  ```
57
57
  passing: 6
58
58
  failing[2]:
59
- - at: math.test.ts:16:17
60
- parameters[2]{expected,got}:
61
- "1","2"
62
- "4","2"
59
+ - at: math.test.ts:16:17
60
+ parameters[2]{expected,got}:
61
+ "1","2"
62
+ "4","2"
63
63
  ```
64
64
 
65
65
  ### With todo/skipped tests
@@ -82,16 +82,19 @@ skipped[2]{at,name}:
82
82
  - **Gray**: `skipped` tests
83
83
  - **Cyan**: `todo` tests
84
84
 
85
- Colors are automatically disabled when:
86
- - `NO_COLOR` environment variable is set
87
- - `CI` environment variable is set
85
+ Colors are enabled when:
86
+ - `COLOR` environment variable is set, OR
87
+ - `color: true` option is passed
88
+
89
+ Colors are always disabled when:
90
+ - `CI` environment variable is set (hard disable)
88
91
  - Output is written to a file
89
92
 
90
93
  ## Options
91
94
 
92
95
  ### `color`
93
96
 
94
- Enable/disable colored output (default: `false`).
97
+ Enable/disable colored output.
95
98
 
96
99
  ```ts
97
100
  // vitest.config.ts
@@ -105,6 +108,12 @@ export default defineConfig({
105
108
  })
106
109
  ```
107
110
 
111
+ Or via environment variable:
112
+
113
+ ```bash
114
+ COLOR=1 npx vitest run --reporter=@epicat/toon-reporter
115
+ ```
116
+
108
117
  ### `outputFile`
109
118
 
110
119
  Write report to a file instead of stdout.
package/dist/index.d.mts CHANGED
@@ -1,121 +1,7 @@
1
- import { Awaitable, SerializedError } from "@vitest/utils";
2
- import { File, TaskEventPack, TaskResultPack, TestAnnotation, TestArtifact } from "@vitest/runner";
3
- import { ReportedHookContext, TestCase, TestModule, TestProject, TestSpecification, TestSuite, Vitest } from "vitest/node";
1
+ import { SerializedError } from "@vitest/utils";
2
+ import { Reporter, TestRunEndReason, Vitest } from "vitest/node";
4
3
 
5
- //#region src/reporter.d.ts
6
- type TestRunEndReason = 'passed' | 'interrupted' | 'failed';
7
- interface UserConsoleLog {
8
- content: string;
9
- origin?: string;
10
- browser?: boolean;
11
- type: 'stdout' | 'stderr';
12
- taskId?: string;
13
- time: number;
14
- size: number;
15
- }
16
- interface Reporter {
17
- onInit?: (vitest: Vitest) => void;
18
- /**
19
- * Called when the project initiated the browser instance.
20
- * project.browser will always be defined.
21
- * @experimental
22
- */
23
- onBrowserInit?: (project: TestProject) => Awaitable<void>;
24
- /** @internal */
25
- onTaskUpdate?: (packs: TaskResultPack[], events: TaskEventPack[]) => Awaitable<void>;
26
- onTestRemoved?: (trigger?: string) => Awaitable<void>;
27
- onWatcherStart?: (files?: File[], errors?: unknown[]) => Awaitable<void>;
28
- onWatcherRerun?: (files: string[], trigger?: string) => Awaitable<void>;
29
- onServerRestart?: (reason?: string) => Awaitable<void>;
30
- onUserConsoleLog?: (log: UserConsoleLog) => Awaitable<void>;
31
- onProcessTimeout?: () => Awaitable<void>;
32
- /**
33
- * Called when the new test run starts.
34
- */
35
- onTestRunStart?: (specifications: ReadonlyArray<TestSpecification>) => Awaitable<void>;
36
- /**
37
- * Called when the test run is finished.
38
- */
39
- onTestRunEnd?: (testModules: ReadonlyArray<TestModule>, unhandledErrors: ReadonlyArray<SerializedError>, reason: TestRunEndReason) => Awaitable<void>;
40
- /**
41
- * Called when the module is enqueued for testing. The file itself is not loaded yet.
42
- */
43
- onTestModuleQueued?: (testModule: TestModule) => Awaitable<void>;
44
- /**
45
- * Called when the test file is loaded and the module is ready to run tests.
46
- */
47
- onTestModuleCollected?: (testModule: TestModule) => Awaitable<void>;
48
- /**
49
- * Called when starting to run tests of the test file
50
- */
51
- onTestModuleStart?: (testModule: TestModule) => Awaitable<void>;
52
- /**
53
- * Called when all tests of the test file have finished running.
54
- */
55
- onTestModuleEnd?: (testModule: TestModule) => Awaitable<void>;
56
- /**
57
- * Called when test case is ready to run.
58
- * Called before the `beforeEach` hooks for the test are run.
59
- */
60
- onTestCaseReady?: (testCase: TestCase) => Awaitable<void>;
61
- /**
62
- * Called after the test and its hooks are finished running.
63
- * The `result()` cannot be `pending`.
64
- */
65
- onTestCaseResult?: (testCase: TestCase) => Awaitable<void>;
66
- /**
67
- * Called when annotation is added via the `task.annotate` API.
68
- */
69
- onTestCaseAnnotate?: (testCase: TestCase, annotation: TestAnnotation) => Awaitable<void>;
70
- /**
71
- * Called when artifacts are recorded on tests via the `recordArtifact` utility.
72
- */
73
- onTestCaseArtifactRecord?: (testCase: TestCase, artifact: TestArtifact) => Awaitable<void>;
74
- /**
75
- * Called when test suite is ready to run.
76
- * Called before the `beforeAll` hooks for the test are run.
77
- */
78
- onTestSuiteReady?: (testSuite: TestSuite) => Awaitable<void>;
79
- /**
80
- * Called after the test suite and its hooks are finished running.
81
- * The `state` cannot be `pending`.
82
- */
83
- onTestSuiteResult?: (testSuite: TestSuite) => Awaitable<void>;
84
- /**
85
- * Called before the hook starts to run.
86
- */
87
- onHookStart?: (hook: ReportedHookContext) => Awaitable<void>;
88
- /**
89
- * Called after the hook finished running.
90
- */
91
- onHookEnd?: (hook: ReportedHookContext) => Awaitable<void>;
92
- onCoverage?: (coverage: unknown) => Awaitable<void>;
93
- }
94
- //#endregion
95
4
  //#region src/toon-reporter.d.ts
96
- interface ToonFailure {
97
- at: string;
98
- absPath: string;
99
- line?: number;
100
- column?: number;
101
- expected?: string;
102
- got?: string;
103
- error?: string;
104
- each?: boolean;
105
- }
106
- interface ToonSkipped {
107
- at: string;
108
- absPath: string;
109
- line?: number;
110
- column?: number;
111
- name: string;
112
- }
113
- interface ToonReport {
114
- passing: number;
115
- failing?: ToonFailure[];
116
- skipped?: ToonSkipped[];
117
- todo?: ToonSkipped[];
118
- }
119
5
  interface ToonReporterOptions {
120
6
  outputFile?: string;
121
7
  color?: boolean;
@@ -126,20 +12,16 @@ declare class ToonReporter implements Reporter {
126
12
  start: number;
127
13
  ctx: Vitest;
128
14
  options: ToonReporterOptions;
15
+ private useColor;
129
16
  constructor(options?: ToonReporterOptions);
130
17
  onInit(ctx: Vitest): void;
131
- private parseLocation;
18
+ private formatLocation;
19
+ private parseErrorLocation;
132
20
  private parseExpectedGot;
21
+ private formatErrorMessage;
133
22
  onTestRunEnd(testModules: ReadonlyArray<any>, _unhandledErrors: ReadonlyArray<SerializedError>, _reason: TestRunEndReason): Promise<void>;
134
- /**
135
- * Quote a string value per TOON spec:
136
- * - Quote if contains delimiter (comma), newline, carriage return, tab, backslash, or quote
137
- * - Quote if matches reserved keywords (true, false, null)
138
- * - Quote if looks like a number but should be a string
139
- */
140
- private toonQuote;
141
- private formatOutput;
23
+ private colorize;
142
24
  writeReport(report: string): Promise<void>;
143
25
  }
144
26
  //#endregion
145
- export { type Reporter, type TestRunEndReason, type ToonFailure, type ToonReport, ToonReporter, ToonReporter as default, type ToonReporterOptions, type ToonSkipped, type UserConsoleLog };
27
+ export { ToonReporter, ToonReporter as default, type ToonReporterOptions };
package/dist/index.mjs CHANGED
@@ -1,4 +1,6 @@
1
1
  import { existsSync, promises } from "node:fs";
2
+ import { dirname, relative, resolve } from "pathe";
3
+ import { encode } from "@toon-format/toon";
2
4
 
3
5
  //#region node_modules/.pnpm/@vitest+utils@4.0.15/node_modules/@vitest/utils/dist/helpers.js
4
6
  function toArray(array) {
@@ -7,105 +9,6 @@ function toArray(array) {
7
9
  return [array];
8
10
  }
9
11
 
10
- //#endregion
11
- //#region node_modules/.pnpm/pathe@2.0.3/node_modules/pathe/dist/shared/pathe.M-eThtNZ.mjs
12
- const _DRIVE_LETTER_START_RE = /^[A-Za-z]:\//;
13
- function normalizeWindowsPath(input = "") {
14
- if (!input) return input;
15
- return input.replace(/\\/g, "/").replace(_DRIVE_LETTER_START_RE, (r) => r.toUpperCase());
16
- }
17
- const _IS_ABSOLUTE_RE = /^[/\\](?![/\\])|^[/\\]{2}(?!\.)|^[A-Za-z]:[/\\]/;
18
- const _DRIVE_LETTER_RE = /^[A-Za-z]:$/;
19
- const _ROOT_FOLDER_RE = /^\/([A-Za-z]:)?$/;
20
- function cwd() {
21
- if (typeof process !== "undefined" && typeof process.cwd === "function") return process.cwd().replace(/\\/g, "/");
22
- return "/";
23
- }
24
- const resolve = function(...arguments_) {
25
- arguments_ = arguments_.map((argument) => normalizeWindowsPath(argument));
26
- let resolvedPath = "";
27
- let resolvedAbsolute = false;
28
- for (let index = arguments_.length - 1; index >= -1 && !resolvedAbsolute; index--) {
29
- const path = index >= 0 ? arguments_[index] : cwd();
30
- if (!path || path.length === 0) continue;
31
- resolvedPath = `${path}/${resolvedPath}`;
32
- resolvedAbsolute = isAbsolute(path);
33
- }
34
- resolvedPath = normalizeString(resolvedPath, !resolvedAbsolute);
35
- if (resolvedAbsolute && !isAbsolute(resolvedPath)) return `/${resolvedPath}`;
36
- return resolvedPath.length > 0 ? resolvedPath : ".";
37
- };
38
- function normalizeString(path, allowAboveRoot) {
39
- let res = "";
40
- let lastSegmentLength = 0;
41
- let lastSlash = -1;
42
- let dots = 0;
43
- let char = null;
44
- for (let index = 0; index <= path.length; ++index) {
45
- if (index < path.length) char = path[index];
46
- else if (char === "/") break;
47
- else char = "/";
48
- if (char === "/") {
49
- if (lastSlash === index - 1 || dots === 1);
50
- else if (dots === 2) {
51
- if (res.length < 2 || lastSegmentLength !== 2 || res[res.length - 1] !== "." || res[res.length - 2] !== ".") {
52
- if (res.length > 2) {
53
- const lastSlashIndex = res.lastIndexOf("/");
54
- if (lastSlashIndex === -1) {
55
- res = "";
56
- lastSegmentLength = 0;
57
- } else {
58
- res = res.slice(0, lastSlashIndex);
59
- lastSegmentLength = res.length - 1 - res.lastIndexOf("/");
60
- }
61
- lastSlash = index;
62
- dots = 0;
63
- continue;
64
- } else if (res.length > 0) {
65
- res = "";
66
- lastSegmentLength = 0;
67
- lastSlash = index;
68
- dots = 0;
69
- continue;
70
- }
71
- }
72
- if (allowAboveRoot) {
73
- res += res.length > 0 ? "/.." : "..";
74
- lastSegmentLength = 2;
75
- }
76
- } else {
77
- if (res.length > 0) res += `/${path.slice(lastSlash + 1, index)}`;
78
- else res = path.slice(lastSlash + 1, index);
79
- lastSegmentLength = index - lastSlash - 1;
80
- }
81
- lastSlash = index;
82
- dots = 0;
83
- } else if (char === "." && dots !== -1) ++dots;
84
- else dots = -1;
85
- }
86
- return res;
87
- }
88
- const isAbsolute = function(p) {
89
- return _IS_ABSOLUTE_RE.test(p);
90
- };
91
- const relative = function(from, to) {
92
- const _from = resolve(from).replace(_ROOT_FOLDER_RE, "$1").split("/");
93
- const _to = resolve(to).replace(_ROOT_FOLDER_RE, "$1").split("/");
94
- if (_to[0][1] === ":" && _from[0][1] === ":" && _from[0] !== _to[0]) return _to.join("/");
95
- const _fromCopy = [..._from];
96
- for (const segment of _fromCopy) {
97
- if (_to[0] !== segment) break;
98
- _from.shift();
99
- _to.shift();
100
- }
101
- return [..._from.map(() => ".."), ..._to].join("/");
102
- };
103
- const dirname = function(p) {
104
- const segments = normalizeWindowsPath(p).replace(/\/$/, "").split("/").slice(0, -1);
105
- if (segments.length === 1 && _DRIVE_LETTER_RE.test(segments[0])) segments[0] += "/";
106
- return segments.join("/") || (isAbsolute(p) ? "/" : ".");
107
- };
108
-
109
12
  //#endregion
110
13
  //#region node_modules/.pnpm/@vitest+runner@4.0.15/node_modules/@vitest/runner/dist/chunk-tasks.js
111
14
  function isTestCase(s) {
@@ -124,69 +27,58 @@ function getTests(suite) {
124
27
  }
125
28
 
126
29
  //#endregion
127
- //#region src/config-helpers.ts
30
+ //#region src/toon-reporter.ts
128
31
  function getOutputFile(config, reporter) {
129
32
  if (!config?.outputFile) return;
130
33
  if (typeof config.outputFile === "string") return config.outputFile;
131
34
  return config.outputFile[reporter];
132
35
  }
133
-
134
- //#endregion
135
- //#region src/toon-reporter.ts
136
36
  const colors = {
137
- green: "\x1B[32m",
138
- red: "\x1B[31m",
139
- yellow: "\x1B[33m",
140
- cyan: "\x1B[36m",
141
- gray: "\x1B[90m",
142
- reset: "\x1B[0m"
37
+ green: (s) => `\x1b[32m${s}\x1b[0m`,
38
+ red: (s) => `\x1b[31m${s}\x1b[0m`,
39
+ yellow: (s) => `\x1b[33m${s}\x1b[0m`,
40
+ gray: (s) => `\x1b[90m${s}\x1b[0m`,
41
+ cyan: (s) => `\x1b[36m${s}\x1b[0m`
143
42
  };
144
- function link(url, text) {
145
- return `\x1b]8;;${url}\x1b\\${text}\x1b]8;;\x1b\\`;
146
- }
147
- function fileLink(filePath, line, column) {
148
- let url = `file://${filePath}`;
149
- if (line) {
150
- url += `:${line}`;
151
- if (column) url += `:${column}`;
152
- }
153
- return url;
43
+ function shouldUseColor(option) {
44
+ if (process.env.CI) return false;
45
+ if (option !== void 0) return option;
46
+ return !!process.env.COLOR;
154
47
  }
155
48
  var ToonReporter = class {
156
49
  constructor(options = {}) {
157
50
  this.start = 0;
158
- this.options = {
159
- color: false,
160
- ...options
161
- };
51
+ this.options = options;
52
+ this.useColor = shouldUseColor(options.color);
162
53
  }
163
54
  onInit(ctx) {
164
55
  this.ctx = ctx;
165
56
  this.start = Date.now();
166
57
  }
167
- parseLocation(error, rootDir) {
168
- const lines = (error?.stack || "").split("\n");
169
- for (const line of lines) {
58
+ formatLocation(relPath, line, column) {
59
+ return line ? `${relPath}:${line}:${column || 0}` : relPath;
60
+ }
61
+ parseErrorLocation(error, rootDir) {
62
+ const stack = error?.stack || "";
63
+ for (const line of stack.split("\n")) {
170
64
  const match = line.match(/at\s+(?:.*?\s+\()?([^)\s]+):(\d+):(\d+)\)?/);
171
65
  if (match) {
172
- const [, filePath, lineNum, column] = match;
66
+ const [, filePath, lineNum, col] = match;
173
67
  if (filePath.includes("node_modules") || filePath.startsWith("file:")) continue;
174
- const absPath = filePath.startsWith(rootDir) ? filePath : resolve(rootDir, filePath);
175
68
  return {
176
- absPath,
177
- relPath: relative(rootDir, absPath),
69
+ relPath: relative(rootDir, filePath.startsWith(rootDir) ? filePath : resolve(rootDir, filePath)),
178
70
  line: parseInt(lineNum, 10),
179
- column: parseInt(column, 10)
71
+ column: parseInt(col, 10)
180
72
  };
181
73
  }
182
74
  }
183
75
  return null;
184
76
  }
185
77
  parseExpectedGot(error) {
186
- const matchToBe = (error?.message || "").match(/expected\s+(.+?)\s+to\s+(?:be|equal|deeply equal)\s+(.+?)(?:\s*\/\/|$)/i);
187
- if (matchToBe) return {
188
- expected: matchToBe[2].trim(),
189
- got: matchToBe[1].trim()
78
+ const match = (error?.message || "").match(/expected\s+(.+?)\s+to\s+(?:be|equal|deeply equal)\s+(.+?)(?:\s*\/\/|$)/i);
79
+ if (match) return {
80
+ expected: match[2].trim(),
81
+ got: match[1].trim()
190
82
  };
191
83
  if (error?.expected !== void 0 && error?.actual !== void 0) return {
192
84
  expected: String(error.expected),
@@ -194,136 +86,60 @@ var ToonReporter = class {
194
86
  };
195
87
  return {};
196
88
  }
89
+ formatErrorMessage(error) {
90
+ let message = error.message || String(error);
91
+ if (message.startsWith("Test timed out")) message = message.split("\n")[0];
92
+ const name = error.name;
93
+ if (name && name !== "Error" && !message.startsWith(name)) message = `${name}: ${message}`;
94
+ return message;
95
+ }
197
96
  async onTestRunEnd(testModules, _unhandledErrors, _reason) {
198
- const tests = getTests(testModules.map((testModule) => testModule.task));
97
+ const tests = getTests(testModules.map((m) => m.task));
199
98
  const rootDir = this.ctx.config.root;
200
99
  const failedTests = tests.filter((t) => t.result?.state === "fail");
201
100
  const passedCount = tests.filter((t) => t.result?.state === "pass").length;
202
101
  const skippedTests = tests.filter((t) => t.mode === "skip");
203
102
  const todoTests = tests.filter((t) => t.mode === "todo");
204
- const failures = failedTests.map((t) => {
103
+ const grouped = /* @__PURE__ */ new Map();
104
+ const failures = [];
105
+ for (const t of failedTests) {
205
106
  const error = t.result?.errors?.[0];
206
- const parsed = this.parseLocation(error, rootDir);
107
+ const loc = this.parseErrorLocation(error, rootDir);
207
108
  const { expected, got } = this.parseExpectedGot(error);
208
- const absPath = parsed?.absPath || t.file.filepath;
209
- const relPath = parsed?.relPath || relative(rootDir, t.file.filepath);
210
- const line = parsed?.line || t.location?.line;
211
- const column = parsed?.column || t.location?.column;
212
- const failure = {
213
- at: line ? `${relPath}:${line}:${column || 0}` : relPath,
214
- absPath,
215
- line,
216
- column,
217
- each: t.each
218
- };
219
- if (expected !== void 0 && got !== void 0) {
220
- failure.expected = expected;
221
- failure.got = got;
222
- } else if (error) {
223
- let message = error.message || String(error);
224
- if (message.startsWith("Test timed out")) message = message.split("\n")[0];
225
- const errorName = error.name;
226
- if (errorName && errorName !== "Error" && !message.startsWith(errorName)) message = `${errorName}: ${message}`;
227
- failure.error = message;
109
+ const relPath = loc?.relPath || relative(rootDir, t.file.filepath);
110
+ const at = this.formatLocation(relPath, loc?.line || t.location?.line, loc?.column || t.location?.column);
111
+ if (t.each) {
112
+ if (!grouped.has(at)) grouped.set(at, []);
113
+ grouped.get(at).push(expected !== void 0 && got !== void 0 ? {
114
+ expected,
115
+ got
116
+ } : { error: this.formatErrorMessage(error) });
117
+ } else {
118
+ const failure = { at };
119
+ if (expected !== void 0 && got !== void 0) {
120
+ failure.expected = expected;
121
+ failure.got = got;
122
+ } else if (error) failure.error = this.formatErrorMessage(error);
123
+ failures.push(failure);
228
124
  }
229
- return failure;
230
- });
231
- const skipped = skippedTests.map((t) => {
232
- const relPath = relative(rootDir, t.file.filepath);
233
- const line = t.location?.line;
234
- const column = t.location?.column;
235
- return {
236
- at: line ? `${relPath}:${line}:${column || 0}` : relPath,
237
- absPath: t.file.filepath,
238
- line,
239
- column,
240
- name: t.name
241
- };
125
+ }
126
+ for (const [at, params] of grouped) failures.push({
127
+ at,
128
+ parameters: params
242
129
  });
243
- const todo = todoTests.map((t) => {
244
- const relPath = relative(rootDir, t.file.filepath);
245
- const line = t.location?.line;
246
- const column = t.location?.column;
247
- return {
248
- at: line ? `${relPath}:${line}:${column || 0}` : relPath,
249
- absPath: t.file.filepath,
250
- line,
251
- column,
252
- name: t.name
253
- };
130
+ const mapToSkipped = (t) => ({
131
+ at: this.formatLocation(relative(rootDir, t.file.filepath), t.location?.line, t.location?.column),
132
+ name: t.name
254
133
  });
255
- const output = this.formatOutput(passedCount, failures, skipped, todo);
256
- await this.writeReport(output);
257
- }
258
- /**
259
- * Quote a string value per TOON spec:
260
- * - Quote if contains delimiter (comma), newline, carriage return, tab, backslash, or quote
261
- * - Quote if matches reserved keywords (true, false, null)
262
- * - Quote if looks like a number but should be a string
263
- */
264
- toonQuote(value, delimiter = ",") {
265
- if (!(value.includes(delimiter) || value.includes("\n") || value.includes("\r") || value.includes(" ") || value.includes("\\") || value.includes("\"") || value === "true" || value === "false" || value === "null" || /^-?\d+(\.\d+)?$/.test(value))) return value;
266
- return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t")}"`;
134
+ const report = { passing: passedCount };
135
+ if (failures.length > 0) report.failing = failures;
136
+ if (todoTests.length > 0) report.todo = todoTests.map(mapToSkipped);
137
+ if (skippedTests.length > 0) report.skipped = skippedTests.map(mapToSkipped);
138
+ await this.writeReport(encode(report));
267
139
  }
268
- formatOutput(passing, failures, skipped, todo) {
269
- const useColor = this.options.color && !this.options.outputFile && !this.options._captureOutput && !process.env.NO_COLOR && !process.env.CI;
270
- const { green, red, yellow, cyan, gray, reset } = useColor ? colors : {
271
- green: "",
272
- red: "",
273
- yellow: "",
274
- cyan: "",
275
- gray: "",
276
- reset: ""
277
- };
278
- const formatLocation = (at, absPath, line, column, color = yellow) => {
279
- if (useColor) return `${color}${link(fileLink(absPath, line, column), at)}${reset}`;
280
- return at;
281
- };
282
- const lines = [];
283
- lines.push(`${green}passing: ${passing}${reset}`);
284
- if (failures.length > 0) {
285
- lines.push(`${red}failing[${failures.length}]:${reset}`);
286
- const grouped = /* @__PURE__ */ new Map();
287
- const regular = [];
288
- for (const failure of failures) if (failure.each) {
289
- const key = failure.at;
290
- if (!grouped.has(key)) grouped.set(key, []);
291
- grouped.get(key).push(failure);
292
- } else regular.push(failure);
293
- for (const failure of regular) {
294
- lines.push(`- at: ${formatLocation(failure.at, failure.absPath, failure.line, failure.column)}`);
295
- if (failure.expected !== void 0 && failure.got !== void 0) {
296
- lines.push(` expected: ${this.toonQuote(failure.expected)}`);
297
- lines.push(` got: ${this.toonQuote(failure.got)}`);
298
- } else if (failure.error) lines.push(` error: ${this.toonQuote(failure.error)}`);
299
- }
300
- for (const [, groupedFailures] of grouped) {
301
- const first = groupedFailures[0];
302
- lines.push(`- at: ${formatLocation(first.at, first.absPath, first.line, first.column)}`);
303
- if (groupedFailures.every((f) => f.expected !== void 0 && f.got !== void 0)) {
304
- lines.push(` parameters[${groupedFailures.length}]{expected,got}:`);
305
- for (const failure of groupedFailures) lines.push(` ${this.toonQuote(failure.expected)},${this.toonQuote(failure.got)}`);
306
- } else {
307
- lines.push(` parameters[${groupedFailures.length}]{error}:`);
308
- for (const failure of groupedFailures) if (failure.error) lines.push(` ${this.toonQuote(failure.error)}`);
309
- }
310
- }
311
- }
312
- if (todo.length > 0) {
313
- lines.push(`${cyan}todo[${todo.length}]{at,name}:${reset}`);
314
- for (const t of todo) {
315
- const at = formatLocation(t.at, t.absPath, t.line, t.column);
316
- lines.push(`${cyan} ${at},${this.toonQuote(t.name)}${reset}`);
317
- }
318
- }
319
- if (skipped.length > 0) {
320
- lines.push(`${gray}skipped[${skipped.length}]{at,name}:${reset}`);
321
- for (const s of skipped) {
322
- const at = formatLocation(s.at, s.absPath, s.line, s.column, gray);
323
- lines.push(`${gray} ${at},${this.toonQuote(s.name)}${reset}`);
324
- }
325
- }
326
- return lines.join("\n");
140
+ colorize(report) {
141
+ if (!this.useColor) return report;
142
+ 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(/(\S+\.test\.\w+:\d+:\d+)/g, colors.yellow("$1"));
327
143
  }
328
144
  async writeReport(report) {
329
145
  if (this.options._captureOutput) {
@@ -337,7 +153,7 @@ var ToonReporter = class {
337
153
  if (!existsSync(outputDirectory)) await promises.mkdir(outputDirectory, { recursive: true });
338
154
  await promises.writeFile(reportFile, report, "utf-8");
339
155
  this.ctx.logger.log(`TOON report written to ${reportFile}`);
340
- } else this.ctx.logger.log(report);
156
+ } else this.ctx.logger.log(this.colorize(report));
341
157
  }
342
158
  };
343
159
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@epicat/toon-reporter",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "A minimal Vitest reporter optimized for LLM consumption",
5
5
  "repository": {
6
6
  "type": "git",
@@ -18,25 +18,22 @@
18
18
  ".": "./dist/index.mjs",
19
19
  "./package.json": "./package.json"
20
20
  },
21
+ "peerDependencies": {
22
+ "vitest": ">=2.0.0"
23
+ },
24
+ "dependencies": {
25
+ "@toon-format/toon": "^2.0.1",
26
+ "pathe": "^2.0.3"
27
+ },
21
28
  "devDependencies": {
22
29
  "@changesets/cli": "^2.29.8",
23
- "@types/istanbul-lib-coverage": "^2.0.6",
24
30
  "@types/node": "^24.10.1",
25
31
  "@vitest/runner": "4.0.15",
26
- "@vitest/snapshot": "4.0.15",
27
32
  "@vitest/utils": "4.0.15",
28
- "execa": "^9.6.1",
29
- "gpt-tokenizer": "^3.4.0",
30
- "istanbul-lib-coverage": "^3.2.2",
31
- "pathe": "^2.0.3",
32
33
  "tsdown": "^0.16.4",
33
34
  "typescript": "^5.9.3",
34
- "vite": "^7.2.6",
35
35
  "vitest": "4.0.15"
36
36
  },
37
- "dependencies": {
38
- "@toon-format/toon": "^2.0.1"
39
- },
40
37
  "scripts": {
41
38
  "build": "tsdown",
42
39
  "dev": "tsdown --watch",