@epicat/toon-reporter 0.0.9 → 0.0.11

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
@@ -112,6 +112,27 @@ skipped[2]{at,name}:
112
112
  "src/utils.test.ts:8:3",handles edge case
113
113
  ```
114
114
 
115
+ ### With test name filtering
116
+
117
+ When using `--testNamePattern` or `-t`, results show a `(filtered)` indicator:
118
+
119
+ ```
120
+ passing: 5 (filtered)
121
+ skipped: 42
122
+ ```
123
+
124
+ ### With timing enabled
125
+
126
+ When `timing: true` is set, shows total duration and per-test timing:
127
+
128
+ ```
129
+ duration: 52ms
130
+ passing[3]{at,name,ms}:
131
+ "utils.test.ts:4:3",should be fast,1
132
+ "api.test.ts:12:3",should handle requests,50
133
+ "db.test.ts:8:3",should be slow,1
134
+ ```
135
+
115
136
  ### With flaky tests (Playwright)
116
137
 
117
138
  Tests that fail initially but pass on retry are reported as flaky:
@@ -160,9 +181,10 @@ With `verbose: true`, all files appear with per-file percentages:
160
181
 
161
182
  - **Green**: `passing` count
162
183
  - **Red**: `failing` header
163
- - **Yellow**: `flaky` header, file paths
164
- - **Gray**: `skipped` tests
184
+ - **Yellow**: `flaky` header, `skipped` count, file paths
185
+ - **Gray**: `skipped` tests (detailed list)
165
186
  - **Cyan**: `todo` tests
187
+ - **Purple**: `(filtered)` indicator when using `testNamePattern`
166
188
 
167
189
  Colors are enabled when:
168
190
  - `COLOR` environment variable is set, OR
@@ -212,11 +234,29 @@ Include per-file coverage percentages (lines, stmts, branch, funcs) alongside un
212
234
  reporters: [new ToonReporter({ verbose: true })]
213
235
  ```
214
236
 
215
- ## Skipped/Todo Line Numbers
237
+ ### `timing`
238
+
239
+ Show per-test timing information and total duration. Useful for identifying slow tests.
240
+
241
+ ```ts
242
+ reporters: [new ToonReporter({ timing: true })]
243
+ ```
244
+
245
+ Output:
246
+
247
+ ```
248
+ duration: 1m30s52ms
249
+ passing[3]{at,name,ms}:
250
+ "utils.test.ts:4:3",should be fast,1
251
+ "api.test.ts:12:3",should handle requests,50
252
+ "db.test.ts:8:3",should query slowly,90000
253
+ ```
254
+
255
+ ## Line Numbers
216
256
 
217
257
  ### Vitest
218
258
 
219
- To get line:column information for skipped and todo tests, enable `includeTaskLocation` in your vitest config:
259
+ To get line:column information for passing tests (with `timing: true`), skipped, and todo tests, enable `includeTaskLocation` in your vitest config:
220
260
 
221
261
  ```ts
222
262
  // vitest.config.ts
@@ -234,7 +274,9 @@ Or via CLI:
234
274
  npx vitest run --reporter=@epicat/toon-reporter --includeTaskLocation
235
275
  ```
236
276
 
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.
277
+ Without this option, passing tests (with `timing: true`), skipped, and 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.
278
+
279
+ **Note:** Failing tests always include line:column from the error stack trace, regardless of this setting.
238
280
 
239
281
  ### Playwright
240
282
 
@@ -3,6 +3,8 @@ import { FullConfig, FullResult, Reporter, Suite } from "@playwright/test/report
3
3
  //#region src/toon-playwright-reporter.d.ts
4
4
  interface ToonPlaywrightReporterOptions {
5
5
  outputFile?: string;
6
+ /** When true, shows per-test timing in passing[N]{at,name,ms} format */
7
+ timing?: boolean;
6
8
  /** @internal Used for testing to capture output */
7
9
  _captureOutput?: (output: string) => void;
8
10
  }
@@ -10,6 +12,7 @@ declare class ToonPlaywrightReporter implements Reporter {
10
12
  private config;
11
13
  private suite;
12
14
  private options;
15
+ private testsDiscovered;
13
16
  constructor(options?: ToonPlaywrightReporterOptions);
14
17
  onBegin(config: FullConfig, suite: Suite): void;
15
18
  private formatLocation;
@@ -3,13 +3,27 @@ import { dirname, relative, resolve } from "pathe";
3
3
  import { encode } from "@toon-format/toon";
4
4
 
5
5
  //#region src/toon-playwright-reporter.ts
6
+ function formatDuration(ms) {
7
+ const h = Math.floor(ms / 36e5);
8
+ const m = Math.floor(ms % 36e5 / 6e4);
9
+ const s = Math.floor(ms % 6e4 / 1e3);
10
+ const remaining = Math.round(ms % 1e3);
11
+ const parts = [];
12
+ if (h) parts.push(`${h}h`);
13
+ if (m) parts.push(`${m}m`);
14
+ if (s) parts.push(`${s}s`);
15
+ if (remaining || parts.length === 0) parts.push(`${remaining}ms`);
16
+ return parts.join("");
17
+ }
6
18
  var ToonPlaywrightReporter = class {
7
19
  constructor(options = {}) {
20
+ this.testsDiscovered = 0;
8
21
  this.options = options;
9
22
  }
10
23
  onBegin(config, suite) {
11
24
  this.config = config;
12
25
  this.suite = suite;
26
+ this.testsDiscovered = suite.allTests().length;
13
27
  }
14
28
  formatLocation(filePath, line, column) {
15
29
  const relPath = relative(process.cwd(), filePath);
@@ -39,6 +53,16 @@ var ToonPlaywrightReporter = class {
39
53
  }
40
54
  async onEnd(result) {
41
55
  const allTests = this.suite.allTests();
56
+ if (allTests.length === 0) {
57
+ let errorMessage;
58
+ if (this.testsDiscovered > 0) errorMessage = `${this.testsDiscovered} test(s) discovered but not executed`;
59
+ else if (result.status === "interrupted") errorMessage = "Test run was interrupted";
60
+ else if (result.status === "timedout") errorMessage = "Test run timed out";
61
+ else errorMessage = "No test files found";
62
+ const report$1 = { error: errorMessage };
63
+ await this.writeReport(encode(report$1));
64
+ return;
65
+ }
42
66
  const passedTests = [];
43
67
  const failedTests = [];
44
68
  const skippedTests = [];
@@ -78,12 +102,26 @@ var ToonPlaywrightReporter = class {
78
102
  name: test.title,
79
103
  retries: test.results.length - 1
80
104
  });
81
- const report = { passing: passedTests.length };
105
+ const mapToTiming = (test) => {
106
+ const lastResult = test.results[test.results.length - 1];
107
+ return {
108
+ at: this.formatLocation(test.location.file, test.location.line, test.location.column),
109
+ name: test.title,
110
+ ms: Math.round(lastResult?.duration ?? 0)
111
+ };
112
+ };
113
+ const report = {};
114
+ if (this.options.timing) {
115
+ report.duration = formatDuration(result.duration);
116
+ report.passing = passedTests.map(mapToTiming);
117
+ } else report.passing = passedTests.length;
82
118
  if (flakyTests.length > 0) report.flaky = flakyTests.map(mapToFlaky);
83
119
  if (failures.length > 0) report.failing = failures;
84
120
  if (todoTests.length > 0) report.todo = todoTests.map(mapToSkipped);
85
121
  if (skippedTests.length > 0) report.skipped = skippedTests.map(mapToSkipped);
86
- await this.writeReport(encode(report));
122
+ let output = encode(report);
123
+ if (!!(this.config.grep || this.config.grepInvert)) output = output.replace(/^(passing: .+)$/m, "$1 (filtered)");
124
+ await this.writeReport(output);
87
125
  }
88
126
  async writeReport(report) {
89
127
  if (this.options._captureOutput) {
@@ -92,7 +130,7 @@ var ToonPlaywrightReporter = class {
92
130
  }
93
131
  const outputFile = this.options.outputFile;
94
132
  if (outputFile) {
95
- const reportFile = resolve(this.rootDir, outputFile);
133
+ const reportFile = resolve(this.config.rootDir, outputFile);
96
134
  const outputDirectory = dirname(reportFile);
97
135
  if (!existsSync(outputDirectory)) await promises.mkdir(outputDirectory, { recursive: true });
98
136
  await promises.writeFile(reportFile, report, "utf-8");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@epicat/toon-reporter",
3
- "version": "0.0.9",
3
+ "version": "0.0.11",
4
4
  "description": "A minimal Vitest reporter optimized for LLM consumption",
5
5
  "repository": {
6
6
  "type": "git",