@epicat/toon-reporter 0.0.1 → 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.
@@ -0,0 +1,27 @@
1
+ import { SerializedError } from "@vitest/utils";
2
+ import { Reporter, TestRunEndReason, Vitest } from "vitest/node";
3
+
4
+ //#region src/toon-reporter.d.ts
5
+ interface ToonReporterOptions {
6
+ outputFile?: string;
7
+ color?: boolean;
8
+ /** @internal Used for testing to capture output */
9
+ _captureOutput?: (output: string) => void;
10
+ }
11
+ declare class ToonReporter implements Reporter {
12
+ start: number;
13
+ ctx: Vitest;
14
+ options: ToonReporterOptions;
15
+ private useColor;
16
+ constructor(options?: ToonReporterOptions);
17
+ onInit(ctx: Vitest): void;
18
+ private formatLocation;
19
+ private parseErrorLocation;
20
+ private parseExpectedGot;
21
+ private formatErrorMessage;
22
+ onTestRunEnd(testModules: ReadonlyArray<any>, _unhandledErrors: ReadonlyArray<SerializedError>, _reason: TestRunEndReason): Promise<void>;
23
+ private colorize;
24
+ writeReport(report: string): Promise<void>;
25
+ }
26
+ //#endregion
27
+ export { ToonReporter, ToonReporter as default, type ToonReporterOptions };
package/dist/index.mjs ADDED
@@ -0,0 +1,161 @@
1
+ import { existsSync, promises } from "node:fs";
2
+ import { dirname, relative, resolve } from "pathe";
3
+ import { encode } from "@toon-format/toon";
4
+
5
+ //#region node_modules/.pnpm/@vitest+utils@4.0.15/node_modules/@vitest/utils/dist/helpers.js
6
+ function toArray(array) {
7
+ if (array === null || array === void 0) array = [];
8
+ if (Array.isArray(array)) return array;
9
+ return [array];
10
+ }
11
+
12
+ //#endregion
13
+ //#region node_modules/.pnpm/@vitest+runner@4.0.15/node_modules/@vitest/runner/dist/chunk-tasks.js
14
+ function isTestCase(s) {
15
+ return s.type === "test";
16
+ }
17
+ function getTests(suite) {
18
+ const tests = [];
19
+ const arraySuites = toArray(suite);
20
+ for (const s of arraySuites) if (isTestCase(s)) tests.push(s);
21
+ else for (const task of s.tasks) if (isTestCase(task)) tests.push(task);
22
+ else {
23
+ const taskTests = getTests(task);
24
+ for (const test of taskTests) tests.push(test);
25
+ }
26
+ return tests;
27
+ }
28
+
29
+ //#endregion
30
+ //#region src/toon-reporter.ts
31
+ function getOutputFile(config, reporter) {
32
+ if (!config?.outputFile) return;
33
+ if (typeof config.outputFile === "string") return config.outputFile;
34
+ return config.outputFile[reporter];
35
+ }
36
+ const colors = {
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`
42
+ };
43
+ function shouldUseColor(option) {
44
+ if (process.env.CI) return false;
45
+ if (option !== void 0) return option;
46
+ return !!process.env.COLOR;
47
+ }
48
+ var ToonReporter = class {
49
+ constructor(options = {}) {
50
+ this.start = 0;
51
+ this.options = options;
52
+ this.useColor = shouldUseColor(options.color);
53
+ }
54
+ onInit(ctx) {
55
+ this.ctx = ctx;
56
+ this.start = Date.now();
57
+ }
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")) {
64
+ const match = line.match(/at\s+(?:.*?\s+\()?([^)\s]+):(\d+):(\d+)\)?/);
65
+ if (match) {
66
+ const [, filePath, lineNum, col] = match;
67
+ if (filePath.includes("node_modules") || filePath.startsWith("file:")) continue;
68
+ return {
69
+ relPath: relative(rootDir, filePath.startsWith(rootDir) ? filePath : resolve(rootDir, filePath)),
70
+ line: parseInt(lineNum, 10),
71
+ column: parseInt(col, 10)
72
+ };
73
+ }
74
+ }
75
+ return null;
76
+ }
77
+ parseExpectedGot(error) {
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()
82
+ };
83
+ if (error?.expected !== void 0 && error?.actual !== void 0) return {
84
+ expected: String(error.expected),
85
+ got: String(error.actual)
86
+ };
87
+ return {};
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
+ }
96
+ async onTestRunEnd(testModules, _unhandledErrors, _reason) {
97
+ const tests = getTests(testModules.map((m) => m.task));
98
+ const rootDir = this.ctx.config.root;
99
+ const failedTests = tests.filter((t) => t.result?.state === "fail");
100
+ const passedCount = tests.filter((t) => t.result?.state === "pass").length;
101
+ const skippedTests = tests.filter((t) => t.mode === "skip");
102
+ const todoTests = tests.filter((t) => t.mode === "todo");
103
+ const grouped = /* @__PURE__ */ new Map();
104
+ const failures = [];
105
+ for (const t of failedTests) {
106
+ const error = t.result?.errors?.[0];
107
+ const loc = this.parseErrorLocation(error, rootDir);
108
+ const { expected, got } = this.parseExpectedGot(error);
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);
124
+ }
125
+ }
126
+ for (const [at, params] of grouped) failures.push({
127
+ at,
128
+ parameters: params
129
+ });
130
+ const mapToSkipped = (t) => ({
131
+ at: this.formatLocation(relative(rootDir, t.file.filepath), t.location?.line, t.location?.column),
132
+ name: t.name
133
+ });
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));
139
+ }
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"));
143
+ }
144
+ async writeReport(report) {
145
+ if (this.options._captureOutput) {
146
+ this.options._captureOutput(report);
147
+ return;
148
+ }
149
+ const outputFile = this.options.outputFile ?? getOutputFile(this.ctx.config, "toon");
150
+ if (outputFile) {
151
+ const reportFile = resolve(this.ctx.config.root, outputFile);
152
+ const outputDirectory = dirname(reportFile);
153
+ if (!existsSync(outputDirectory)) await promises.mkdir(outputDirectory, { recursive: true });
154
+ await promises.writeFile(reportFile, report, "utf-8");
155
+ this.ctx.logger.log(`TOON report written to ${reportFile}`);
156
+ } else this.ctx.logger.log(this.colorize(report));
157
+ }
158
+ };
159
+
160
+ //#endregion
161
+ export { ToonReporter, ToonReporter as default };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@epicat/toon-reporter",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "A minimal Vitest reporter optimized for LLM consumption",
5
5
  "repository": {
6
6
  "type": "git",
@@ -18,30 +18,26 @@
18
18
  ".": "./dist/index.mjs",
19
19
  "./package.json": "./package.json"
20
20
  },
21
- "scripts": {
22
- "build": "tsdown",
23
- "dev": "tsdown --watch",
24
- "test": "vitest run",
25
- "test:watch": "vitest"
21
+ "peerDependencies": {
22
+ "vitest": ">=2.0.0"
23
+ },
24
+ "dependencies": {
25
+ "@toon-format/toon": "^2.0.1",
26
+ "pathe": "^2.0.3"
26
27
  },
27
28
  "devDependencies": {
28
29
  "@changesets/cli": "^2.29.8",
29
- "@types/istanbul-lib-coverage": "^2.0.6",
30
30
  "@types/node": "^24.10.1",
31
31
  "@vitest/runner": "4.0.15",
32
- "@vitest/snapshot": "4.0.15",
33
32
  "@vitest/utils": "4.0.15",
34
- "execa": "^9.6.1",
35
- "gpt-tokenizer": "^3.4.0",
36
- "istanbul-lib-coverage": "^3.2.2",
37
- "pathe": "^2.0.3",
38
33
  "tsdown": "^0.16.4",
39
34
  "typescript": "^5.9.3",
40
- "vite": "^7.2.6",
41
35
  "vitest": "4.0.15"
42
36
  },
43
- "packageManager": "pnpm@10.22.0+sha512.bf049efe995b28f527fd2b41ae0474ce29186f7edcb3bf545087bd61fbbebb2bf75362d1307fda09c2d288e1e499787ac12d4fcb617a974718a6051f2eee741c",
44
- "dependencies": {
45
- "@toon-format/toon": "^2.0.1"
37
+ "scripts": {
38
+ "build": "tsdown",
39
+ "dev": "tsdown --watch",
40
+ "test": "vitest run",
41
+ "test:watch": "vitest"
46
42
  }
47
- }
43
+ }