@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 +22 -13
- package/dist/index.d.mts +8 -126
- package/dist/index.mjs +70 -254
- package/package.json +8 -11
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
CHANGED
|
@@ -1,121 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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
|
|
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 {
|
|
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/
|
|
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:
|
|
138
|
-
red:
|
|
139
|
-
yellow:
|
|
140
|
-
|
|
141
|
-
|
|
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
|
|
145
|
-
return
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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,
|
|
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
|
-
|
|
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(
|
|
71
|
+
column: parseInt(col, 10)
|
|
180
72
|
};
|
|
181
73
|
}
|
|
182
74
|
}
|
|
183
75
|
return null;
|
|
184
76
|
}
|
|
185
77
|
parseExpectedGot(error) {
|
|
186
|
-
const
|
|
187
|
-
if (
|
|
188
|
-
expected:
|
|
189
|
-
got:
|
|
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((
|
|
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
|
|
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
|
|
107
|
+
const loc = this.parseErrorLocation(error, rootDir);
|
|
207
108
|
const { expected, got } = this.parseExpectedGot(error);
|
|
208
|
-
const
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
failure.
|
|
222
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
|
244
|
-
|
|
245
|
-
|
|
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
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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.
|
|
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",
|