@epicat/toon-reporter 0.0.5 → 0.0.7

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
@@ -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
- ### CLI
13
+ ### Vitest
14
+
15
+ #### CLI
14
16
 
15
17
  ```bash
16
18
  npx vitest run --reporter=@epicat/toon-reporter
17
19
  ```
18
20
 
19
- ### Config
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 parameterized test failures
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=toon-reporter --includeTaskLocation
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 (when `--coverage` is enabled)
267
+ - Coverage totals and uncovered lines (Vitest with `--coverage`)
197
268
 
198
269
  ## Token Efficiency
199
270
 
package/dist/index.d.mts CHANGED
@@ -11,7 +11,6 @@ interface ToonReporterOptions {
11
11
  _captureOutput?: (output: string) => void;
12
12
  }
13
13
  declare class ToonReporter implements Reporter {
14
- start: number;
15
14
  ctx: Vitest;
16
15
  options: ToonReporterOptions;
17
16
  private useColor;
@@ -25,6 +24,7 @@ declare class ToonReporter implements Reporter {
25
24
  private parseErrorLocation;
26
25
  private parseExpectedGot;
27
26
  private formatErrorMessage;
27
+ private buildReportForModules;
28
28
  onTestRunEnd(testModules: ReadonlyArray<any>, _unhandledErrors: ReadonlyArray<SerializedError>, _reason: TestRunEndReason): Promise<void>;
29
29
  private colorize;
30
30
  writeReport(report: string): Promise<void>;
package/dist/index.mjs CHANGED
@@ -47,14 +47,14 @@ function shouldUseColor(option) {
47
47
  }
48
48
  var ToonReporter = class {
49
49
  constructor(options = {}) {
50
- this.start = 0;
51
50
  this.options = options;
52
51
  this.useColor = shouldUseColor(options.color);
53
52
  }
54
53
  onInit(ctx) {
55
54
  this.ctx = ctx;
56
- this.start = Date.now();
57
55
  this.coverageMap = void 0;
56
+ const coverage = ctx.config.coverage;
57
+ if (coverage?.reporter) coverage.reporter = [];
58
58
  }
59
59
  onCoverage(coverage) {
60
60
  this.coverageMap = coverage;
@@ -148,8 +148,8 @@ var ToonReporter = class {
148
148
  if (name && name !== "Error" && !message.startsWith(name)) message = `${name}: ${message}`;
149
149
  return message;
150
150
  }
151
- async onTestRunEnd(testModules, _unhandledErrors, _reason) {
152
- const tests = getTests(testModules.map((m) => m.task));
151
+ buildReportForModules(modules) {
152
+ const tests = getTests(modules.map((m) => m.task));
153
153
  const rootDir = this.ctx.config.root;
154
154
  const failedTests = tests.filter((t) => t.result?.state === "fail");
155
155
  const passedCount = tests.filter((t) => t.result?.state === "pass").length;
@@ -163,20 +163,17 @@ var ToonReporter = class {
163
163
  const { expected, got } = this.parseExpectedGot(error);
164
164
  const relPath = loc?.relPath || relative(rootDir, t.file.filepath);
165
165
  const at = this.formatLocation(relPath, loc?.line || t.location?.line, loc?.column || t.location?.column);
166
+ const failureDetails = expected !== void 0 && got !== void 0 ? {
167
+ expected,
168
+ got
169
+ } : { error: this.formatErrorMessage(error) };
166
170
  if (t.each) {
167
171
  if (!grouped.has(at)) grouped.set(at, []);
168
- grouped.get(at).push(expected !== void 0 && got !== void 0 ? {
169
- expected,
170
- got
171
- } : { error: this.formatErrorMessage(error) });
172
- } else {
173
- const failure = { at };
174
- if (expected !== void 0 && got !== void 0) {
175
- failure.expected = expected;
176
- failure.got = got;
177
- } else if (error) failure.error = this.formatErrorMessage(error);
178
- failures.push(failure);
179
- }
172
+ grouped.get(at).push(failureDetails);
173
+ } else failures.push({
174
+ at,
175
+ ...failureDetails
176
+ });
180
177
  }
181
178
  for (const [at, params] of grouped) failures.push({
182
179
  at,
@@ -190,6 +187,29 @@ var ToonReporter = class {
190
187
  if (failures.length > 0) report.failing = failures;
191
188
  if (todoTests.length > 0) report.todo = todoTests.map(mapToSkipped);
192
189
  if (skippedTests.length > 0) report.skipped = skippedTests.map(mapToSkipped);
190
+ return report;
191
+ }
192
+ async onTestRunEnd(testModules, _unhandledErrors, _reason) {
193
+ const projectNames = /* @__PURE__ */ new Set();
194
+ const modulesByProject = /* @__PURE__ */ new Map();
195
+ for (const m of testModules) {
196
+ const projectName = m.project?.name || "";
197
+ projectNames.add(projectName);
198
+ if (!modulesByProject.has(projectName)) modulesByProject.set(projectName, []);
199
+ modulesByProject.get(projectName).push(m);
200
+ }
201
+ if (projectNames.size > 1) {
202
+ const projectReports = {};
203
+ for (const [projectName, modules] of modulesByProject) projectReports[projectName || "default"] = this.buildReportForModules(modules);
204
+ const coverage$1 = this.getCoverageSummary();
205
+ if (coverage$1) await this.writeReport(encode({
206
+ ...projectReports,
207
+ coverage: coverage$1
208
+ }));
209
+ else await this.writeReport(encode(projectReports));
210
+ return;
211
+ }
212
+ const report = this.buildReportForModules([...testModules]);
193
213
  const coverage = this.getCoverageSummary();
194
214
  if (coverage) report.coverage = coverage;
195
215
  await this.writeReport(encode(report));
@@ -0,0 +1,24 @@
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 formatLocation;
16
+ private stripAnsi;
17
+ private stripOuterQuotes;
18
+ private parseExpectedGot;
19
+ onEnd(result: FullResult): Promise<void>;
20
+ writeReport(report: string): Promise<void>;
21
+ printsToStdio(): boolean;
22
+ }
23
+ //#endregion
24
+ export { ToonPlaywrightReporter, ToonPlaywrightReporter as default, type ToonPlaywrightReporterOptions };
@@ -0,0 +1,108 @@
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
+ formatLocation(filePath, line, column) {
15
+ const relPath = relative(process.cwd(), filePath);
16
+ return line ? `${relPath}:${line}:${column || 0}` : relPath;
17
+ }
18
+ stripAnsi(str) {
19
+ return str.replace(/\x1b\[[0-9;]*m/g, "");
20
+ }
21
+ stripOuterQuotes(str) {
22
+ if (str.startsWith("\"") && str.endsWith("\"") || str.startsWith("'") && str.endsWith("'")) return str.slice(1, -1);
23
+ return str;
24
+ }
25
+ parseExpectedGot(error) {
26
+ const message = this.stripAnsi(error?.message || "");
27
+ const expectedMatch = message.match(/Expected:\s*(.+?)(?:\n|$)/i);
28
+ const receivedMatch = message.match(/Received:\s*(.+?)(?:\n|$)/i);
29
+ if (expectedMatch && receivedMatch) return {
30
+ expected: this.stripOuterQuotes(expectedMatch[1].trim()),
31
+ got: this.stripOuterQuotes(receivedMatch[1].trim())
32
+ };
33
+ const toBeMatch = message.match(/expect\((.+?)\)\.toBe\((.+?)\)/i);
34
+ if (toBeMatch) return {
35
+ expected: this.stripOuterQuotes(toBeMatch[2].trim()),
36
+ got: this.stripOuterQuotes(toBeMatch[1].trim())
37
+ };
38
+ return {};
39
+ }
40
+ async onEnd(result) {
41
+ const allTests = this.suite.allTests();
42
+ const passedTests = [];
43
+ const failedTests = [];
44
+ const skippedTests = [];
45
+ const todoTests = [];
46
+ const flakyTests = [];
47
+ for (const test of allTests) {
48
+ const lastResult = test.results[test.results.length - 1];
49
+ if (!lastResult) continue;
50
+ const status = lastResult.status;
51
+ const isFixme = test.annotations.some((a) => a.type === "fixme");
52
+ if (status === "passed" && test.results.length > 1 && test.results.some((r) => r.status === "failed")) flakyTests.push(test);
53
+ else if (status === "passed") passedTests.push(test);
54
+ else if (status === "failed" || status === "timedOut" || status === "interrupted") failedTests.push(test);
55
+ else if (status === "skipped") if (isFixme) todoTests.push(test);
56
+ else skippedTests.push(test);
57
+ }
58
+ const failures = [];
59
+ for (const test of failedTests) {
60
+ const error = test.results[test.results.length - 1]?.errors?.[0];
61
+ const loc = error?.location || test.location;
62
+ const failure = { at: this.formatLocation(loc.file, loc.line, loc.column) };
63
+ if (error) {
64
+ const { expected, got } = this.parseExpectedGot(error);
65
+ if (expected !== void 0 && got !== void 0) {
66
+ failure.expected = expected;
67
+ failure.got = got;
68
+ } else failure.error = error.message;
69
+ }
70
+ failures.push(failure);
71
+ }
72
+ const mapToSkipped = (test) => ({
73
+ at: this.formatLocation(test.location.file, test.location.line, test.location.column),
74
+ name: test.title
75
+ });
76
+ const mapToFlaky = (test) => ({
77
+ at: this.formatLocation(test.location.file, test.location.line, test.location.column),
78
+ name: test.title,
79
+ retries: test.results.length - 1
80
+ });
81
+ const report = { passing: passedTests.length };
82
+ if (flakyTests.length > 0) report.flaky = flakyTests.map(mapToFlaky);
83
+ if (failures.length > 0) report.failing = failures;
84
+ if (todoTests.length > 0) report.todo = todoTests.map(mapToSkipped);
85
+ if (skippedTests.length > 0) report.skipped = skippedTests.map(mapToSkipped);
86
+ await this.writeReport(encode(report));
87
+ }
88
+ async writeReport(report) {
89
+ if (this.options._captureOutput) {
90
+ this.options._captureOutput(report);
91
+ return;
92
+ }
93
+ const outputFile = this.options.outputFile;
94
+ if (outputFile) {
95
+ const reportFile = resolve(this.rootDir, outputFile);
96
+ const outputDirectory = dirname(reportFile);
97
+ if (!existsSync(outputDirectory)) await promises.mkdir(outputDirectory, { recursive: true });
98
+ await promises.writeFile(reportFile, report, "utf-8");
99
+ console.log(`TOON report written to ${reportFile}`);
100
+ } else console.log(report);
101
+ }
102
+ printsToStdio() {
103
+ return !this.options.outputFile;
104
+ }
105
+ };
106
+
107
+ //#endregion
108
+ 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.5",
3
+ "version": "0.0.7",
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
  }