@arpanp/zen-reporter 0.10.1
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/LICENSE +21 -0
- package/README.md +146 -0
- package/bin/zen-reporter.js +673 -0
- package/dist/dataProcessor.d.ts +47 -0
- package/dist/exportToCsv.d.ts +15 -0
- package/dist/index.html +105 -0
- package/dist/reporter.cjs +871 -0
- package/dist/reporter.d.ts +61 -0
- package/dist/reporter.js +846 -0
- package/dist/runHistory.d.ts +21 -0
- package/dist/tagColors.d.ts +19 -0
- package/dist/types.d.ts +192 -0
- package/dist/utils.d.ts +126 -0
- package/package.json +102 -0
|
@@ -0,0 +1,871 @@
|
|
|
1
|
+
Object.defineProperties(exports, {
|
|
2
|
+
__esModule: { value: true },
|
|
3
|
+
[Symbol.toStringTag]: { value: "Module" }
|
|
4
|
+
});
|
|
5
|
+
//#region \0rolldown/runtime.js
|
|
6
|
+
var __create = Object.create;
|
|
7
|
+
var __defProp = Object.defineProperty;
|
|
8
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
9
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
10
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
11
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
14
|
+
key = keys[i];
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
16
|
+
get: ((k) => from[k]).bind(null, key),
|
|
17
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
return to;
|
|
21
|
+
};
|
|
22
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
|
|
23
|
+
value: mod,
|
|
24
|
+
enumerable: true
|
|
25
|
+
}) : target, mod));
|
|
26
|
+
//#endregion
|
|
27
|
+
let child_process = require("child_process");
|
|
28
|
+
let fs = require("fs");
|
|
29
|
+
fs = __toESM(fs, 1);
|
|
30
|
+
let path = require("path");
|
|
31
|
+
path = __toESM(path, 1);
|
|
32
|
+
let readline = require("readline");
|
|
33
|
+
readline = __toESM(readline, 1);
|
|
34
|
+
let url = require("url");
|
|
35
|
+
//#region src/lib/utils.ts
|
|
36
|
+
var customFsInstance = null;
|
|
37
|
+
function setFsModule(fsInstance) {
|
|
38
|
+
customFsInstance = fsInstance;
|
|
39
|
+
}
|
|
40
|
+
function getNodeFs() {
|
|
41
|
+
if (typeof window !== "undefined") return null;
|
|
42
|
+
if (customFsInstance) return customFsInstance;
|
|
43
|
+
try {
|
|
44
|
+
const proc = typeof process !== "undefined" ? process : void 0;
|
|
45
|
+
if (proc && typeof proc.getBuiltinModule === "function") {
|
|
46
|
+
const builtinFs = proc.getBuiltinModule("fs");
|
|
47
|
+
if (builtinFs) return builtinFs;
|
|
48
|
+
}
|
|
49
|
+
const g = globalThis;
|
|
50
|
+
if (typeof g.require === "function") return g.require("fs");
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
var fileSnippetCache = /* @__PURE__ */ new Map();
|
|
57
|
+
function getStepCodeSnippet(location, fsModule) {
|
|
58
|
+
if (!location || !location.file || !location.line) return;
|
|
59
|
+
try {
|
|
60
|
+
const fs = fsModule || getNodeFs();
|
|
61
|
+
if (!fs) return void 0;
|
|
62
|
+
let filePath = location.file;
|
|
63
|
+
if (!fs.existsSync(filePath)) {
|
|
64
|
+
const resolved = typeof process !== "undefined" && process.cwd ? `${process.cwd()}/${filePath}` : filePath;
|
|
65
|
+
if (fs.existsSync(resolved)) filePath = resolved;
|
|
66
|
+
else return;
|
|
67
|
+
}
|
|
68
|
+
let lines = fileSnippetCache.get(filePath);
|
|
69
|
+
if (!lines) {
|
|
70
|
+
lines = fs.readFileSync(filePath, "utf8").split(/\r?\n/);
|
|
71
|
+
fileSnippetCache.set(filePath, lines);
|
|
72
|
+
}
|
|
73
|
+
const targetLine = location.line;
|
|
74
|
+
if (targetLine < 1 || targetLine > lines.length) return void 0;
|
|
75
|
+
const startLine = Math.max(1, targetLine - 1);
|
|
76
|
+
const endLine = Math.min(lines.length, targetLine + 1);
|
|
77
|
+
const snippetLines = [];
|
|
78
|
+
for (let l = startLine; l <= endLine; l++) {
|
|
79
|
+
const prefix = l === targetLine ? ">" : " ";
|
|
80
|
+
const lineNum = String(l).padStart(4, " ");
|
|
81
|
+
snippetLines.push(`${prefix} ${lineNum} | ${lines[l - 1]}`);
|
|
82
|
+
}
|
|
83
|
+
return snippetLines.join("\n");
|
|
84
|
+
} catch {}
|
|
85
|
+
}
|
|
86
|
+
function convertPlaywrightSteps(pwSteps, fsModule) {
|
|
87
|
+
const activeFs = fsModule || getNodeFs();
|
|
88
|
+
return pwSteps.map((step) => {
|
|
89
|
+
const rawStep = step;
|
|
90
|
+
const rawError = step.error;
|
|
91
|
+
const error = step.error ? {
|
|
92
|
+
name: "Error",
|
|
93
|
+
message: step.error.message ?? "",
|
|
94
|
+
stack: step.error.stack || "",
|
|
95
|
+
location: step.error.location ? {
|
|
96
|
+
file: step.error.location.file,
|
|
97
|
+
line: step.error.location.line,
|
|
98
|
+
column: step.error.location.column
|
|
99
|
+
} : null,
|
|
100
|
+
snippet: step.error.snippet || "",
|
|
101
|
+
cause: rawError?.cause || null
|
|
102
|
+
} : null;
|
|
103
|
+
const status = step.error ? "failed" : "passed";
|
|
104
|
+
const location = step.location ? {
|
|
105
|
+
file: step.location.file,
|
|
106
|
+
line: step.location.line,
|
|
107
|
+
column: step.location.column
|
|
108
|
+
} : null;
|
|
109
|
+
const snippet = (typeof rawStep.snippet === "string" ? rawStep.snippet : void 0) || getStepCodeSnippet(location, activeFs);
|
|
110
|
+
const rawParams = rawStep.params;
|
|
111
|
+
const params = rawParams && typeof rawParams === "object" ? rawParams : void 0;
|
|
112
|
+
return {
|
|
113
|
+
title: step.title,
|
|
114
|
+
subtitle: typeof rawStep.subtitle === "string" ? rawStep.subtitle : void 0,
|
|
115
|
+
location,
|
|
116
|
+
snippet,
|
|
117
|
+
params,
|
|
118
|
+
duration: step.duration,
|
|
119
|
+
status,
|
|
120
|
+
annotations: Array.isArray(rawStep.annotations) ? rawStep.annotations : [],
|
|
121
|
+
attachments: step.attachments ? step.attachments.map((att) => {
|
|
122
|
+
let bodyData = null;
|
|
123
|
+
if (att.body) bodyData = Buffer.isBuffer(att.body) ? att.body.toString("base64") : typeof att.body === "string" ? att.body : Buffer.from(att.body).toString("base64");
|
|
124
|
+
else if (att.path) {
|
|
125
|
+
if (activeFs && activeFs.existsSync(att.path)) try {
|
|
126
|
+
bodyData = activeFs.readFileSync(att.path).toString("base64");
|
|
127
|
+
} catch {}
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
name: att.name,
|
|
131
|
+
contentType: att.contentType,
|
|
132
|
+
path: att.path || null,
|
|
133
|
+
body: bodyData
|
|
134
|
+
};
|
|
135
|
+
}) : [],
|
|
136
|
+
error,
|
|
137
|
+
subSteps: step.steps && step.steps.length > 0 ? convertPlaywrightSteps(step.steps, activeFs) : void 0
|
|
138
|
+
};
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
function sanitizeAnsi(str) {
|
|
142
|
+
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
143
|
+
}
|
|
144
|
+
function buildHierarchyForFile(fileName, cases) {
|
|
145
|
+
const rootSuite = {
|
|
146
|
+
title: fileName,
|
|
147
|
+
cases: [],
|
|
148
|
+
subSuites: []
|
|
149
|
+
};
|
|
150
|
+
for (const tc of cases) {
|
|
151
|
+
const describePath = tc.describePath || (tc.parent && tc.parent !== tc.fileName ? [tc.parent] : []);
|
|
152
|
+
let currentSuite = rootSuite;
|
|
153
|
+
for (const title of describePath) {
|
|
154
|
+
if (!currentSuite.subSuites) currentSuite.subSuites = [];
|
|
155
|
+
let child = currentSuite.subSuites.find((s) => s.title === title);
|
|
156
|
+
if (!child) {
|
|
157
|
+
child = {
|
|
158
|
+
title,
|
|
159
|
+
cases: [],
|
|
160
|
+
subSuites: []
|
|
161
|
+
};
|
|
162
|
+
currentSuite.subSuites.push(child);
|
|
163
|
+
}
|
|
164
|
+
currentSuite = child;
|
|
165
|
+
}
|
|
166
|
+
currentSuite.cases.push(tc);
|
|
167
|
+
}
|
|
168
|
+
return rootSuite;
|
|
169
|
+
}
|
|
170
|
+
function buildSuitesFromCases(testCases) {
|
|
171
|
+
const fileGroups = {};
|
|
172
|
+
for (const tc of testCases) {
|
|
173
|
+
const key = tc.fileName || "unknown file";
|
|
174
|
+
if (!fileGroups[key]) fileGroups[key] = [];
|
|
175
|
+
fileGroups[key].push(tc);
|
|
176
|
+
}
|
|
177
|
+
const suites = [];
|
|
178
|
+
for (const [fileName, cases] of Object.entries(fileGroups)) suites.push(buildHierarchyForFile(fileName, cases));
|
|
179
|
+
return suites;
|
|
180
|
+
}
|
|
181
|
+
function formatDuration(ms) {
|
|
182
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
183
|
+
const seconds = Math.floor(ms / 1e3);
|
|
184
|
+
const minutes = Math.floor(seconds / 60);
|
|
185
|
+
const secs = seconds % 60;
|
|
186
|
+
if (minutes > 0) return `${minutes}m ${secs}s`;
|
|
187
|
+
return `${secs}s`;
|
|
188
|
+
}
|
|
189
|
+
function generateTerminalSummaryTable(summary, projectName, testRunName) {
|
|
190
|
+
const total = summary.total || 0;
|
|
191
|
+
const passed = summary.passed || 0;
|
|
192
|
+
const failed = summary.failed || 0;
|
|
193
|
+
const timedOut = summary.timedOut || 0;
|
|
194
|
+
const skipped = summary.skipped || 0;
|
|
195
|
+
const interrupted = summary.interrupted || 0;
|
|
196
|
+
const statusStr = failed > 0 || timedOut > 0 || interrupted > 0 ? "FAILED" : "PASSED";
|
|
197
|
+
const passRate = total > 0 ? `${(passed / total * 100).toFixed(1).replace(/\.0$/, "")}%` : "0%";
|
|
198
|
+
const durationStr = formatDuration(summary.duration || 0);
|
|
199
|
+
const headers = [
|
|
200
|
+
"Status",
|
|
201
|
+
"Duration",
|
|
202
|
+
"Pass Rate",
|
|
203
|
+
"Total",
|
|
204
|
+
"Passed",
|
|
205
|
+
"Failed",
|
|
206
|
+
"Timed Out",
|
|
207
|
+
"Skipped"
|
|
208
|
+
];
|
|
209
|
+
const values = [
|
|
210
|
+
statusStr,
|
|
211
|
+
durationStr,
|
|
212
|
+
passRate,
|
|
213
|
+
String(total),
|
|
214
|
+
String(passed),
|
|
215
|
+
String(failed),
|
|
216
|
+
String(timedOut),
|
|
217
|
+
String(skipped)
|
|
218
|
+
];
|
|
219
|
+
if (interrupted > 0) {
|
|
220
|
+
headers.push("Interrupted");
|
|
221
|
+
values.push(String(interrupted));
|
|
222
|
+
}
|
|
223
|
+
const widths = headers.map((h, i) => Math.max(h.length, values[i].length));
|
|
224
|
+
const border = `+${widths.map((w) => "-".repeat(w + 2)).join("+")}+`;
|
|
225
|
+
const formatRow = (cells) => `| ${cells.map((c, i) => c.padEnd(widths[i])).join(" | ")} |`;
|
|
226
|
+
return `${`Test Summary: ${projectName || "Test Automation Project"} — ${testRunName || "Test Run"}`}\n${border}\n${formatRow(headers)}\n${border}\n${formatRow(values)}\n${border}`;
|
|
227
|
+
}
|
|
228
|
+
//#endregion
|
|
229
|
+
//#region src/lib/dataProcessor.ts
|
|
230
|
+
function detectPackageManager(cwd = process.cwd()) {
|
|
231
|
+
for (const [file, manager] of [
|
|
232
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
233
|
+
["yarn.lock", "yarn"],
|
|
234
|
+
["bun.lock", "bun"],
|
|
235
|
+
["package-lock.json", "npm"]
|
|
236
|
+
]) if ((0, fs.existsSync)((0, path.resolve)(cwd, file))) return manager;
|
|
237
|
+
if ((0, fs.existsSync)((0, path.resolve)(cwd, ".npmrc"))) return "npm";
|
|
238
|
+
try {
|
|
239
|
+
const pkgPath = (0, path.resolve)(cwd, "package.json");
|
|
240
|
+
if ((0, fs.existsSync)(pkgPath)) {
|
|
241
|
+
const pkgJson = JSON.parse((0, fs.readFileSync)(pkgPath, "utf8"));
|
|
242
|
+
if (pkgJson.packageManager) return pkgJson.packageManager.split("@")[0];
|
|
243
|
+
}
|
|
244
|
+
} catch {}
|
|
245
|
+
return "npm";
|
|
246
|
+
}
|
|
247
|
+
function getViteBuildCommand(pm, cwd = process.cwd()) {
|
|
248
|
+
switch (pm || detectPackageManager(cwd)) {
|
|
249
|
+
case "pnpm": return "pnpm exec vite build";
|
|
250
|
+
case "yarn": return "yarn exec vite build";
|
|
251
|
+
case "bun": return "bunx vite build";
|
|
252
|
+
default: return "npx vite build";
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
//#endregion
|
|
256
|
+
//#region src/lib/runHistory.ts
|
|
257
|
+
/**
|
|
258
|
+
* Exact ordered field list for a single history row (single source of truth
|
|
259
|
+
* for the JSONL row shape that DuckDB's read_json consumes).
|
|
260
|
+
*/
|
|
261
|
+
var RUN_ROW_FIELDS = [
|
|
262
|
+
"run_id",
|
|
263
|
+
"run_name",
|
|
264
|
+
"project_name",
|
|
265
|
+
"started_at",
|
|
266
|
+
"ended_at",
|
|
267
|
+
"run_duration_ms",
|
|
268
|
+
"run_sequential_duration_ms",
|
|
269
|
+
"run_interrupted",
|
|
270
|
+
"run_passed",
|
|
271
|
+
"run_failed",
|
|
272
|
+
"run_skipped",
|
|
273
|
+
"run_timed_out",
|
|
274
|
+
"run_interrupted",
|
|
275
|
+
"run_workers",
|
|
276
|
+
"run_projects",
|
|
277
|
+
"file",
|
|
278
|
+
"suite",
|
|
279
|
+
"title",
|
|
280
|
+
"project",
|
|
281
|
+
"status",
|
|
282
|
+
"duration_ms",
|
|
283
|
+
"attempts",
|
|
284
|
+
"failed_attempts",
|
|
285
|
+
"passed_on_retry",
|
|
286
|
+
"error_message",
|
|
287
|
+
"error_stack",
|
|
288
|
+
"error_file",
|
|
289
|
+
"error_line",
|
|
290
|
+
"tags"
|
|
291
|
+
];
|
|
292
|
+
/** Lowercase, trim, every non-alphanumeric run -> single '-', strip edge '-'. */
|
|
293
|
+
function slugify(name) {
|
|
294
|
+
return name.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Deterministic run identifier: ISO start time (colons/dots -> '-') + slugified
|
|
298
|
+
* test run name, plus a shard suffix when running sharded.
|
|
299
|
+
*/
|
|
300
|
+
function buildRunId(startedAtIso, testRunName, shard) {
|
|
301
|
+
const base = `${startedAtIso.replace(/[:.]/g, "-")}__${slugify(testRunName)}`;
|
|
302
|
+
if (shard && shard.total > 1) return `${base}__shard${shard.current}of${shard.total}`;
|
|
303
|
+
return base;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* One row per test case, each carrying the run-level fields plus the per-test
|
|
307
|
+
* fields, so DuckDB can group/join across runs on the uniform schema.
|
|
308
|
+
*/
|
|
309
|
+
function flattenRunRows(runId, testRunName, projectName, summary, endedAt, testCases) {
|
|
310
|
+
const runLevel = {
|
|
311
|
+
run_id: runId,
|
|
312
|
+
run_name: testRunName,
|
|
313
|
+
project_name: projectName,
|
|
314
|
+
started_at: summary.startTime,
|
|
315
|
+
ended_at: endedAt,
|
|
316
|
+
run_duration_ms: summary.duration,
|
|
317
|
+
run_sequential_duration_ms: summary.totalSequentialDuration ?? null,
|
|
318
|
+
run_total: summary.total,
|
|
319
|
+
run_passed: summary.passed,
|
|
320
|
+
run_failed: summary.failed,
|
|
321
|
+
run_skipped: summary.skipped,
|
|
322
|
+
run_timed_out: summary.timedOut,
|
|
323
|
+
run_interrupted: summary.interrupted ?? 0,
|
|
324
|
+
run_workers: summary.workers ?? null,
|
|
325
|
+
run_projects: summary.numberOfProjects
|
|
326
|
+
};
|
|
327
|
+
if (testCases.length === 0) {
|
|
328
|
+
const row = { ...runLevel };
|
|
329
|
+
for (const field of RUN_ROW_FIELDS) if (!(field in row)) row[field] = null;
|
|
330
|
+
return [row];
|
|
331
|
+
}
|
|
332
|
+
return testCases.map((tc) => {
|
|
333
|
+
const row = { ...runLevel };
|
|
334
|
+
row.file = tc.fileName;
|
|
335
|
+
row.suite = (tc.describePath?.length ? tc.describePath.join(" > ") : tc.parent) || "";
|
|
336
|
+
row.title = tc.title;
|
|
337
|
+
row.project = tc.project;
|
|
338
|
+
row.status = tc.status;
|
|
339
|
+
row.duration_ms = Math.round(tc.duration);
|
|
340
|
+
row.attempts = tc.attempts ?? 0;
|
|
341
|
+
row.failed_attempts = tc.failedAttempts?.length ?? 0;
|
|
342
|
+
row.passed_on_retry = (tc.attempts ?? 0) > 1 && tc.status === "passed";
|
|
343
|
+
row.error_message = tc.errors?.[0]?.message ?? null;
|
|
344
|
+
row.error_stack = tc.errors?.[0]?.stack ?? null;
|
|
345
|
+
row.error_file = tc.errors?.[0]?.location?.file ?? null;
|
|
346
|
+
row.error_line = tc.errors?.[0]?.location?.line ?? null;
|
|
347
|
+
row.tags = tc.tags ?? [];
|
|
348
|
+
return row;
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
//#endregion
|
|
352
|
+
//#region src/lib/reporter.ts
|
|
353
|
+
setFsModule(fs);
|
|
354
|
+
function getDescribePath(test) {
|
|
355
|
+
const path$1 = [];
|
|
356
|
+
let curr = test.parent;
|
|
357
|
+
while (curr) {
|
|
358
|
+
if (curr.type === "file" || curr.type === "project" || curr.type === "root") break;
|
|
359
|
+
if (curr.type === "describe" && curr.title) path$1.unshift(curr.title);
|
|
360
|
+
curr = curr.parent;
|
|
361
|
+
}
|
|
362
|
+
return path$1;
|
|
363
|
+
}
|
|
364
|
+
function resolveConfig(rawConfig, cwd = process.cwd()) {
|
|
365
|
+
const outputDir = rawConfig?.outputDir ? String(rawConfig.outputDir) : "zen-report";
|
|
366
|
+
const packageManager = detectPackageManager(cwd);
|
|
367
|
+
const projectName = rawConfig?.projectName ? String(rawConfig.projectName) : "Test Automation Project";
|
|
368
|
+
let runNumber = 1;
|
|
369
|
+
try {
|
|
370
|
+
const runsDir = path.resolve(cwd, outputDir, "runs");
|
|
371
|
+
if (fs.existsSync(runsDir)) runNumber = fs.readdirSync(runsDir).filter((f) => f.endsWith(".jsonl")).length + 1;
|
|
372
|
+
} catch {
|
|
373
|
+
runNumber = 1;
|
|
374
|
+
}
|
|
375
|
+
const testRunName = (rawConfig?.testRunName ? String(rawConfig.testRunName) : "Test Run #{N}").replaceAll("{N}", String(runNumber));
|
|
376
|
+
const singleSummaryFile = Boolean(rawConfig?.singleSummaryFile);
|
|
377
|
+
const theme = rawConfig?.theme !== void 0 ? String(rawConfig.theme) : "Cafe";
|
|
378
|
+
const darkMode = Boolean(rawConfig?.darkMode);
|
|
379
|
+
const minimalReport = Boolean(rawConfig?.minimalReport);
|
|
380
|
+
let enableHistory = "auto";
|
|
381
|
+
if (minimalReport) enableHistory = false;
|
|
382
|
+
else if (rawConfig?.enableHistory !== void 0) {
|
|
383
|
+
if (typeof rawConfig.enableHistory === "boolean") enableHistory = rawConfig.enableHistory;
|
|
384
|
+
else if (rawConfig.enableHistory === "auto") enableHistory = "auto";
|
|
385
|
+
}
|
|
386
|
+
let consoleProgress = "auto";
|
|
387
|
+
if (rawConfig?.consoleProgress !== void 0) {
|
|
388
|
+
if (typeof rawConfig.consoleProgress === "boolean") consoleProgress = rawConfig.consoleProgress;
|
|
389
|
+
else if (rawConfig.consoleProgress === "auto" || rawConfig.consoleProgress === "line" || rawConfig.consoleProgress === "dot") consoleProgress = rawConfig.consoleProgress;
|
|
390
|
+
}
|
|
391
|
+
return {
|
|
392
|
+
outputDir,
|
|
393
|
+
packageManager,
|
|
394
|
+
projectName,
|
|
395
|
+
testRunName,
|
|
396
|
+
singleSummaryFile,
|
|
397
|
+
theme,
|
|
398
|
+
darkMode,
|
|
399
|
+
enableHistory,
|
|
400
|
+
minimalReport,
|
|
401
|
+
consoleProgress
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
var ZenReporter = class {
|
|
405
|
+
constructor(options) {
|
|
406
|
+
this.testCaseMap = /* @__PURE__ */ new Map();
|
|
407
|
+
this.startTime = "";
|
|
408
|
+
this.testCases = [];
|
|
409
|
+
this.totalTests = 0;
|
|
410
|
+
this.completedTests = 0;
|
|
411
|
+
this.completedTestIds = /* @__PURE__ */ new Set();
|
|
412
|
+
this.passedCount = 0;
|
|
413
|
+
this.failedCount = 0;
|
|
414
|
+
this.timedOutCount = 0;
|
|
415
|
+
this.skippedCount = 0;
|
|
416
|
+
this.activeWorkers = /* @__PURE__ */ new Set();
|
|
417
|
+
this.lastCompletedTest = "";
|
|
418
|
+
this.dotLineLength = 0;
|
|
419
|
+
this.progressMode = "none";
|
|
420
|
+
this.options = options;
|
|
421
|
+
}
|
|
422
|
+
printLineProgress() {
|
|
423
|
+
if (this.progressMode !== "line") return;
|
|
424
|
+
if (!(typeof process !== "undefined" && process.stdout && Boolean(process.stdout.isTTY))) return;
|
|
425
|
+
const pct = this.totalTests > 0 ? Math.floor(this.completedTests / this.totalTests * 100) : 0;
|
|
426
|
+
const workersCount = this.activeWorkers.size;
|
|
427
|
+
const workerStr = `${workersCount} active worker${workersCount === 1 ? "" : "s"}`;
|
|
428
|
+
let line = `[ ${this.completedTests}/${this.totalTests} ] ${pct}% | ✅ ${this.passedCount} | ❌ ${this.failedCount + this.timedOutCount} | ⏭️ ${this.skippedCount} (${workerStr})`;
|
|
429
|
+
if (this.lastCompletedTest) line += ` | Last: ${this.lastCompletedTest}`;
|
|
430
|
+
const cols = process.stdout.columns || 100;
|
|
431
|
+
if (line.length > cols - 1) line = line.slice(0, Math.max(10, cols - 4)) + "...";
|
|
432
|
+
readline.clearLine(process.stdout, 0);
|
|
433
|
+
readline.cursorTo(process.stdout, 0);
|
|
434
|
+
process.stdout.write(line);
|
|
435
|
+
}
|
|
436
|
+
clearLineProgress() {
|
|
437
|
+
if (this.progressMode === "line" && typeof process !== "undefined" && process.stdout && Boolean(process.stdout.isTTY)) {
|
|
438
|
+
readline.clearLine(process.stdout, 0);
|
|
439
|
+
readline.cursorTo(process.stdout, 0);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
handleDotProgress(status) {
|
|
443
|
+
if (this.progressMode !== "dot") return;
|
|
444
|
+
const symbol = status === "failed" || status === "timedOut" || status === "interrupted" ? "F" : status === "skipped" ? "s" : status === "flaky" ? "±" : ".";
|
|
445
|
+
process.stdout.write(symbol);
|
|
446
|
+
this.dotLineLength += 1;
|
|
447
|
+
if (this.dotLineLength >= 80 || this.completedTestIds.size === this.totalTests) {
|
|
448
|
+
process.stdout.write(` [ ${this.completedTestIds.size}/${this.totalTests} ]\n`);
|
|
449
|
+
this.dotLineLength = 0;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
buildSuites() {
|
|
453
|
+
return buildSuitesFromCases(this.testCases);
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Persist every run as a self-contained per-run JSONL file under
|
|
457
|
+
* <outputDir>/runs/ so historic runs survive report rebuilds.
|
|
458
|
+
* Failure policy: catch-and-log only — history writing must never break
|
|
459
|
+
* report/HTML generation.
|
|
460
|
+
*/
|
|
461
|
+
writeRunHistory(summary, endedAt) {
|
|
462
|
+
try {
|
|
463
|
+
const runId = buildRunId(this.startTime, this.reportConfig.testRunName, this.config.shard);
|
|
464
|
+
const cwd = process.cwd();
|
|
465
|
+
const normalizedCwd = cwd.endsWith("/") ? cwd.slice(0, -1) : cwd;
|
|
466
|
+
const runsDir = path.resolve(normalizedCwd, this.reportConfig.outputDir, "runs");
|
|
467
|
+
let finalRunId = runId;
|
|
468
|
+
let suffix = 2;
|
|
469
|
+
while (fs.existsSync(path.join(runsDir, `${finalRunId}.jsonl`))) {
|
|
470
|
+
finalRunId = `${runId}-${suffix}`;
|
|
471
|
+
suffix += 1;
|
|
472
|
+
}
|
|
473
|
+
const rows = flattenRunRows(finalRunId, this.reportConfig.testRunName, this.reportConfig.projectName, summary, endedAt, this.testCases);
|
|
474
|
+
fs.mkdirSync(runsDir, { recursive: true });
|
|
475
|
+
fs.writeFileSync(path.join(runsDir, `${finalRunId}.jsonl`), rows.map((r) => JSON.stringify(r)).join("\n") + "\n", "utf8");
|
|
476
|
+
} catch (e) {
|
|
477
|
+
console.error("Run history write failed:", e);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Regenerate `history.json` by running the CLI's `history report`
|
|
482
|
+
* subcommand (the CLI owns the DuckDB aggregate queries — single source
|
|
483
|
+
* of truth) and inject the fresh snapshot into `<outputDir>/index.html`.
|
|
484
|
+
* Runs after the new run's JSONL has been written, so it includes the
|
|
485
|
+
* just-finished run. Best-effort: any failure (missing @duckdb/node-api,
|
|
486
|
+
* missing bin, spawn error) is caught and logged; the snapshot that was
|
|
487
|
+
* re-injected above remains in the report.
|
|
488
|
+
*/
|
|
489
|
+
refreshHistory() {
|
|
490
|
+
try {
|
|
491
|
+
const moduleFile = typeof __filename !== "undefined" ? __filename : (0, url.fileURLToPath)(require("url").pathToFileURL(__filename).href);
|
|
492
|
+
let dir = path.dirname(moduleFile);
|
|
493
|
+
let binPath = null;
|
|
494
|
+
for (let i = 0; i < 4; i++) {
|
|
495
|
+
const candidate = path.join(dir, "bin", "zen-reporter.js");
|
|
496
|
+
if (fs.existsSync(candidate)) {
|
|
497
|
+
binPath = candidate;
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
const parent = path.dirname(dir);
|
|
501
|
+
if (parent === dir) break;
|
|
502
|
+
dir = parent;
|
|
503
|
+
}
|
|
504
|
+
if (!binPath) return;
|
|
505
|
+
(0, child_process.execFileSync)(process.execPath, [
|
|
506
|
+
binPath,
|
|
507
|
+
"history",
|
|
508
|
+
"report"
|
|
509
|
+
], {
|
|
510
|
+
cwd: process.cwd(),
|
|
511
|
+
stdio: "inherit",
|
|
512
|
+
env: {
|
|
513
|
+
...process.env,
|
|
514
|
+
PW_REPORTER_OUTPUT: this.reportConfig.outputDir
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
} catch {
|
|
518
|
+
console.warn("History refresh skipped (keeping existing history data).");
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
onBegin(config, suite) {
|
|
522
|
+
this.config = config;
|
|
523
|
+
this.reportConfig = resolveConfig(this.options || config.reporterConfig);
|
|
524
|
+
if (this.reportConfig.enableHistory === true) try {
|
|
525
|
+
require.resolve("@duckdb/node-api");
|
|
526
|
+
} catch {
|
|
527
|
+
throw new Error("Zen Reporter: history is explicitly enabled (enableHistory: true), but \"@duckdb/node-api\" is not installed. Please install \"@duckdb/node-api\" or set enableHistory to \"auto\" or false.");
|
|
528
|
+
}
|
|
529
|
+
this.startTime = (/* @__PURE__ */ new Date()).toISOString();
|
|
530
|
+
this.testCaseMap.clear();
|
|
531
|
+
this.testCases = [];
|
|
532
|
+
const allTests = suite.allTests();
|
|
533
|
+
this.totalTests = allTests.length;
|
|
534
|
+
this.completedTests = 0;
|
|
535
|
+
this.completedTestIds.clear();
|
|
536
|
+
this.passedCount = 0;
|
|
537
|
+
this.failedCount = 0;
|
|
538
|
+
this.timedOutCount = 0;
|
|
539
|
+
this.skippedCount = 0;
|
|
540
|
+
this.activeWorkers.clear();
|
|
541
|
+
this.lastCompletedTest = "";
|
|
542
|
+
this.dotLineLength = 0;
|
|
543
|
+
const cp = this.reportConfig.consoleProgress;
|
|
544
|
+
if (cp === false) this.progressMode = "none";
|
|
545
|
+
else if (cp === "line") this.progressMode = "line";
|
|
546
|
+
else if (cp === "dot") this.progressMode = "dot";
|
|
547
|
+
else {
|
|
548
|
+
const isTTY = typeof process !== "undefined" && process.stdout && Boolean(process.stdout.isTTY);
|
|
549
|
+
this.progressMode = isTTY ? "line" : "dot";
|
|
550
|
+
}
|
|
551
|
+
if (this.progressMode === "dot") {
|
|
552
|
+
const workers = this.config.workers || 1;
|
|
553
|
+
const workerStr = workers === 1 ? "1 worker (serially)" : `${workers} workers (in parallel)`;
|
|
554
|
+
console.log(`Running total ${this.totalTests} tests, ${workerStr}\n\nLegend: . passed | F failed | ± flaky | s skipped\n`);
|
|
555
|
+
}
|
|
556
|
+
for (const test of allTests) {
|
|
557
|
+
const fileName = test.location?.file ? test.location.file.replace(process.cwd(), "") : "unknown file";
|
|
558
|
+
const testCase = {
|
|
559
|
+
title: test.title,
|
|
560
|
+
parent: test.parent.title,
|
|
561
|
+
project: test.parent.project()?.name || "unknown",
|
|
562
|
+
fileName,
|
|
563
|
+
status: "skipped",
|
|
564
|
+
duration: 0,
|
|
565
|
+
steps: [],
|
|
566
|
+
stdout: [],
|
|
567
|
+
stderr: [],
|
|
568
|
+
annotations: test.annotations ? test.annotations.map((a) => ({
|
|
569
|
+
type: a.type,
|
|
570
|
+
description: a.description || null,
|
|
571
|
+
location: a.location ? {
|
|
572
|
+
file: a.location.file,
|
|
573
|
+
line: a.location.line,
|
|
574
|
+
column: a.location.column
|
|
575
|
+
} : null
|
|
576
|
+
})) : [],
|
|
577
|
+
attachments: [],
|
|
578
|
+
tags: test.tags.flatMap((tag) => tag.replace("@", "")),
|
|
579
|
+
describePath: getDescribePath(test),
|
|
580
|
+
attempts: 0,
|
|
581
|
+
failedAttempts: []
|
|
582
|
+
};
|
|
583
|
+
testCase.tags?.push(testCase.project);
|
|
584
|
+
this.testCaseMap.set(test.id, testCase);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
onTestBegin(test, _result) {
|
|
588
|
+
this.activeWorkers.add(test.id);
|
|
589
|
+
if (this.progressMode === "line") this.printLineProgress();
|
|
590
|
+
let existing = this.testCaseMap.get(test.id);
|
|
591
|
+
if (!existing) {
|
|
592
|
+
const fileName = test.parent.location?.file ? test.parent.location.file.replace(process.cwd(), "") : "unknown file";
|
|
593
|
+
existing = {
|
|
594
|
+
title: test.title,
|
|
595
|
+
parent: test.parent.title,
|
|
596
|
+
project: test.parent.project()?.name || "unknown",
|
|
597
|
+
fileName,
|
|
598
|
+
status: "passed",
|
|
599
|
+
duration: 0,
|
|
600
|
+
steps: [],
|
|
601
|
+
stdout: [],
|
|
602
|
+
stderr: [],
|
|
603
|
+
annotations: test.annotations ? test.annotations.map((a) => ({
|
|
604
|
+
type: a.type,
|
|
605
|
+
description: a.description || null,
|
|
606
|
+
location: a.location ? {
|
|
607
|
+
file: a.location.file,
|
|
608
|
+
line: a.location.line,
|
|
609
|
+
column: a.location.column
|
|
610
|
+
} : null
|
|
611
|
+
})) : [],
|
|
612
|
+
attachments: [],
|
|
613
|
+
tags: test.tags.flatMap((tag) => tag.replace("@", "")),
|
|
614
|
+
describePath: getDescribePath(test),
|
|
615
|
+
attempts: 0,
|
|
616
|
+
failedAttempts: []
|
|
617
|
+
};
|
|
618
|
+
existing.tags?.push(existing.project);
|
|
619
|
+
this.testCaseMap.set(test.id, existing);
|
|
620
|
+
}
|
|
621
|
+
existing.attempts = (existing.attempts ?? 0) + 1;
|
|
622
|
+
}
|
|
623
|
+
onTestEnd(test, result) {
|
|
624
|
+
const testCase = this.testCaseMap.get(test.id);
|
|
625
|
+
if (!testCase) return;
|
|
626
|
+
testCase.status = result.status === "passed" ? "passed" : result.status === "failed" ? "failed" : result.status === "skipped" ? "skipped" : result.status === "timedOut" ? "timedOut" : result.status === "interrupted" ? "interrupted" : "failed";
|
|
627
|
+
const attemptSteps = result.steps && result.steps.length > 0 ? convertPlaywrightSteps(result.steps, fs) : void 0;
|
|
628
|
+
const stepDurationSum = attemptSteps ? attemptSteps.reduce((sum, step) => sum + (step.duration || 0), 0) : 0;
|
|
629
|
+
const attemptDuration = typeof result.duration === "number" && result.duration > 0 ? result.duration : stepDurationSum;
|
|
630
|
+
testCase.duration = attemptDuration;
|
|
631
|
+
if (test.annotations) testCase.annotations = test.annotations.map((a) => ({
|
|
632
|
+
type: a.type,
|
|
633
|
+
description: a.description || null,
|
|
634
|
+
location: a.location ? {
|
|
635
|
+
file: a.location.file,
|
|
636
|
+
line: a.location.line,
|
|
637
|
+
column: a.location.column
|
|
638
|
+
} : null
|
|
639
|
+
}));
|
|
640
|
+
const attemptStdout = (result.stdout || []).map((entry) => typeof entry === "string" ? entry : entry.toString("utf8"));
|
|
641
|
+
const attemptStderr = (result.stderr || []).map((entry) => typeof entry === "string" ? entry : entry.toString("utf8"));
|
|
642
|
+
const cwd = process.cwd();
|
|
643
|
+
const normalizedCwd = cwd.endsWith("/") ? cwd.slice(0, -1) : cwd;
|
|
644
|
+
const outputDir = path.resolve(normalizedCwd, this.reportConfig.outputDir);
|
|
645
|
+
const attachmentsDir = path.join(outputDir, "attachments");
|
|
646
|
+
const attemptAttachments = (result.attachments || []).map((att, attIdx) => {
|
|
647
|
+
let bodyData = null;
|
|
648
|
+
let finalPath = att.path || null;
|
|
649
|
+
const isText = att.contentType?.startsWith("text/") || /\.(txt|log|json|csv|html|xml|md|yaml|yml|js|ts|jsx|tsx|css)$/i.test(att.name || att.path || "");
|
|
650
|
+
if (att.path && fs.existsSync(att.path)) try {
|
|
651
|
+
fs.mkdirSync(attachmentsDir, { recursive: true });
|
|
652
|
+
const safeName = path.basename(att.path).replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
653
|
+
const targetFilename = `${test.id.replace(/[^a-zA-Z0-9_-]/g, "_")}_att${attIdx}_${safeName}`;
|
|
654
|
+
const targetPath = path.join(attachmentsDir, targetFilename);
|
|
655
|
+
fs.copyFileSync(att.path, targetPath);
|
|
656
|
+
finalPath = `./attachments/${targetFilename}`;
|
|
657
|
+
if (isText) bodyData = fs.readFileSync(att.path, "utf8");
|
|
658
|
+
} catch (e) {
|
|
659
|
+
console.error(`Failed to copy attachment file from ${att.path}:`, e);
|
|
660
|
+
}
|
|
661
|
+
else if (att.body) {
|
|
662
|
+
if (isText) bodyData = Buffer.isBuffer(att.body) ? att.body.toString("utf8") : typeof att.body === "string" ? att.body : String(att.body);
|
|
663
|
+
else try {
|
|
664
|
+
fs.mkdirSync(attachmentsDir, { recursive: true });
|
|
665
|
+
const ext = att.contentType === "image/png" ? ".png" : att.contentType === "image/jpeg" ? ".jpg" : ".bin";
|
|
666
|
+
const targetFilename = `${test.id.replace(/[^a-zA-Z0-9_-]/g, "_")}_att${attIdx}${ext}`;
|
|
667
|
+
const targetPath = path.join(attachmentsDir, targetFilename);
|
|
668
|
+
const buf = Buffer.isBuffer(att.body) ? att.body : Buffer.from(att.body);
|
|
669
|
+
fs.writeFileSync(targetPath, buf);
|
|
670
|
+
finalPath = `./attachments/${targetFilename}`;
|
|
671
|
+
} catch (e) {
|
|
672
|
+
console.error("Failed to write in-memory attachment to disk:", e);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
return {
|
|
676
|
+
name: att.name,
|
|
677
|
+
contentType: att.contentType,
|
|
678
|
+
path: finalPath,
|
|
679
|
+
body: bodyData
|
|
680
|
+
};
|
|
681
|
+
});
|
|
682
|
+
testCase.attachments = attemptAttachments.length > 0 ? attemptAttachments : void 0;
|
|
683
|
+
testCase.errors = result.error ? [{
|
|
684
|
+
name: "Error",
|
|
685
|
+
message: sanitizeAnsi(result.error.message || ""),
|
|
686
|
+
stack: sanitizeAnsi(result.error.stack || ""),
|
|
687
|
+
location: result.error.location ? {
|
|
688
|
+
file: result.error.location.file,
|
|
689
|
+
line: result.error.location.line,
|
|
690
|
+
column: result.error.location.column
|
|
691
|
+
} : null,
|
|
692
|
+
snippet: result.error.snippet || "",
|
|
693
|
+
cause: result.error.cause || null
|
|
694
|
+
}] : void 0;
|
|
695
|
+
if (result.status === "failed" || result.status === "timedOut" || result.status === "interrupted") {
|
|
696
|
+
testCase.failedAttempts = testCase.failedAttempts ?? [];
|
|
697
|
+
testCase.failedAttempts.push({
|
|
698
|
+
status: result.status,
|
|
699
|
+
duration: attemptDuration,
|
|
700
|
+
error: testCase.errors && testCase.errors.length > 0 ? testCase.errors[0] : null,
|
|
701
|
+
steps: attemptSteps,
|
|
702
|
+
stdout: attemptStdout.length > 0 ? attemptStdout : void 0,
|
|
703
|
+
stderr: attemptStderr.length > 0 ? attemptStderr : void 0,
|
|
704
|
+
attachments: attemptAttachments.length > 0 ? attemptAttachments : void 0
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
testCase.steps = attemptSteps;
|
|
708
|
+
testCase.stdout = attemptStdout.length > 0 ? attemptStdout : void 0;
|
|
709
|
+
testCase.stderr = attemptStderr.length > 0 ? attemptStderr : void 0;
|
|
710
|
+
this.activeWorkers.delete(test.id);
|
|
711
|
+
this.completedTestIds.add(test.id);
|
|
712
|
+
this.completedTests = this.completedTestIds.size;
|
|
713
|
+
const status = testCase.status;
|
|
714
|
+
if (status === "passed") this.passedCount += 1;
|
|
715
|
+
else if (status === "failed") this.failedCount += 1;
|
|
716
|
+
else if (status === "timedOut") this.timedOutCount += 1;
|
|
717
|
+
else if (status === "skipped") this.skippedCount += 1;
|
|
718
|
+
const shortName = `${testCase.project && testCase.project !== "unknown" ? `[${testCase.project}] ` : ""}${testCase.fileName} › ${testCase.title}`;
|
|
719
|
+
this.lastCompletedTest = shortName;
|
|
720
|
+
if (this.progressMode === "line") {
|
|
721
|
+
if (status === "failed" || status === "timedOut" || status === "interrupted") {
|
|
722
|
+
this.clearLineProgress();
|
|
723
|
+
const durationStr = testCase.duration > 0 ? ` (${testCase.duration}ms)` : "";
|
|
724
|
+
console.log(`❌ [FAILED] ${shortName}${durationStr}`);
|
|
725
|
+
}
|
|
726
|
+
this.printLineProgress();
|
|
727
|
+
} else if (this.progressMode === "dot") {
|
|
728
|
+
let dotStatus = status;
|
|
729
|
+
if (status === "passed" && (testCase.failedAttempts?.length ?? 0) > 0) dotStatus = "flaky";
|
|
730
|
+
this.handleDotProgress(dotStatus);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
onEnd(result) {
|
|
734
|
+
if (this.progressMode === "line") this.clearLineProgress();
|
|
735
|
+
else if (this.progressMode === "dot") {
|
|
736
|
+
if (this.dotLineLength > 0) {
|
|
737
|
+
process.stdout.write(` [ ${this.completedTests}/${this.totalTests} ]\n`);
|
|
738
|
+
this.dotLineLength = 0;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
if (result.status === "interrupted") {
|
|
742
|
+
for (const testCase of this.testCaseMap.values()) if ((testCase.attempts ?? 0) === 0) testCase.status = "interrupted";
|
|
743
|
+
}
|
|
744
|
+
this.testCases = Array.from(this.testCaseMap.values());
|
|
745
|
+
const endTime = (/* @__PURE__ */ new Date()).toISOString();
|
|
746
|
+
const startTimeMs = new Date(this.startTime).getTime();
|
|
747
|
+
const endTimeMs = new Date(endTime).getTime();
|
|
748
|
+
const wallClockDuration = Math.max(0, endTimeMs - startTimeMs);
|
|
749
|
+
const passed = this.testCases.filter((tc) => tc.status === "passed").length;
|
|
750
|
+
const failed = this.testCases.filter((tc) => tc.status === "failed").length;
|
|
751
|
+
const skipped = this.testCases.filter((tc) => tc.status === "skipped").length;
|
|
752
|
+
const timedOut = this.testCases.filter((tc) => tc.status === "timedOut").length;
|
|
753
|
+
const interrupted = this.testCases.filter((tc) => tc.status === "interrupted").length;
|
|
754
|
+
const numberOfProjects = this.config.projects.length;
|
|
755
|
+
const totalSequentialDuration = this.testCases.reduce((sum, tc) => sum + (tc.duration || 0), 0);
|
|
756
|
+
const summary = {
|
|
757
|
+
total: this.testCases.length,
|
|
758
|
+
passed,
|
|
759
|
+
failed,
|
|
760
|
+
skipped,
|
|
761
|
+
timedOut,
|
|
762
|
+
interrupted,
|
|
763
|
+
startTime: this.startTime,
|
|
764
|
+
endTime,
|
|
765
|
+
duration: wallClockDuration,
|
|
766
|
+
totalSequentialDuration,
|
|
767
|
+
numberOfProjects,
|
|
768
|
+
workers: this.config?.workers
|
|
769
|
+
};
|
|
770
|
+
const suites = this.buildSuites();
|
|
771
|
+
if (this.reportConfig.enableHistory !== false) this.writeRunHistory(summary, endTime);
|
|
772
|
+
const reportData = { testRun: {
|
|
773
|
+
summary,
|
|
774
|
+
suites,
|
|
775
|
+
projectName: this.reportConfig.projectName,
|
|
776
|
+
testRunName: this.reportConfig.testRunName,
|
|
777
|
+
theme: this.reportConfig.theme,
|
|
778
|
+
darkMode: this.reportConfig.darkMode,
|
|
779
|
+
enableHistory: this.reportConfig.enableHistory,
|
|
780
|
+
minimalReport: this.reportConfig.minimalReport
|
|
781
|
+
} };
|
|
782
|
+
const cwd = process.cwd();
|
|
783
|
+
const normalizedCwd = cwd.endsWith("/") ? cwd.slice(0, -1) : cwd;
|
|
784
|
+
const outputDir = path.resolve(normalizedCwd, this.reportConfig.outputDir);
|
|
785
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
786
|
+
const jsonPath = path.join(outputDir, "report.json");
|
|
787
|
+
fs.writeFileSync(jsonPath, JSON.stringify(reportData, null, 2), "utf8");
|
|
788
|
+
if (this.progressMode === "dot") {
|
|
789
|
+
const failedCases = this.testCases.filter((tc) => tc.status === "failed" || tc.status === "timedOut" || tc.status === "interrupted");
|
|
790
|
+
if (failedCases.length > 0) {
|
|
791
|
+
console.log("\nFailed Tests:");
|
|
792
|
+
for (const tc of failedCases) {
|
|
793
|
+
const projStr = tc.project && tc.project !== "unknown" ? `[${tc.project}] ` : "";
|
|
794
|
+
const durationStr = tc.duration > 0 ? ` (${tc.duration}ms)` : "";
|
|
795
|
+
console.log(`❌ [FAILED] ${projStr}${tc.fileName} › ${tc.title}${durationStr}`);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
const summaryTable = generateTerminalSummaryTable(summary, this.reportConfig.projectName, this.reportConfig.testRunName);
|
|
800
|
+
console.log("\n" + summaryTable);
|
|
801
|
+
try {
|
|
802
|
+
const pm = this.reportConfig.packageManager;
|
|
803
|
+
const moduleFile = typeof __filename !== "undefined" ? __filename : (0, url.fileURLToPath)(require("url").pathToFileURL(__filename).href);
|
|
804
|
+
const moduleDir = path.dirname(moduleFile);
|
|
805
|
+
const packageRoot = moduleDir.endsWith(path.join("src", "lib")) || moduleDir.endsWith("src/lib") ? path.resolve(moduleDir, "..", "..") : path.resolve(moduleDir, "..");
|
|
806
|
+
const possibleTemplatePaths = [
|
|
807
|
+
path.join(packageRoot, "assets", "template.html"),
|
|
808
|
+
path.join(packageRoot, "dist", "index.html"),
|
|
809
|
+
path.join(packageRoot, "dist", "template.html")
|
|
810
|
+
];
|
|
811
|
+
let templateContent = null;
|
|
812
|
+
for (const p of possibleTemplatePaths) if (fs.existsSync(p)) try {
|
|
813
|
+
templateContent = fs.readFileSync(p, "utf8");
|
|
814
|
+
break;
|
|
815
|
+
} catch {}
|
|
816
|
+
if (!templateContent) {
|
|
817
|
+
const buildCmd = getViteBuildCommand(pm, packageRoot);
|
|
818
|
+
(0, child_process.execSync)(buildCmd, {
|
|
819
|
+
cwd: packageRoot,
|
|
820
|
+
stdio: "pipe",
|
|
821
|
+
env: {
|
|
822
|
+
...process.env,
|
|
823
|
+
PW_REPORTER_OUTPUT: outputDir
|
|
824
|
+
}
|
|
825
|
+
});
|
|
826
|
+
for (const p of possibleTemplatePaths) if (fs.existsSync(p)) try {
|
|
827
|
+
templateContent = fs.readFileSync(p, "utf8");
|
|
828
|
+
break;
|
|
829
|
+
} catch {}
|
|
830
|
+
}
|
|
831
|
+
if (templateContent) {
|
|
832
|
+
const dataScript = `<script id="report-data" type="application/json">${JSON.stringify(reportData).replace(/</g, "\\u003c")}<\/script>`;
|
|
833
|
+
const cleanTemplate = templateContent.replace(/<script id="report-data" type="application\/json">[\s\S]*?<\/script>/, "");
|
|
834
|
+
let finalHtml = cleanTemplate.replace("</head>", `${dataScript}\n</head>`);
|
|
835
|
+
const historyPath = path.join(outputDir, "history.json");
|
|
836
|
+
if (fs.existsSync(historyPath)) try {
|
|
837
|
+
const history = JSON.parse(fs.readFileSync(historyPath, "utf8"));
|
|
838
|
+
const historyScript = `<script id="history-data" type="application/json">${JSON.stringify(history).replace(/</g, "\\u003c")}<\/script>`;
|
|
839
|
+
finalHtml = finalHtml.replace(/<script id="history-data" type="application\/json">[\s\S]*?<\/script>/, "").replace("</head>", `${historyScript}\n</head>`);
|
|
840
|
+
} catch (e) {
|
|
841
|
+
console.error("History data injection failed:", e);
|
|
842
|
+
}
|
|
843
|
+
const destHtml = path.join(outputDir, "index.html");
|
|
844
|
+
fs.writeFileSync(destHtml, finalHtml, "utf8");
|
|
845
|
+
console.debug(`\n✓ Report generated: ${destHtml}`);
|
|
846
|
+
console.log(`\n💡 Run "${getShowReportCommand(pm)}" to view the report in web browser.\n`);
|
|
847
|
+
if (this.reportConfig.enableHistory !== false) this.refreshHistory();
|
|
848
|
+
if (this.reportConfig.singleSummaryFile) {
|
|
849
|
+
const summaryHtmlPath = path.join(outputDir, "summary.html");
|
|
850
|
+
const summaryHtml = cleanTemplate.replace("</head>", `${dataScript}\n<script>window.__ZEN_SUMMARY_ONLY__ = true;<\/script>\n</head>`);
|
|
851
|
+
fs.writeFileSync(summaryHtmlPath, summaryHtml, "utf8");
|
|
852
|
+
console.debug(`✓ Standalone Summary generated: ${summaryHtmlPath}`);
|
|
853
|
+
}
|
|
854
|
+
} else console.error("Report template not found and build failed.");
|
|
855
|
+
} catch (err) {
|
|
856
|
+
console.error("Report generation failed, keeping report.json:", err);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
};
|
|
860
|
+
function getShowReportCommand(pm, cwd = process.cwd()) {
|
|
861
|
+
switch (pm || detectPackageManager(cwd)) {
|
|
862
|
+
case "pnpm": return "pnpm zr show";
|
|
863
|
+
case "yarn": return "yarn zr show";
|
|
864
|
+
case "bun": return "bunx zr show";
|
|
865
|
+
default: return "npx zr show";
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
//#endregion
|
|
869
|
+
exports.default = ZenReporter;
|
|
870
|
+
exports.getShowReportCommand = getShowReportCommand;
|
|
871
|
+
exports.resolveConfig = resolveConfig;
|