@kaizenreport/kensho-vitest 0.1.1 → 0.2.0

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.
Files changed (2) hide show
  1. package/index.js +88 -49
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -1,14 +1,17 @@
1
- // @kaizenreport/kensho-vitest — Vitest custom reporter. Walks the Vitest task
2
- // tree on onFinished() and writes kensho-results/run.json + cases/<id>.json.
1
+ // @kaizenreport/kensho-vitest — Vitest custom reporter kensho-results/.
2
+ //
3
+ // Supports both reporter APIs:
4
+ // • vitest 2.1+/3/4: onTestRunEnd(testModules) — the stable Reported Tasks API
5
+ // (TestModule / TestCase). This is what current Vitest invokes.
6
+ // • vitest 1.x–2.0: onFinished(files) — the legacy task-tree API (fallback).
7
+ // A guard ensures only the first hook to fire writes the report.
3
8
 
4
9
  import { mkdirSync, writeFileSync } from 'node:fs';
5
10
  import { resolve, relative } from 'node:path';
6
11
  import { emptyRun, computeTotals, stableCaseId, validateRun, envInfo } from '@kaizenreport/kensho-schema';
7
12
 
8
- // envInfo() is imported from @kaizenreport/kensho-schema below.
9
-
10
13
  function mapStatus(task) {
11
- // Vitest task.result.state: 'pass' | 'fail' | 'skip' | 'todo' | 'only' | 'run'
14
+ // Legacy task.result.state: 'pass' | 'fail' | 'skip' | 'todo' | 'only' | 'run'
12
15
  const state = task?.result?.state;
13
16
  const mode = task?.mode;
14
17
  if (mode === 'skip' || mode === 'todo' || state === 'skip' || state === 'todo') return 'skip';
@@ -17,6 +20,14 @@ function mapStatus(task) {
17
20
  return 'broken';
18
21
  }
19
22
 
23
+ function mapReportedState(state) {
24
+ // Reported Tasks API TestResult.state: 'passed' | 'failed' | 'skipped' | 'pending'
25
+ if (state === 'passed') return 'pass';
26
+ if (state === 'failed') return 'fail';
27
+ if (state === 'skipped' || state === 'pending') return 'skip';
28
+ return 'broken';
29
+ }
30
+
20
31
  function extractInlineTags(title) {
21
32
  const tags = [];
22
33
  const re = /@([\w-]+)/g;
@@ -34,7 +45,6 @@ function severityFromTags(tags) {
34
45
  }
35
46
 
36
47
  function walkTests(task, suiteChain, out) {
37
- // Vitest task types: 'suite' | 'test' | 'custom'
38
48
  if (task.type === 'suite') {
39
49
  const nextChain = task.name ? suiteChain.concat(task.name) : suiteChain;
40
50
  for (const child of task.tasks || []) walkTests(child, nextChain, out);
@@ -53,6 +63,7 @@ export default class KenshoVitestReporter {
53
63
  this.runId = opts.runId || ('run_' + new Date().toISOString().replace(/[^0-9]/g, '').slice(0, 14));
54
64
  this.startedAt = new Date().toISOString();
55
65
  this.casesById = new Map();
66
+ this._emitted = false;
56
67
  }
57
68
 
58
69
  onInit(/* ctx */) {
@@ -62,81 +73,109 @@ export default class KenshoVitestReporter {
62
73
  this.startedAt = new Date().toISOString();
63
74
  }
64
75
 
76
+ // ── vitest 2.1+/3/4 — Reported Tasks API ────────────────────────────────
77
+ onTestRunEnd(testModules /* , unhandledErrors, reason */) {
78
+ if (this._emitted) return;
79
+ try {
80
+ for (const mod of testModules || []) {
81
+ const filePath = mod.moduleId ? relative(process.cwd(), mod.moduleId) : undefined;
82
+ for (const tc of mod.children.allTests()) {
83
+ const caseObj = this._toKenshoCaseReported(tc, filePath);
84
+ writeFileSync(resolve(this.casesDir, caseObj.id + '.json'), JSON.stringify(caseObj, null, 2));
85
+ this.casesById.set(caseObj.id, caseObj);
86
+ }
87
+ }
88
+ this._emitted = true;
89
+ this._writeManifest();
90
+ } catch (e) {
91
+ console.error('[kensho] vitest reporter (onTestRunEnd) failed:', (e && e.stack) || e);
92
+ }
93
+ }
94
+
95
+ // ── vitest 1.x–2.0 — legacy task-tree API ───────────────────────────────
65
96
  onFinished(files = [] /* , errors = [] */) {
97
+ if (this._emitted) return;
66
98
  try {
67
99
  const collected = [];
68
- for (const f of files || []) {
69
- // File-level task: its own name is the file path; its tasks are describe/suite blocks.
70
- walkTests(f, [], collected);
71
- this._filePath = f.filepath || f.name;
72
- }
100
+ for (const f of files || []) walkTests(f, [], collected);
73
101
  for (const { task, suiteChain } of collected) {
74
102
  const caseObj = this._toKenshoCase(task, suiteChain);
75
- writeFileSync(
76
- resolve(this.casesDir, caseObj.id + '.json'),
77
- JSON.stringify(caseObj, null, 2),
78
- );
103
+ writeFileSync(resolve(this.casesDir, caseObj.id + '.json'), JSON.stringify(caseObj, null, 2));
79
104
  this.casesById.set(caseObj.id, caseObj);
80
105
  }
106
+ this._emitted = true;
81
107
  this._writeManifest();
82
108
  } catch (e) {
83
- console.error('[kensho] vitest reporter failed:', e && e.message);
109
+ console.error('[kensho] vitest reporter (onFinished) failed:', (e && e.stack) || e);
84
110
  }
85
111
  }
86
112
 
87
- _toKenshoCase(task, suiteChain) {
88
- const name = task.name || 'unnamed';
89
- const filePath = (task.file?.filepath || task.file?.name)
90
- ? relative(process.cwd(), task.file.filepath || task.file.name)
91
- : undefined;
92
- const fullName = suiteChain.concat(name).join(' › ');
113
+ _uniqueId(fullName, filePath) {
93
114
  let id = stableCaseId(fullName, filePath);
94
115
  if (this.casesById.has(id)) {
95
116
  let i = 2;
96
117
  while (this.casesById.has(id + '_' + i)) i++;
97
118
  id = id + '_' + i;
98
119
  }
120
+ return id;
121
+ }
122
+
123
+ _toKenshoCaseReported(tc, filePath) {
124
+ const name = tc.name || 'unnamed';
125
+ const fullName = tc.fullName || name;
126
+ const suiteChain = fullName.includes(' > ') ? fullName.split(' > ').slice(0, -1) : [];
127
+ const id = this._uniqueId(fullName, filePath);
128
+ const res = (typeof tc.result === 'function' ? tc.result() : tc.result) || {};
129
+ const diag = (typeof tc.diagnostic === 'function' ? tc.diagnostic() : {}) || {};
130
+ const duration = Math.max(0, Math.round(diag.duration || 0));
131
+ const startMs = diag.startTime || Date.now();
132
+ const tags = extractInlineTags(name);
133
+ const errors = (res.errors || []).map(e => ({
134
+ message: String(e.message || e), stack: e.stack, type: e.name,
135
+ }));
136
+ return {
137
+ id, name, fullName, filePath,
138
+ suite: suiteChain,
139
+ tags,
140
+ severity: this.severityFromTag ? severityFromTags(tags) : undefined,
141
+ status: mapReportedState(res.state),
142
+ startedAt: new Date(startMs).toISOString(),
143
+ finishedAt: new Date(startMs + duration).toISOString(),
144
+ duration,
145
+ retries: diag.retryCount || 0,
146
+ platform: process.platform,
147
+ steps: [],
148
+ errors: errors.length ? errors : undefined,
149
+ attachments: [],
150
+ logs: [],
151
+ };
152
+ }
153
+
154
+ _toKenshoCase(task, suiteChain) {
155
+ const name = task.name || 'unnamed';
156
+ const filePath = (task.file?.filepath || task.file?.name)
157
+ ? relative(process.cwd(), task.file.filepath || task.file.name)
158
+ : undefined;
159
+ const fullName = suiteChain.concat(name).join(' › ');
160
+ const id = this._uniqueId(fullName, filePath);
99
161
  const tags = extractInlineTags(name);
100
162
  const duration = Math.max(0, Math.round(task.result?.duration || 0));
101
163
  const startMs = task.result?.startTime || Date.now();
102
- const startedAt = new Date(startMs).toISOString();
103
-
104
164
  const errors = (task.result?.errors || []).map(e => ({
105
- message: String(e.message || e),
106
- stack: e.stack,
107
- type: e.name,
165
+ message: String(e.message || e), stack: e.stack, type: e.name,
108
166
  }));
109
-
110
- // Nested tasks (e.g. test.each or custom child tasks) → Kensho steps.
111
- const steps = [];
112
- if (Array.isArray(task.tasks) && task.tasks.length) {
113
- task.tasks.forEach((child, i) => {
114
- const childStatus = mapStatus(child);
115
- steps.push({
116
- id: 'step_' + i + '_' + Math.random().toString(36).slice(2, 6),
117
- title: child.name || `Step ${i + 1}`,
118
- status: childStatus === 'fail' ? 'fail' : childStatus === 'skip' ? 'skip' : 'pass',
119
- startedAt: new Date(child.result?.startTime || startMs).toISOString(),
120
- duration: Math.max(0, Math.round(child.result?.duration || 0)),
121
- });
122
- });
123
- }
124
-
125
167
  return {
126
- id,
127
- name,
128
- fullName,
129
- filePath,
168
+ id, name, fullName, filePath,
130
169
  suite: suiteChain,
131
170
  tags,
132
171
  severity: this.severityFromTag ? severityFromTags(tags) : undefined,
133
172
  status: mapStatus(task),
134
- startedAt,
173
+ startedAt: new Date(startMs).toISOString(),
135
174
  finishedAt: new Date(startMs + duration).toISOString(),
136
175
  duration,
137
176
  retries: task.result?.retryCount || 0,
138
177
  platform: process.platform,
139
- steps,
178
+ steps: [],
140
179
  errors: errors.length ? errors : undefined,
141
180
  attachments: [],
142
181
  logs: [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaizenreport/kensho-vitest",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Kensho reporter for Vitest — writes kensho-results/ so the Kensho CLI can generate a rich HTML report.",
5
5
  "type": "module",
6
6
  "main": "index.js",