ortoni-report 4.0.0 → 4.0.1-beta.3

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/dist/cli.js ADDED
@@ -0,0 +1,911 @@
1
+ #!/usr/bin/env node
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") {
10
+ for (let key of __getOwnPropNames(from))
11
+ if (!__hasOwnProp.call(to, key) && key !== except)
12
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
13
+ }
14
+ return to;
15
+ };
16
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
17
+ // If the importer is in node compatibility mode or this is not an ESM
18
+ // file that has been converted to a CommonJS file using a Babel-
19
+ // compatible transform (i.e. "__esModule" has not been set), then set
20
+ // "default" to the CommonJS "module.exports" for node compatibility.
21
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
22
+ mod
23
+ ));
24
+
25
+ // src/cli.ts
26
+ var import_commander = require("commander");
27
+ var fs4 = __toESM(require("fs"));
28
+ var path5 = __toESM(require("path"));
29
+
30
+ // src/mergeData.ts
31
+ var fs3 = __toESM(require("fs"));
32
+ var path3 = __toESM(require("path"));
33
+
34
+ // src/helpers/databaseManager.ts
35
+ var import_sqlite = require("sqlite");
36
+ var import_sqlite3 = __toESM(require("sqlite3"));
37
+
38
+ // src/utils/utils.ts
39
+ function formatDateLocal(dateInput) {
40
+ const date = typeof dateInput === "string" ? new Date(dateInput) : dateInput;
41
+ const options = {
42
+ year: "numeric",
43
+ month: "short",
44
+ day: "2-digit",
45
+ hour: "2-digit",
46
+ minute: "2-digit",
47
+ hour12: true,
48
+ timeZoneName: "short"
49
+ // or "Asia/Kolkata"
50
+ };
51
+ return new Intl.DateTimeFormat(void 0, options).format(date);
52
+ }
53
+
54
+ // src/helpers/databaseManager.ts
55
+ var DatabaseManager = class {
56
+ constructor() {
57
+ this.db = null;
58
+ }
59
+ async initialize(dbPath) {
60
+ try {
61
+ this.db = await (0, import_sqlite.open)({
62
+ filename: dbPath,
63
+ driver: import_sqlite3.default.Database
64
+ });
65
+ await this.createTables();
66
+ await this.createIndexes();
67
+ } catch (error) {
68
+ console.error("OrtoniReport: Error initializing database:", error);
69
+ }
70
+ }
71
+ async createTables() {
72
+ if (!this.db) {
73
+ console.error("OrtoniReport: Database not initialized");
74
+ return;
75
+ }
76
+ try {
77
+ await this.db.exec(`
78
+ CREATE TABLE IF NOT EXISTS test_runs (
79
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
80
+ run_date TEXT
81
+ );
82
+
83
+ CREATE TABLE IF NOT EXISTS test_results (
84
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
85
+ run_id INTEGER,
86
+ test_id TEXT,
87
+ status TEXT,
88
+ duration INTEGER, -- store duration as raw ms
89
+ error_message TEXT,
90
+ FOREIGN KEY (run_id) REFERENCES test_runs (id)
91
+ );
92
+ `);
93
+ } catch (error) {
94
+ console.error("OrtoniReport: Error creating tables:", error);
95
+ }
96
+ }
97
+ async createIndexes() {
98
+ if (!this.db) {
99
+ console.error("OrtoniReport: Database not initialized");
100
+ return;
101
+ }
102
+ try {
103
+ await this.db.exec(`
104
+ CREATE INDEX IF NOT EXISTS idx_test_id ON test_results (test_id);
105
+ CREATE INDEX IF NOT EXISTS idx_run_id ON test_results (run_id);
106
+ `);
107
+ } catch (error) {
108
+ console.error("OrtoniReport: Error creating indexes:", error);
109
+ }
110
+ }
111
+ async saveTestRun() {
112
+ if (!this.db) {
113
+ console.error("OrtoniReport: Database not initialized");
114
+ return null;
115
+ }
116
+ try {
117
+ const runDate = (/* @__PURE__ */ new Date()).toISOString();
118
+ const { lastID } = await this.db.run(
119
+ `
120
+ INSERT INTO test_runs (run_date)
121
+ VALUES (?)
122
+ `,
123
+ [runDate]
124
+ );
125
+ return lastID;
126
+ } catch (error) {
127
+ console.error("OrtoniReport: Error saving test run:", error);
128
+ return null;
129
+ }
130
+ }
131
+ async saveTestResults(runId, results) {
132
+ if (!this.db) {
133
+ console.error("OrtoniReport: Database not initialized");
134
+ return;
135
+ }
136
+ try {
137
+ await this.db.exec("BEGIN TRANSACTION;");
138
+ const stmt = await this.db.prepare(`
139
+ INSERT INTO test_results (run_id, test_id, status, duration, error_message)
140
+ VALUES (?, ?, ?, ?, ?)
141
+ `);
142
+ for (const result of results) {
143
+ await stmt.run([
144
+ runId,
145
+ `${result.filePath}:${result.projectName}:${result.title}`,
146
+ result.status,
147
+ result.duration,
148
+ // store raw ms
149
+ result.errors.join("\n")
150
+ ]);
151
+ }
152
+ await stmt.finalize();
153
+ await this.db.exec("COMMIT;");
154
+ } catch (error) {
155
+ await this.db.exec("ROLLBACK;");
156
+ console.error("OrtoniReport: Error saving test results:", error);
157
+ }
158
+ }
159
+ async getTestHistory(testId, limit = 10) {
160
+ if (!this.db) {
161
+ console.error("OrtoniReport: Database not initialized");
162
+ return [];
163
+ }
164
+ try {
165
+ const results = await this.db.all(
166
+ `
167
+ SELECT tr.status, tr.duration, tr.error_message, trun.run_date
168
+ FROM test_results tr
169
+ JOIN test_runs trun ON tr.run_id = trun.id
170
+ WHERE tr.test_id = ?
171
+ ORDER BY trun.run_date DESC
172
+ LIMIT ?
173
+ `,
174
+ [testId, limit]
175
+ );
176
+ return results.map((result) => ({
177
+ ...result,
178
+ run_date: formatDateLocal(result.run_date)
179
+ }));
180
+ } catch (error) {
181
+ console.error("OrtoniReport: Error getting test history:", error);
182
+ return [];
183
+ }
184
+ }
185
+ async close() {
186
+ if (this.db) {
187
+ try {
188
+ await this.db.close();
189
+ } catch (error) {
190
+ console.error("OrtoniReport: Error closing database:", error);
191
+ } finally {
192
+ this.db = null;
193
+ }
194
+ }
195
+ }
196
+ async getSummaryData() {
197
+ if (!this.db) {
198
+ console.error("OrtoniReport: Database not initialized");
199
+ return {
200
+ totalRuns: 0,
201
+ totalTests: 0,
202
+ passed: 0,
203
+ failed: 0,
204
+ passRate: 0,
205
+ avgDuration: 0
206
+ };
207
+ }
208
+ try {
209
+ const summary = await this.db.get(`
210
+ SELECT
211
+ (SELECT COUNT(*) FROM test_runs) as totalRuns,
212
+ (SELECT COUNT(*) FROM test_results) as totalTests,
213
+ (SELECT COUNT(*) FROM test_results WHERE status = 'passed') as passed,
214
+ (SELECT COUNT(*) FROM test_results WHERE status = 'failed') as failed,
215
+ (SELECT AVG(duration) FROM test_results) as avgDuration
216
+ `);
217
+ const passRate = summary.totalTests ? (summary.passed / summary.totalTests * 100).toFixed(2) : 0;
218
+ return {
219
+ totalRuns: summary.totalRuns,
220
+ totalTests: summary.totalTests,
221
+ passed: summary.passed,
222
+ failed: summary.failed,
223
+ passRate: parseFloat(passRate.toString()),
224
+ avgDuration: Math.round(summary.avgDuration || 0)
225
+ // raw ms avg
226
+ };
227
+ } catch (error) {
228
+ console.error("OrtoniReport: Error getting summary data:", error);
229
+ return {
230
+ totalRuns: 0,
231
+ totalTests: 0,
232
+ passed: 0,
233
+ failed: 0,
234
+ passRate: 0,
235
+ avgDuration: 0
236
+ };
237
+ }
238
+ }
239
+ async getTrends(limit = 100) {
240
+ if (!this.db) {
241
+ console.error("OrtoniReport: Database not initialized");
242
+ return [];
243
+ }
244
+ try {
245
+ const rows = await this.db.all(
246
+ `
247
+ SELECT trun.run_date,
248
+ SUM(CASE WHEN tr.status = 'passed' THEN 1 ELSE 0 END) AS passed,
249
+ SUM(CASE WHEN tr.status = 'failed' THEN 1 ELSE 0 END) AS failed,
250
+ AVG(tr.duration) AS avg_duration
251
+ FROM test_results tr
252
+ JOIN test_runs trun ON tr.run_id = trun.id
253
+ GROUP BY trun.run_date
254
+ ORDER BY trun.run_date DESC
255
+ LIMIT ?
256
+ `,
257
+ [limit]
258
+ );
259
+ return rows.reverse().map((row) => ({
260
+ ...row,
261
+ run_date: formatDateLocal(row.run_date),
262
+ avg_duration: Math.round(row.avg_duration || 0)
263
+ // raw ms avg
264
+ }));
265
+ } catch (error) {
266
+ console.error("OrtoniReport: Error getting trends data:", error);
267
+ return [];
268
+ }
269
+ }
270
+ async getFlakyTests(limit = 10) {
271
+ if (!this.db) {
272
+ console.error("OrtoniReport: Database not initialized");
273
+ return [];
274
+ }
275
+ try {
276
+ return await this.db.all(
277
+ `
278
+ SELECT
279
+ test_id,
280
+ COUNT(*) AS total,
281
+ SUM(CASE WHEN status = 'flaky' THEN 1 ELSE 0 END) AS flaky,
282
+ AVG(duration) AS avg_duration
283
+ FROM test_results
284
+ GROUP BY test_id
285
+ HAVING flaky > 0
286
+ ORDER BY flaky DESC
287
+ LIMIT ?
288
+ `,
289
+ [limit]
290
+ );
291
+ } catch (error) {
292
+ console.error("OrtoniReport: Error getting flaky tests:", error);
293
+ return [];
294
+ }
295
+ }
296
+ async getSlowTests(limit = 10) {
297
+ if (!this.db) {
298
+ console.error("OrtoniReport: Database not initialized");
299
+ return [];
300
+ }
301
+ try {
302
+ const rows = await this.db.all(
303
+ `
304
+ SELECT
305
+ test_id,
306
+ AVG(duration) AS avg_duration
307
+ FROM test_results
308
+ GROUP BY test_id
309
+ ORDER BY avg_duration DESC
310
+ LIMIT ?
311
+ `,
312
+ [limit]
313
+ );
314
+ return rows.map((row) => ({
315
+ test_id: row.test_id,
316
+ avg_duration: Math.round(row.avg_duration || 0)
317
+ // raw ms avg
318
+ }));
319
+ } catch (error) {
320
+ console.error("OrtoniReport: Error getting slow tests:", error);
321
+ return [];
322
+ }
323
+ }
324
+ };
325
+
326
+ // src/utils/groupProjects.ts
327
+ function groupResults(config, results) {
328
+ if (config.showProject) {
329
+ const groupedResults = results.reduce((acc, result, index) => {
330
+ const testId = `${result.filePath}:${result.projectName}:${result.title}`;
331
+ const key = `${testId}-${result.key}-${result.retryAttemptCount}`;
332
+ const { filePath, suite, projectName } = result;
333
+ acc[filePath] = acc[filePath] || {};
334
+ acc[filePath][suite] = acc[filePath][suite] || {};
335
+ acc[filePath][suite][projectName] = acc[filePath][suite][projectName] || [];
336
+ acc[filePath][suite][projectName].push({ ...result, index, testId, key });
337
+ return acc;
338
+ }, {});
339
+ return groupedResults;
340
+ } else {
341
+ const groupedResults = results.reduce((acc, result, index) => {
342
+ const testId = `${result.filePath}:${result.projectName}:${result.title}`;
343
+ const key = `${testId}-${result.key}-${result.retryAttemptCount}`;
344
+ const { filePath, suite } = result;
345
+ acc[filePath] = acc[filePath] || {};
346
+ acc[filePath][suite] = acc[filePath][suite] || [];
347
+ acc[filePath][suite].push({ ...result, index, testId, key });
348
+ return acc;
349
+ }, {});
350
+ return groupedResults;
351
+ }
352
+ }
353
+
354
+ // src/helpers/HTMLGenerator.ts
355
+ var HTMLGenerator = class {
356
+ constructor(ortoniConfig, dbManager) {
357
+ this.ortoniConfig = ortoniConfig;
358
+ this.dbManager = dbManager;
359
+ }
360
+ async generateFinalReport(filteredResults, totalDuration, results, projectSet) {
361
+ const data = await this.prepareReportData(
362
+ filteredResults,
363
+ totalDuration,
364
+ results,
365
+ projectSet
366
+ );
367
+ return data;
368
+ }
369
+ /**
370
+ * Return safe analytics/report data.
371
+ * If no dbManager is provided, return empty defaults and a note explaining why.
372
+ */
373
+ async getReportData() {
374
+ if (!this.dbManager) {
375
+ return {
376
+ summary: {},
377
+ trends: {},
378
+ flakyTests: [],
379
+ slowTests: [],
380
+ note: "Test history/trends are unavailable (saveHistory disabled or DB not initialized)."
381
+ };
382
+ }
383
+ try {
384
+ const [summary, trends, flakyTests, slowTests] = await Promise.all([
385
+ this.dbManager.getSummaryData ? this.dbManager.getSummaryData() : Promise.resolve({}),
386
+ this.dbManager.getTrends ? this.dbManager.getTrends() : Promise.resolve({}),
387
+ this.dbManager.getFlakyTests ? this.dbManager.getFlakyTests() : Promise.resolve([]),
388
+ this.dbManager.getSlowTests ? this.dbManager.getSlowTests() : Promise.resolve([])
389
+ ]);
390
+ return {
391
+ summary: summary ?? {},
392
+ trends: trends ?? {},
393
+ flakyTests: flakyTests ?? [],
394
+ slowTests: slowTests ?? []
395
+ };
396
+ } catch (err) {
397
+ console.warn(
398
+ "HTMLGenerator: failed to read analytics from DB, continuing without history.",
399
+ err
400
+ );
401
+ return {
402
+ summary: {},
403
+ trends: {},
404
+ flakyTests: [],
405
+ slowTests: [],
406
+ note: "Test history/trends could not be loaded due to a DB error."
407
+ };
408
+ }
409
+ }
410
+ async prepareReportData(filteredResults, totalDuration, results, projectSet) {
411
+ const totalTests = filteredResults.length;
412
+ const passedTests = results.filter((r) => r.status === "passed").length;
413
+ const flakyTests = results.filter((r) => r.status === "flaky").length;
414
+ const failed = filteredResults.filter(
415
+ (r) => r.status === "failed" || r.status === "timedOut"
416
+ ).length;
417
+ const successRate = totalTests === 0 ? "0.00" : ((passedTests + flakyTests) / totalTests * 100).toFixed(2);
418
+ const allTags = /* @__PURE__ */ new Set();
419
+ results.forEach(
420
+ (result) => (result.testTags || []).forEach((tag) => allTags.add(tag))
421
+ );
422
+ const projectResults = this.calculateProjectResults(
423
+ filteredResults,
424
+ results,
425
+ projectSet
426
+ );
427
+ const lastRunDate = (/* @__PURE__ */ new Date()).toLocaleString();
428
+ const testHistories = await Promise.all(
429
+ results.map(async (result) => {
430
+ const testId = `${result.filePath}:${result.projectName}:${result.title}`;
431
+ if (!this.dbManager || !this.dbManager.getTestHistory) {
432
+ return {
433
+ testId,
434
+ history: []
435
+ };
436
+ }
437
+ try {
438
+ const history = await this.dbManager.getTestHistory(testId);
439
+ return {
440
+ testId,
441
+ history: history ?? []
442
+ };
443
+ } catch (err) {
444
+ console.warn(
445
+ `HTMLGenerator: failed to read history for ${testId}`,
446
+ err
447
+ );
448
+ return {
449
+ testId,
450
+ history: []
451
+ };
452
+ }
453
+ })
454
+ );
455
+ const reportData = await this.getReportData();
456
+ return {
457
+ summary: {
458
+ overAllResult: {
459
+ pass: passedTests,
460
+ fail: failed,
461
+ skip: results.filter((r) => r.status === "skipped").length,
462
+ retry: results.filter((r) => r.retryAttemptCount).length,
463
+ flaky: flakyTests,
464
+ total: filteredResults.length
465
+ },
466
+ successRate,
467
+ lastRunDate,
468
+ totalDuration,
469
+ stats: this.extractProjectStats(projectResults)
470
+ },
471
+ testResult: {
472
+ tests: groupResults(this.ortoniConfig, results),
473
+ testHistories,
474
+ allTags: Array.from(allTags),
475
+ set: projectSet
476
+ },
477
+ userConfig: {
478
+ projectName: this.ortoniConfig.projectName,
479
+ authorName: this.ortoniConfig.authorName,
480
+ type: this.ortoniConfig.testType,
481
+ title: this.ortoniConfig.title
482
+ },
483
+ userMeta: {
484
+ meta: this.ortoniConfig.meta
485
+ },
486
+ preferences: {
487
+ logo: this.ortoniConfig.logo || void 0,
488
+ showProject: this.ortoniConfig.showProject || false
489
+ },
490
+ analytics: {
491
+ reportData
492
+ }
493
+ };
494
+ }
495
+ calculateProjectResults(filteredResults, results, projectSet) {
496
+ return Array.from(projectSet).map((projectName) => {
497
+ const projectTests = filteredResults.filter(
498
+ (r) => r.projectName === projectName
499
+ );
500
+ const allProjectTests = results.filter(
501
+ (r) => r.projectName === projectName
502
+ );
503
+ return {
504
+ projectName,
505
+ passedTests: projectTests.filter((r) => r.status === "passed").length,
506
+ failedTests: projectTests.filter(
507
+ (r) => r.status === "failed" || r.status === "timedOut"
508
+ ).length,
509
+ skippedTests: allProjectTests.filter((r) => r.status === "skipped").length,
510
+ retryTests: allProjectTests.filter((r) => r.retryAttemptCount).length,
511
+ flakyTests: allProjectTests.filter((r) => r.status === "flaky").length,
512
+ totalTests: projectTests.length
513
+ };
514
+ });
515
+ }
516
+ extractProjectStats(projectResults) {
517
+ return {
518
+ projectNames: projectResults.map((result) => result.projectName),
519
+ totalTests: projectResults.map((result) => result.totalTests),
520
+ passedTests: projectResults.map((result) => result.passedTests),
521
+ failedTests: projectResults.map((result) => result.failedTests),
522
+ skippedTests: projectResults.map((result) => result.skippedTests),
523
+ retryTests: projectResults.map((result) => result.retryTests),
524
+ flakyTests: projectResults.map((result) => result.flakyTests)
525
+ };
526
+ }
527
+ };
528
+
529
+ // src/helpers/fileManager.ts
530
+ var import_fs2 = __toESM(require("fs"));
531
+ var import_path2 = __toESM(require("path"));
532
+
533
+ // src/helpers/templateLoader.ts
534
+ var import_fs = __toESM(require("fs"));
535
+ var import_path = __toESM(require("path"));
536
+ var import_meta = {};
537
+ async function readBundledTemplate(pkgName = "ortoni-report") {
538
+ const packagedRel = "dist/index.html";
539
+ try {
540
+ if (typeof require === "function") {
541
+ const resolved = require.resolve(`${pkgName}/${packagedRel}`);
542
+ if (import_fs.default.existsSync(resolved)) {
543
+ return import_fs.default.readFileSync(resolved, "utf-8");
544
+ }
545
+ }
546
+ } catch {
547
+ }
548
+ try {
549
+ const moduleNS = await import("module");
550
+ if (moduleNS && typeof moduleNS.createRequire === "function") {
551
+ const createRequire = moduleNS.createRequire;
552
+ const req = createRequire(
553
+ // @ts-ignore
554
+ typeof __filename !== "undefined" ? __filename : import_meta.url
555
+ );
556
+ const resolved = req.resolve(`${pkgName}/${packagedRel}`);
557
+ if (import_fs.default.existsSync(resolved)) {
558
+ return import_fs.default.readFileSync(resolved, "utf-8");
559
+ }
560
+ }
561
+ } catch {
562
+ }
563
+ try {
564
+ const here = import_path.default.resolve(__dirname, "../dist/index.html");
565
+ if (import_fs.default.existsSync(here)) return import_fs.default.readFileSync(here, "utf-8");
566
+ } catch {
567
+ }
568
+ try {
569
+ const nm = import_path.default.join(process.cwd(), "node_modules", pkgName, packagedRel);
570
+ if (import_fs.default.existsSync(nm)) return import_fs.default.readFileSync(nm, "utf-8");
571
+ } catch {
572
+ }
573
+ try {
574
+ const alt = import_path.default.join(process.cwd(), "dist", "index.html");
575
+ if (import_fs.default.existsSync(alt)) return import_fs.default.readFileSync(alt, "utf-8");
576
+ } catch {
577
+ }
578
+ throw new Error(
579
+ `ortoni-report template not found (tried:
580
+ - require.resolve('${pkgName}/${packagedRel}')
581
+ - import('module').createRequire(...).resolve('${pkgName}/${packagedRel}')
582
+ - relative ../dist/index.html
583
+ - ${import_path.default.join(
584
+ process.cwd(),
585
+ "node_modules",
586
+ pkgName,
587
+ packagedRel
588
+ )}
589
+ - ${import_path.default.join(process.cwd(), "dist", "index.html")}
590
+ Ensure 'dist/index.html' is present in the published package and package.json 'files' includes 'dist/'.`
591
+ );
592
+ }
593
+
594
+ // src/helpers/fileManager.ts
595
+ var FileManager = class {
596
+ constructor(folderPath) {
597
+ this.folderPath = folderPath;
598
+ }
599
+ ensureReportDirectory() {
600
+ const ortoniDataFolder = import_path2.default.join(this.folderPath, "ortoni-data");
601
+ if (!import_fs2.default.existsSync(this.folderPath)) {
602
+ import_fs2.default.mkdirSync(this.folderPath, { recursive: true });
603
+ } else {
604
+ if (import_fs2.default.existsSync(ortoniDataFolder)) {
605
+ import_fs2.default.rmSync(ortoniDataFolder, { recursive: true, force: true });
606
+ }
607
+ }
608
+ }
609
+ async writeReportFile(filename, data) {
610
+ let html = await readBundledTemplate();
611
+ const reportJSON = JSON.stringify({
612
+ data
613
+ });
614
+ html = html.replace("__ORTONI_TEST_REPORTDATA__", reportJSON);
615
+ const outputPath = import_path2.default.join(process.cwd(), this.folderPath, filename);
616
+ import_fs2.default.writeFileSync(outputPath, html);
617
+ return outputPath;
618
+ }
619
+ writeRawFile(filename, data) {
620
+ const outputPath = import_path2.default.join(process.cwd(), this.folderPath, filename);
621
+ import_fs2.default.mkdirSync(import_path2.default.dirname(outputPath), { recursive: true });
622
+ const content = typeof data === "string" ? data : JSON.stringify(data, null, 2);
623
+ import_fs2.default.writeFileSync(outputPath, content, "utf-8");
624
+ return outputPath;
625
+ }
626
+ copyTraceViewerAssets(skip) {
627
+ if (skip) return;
628
+ const traceViewerFolder = import_path2.default.join(
629
+ require.resolve("playwright-core"),
630
+ "..",
631
+ "lib",
632
+ "vite",
633
+ "traceViewer"
634
+ );
635
+ const traceViewerTargetFolder = import_path2.default.join(this.folderPath, "trace");
636
+ const traceViewerAssetsTargetFolder = import_path2.default.join(
637
+ traceViewerTargetFolder,
638
+ "assets"
639
+ );
640
+ import_fs2.default.mkdirSync(traceViewerAssetsTargetFolder, { recursive: true });
641
+ for (const file of import_fs2.default.readdirSync(traceViewerFolder)) {
642
+ if (file.endsWith(".map") || file.includes("watch") || file.includes("assets"))
643
+ continue;
644
+ import_fs2.default.copyFileSync(
645
+ import_path2.default.join(traceViewerFolder, file),
646
+ import_path2.default.join(traceViewerTargetFolder, file)
647
+ );
648
+ }
649
+ const assetsFolder = import_path2.default.join(traceViewerFolder, "assets");
650
+ for (const file of import_fs2.default.readdirSync(assetsFolder)) {
651
+ if (file.endsWith(".map") || file.includes("xtermModule")) continue;
652
+ import_fs2.default.copyFileSync(
653
+ import_path2.default.join(assetsFolder, file),
654
+ import_path2.default.join(traceViewerAssetsTargetFolder, file)
655
+ );
656
+ }
657
+ }
658
+ };
659
+
660
+ // src/mergeData.ts
661
+ async function mergeAllData(options = {}) {
662
+ const folderPath = options.dir || "ortoni-report";
663
+ console.info(`Ortoni Report: Merging shard files in folder: ${folderPath}`);
664
+ if (!fs3.existsSync(folderPath)) {
665
+ console.error(`Ortoni Report: folder "${folderPath}" does not exist.`);
666
+ process.exitCode = 1;
667
+ return;
668
+ }
669
+ const filenames = fs3.readdirSync(folderPath).filter((f) => f.startsWith("ortoni-shard-") && f.endsWith(".json"));
670
+ if (filenames.length === 0) {
671
+ console.error("Ortoni Report: \u274C No shard files found to merge.");
672
+ process.exitCode = 1;
673
+ return;
674
+ }
675
+ const shardFileIndex = (name) => {
676
+ const m = name.match(/ortoni-shard-(\d+)-of-(\d+)\.json$/);
677
+ return m ? parseInt(m[1], 10) : null;
678
+ };
679
+ const sortedFiles = filenames.map((f) => ({ f, idx: shardFileIndex(f) })).sort((a, b) => {
680
+ if (a.idx === null && b.idx === null) return a.f.localeCompare(b.f);
681
+ if (a.idx === null) return 1;
682
+ if (b.idx === null) return -1;
683
+ return a.idx - b.idx;
684
+ }).map((x) => x.f);
685
+ const dedupeByTestId = true;
686
+ const resultsById = /* @__PURE__ */ new Map();
687
+ const projectSet = /* @__PURE__ */ new Set();
688
+ let totalDurationSum = 0;
689
+ let totalDurationMax = 0;
690
+ let mergedUserConfig = null;
691
+ let mergedUserMeta = null;
692
+ const badShards = [];
693
+ for (const file of sortedFiles) {
694
+ const fullPath = path3.join(folderPath, file);
695
+ try {
696
+ const shardRaw = fs3.readFileSync(fullPath, "utf-8");
697
+ const shardData = JSON.parse(shardRaw);
698
+ if (!Array.isArray(shardData.results)) {
699
+ console.warn(
700
+ `Ortoni Report: Shard ${file} missing results array \u2014 skipping.`
701
+ );
702
+ badShards.push(file);
703
+ continue;
704
+ }
705
+ for (const r of shardData.results) {
706
+ const id = r.key;
707
+ if (dedupeByTestId) {
708
+ if (!resultsById.has(id)) {
709
+ resultsById.set(id, r);
710
+ } else {
711
+ console.info(
712
+ `Ortoni Report: Duplicate test ${id} found in ${file} \u2014 keeping first occurrence.`
713
+ );
714
+ }
715
+ } else {
716
+ resultsById.set(`${id}::${Math.random().toString(36).slice(2)}`, r);
717
+ }
718
+ }
719
+ if (Array.isArray(shardData.projectSet)) {
720
+ for (const p of shardData.projectSet) projectSet.add(p);
721
+ }
722
+ const dur = Number(shardData.duration) || 0;
723
+ totalDurationSum += dur;
724
+ if (dur > totalDurationMax) totalDurationMax = dur;
725
+ if (shardData.userConfig) {
726
+ if (!mergedUserConfig) mergedUserConfig = { ...shardData.userConfig };
727
+ else {
728
+ Object.keys(shardData.userConfig).forEach((k) => {
729
+ if (mergedUserConfig[k] === void 0 || mergedUserConfig[k] === null || mergedUserConfig[k] === "") {
730
+ mergedUserConfig[k] = shardData.userConfig[k];
731
+ } else if (shardData.userConfig[k] !== mergedUserConfig[k]) {
732
+ console.warn(
733
+ `Ortoni Report: userConfig mismatch for key "${k}" between shards. Using first value "${mergedUserConfig[k]}".`
734
+ );
735
+ }
736
+ });
737
+ }
738
+ }
739
+ if (shardData.userMeta) {
740
+ if (!mergedUserMeta) mergedUserMeta = { ...shardData.userMeta };
741
+ else {
742
+ mergedUserMeta.meta = {
743
+ ...mergedUserMeta.meta || {},
744
+ ...shardData.userMeta.meta || {}
745
+ };
746
+ }
747
+ }
748
+ } catch (err) {
749
+ console.error(`Ortoni Report: Failed to parse shard ${file}:`, err);
750
+ badShards.push(file);
751
+ continue;
752
+ }
753
+ }
754
+ if (badShards.length > 0) {
755
+ console.warn(
756
+ `Ortoni Report: Completed merge with ${badShards.length} bad shard(s) skipped:`,
757
+ badShards
758
+ );
759
+ }
760
+ const allResults = Array.from(resultsById.values());
761
+ const totalDuration = totalDurationSum;
762
+ const saveHistoryFromOptions = typeof options.saveHistory === "boolean" ? options.saveHistory : void 0;
763
+ const saveHistoryFromShard = mergedUserConfig && typeof mergedUserConfig.saveHistory === "boolean" ? mergedUserConfig.saveHistory : void 0;
764
+ const saveHistory = saveHistoryFromOptions ?? saveHistoryFromShard ?? true;
765
+ let dbManager;
766
+ let runId;
767
+ if (saveHistory) {
768
+ try {
769
+ dbManager = new DatabaseManager();
770
+ const dbPath = path3.join(folderPath, "ortoni-data-history.sqlite");
771
+ await dbManager.initialize(dbPath);
772
+ runId = await dbManager.saveTestRun();
773
+ if (typeof runId === "number") {
774
+ await dbManager.saveTestResults(runId, allResults);
775
+ console.info(
776
+ `Ortoni Report: Saved ${allResults.length} results to DB (runId=${runId}).`
777
+ );
778
+ } else {
779
+ console.warn(
780
+ "Ortoni Report: Failed to create test run in DB; proceeding without saving results."
781
+ );
782
+ }
783
+ } catch (err) {
784
+ console.error(
785
+ "Ortoni Report: Error while saving history to DB. Proceeding without DB:",
786
+ err
787
+ );
788
+ dbManager = void 0;
789
+ runId = void 0;
790
+ }
791
+ } else {
792
+ console.info(
793
+ "Ortoni Report: Skipping history save (saveHistory=false). (Typical for CI runs)"
794
+ );
795
+ }
796
+ const htmlGenerator = new HTMLGenerator(
797
+ { ...mergedUserConfig || {}, meta: mergedUserMeta?.meta },
798
+ dbManager
799
+ );
800
+ const finalReportData = await htmlGenerator.generateFinalReport(
801
+ allResults.filter((r) => r.status !== "skipped"),
802
+ totalDuration,
803
+ allResults,
804
+ projectSet
805
+ );
806
+ const fileManager = new FileManager(folderPath);
807
+ const outputFileName = options.file || "ortoni-report.html";
808
+ const outputPath = await fileManager.writeReportFile(
809
+ outputFileName,
810
+ finalReportData
811
+ );
812
+ console.log(`\u2705 Final merged report generated at ${outputPath}`);
813
+ if (runId) console.log(`\u2705 DB runId: ${runId}`);
814
+ }
815
+
816
+ // src/utils/expressServer.ts
817
+ var import_express = __toESM(require("express"));
818
+ var import_path3 = __toESM(require("path"));
819
+ var import_child_process = require("child_process");
820
+ function startReportServer(reportFolder, reportFilename, port = 2004, open2) {
821
+ const app = (0, import_express.default)();
822
+ app.use(import_express.default.static(reportFolder, { index: false }));
823
+ app.get("/", (_req, res) => {
824
+ try {
825
+ res.sendFile(import_path3.default.resolve(reportFolder, reportFilename));
826
+ } catch (error) {
827
+ console.error("Ortoni Report: Error sending report file:", error);
828
+ res.status(500).send("Error loading report");
829
+ }
830
+ });
831
+ try {
832
+ const server = app.listen(port, () => {
833
+ console.log(
834
+ `Server is running at http://localhost:${port}
835
+ Press Ctrl+C to stop.`
836
+ );
837
+ if (open2 === "always" || open2 === "on-failure") {
838
+ try {
839
+ openBrowser(`http://localhost:${port}`);
840
+ } catch (error) {
841
+ console.error("Ortoni Report: Error opening browser:", error);
842
+ }
843
+ }
844
+ });
845
+ server.on("error", (error) => {
846
+ if (error.code === "EADDRINUSE") {
847
+ console.error(
848
+ `Ortoni Report: Port ${port} is already in use. Trying a different port...`
849
+ );
850
+ } else {
851
+ console.error("Ortoni Report: Server error:", error);
852
+ }
853
+ });
854
+ } catch (error) {
855
+ console.error("Ortoni Report: Error starting the server:", error);
856
+ }
857
+ }
858
+ function openBrowser(url) {
859
+ const platform = process.platform;
860
+ let command;
861
+ try {
862
+ if (platform === "win32") {
863
+ command = "cmd";
864
+ (0, import_child_process.spawn)(command, ["/c", "start", url]);
865
+ } else if (platform === "darwin") {
866
+ command = "open";
867
+ (0, import_child_process.spawn)(command, [url]);
868
+ } else {
869
+ command = "xdg-open";
870
+ (0, import_child_process.spawn)(command, [url]);
871
+ }
872
+ } catch (error) {
873
+ console.error("Ortoni Report: Error opening the browser:", error);
874
+ }
875
+ }
876
+
877
+ // src/cli.ts
878
+ import_commander.program.version("4.0.1").description("Ortoni Report - CLI");
879
+ import_commander.program.command("show-report").description("Open Ortoni Report").option(
880
+ "-d, --dir <path>",
881
+ "Path to the folder containing the report",
882
+ "ortoni-report"
883
+ ).option(
884
+ "-f, --file <filename>",
885
+ "Name of the report file",
886
+ "ortoni-report.html"
887
+ ).option("-p, --port <port>", "Port to run the server", "2004").action((options) => {
888
+ const projectRoot = process.cwd();
889
+ const folderPath = path5.resolve(projectRoot, options.dir);
890
+ const filePath = path5.resolve(folderPath, options.file);
891
+ const port = parseInt(options.port) || 2004;
892
+ if (!fs4.existsSync(filePath)) {
893
+ console.error(
894
+ `\u274C Error: The file "${filePath}" does not exist in "${folderPath}".`
895
+ );
896
+ process.exit(1);
897
+ }
898
+ startReportServer(folderPath, path5.basename(filePath), port, "always");
899
+ });
900
+ import_commander.program.command("merge-report").description("Merge sharded reports into one final report").option(
901
+ "-d, --dir <path>",
902
+ "Path to the folder containing shard files",
903
+ "ortoni-report"
904
+ ).option("-f, --file <filename>", "Output report file", "ortoni-report.html").action(async (options) => {
905
+ await mergeAllData({
906
+ dir: options.dir,
907
+ file: options.file,
908
+ saveHistory: false
909
+ });
910
+ });
911
+ import_commander.program.parse(process.argv);