@epicat/toon-reporter 0.0.5 → 0.0.6
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 +84 -13
- package/dist/index.mjs +2 -0
- package/dist/playwright.d.mts +25 -0
- package/dist/playwright.mjs +111 -0
- package/package.json +14 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# toon-reporter
|
|
2
2
|
|
|
3
|
-
A minimal Vitest reporter optimized for LLM consumption. Outputs test results in a compact, token-efficient format.
|
|
3
|
+
A minimal Vitest and Playwright reporter optimized for LLM consumption. Outputs test results in a compact, token-efficient format.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -10,13 +10,15 @@ npm install @epicat/toon-reporter
|
|
|
10
10
|
|
|
11
11
|
## Usage
|
|
12
12
|
|
|
13
|
-
###
|
|
13
|
+
### Vitest
|
|
14
|
+
|
|
15
|
+
#### CLI
|
|
14
16
|
|
|
15
17
|
```bash
|
|
16
18
|
npx vitest run --reporter=@epicat/toon-reporter
|
|
17
19
|
```
|
|
18
20
|
|
|
19
|
-
|
|
21
|
+
#### Config
|
|
20
22
|
|
|
21
23
|
```ts
|
|
22
24
|
// vitest.config.ts
|
|
@@ -29,6 +31,34 @@ export default defineConfig({
|
|
|
29
31
|
})
|
|
30
32
|
```
|
|
31
33
|
|
|
34
|
+
### Playwright
|
|
35
|
+
|
|
36
|
+
#### Config
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
// playwright.config.ts
|
|
40
|
+
import { defineConfig } from '@playwright/test'
|
|
41
|
+
|
|
42
|
+
export default defineConfig({
|
|
43
|
+
reporter: [['@epicat/toon-reporter/playwright']],
|
|
44
|
+
})
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
#### With options
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
// playwright.config.ts
|
|
51
|
+
import { defineConfig } from '@playwright/test'
|
|
52
|
+
|
|
53
|
+
export default defineConfig({
|
|
54
|
+
reporter: [
|
|
55
|
+
['@epicat/toon-reporter/playwright', {
|
|
56
|
+
outputFile: 'test-results.toon'
|
|
57
|
+
}]
|
|
58
|
+
],
|
|
59
|
+
})
|
|
60
|
+
```
|
|
61
|
+
|
|
32
62
|
## Output Format
|
|
33
63
|
|
|
34
64
|
### All tests passing
|
|
@@ -37,7 +67,7 @@ export default defineConfig({
|
|
|
37
67
|
passing: 42
|
|
38
68
|
```
|
|
39
69
|
|
|
40
|
-
### With failures
|
|
70
|
+
### With failures (Vitest)
|
|
41
71
|
|
|
42
72
|
```
|
|
43
73
|
passing: 40
|
|
@@ -49,7 +79,15 @@ failing[2]:
|
|
|
49
79
|
error: TypeError: Cannot read property 'id' of undefined
|
|
50
80
|
```
|
|
51
81
|
|
|
52
|
-
### With
|
|
82
|
+
### With failures (Playwright)
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
passing: 1
|
|
86
|
+
failing[1]{at,expected,got}:
|
|
87
|
+
"login.spec.ts:7:42",Welcome,Hello World
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### With parameterized test failures (Vitest)
|
|
53
91
|
|
|
54
92
|
Uses TOON tabular format for uniform parameter arrays:
|
|
55
93
|
|
|
@@ -69,12 +107,22 @@ Uses TOON tabular format for uniform arrays:
|
|
|
69
107
|
```
|
|
70
108
|
passing: 38
|
|
71
109
|
todo[1]{at,name}:
|
|
72
|
-
src/api.test.ts,implement error handling
|
|
110
|
+
"src/api.test.ts:15:3",implement error handling
|
|
73
111
|
skipped[2]{at,name}:
|
|
74
|
-
src/utils.test.ts,handles edge case
|
|
112
|
+
"src/utils.test.ts:8:3",handles edge case
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### With flaky tests (Playwright)
|
|
116
|
+
|
|
117
|
+
Tests that fail initially but pass on retry are reported as flaky:
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
passing: 5
|
|
121
|
+
flaky[1]{at,name,retries}:
|
|
122
|
+
"checkout.spec.ts:12:3",should complete payment,2
|
|
75
123
|
```
|
|
76
124
|
|
|
77
|
-
### With coverage
|
|
125
|
+
### With coverage (Vitest only)
|
|
78
126
|
|
|
79
127
|
Coverage is automatically included when running with `--coverage`. No extra configuration needed:
|
|
80
128
|
|
|
@@ -112,7 +160,7 @@ With `verbose: true`, all files appear with per-file percentages:
|
|
|
112
160
|
|
|
113
161
|
- **Green**: `passing` count
|
|
114
162
|
- **Red**: `failing` header
|
|
115
|
-
- **Yellow**: file paths
|
|
163
|
+
- **Yellow**: `flaky` header, file paths
|
|
116
164
|
- **Gray**: `skipped` tests
|
|
117
165
|
- **Cyan**: `todo` tests
|
|
118
166
|
|
|
@@ -153,7 +201,7 @@ COLOR=1 npx vitest run --reporter=@epicat/toon-reporter
|
|
|
153
201
|
Write report to a file instead of stdout.
|
|
154
202
|
|
|
155
203
|
```ts
|
|
156
|
-
reporters: [['toon-reporter', { outputFile: 'test-results.txt' }]]
|
|
204
|
+
reporters: [['@epicat/toon-reporter', { outputFile: 'test-results.txt' }]]
|
|
157
205
|
```
|
|
158
206
|
|
|
159
207
|
### `verbose`
|
|
@@ -166,6 +214,8 @@ reporters: [new ToonReporter({ verbose: true })]
|
|
|
166
214
|
|
|
167
215
|
## Skipped/Todo Line Numbers
|
|
168
216
|
|
|
217
|
+
### Vitest
|
|
218
|
+
|
|
169
219
|
To get line:column information for skipped and todo tests, enable `includeTaskLocation` in your vitest config:
|
|
170
220
|
|
|
171
221
|
```ts
|
|
@@ -173,7 +223,7 @@ To get line:column information for skipped and todo tests, enable `includeTaskLo
|
|
|
173
223
|
export default defineConfig({
|
|
174
224
|
test: {
|
|
175
225
|
includeTaskLocation: true,
|
|
176
|
-
reporters: ['toon-reporter'],
|
|
226
|
+
reporters: ['@epicat/toon-reporter'],
|
|
177
227
|
},
|
|
178
228
|
})
|
|
179
229
|
```
|
|
@@ -181,19 +231,40 @@ export default defineConfig({
|
|
|
181
231
|
Or via CLI:
|
|
182
232
|
|
|
183
233
|
```bash
|
|
184
|
-
npx vitest run --reporter
|
|
234
|
+
npx vitest run --reporter=@epicat/toon-reporter --includeTaskLocation
|
|
185
235
|
```
|
|
186
236
|
|
|
187
237
|
Without this option, skipped/todo tests will only show the file path (not line:column). This is a Vitest limitation - test locations are only collected when this config is enabled before test collection.
|
|
188
238
|
|
|
239
|
+
### Playwright
|
|
240
|
+
|
|
241
|
+
Playwright always includes test locations. Tests marked with `test.fixme()` are reported as `todo`, while `test.skip()` tests are reported as `skipped`.
|
|
242
|
+
|
|
243
|
+
## Playwright-Specific Features
|
|
244
|
+
|
|
245
|
+
### Flaky Test Detection
|
|
246
|
+
|
|
247
|
+
When `retries` is configured in your Playwright config, tests that fail initially but pass on retry are reported as flaky:
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
// playwright.config.ts
|
|
251
|
+
export default defineConfig({
|
|
252
|
+
retries: 2,
|
|
253
|
+
reporter: [['@epicat/toon-reporter/playwright']],
|
|
254
|
+
})
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Output: `flaky[1]{at,name,retries}: "test.spec.ts:5:3",should work,1`
|
|
258
|
+
|
|
189
259
|
## Why?
|
|
190
260
|
|
|
191
261
|
Traditional test reporters output verbose information optimized for human readability. When feeding test results to an LLM for automated fixing, this verbosity wastes tokens. This reporter outputs only what's needed:
|
|
192
262
|
|
|
193
263
|
- Pass count
|
|
194
264
|
- Failure locations with expected/got values
|
|
265
|
+
- Flaky test detection with retry counts (Playwright)
|
|
195
266
|
- Skipped/todo test names for context
|
|
196
|
-
- Coverage totals and uncovered lines (
|
|
267
|
+
- Coverage totals and uncovered lines (Vitest with `--coverage`)
|
|
197
268
|
|
|
198
269
|
## Token Efficiency
|
|
199
270
|
|
package/dist/index.mjs
CHANGED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { FullConfig, FullResult, Reporter, Suite } from "@playwright/test/reporter";
|
|
2
|
+
|
|
3
|
+
//#region src/toon-playwright-reporter.d.ts
|
|
4
|
+
interface ToonPlaywrightReporterOptions {
|
|
5
|
+
outputFile?: string;
|
|
6
|
+
/** @internal Used for testing to capture output */
|
|
7
|
+
_captureOutput?: (output: string) => void;
|
|
8
|
+
}
|
|
9
|
+
declare class ToonPlaywrightReporter implements Reporter {
|
|
10
|
+
private config;
|
|
11
|
+
private suite;
|
|
12
|
+
private options;
|
|
13
|
+
constructor(options?: ToonPlaywrightReporterOptions);
|
|
14
|
+
onBegin(config: FullConfig, suite: Suite): void;
|
|
15
|
+
private get rootDir();
|
|
16
|
+
private formatLocation;
|
|
17
|
+
private stripAnsi;
|
|
18
|
+
private stripOuterQuotes;
|
|
19
|
+
private parseExpectedGot;
|
|
20
|
+
onEnd(result: FullResult): Promise<void>;
|
|
21
|
+
writeReport(report: string): Promise<void>;
|
|
22
|
+
printsToStdio(): boolean;
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
export { ToonPlaywrightReporter, ToonPlaywrightReporter as default, type ToonPlaywrightReporterOptions };
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { existsSync, promises } from "node:fs";
|
|
2
|
+
import { dirname, relative, resolve } from "pathe";
|
|
3
|
+
import { encode } from "@toon-format/toon";
|
|
4
|
+
|
|
5
|
+
//#region src/toon-playwright-reporter.ts
|
|
6
|
+
var ToonPlaywrightReporter = class {
|
|
7
|
+
constructor(options = {}) {
|
|
8
|
+
this.options = options;
|
|
9
|
+
}
|
|
10
|
+
onBegin(config, suite) {
|
|
11
|
+
this.config = config;
|
|
12
|
+
this.suite = suite;
|
|
13
|
+
}
|
|
14
|
+
get rootDir() {
|
|
15
|
+
return this.config.rootDir;
|
|
16
|
+
}
|
|
17
|
+
formatLocation(filePath, line, column) {
|
|
18
|
+
const relPath = relative(this.rootDir, filePath);
|
|
19
|
+
return line ? `${relPath}:${line}:${column || 0}` : relPath;
|
|
20
|
+
}
|
|
21
|
+
stripAnsi(str) {
|
|
22
|
+
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
23
|
+
}
|
|
24
|
+
stripOuterQuotes(str) {
|
|
25
|
+
if (str.startsWith("\"") && str.endsWith("\"") || str.startsWith("'") && str.endsWith("'")) return str.slice(1, -1);
|
|
26
|
+
return str;
|
|
27
|
+
}
|
|
28
|
+
parseExpectedGot(error) {
|
|
29
|
+
const message = this.stripAnsi(error?.message || "");
|
|
30
|
+
const expectedMatch = message.match(/Expected:\s*(.+?)(?:\n|$)/i);
|
|
31
|
+
const receivedMatch = message.match(/Received:\s*(.+?)(?:\n|$)/i);
|
|
32
|
+
if (expectedMatch && receivedMatch) return {
|
|
33
|
+
expected: this.stripOuterQuotes(expectedMatch[1].trim()),
|
|
34
|
+
got: this.stripOuterQuotes(receivedMatch[1].trim())
|
|
35
|
+
};
|
|
36
|
+
const toBeMatch = message.match(/expect\((.+?)\)\.toBe\((.+?)\)/i);
|
|
37
|
+
if (toBeMatch) return {
|
|
38
|
+
expected: this.stripOuterQuotes(toBeMatch[2].trim()),
|
|
39
|
+
got: this.stripOuterQuotes(toBeMatch[1].trim())
|
|
40
|
+
};
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
async onEnd(result) {
|
|
44
|
+
const allTests = this.suite.allTests();
|
|
45
|
+
const passedTests = [];
|
|
46
|
+
const failedTests = [];
|
|
47
|
+
const skippedTests = [];
|
|
48
|
+
const todoTests = [];
|
|
49
|
+
const flakyTests = [];
|
|
50
|
+
for (const test of allTests) {
|
|
51
|
+
const lastResult = test.results[test.results.length - 1];
|
|
52
|
+
if (!lastResult) continue;
|
|
53
|
+
const status = lastResult.status;
|
|
54
|
+
const isFixme = test.annotations.some((a) => a.type === "fixme");
|
|
55
|
+
if (status === "passed" && test.results.length > 1 && test.results.some((r) => r.status === "failed")) flakyTests.push(test);
|
|
56
|
+
else if (status === "passed") passedTests.push(test);
|
|
57
|
+
else if (status === "failed" || status === "timedOut" || status === "interrupted") failedTests.push(test);
|
|
58
|
+
else if (status === "skipped") if (isFixme) todoTests.push(test);
|
|
59
|
+
else skippedTests.push(test);
|
|
60
|
+
}
|
|
61
|
+
const failures = [];
|
|
62
|
+
for (const test of failedTests) {
|
|
63
|
+
const error = test.results[test.results.length - 1]?.errors?.[0];
|
|
64
|
+
const loc = error?.location || test.location;
|
|
65
|
+
const failure = { at: this.formatLocation(loc.file, loc.line, loc.column) };
|
|
66
|
+
if (error) {
|
|
67
|
+
const { expected, got } = this.parseExpectedGot(error);
|
|
68
|
+
if (expected !== void 0 && got !== void 0) {
|
|
69
|
+
failure.expected = expected;
|
|
70
|
+
failure.got = got;
|
|
71
|
+
} else failure.error = error.message;
|
|
72
|
+
}
|
|
73
|
+
failures.push(failure);
|
|
74
|
+
}
|
|
75
|
+
const mapToSkipped = (test) => ({
|
|
76
|
+
at: this.formatLocation(test.location.file, test.location.line, test.location.column),
|
|
77
|
+
name: test.title
|
|
78
|
+
});
|
|
79
|
+
const mapToFlaky = (test) => ({
|
|
80
|
+
at: this.formatLocation(test.location.file, test.location.line, test.location.column),
|
|
81
|
+
name: test.title,
|
|
82
|
+
retries: test.results.length - 1
|
|
83
|
+
});
|
|
84
|
+
const report = { passing: passedTests.length };
|
|
85
|
+
if (flakyTests.length > 0) report.flaky = flakyTests.map(mapToFlaky);
|
|
86
|
+
if (failures.length > 0) report.failing = failures;
|
|
87
|
+
if (todoTests.length > 0) report.todo = todoTests.map(mapToSkipped);
|
|
88
|
+
if (skippedTests.length > 0) report.skipped = skippedTests.map(mapToSkipped);
|
|
89
|
+
await this.writeReport(encode(report));
|
|
90
|
+
}
|
|
91
|
+
async writeReport(report) {
|
|
92
|
+
if (this.options._captureOutput) {
|
|
93
|
+
this.options._captureOutput(report);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const outputFile = this.options.outputFile;
|
|
97
|
+
if (outputFile) {
|
|
98
|
+
const reportFile = resolve(this.rootDir, outputFile);
|
|
99
|
+
const outputDirectory = dirname(reportFile);
|
|
100
|
+
if (!existsSync(outputDirectory)) await promises.mkdir(outputDirectory, { recursive: true });
|
|
101
|
+
await promises.writeFile(reportFile, report, "utf-8");
|
|
102
|
+
console.log(`TOON report written to ${reportFile}`);
|
|
103
|
+
} else console.log(report);
|
|
104
|
+
}
|
|
105
|
+
printsToStdio() {
|
|
106
|
+
return !this.options.outputFile;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
//#endregion
|
|
111
|
+
export { ToonPlaywrightReporter, ToonPlaywrightReporter 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.6",
|
|
4
4
|
"description": "A minimal Vitest reporter optimized for LLM consumption",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -16,23 +16,35 @@
|
|
|
16
16
|
"types": "./dist/index.d.mts",
|
|
17
17
|
"exports": {
|
|
18
18
|
".": "./dist/index.mjs",
|
|
19
|
+
"./playwright": "./dist/playwright.mjs",
|
|
19
20
|
"./package.json": "./package.json"
|
|
20
21
|
},
|
|
21
22
|
"peerDependencies": {
|
|
23
|
+
"@playwright/test": ">=1.40.0",
|
|
22
24
|
"vitest": ">=2.0.0"
|
|
23
25
|
},
|
|
26
|
+
"peerDependenciesMeta": {
|
|
27
|
+
"@playwright/test": {
|
|
28
|
+
"optional": true
|
|
29
|
+
},
|
|
30
|
+
"vitest": {
|
|
31
|
+
"optional": true
|
|
32
|
+
}
|
|
33
|
+
},
|
|
24
34
|
"dependencies": {
|
|
25
35
|
"@toon-format/toon": "^2.0.1",
|
|
26
36
|
"pathe": "^2.0.3"
|
|
27
37
|
},
|
|
28
38
|
"devDependencies": {
|
|
29
39
|
"@changesets/cli": "^2.29.8",
|
|
40
|
+
"@playwright/test": "^1.57.0",
|
|
30
41
|
"@types/node": "^24.10.1",
|
|
31
42
|
"@vitest/coverage-v8": "^4.0.15",
|
|
32
43
|
"@vitest/runner": "4.0.15",
|
|
33
44
|
"@vitest/utils": "4.0.15",
|
|
34
45
|
"gpt-tokenizer": "^3.4.0",
|
|
35
46
|
"istanbul-lib-coverage": "^3.2.2",
|
|
47
|
+
"serve": "^14.2.5",
|
|
36
48
|
"tsdown": "^0.16.4",
|
|
37
49
|
"typescript": "^5.9.3",
|
|
38
50
|
"vitest": "4.0.15"
|
|
@@ -41,6 +53,7 @@
|
|
|
41
53
|
"build": "tsdown",
|
|
42
54
|
"dev": "tsdown --watch",
|
|
43
55
|
"test": "vitest run",
|
|
56
|
+
"test:coverage": "vitest run --coverage",
|
|
44
57
|
"test:watch": "vitest"
|
|
45
58
|
}
|
|
46
59
|
}
|