@epicat/toon-reporter 0.0.1 → 0.0.2
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 +145 -0
- package/dist/index.mjs +345 -0
- package/package.json +8 -9
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
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";
|
|
4
|
+
|
|
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
|
+
//#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
|
+
interface ToonReporterOptions {
|
|
120
|
+
outputFile?: string;
|
|
121
|
+
color?: boolean;
|
|
122
|
+
/** @internal Used for testing to capture output */
|
|
123
|
+
_captureOutput?: (output: string) => void;
|
|
124
|
+
}
|
|
125
|
+
declare class ToonReporter implements Reporter {
|
|
126
|
+
start: number;
|
|
127
|
+
ctx: Vitest;
|
|
128
|
+
options: ToonReporterOptions;
|
|
129
|
+
constructor(options?: ToonReporterOptions);
|
|
130
|
+
onInit(ctx: Vitest): void;
|
|
131
|
+
private parseLocation;
|
|
132
|
+
private parseExpectedGot;
|
|
133
|
+
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;
|
|
142
|
+
writeReport(report: string): Promise<void>;
|
|
143
|
+
}
|
|
144
|
+
//#endregion
|
|
145
|
+
export { type Reporter, type TestRunEndReason, type ToonFailure, type ToonReport, ToonReporter, ToonReporter as default, type ToonReporterOptions, type ToonSkipped, type UserConsoleLog };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import { existsSync, promises } from "node:fs";
|
|
2
|
+
|
|
3
|
+
//#region node_modules/.pnpm/@vitest+utils@4.0.15/node_modules/@vitest/utils/dist/helpers.js
|
|
4
|
+
function toArray(array) {
|
|
5
|
+
if (array === null || array === void 0) array = [];
|
|
6
|
+
if (Array.isArray(array)) return array;
|
|
7
|
+
return [array];
|
|
8
|
+
}
|
|
9
|
+
|
|
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
|
+
//#endregion
|
|
110
|
+
//#region node_modules/.pnpm/@vitest+runner@4.0.15/node_modules/@vitest/runner/dist/chunk-tasks.js
|
|
111
|
+
function isTestCase(s) {
|
|
112
|
+
return s.type === "test";
|
|
113
|
+
}
|
|
114
|
+
function getTests(suite) {
|
|
115
|
+
const tests = [];
|
|
116
|
+
const arraySuites = toArray(suite);
|
|
117
|
+
for (const s of arraySuites) if (isTestCase(s)) tests.push(s);
|
|
118
|
+
else for (const task of s.tasks) if (isTestCase(task)) tests.push(task);
|
|
119
|
+
else {
|
|
120
|
+
const taskTests = getTests(task);
|
|
121
|
+
for (const test of taskTests) tests.push(test);
|
|
122
|
+
}
|
|
123
|
+
return tests;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
//#endregion
|
|
127
|
+
//#region src/config-helpers.ts
|
|
128
|
+
function getOutputFile(config, reporter) {
|
|
129
|
+
if (!config?.outputFile) return;
|
|
130
|
+
if (typeof config.outputFile === "string") return config.outputFile;
|
|
131
|
+
return config.outputFile[reporter];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
//#endregion
|
|
135
|
+
//#region src/toon-reporter.ts
|
|
136
|
+
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"
|
|
143
|
+
};
|
|
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;
|
|
154
|
+
}
|
|
155
|
+
var ToonReporter = class {
|
|
156
|
+
constructor(options = {}) {
|
|
157
|
+
this.start = 0;
|
|
158
|
+
this.options = {
|
|
159
|
+
color: false,
|
|
160
|
+
...options
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
onInit(ctx) {
|
|
164
|
+
this.ctx = ctx;
|
|
165
|
+
this.start = Date.now();
|
|
166
|
+
}
|
|
167
|
+
parseLocation(error, rootDir) {
|
|
168
|
+
const lines = (error?.stack || "").split("\n");
|
|
169
|
+
for (const line of lines) {
|
|
170
|
+
const match = line.match(/at\s+(?:.*?\s+\()?([^)\s]+):(\d+):(\d+)\)?/);
|
|
171
|
+
if (match) {
|
|
172
|
+
const [, filePath, lineNum, column] = match;
|
|
173
|
+
if (filePath.includes("node_modules") || filePath.startsWith("file:")) continue;
|
|
174
|
+
const absPath = filePath.startsWith(rootDir) ? filePath : resolve(rootDir, filePath);
|
|
175
|
+
return {
|
|
176
|
+
absPath,
|
|
177
|
+
relPath: relative(rootDir, absPath),
|
|
178
|
+
line: parseInt(lineNum, 10),
|
|
179
|
+
column: parseInt(column, 10)
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
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()
|
|
190
|
+
};
|
|
191
|
+
if (error?.expected !== void 0 && error?.actual !== void 0) return {
|
|
192
|
+
expected: String(error.expected),
|
|
193
|
+
got: String(error.actual)
|
|
194
|
+
};
|
|
195
|
+
return {};
|
|
196
|
+
}
|
|
197
|
+
async onTestRunEnd(testModules, _unhandledErrors, _reason) {
|
|
198
|
+
const tests = getTests(testModules.map((testModule) => testModule.task));
|
|
199
|
+
const rootDir = this.ctx.config.root;
|
|
200
|
+
const failedTests = tests.filter((t) => t.result?.state === "fail");
|
|
201
|
+
const passedCount = tests.filter((t) => t.result?.state === "pass").length;
|
|
202
|
+
const skippedTests = tests.filter((t) => t.mode === "skip");
|
|
203
|
+
const todoTests = tests.filter((t) => t.mode === "todo");
|
|
204
|
+
const failures = failedTests.map((t) => {
|
|
205
|
+
const error = t.result?.errors?.[0];
|
|
206
|
+
const parsed = this.parseLocation(error, rootDir);
|
|
207
|
+
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;
|
|
228
|
+
}
|
|
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
|
+
};
|
|
242
|
+
});
|
|
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
|
+
};
|
|
254
|
+
});
|
|
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")}"`;
|
|
267
|
+
}
|
|
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");
|
|
327
|
+
}
|
|
328
|
+
async writeReport(report) {
|
|
329
|
+
if (this.options._captureOutput) {
|
|
330
|
+
this.options._captureOutput(report);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
const outputFile = this.options.outputFile ?? getOutputFile(this.ctx.config, "toon");
|
|
334
|
+
if (outputFile) {
|
|
335
|
+
const reportFile = resolve(this.ctx.config.root, outputFile);
|
|
336
|
+
const outputDirectory = dirname(reportFile);
|
|
337
|
+
if (!existsSync(outputDirectory)) await promises.mkdir(outputDirectory, { recursive: true });
|
|
338
|
+
await promises.writeFile(reportFile, report, "utf-8");
|
|
339
|
+
this.ctx.logger.log(`TOON report written to ${reportFile}`);
|
|
340
|
+
} else this.ctx.logger.log(report);
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
//#endregion
|
|
345
|
+
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.2",
|
|
4
4
|
"description": "A minimal Vitest reporter optimized for LLM consumption",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -18,12 +18,6 @@
|
|
|
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"
|
|
26
|
-
},
|
|
27
21
|
"devDependencies": {
|
|
28
22
|
"@changesets/cli": "^2.29.8",
|
|
29
23
|
"@types/istanbul-lib-coverage": "^2.0.6",
|
|
@@ -40,8 +34,13 @@
|
|
|
40
34
|
"vite": "^7.2.6",
|
|
41
35
|
"vitest": "4.0.15"
|
|
42
36
|
},
|
|
43
|
-
"packageManager": "pnpm@10.22.0+sha512.bf049efe995b28f527fd2b41ae0474ce29186f7edcb3bf545087bd61fbbebb2bf75362d1307fda09c2d288e1e499787ac12d4fcb617a974718a6051f2eee741c",
|
|
44
37
|
"dependencies": {
|
|
45
38
|
"@toon-format/toon": "^2.0.1"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsdown",
|
|
42
|
+
"dev": "tsdown --watch",
|
|
43
|
+
"test": "vitest run",
|
|
44
|
+
"test:watch": "vitest"
|
|
46
45
|
}
|
|
47
|
-
}
|
|
46
|
+
}
|