@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 +22 -13
- package/dist/index.d.mts +27 -0
- package/dist/index.mjs +161 -0
- package/package.json +13 -17
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
|
-
|
|
47
|
-
|
|
48
|
-
- at: src/api.test.ts:42:8
|
|
49
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
86
|
-
- `
|
|
87
|
-
- `
|
|
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
|
|
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
ADDED
|
@@ -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.
|
|
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
|
-
"
|
|
22
|
-
"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
"
|
|
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
|
-
"
|
|
44
|
-
|
|
45
|
-
"
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsdown",
|
|
39
|
+
"dev": "tsdown --watch",
|
|
40
|
+
"test": "vitest run",
|
|
41
|
+
"test:watch": "vitest"
|
|
46
42
|
}
|
|
47
|
-
}
|
|
43
|
+
}
|