@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.
@@ -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;