ortoni-report 4.0.2-beta.1 → 4.0.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.
Files changed (62) hide show
  1. package/changelog.md +27 -0
  2. package/dist/{chunk-INS3E7E6.mjs → chunk-4RZ5C7KY.mjs} +402 -296
  3. package/dist/{cli/cli.js → cli.js} +405 -190
  4. package/dist/cli.mjs +208 -0
  5. package/dist/helpers/HTMLGenerator.d.ts +89 -0
  6. package/dist/helpers/HTMLGenerator.js +163 -0
  7. package/dist/helpers/databaseManager.d.ts +35 -0
  8. package/dist/helpers/databaseManager.js +268 -0
  9. package/dist/helpers/fileManager.d.ts +8 -0
  10. package/dist/helpers/fileManager.js +60 -0
  11. package/dist/helpers/markdownConverter.d.ts +1 -0
  12. package/dist/helpers/markdownConverter.js +14 -0
  13. package/dist/helpers/resultProcessor.d.ts +10 -0
  14. package/dist/helpers/resultProcessor.js +60 -0
  15. package/dist/helpers/serverManager.d.ts +6 -0
  16. package/dist/helpers/serverManager.js +15 -0
  17. package/dist/helpers/templateLoader.d.ts +15 -0
  18. package/dist/helpers/templateLoader.js +88 -0
  19. package/dist/index.html +1 -1
  20. package/dist/mergeData.d.ts +13 -0
  21. package/dist/mergeData.js +182 -0
  22. package/dist/ortoni-report.d.mts +8 -1
  23. package/dist/ortoni-report.d.ts +8 -1
  24. package/dist/ortoni-report.js +211 -96
  25. package/dist/ortoni-report.mjs +25 -22
  26. package/dist/{ortoni-report.d.cts → types/reporterConfig.d.ts} +8 -33
  27. package/dist/types/reporterConfig.js +1 -0
  28. package/dist/types/testResults.d.ts +31 -0
  29. package/dist/types/testResults.js +1 -0
  30. package/dist/utils/attachFiles.d.ts +4 -0
  31. package/dist/utils/attachFiles.js +87 -0
  32. package/dist/utils/expressServer.d.ts +1 -0
  33. package/dist/utils/expressServer.js +61 -0
  34. package/dist/utils/groupProjects.d.ts +3 -0
  35. package/dist/utils/groupProjects.js +30 -0
  36. package/dist/utils/utils.d.ts +15 -0
  37. package/dist/utils/utils.js +84 -0
  38. package/package.json +11 -4
  39. package/readme.md +60 -75
  40. package/dist/chunk-45EJSEX2.mjs +0 -632
  41. package/dist/chunk-75EAJL2U.mjs +0 -632
  42. package/dist/chunk-A6HCKATU.mjs +0 -76
  43. package/dist/chunk-FGIYOFIC.mjs +0 -632
  44. package/dist/chunk-FHKWBHU6.mjs +0 -633
  45. package/dist/chunk-GLICR3VS.mjs +0 -637
  46. package/dist/chunk-HFO6XSKC.mjs +0 -633
  47. package/dist/chunk-HOZD6YIV.mjs +0 -634
  48. package/dist/chunk-IJO2YIFE.mjs +0 -637
  49. package/dist/chunk-JEIWNUQY.mjs +0 -632
  50. package/dist/chunk-JPLAGYR7.mjs +0 -632
  51. package/dist/chunk-NM6ULN2O.mjs +0 -632
  52. package/dist/chunk-OZS6QIJS.mjs +0 -638
  53. package/dist/chunk-P57227VN.mjs +0 -633
  54. package/dist/chunk-QMTRYN5N.js +0 -635
  55. package/dist/chunk-TI33PMMQ.mjs +0 -639
  56. package/dist/chunk-Z5NBP5TS.mjs +0 -635
  57. package/dist/cli/cli.cjs +0 -678
  58. package/dist/cli/cli.d.cts +0 -1
  59. package/dist/cli/cli.mjs +0 -103
  60. package/dist/ortoni-report.cjs +0 -2134
  61. /package/dist/{cli/cli.d.mts → cli.d.mts} +0 -0
  62. /package/dist/{cli/cli.d.ts → cli.d.ts} +0 -0
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- "use strict";
3
2
  var __create = Object.create;
4
3
  var __defProp = Object.defineProperty;
5
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -23,76 +22,13 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
23
22
  mod
24
23
  ));
25
24
 
26
- // src/cli/cli.ts
25
+ // src/cli.ts
27
26
  var import_commander = require("commander");
27
+ var fs4 = __toESM(require("fs"));
28
+ var path5 = __toESM(require("path"));
28
29
 
29
- // src/utils/expressServer.ts
30
- var import_express = __toESM(require("express"));
31
- var import_path = __toESM(require("path"));
32
- var import_child_process = require("child_process");
33
- function startReportServer(reportFolder, reportFilename, port = 2004, open2) {
34
- const app = (0, import_express.default)();
35
- app.use(import_express.default.static(reportFolder));
36
- app.get("/", (_req, res) => {
37
- try {
38
- res.sendFile(import_path.default.resolve(reportFolder, reportFilename));
39
- } catch (error) {
40
- console.error("Ortoni-Report: Error sending report file:", error);
41
- res.status(500).send("Error loading report");
42
- }
43
- });
44
- try {
45
- const server = app.listen(port, () => {
46
- console.log(
47
- `Server is running at http://localhost:${port}
48
- Press Ctrl+C to stop.`
49
- );
50
- if (open2 === "always" || open2 === "on-failure") {
51
- try {
52
- openBrowser(`http://localhost:${port}`);
53
- } catch (error) {
54
- console.error("Ortoni-Report: Error opening browser:", error);
55
- }
56
- }
57
- });
58
- server.on("error", (error) => {
59
- if (error.code === "EADDRINUSE") {
60
- console.error(
61
- `Ortoni-Report: Port ${port} is already in use. Trying a different port...`
62
- );
63
- } else {
64
- console.error("Ortoni-Report: Server error:", error);
65
- }
66
- });
67
- } catch (error) {
68
- console.error("Ortoni-Report: Error starting the server:", error);
69
- }
70
- }
71
- function openBrowser(url) {
72
- const platform = process.platform;
73
- let command;
74
- try {
75
- if (platform === "win32") {
76
- command = "cmd";
77
- (0, import_child_process.spawn)(command, ["/c", "start", url]);
78
- } else if (platform === "darwin") {
79
- command = "open";
80
- (0, import_child_process.spawn)(command, [url]);
81
- } else {
82
- command = "xdg-open";
83
- (0, import_child_process.spawn)(command, [url]);
84
- }
85
- } catch (error) {
86
- console.error("Ortoni-Report: Error opening the browser:", error);
87
- }
88
- }
89
-
90
- // src/cli/cli.ts
30
+ // src/mergeData.ts
91
31
  var fs3 = __toESM(require("fs"));
92
- var path4 = __toESM(require("path"));
93
-
94
- // src/cli/mergeData.ts
95
- var fs2 = __toESM(require("fs"));
96
32
  var path3 = __toESM(require("path"));
97
33
 
98
34
  // src/helpers/databaseManager.ts
@@ -387,75 +323,6 @@ var DatabaseManager = class {
387
323
  }
388
324
  };
389
325
 
390
- // src/helpers/fileManager.ts
391
- var import_fs = __toESM(require("fs"));
392
- var import_path2 = __toESM(require("path"));
393
- var FileManager = class {
394
- constructor(folderPath) {
395
- this.folderPath = folderPath;
396
- }
397
- ensureReportDirectory() {
398
- const ortoniDataFolder = import_path2.default.join(this.folderPath, "ortoni-data");
399
- if (!import_fs.default.existsSync(this.folderPath)) {
400
- import_fs.default.mkdirSync(this.folderPath, { recursive: true });
401
- } else {
402
- if (import_fs.default.existsSync(ortoniDataFolder)) {
403
- import_fs.default.rmSync(ortoniDataFolder, { recursive: true, force: true });
404
- }
405
- }
406
- }
407
- writeReportFile(filename, data) {
408
- const templatePath = import_path2.default.join(__dirname, "..", "index.html");
409
- console.log("temp path - " + templatePath);
410
- let html = import_fs.default.readFileSync(templatePath, "utf-8");
411
- const reportJSON = JSON.stringify({
412
- data
413
- });
414
- html = html.replace("__ORTONI_TEST_REPORTDATA__", reportJSON);
415
- import_fs.default.writeFileSync(filename, html);
416
- return filename;
417
- }
418
- writeRawFile(filename, data) {
419
- const outputPath = import_path2.default.join(process.cwd(), this.folderPath, filename);
420
- import_fs.default.mkdirSync(import_path2.default.dirname(outputPath), { recursive: true });
421
- const content = typeof data === "string" ? data : JSON.stringify(data, null, 2);
422
- import_fs.default.writeFileSync(outputPath, content, "utf-8");
423
- return outputPath;
424
- }
425
- copyTraceViewerAssets(skip) {
426
- if (skip) return;
427
- const traceViewerFolder = import_path2.default.join(
428
- require.resolve("playwright-core"),
429
- "..",
430
- "lib",
431
- "vite",
432
- "traceViewer"
433
- );
434
- const traceViewerTargetFolder = import_path2.default.join(this.folderPath, "trace");
435
- const traceViewerAssetsTargetFolder = import_path2.default.join(
436
- traceViewerTargetFolder,
437
- "assets"
438
- );
439
- import_fs.default.mkdirSync(traceViewerAssetsTargetFolder, { recursive: true });
440
- for (const file of import_fs.default.readdirSync(traceViewerFolder)) {
441
- if (file.endsWith(".map") || file.includes("watch") || file.includes("assets"))
442
- continue;
443
- import_fs.default.copyFileSync(
444
- import_path2.default.join(traceViewerFolder, file),
445
- import_path2.default.join(traceViewerTargetFolder, file)
446
- );
447
- }
448
- const assetsFolder = import_path2.default.join(traceViewerFolder, "assets");
449
- for (const file of import_fs.default.readdirSync(assetsFolder)) {
450
- if (file.endsWith(".map") || file.includes("xtermModule")) continue;
451
- import_fs.default.copyFileSync(
452
- import_path2.default.join(assetsFolder, file),
453
- import_path2.default.join(traceViewerAssetsTargetFolder, file)
454
- );
455
- }
456
- }
457
- };
458
-
459
326
  // src/utils/groupProjects.ts
460
327
  function groupResults(config, results) {
461
328
  if (config.showProject) {
@@ -499,13 +366,46 @@ var HTMLGenerator = class {
499
366
  );
500
367
  return data;
501
368
  }
369
+ /**
370
+ * Return safe analytics/report data.
371
+ * If no dbManager is provided, return empty defaults and a note explaining why.
372
+ */
502
373
  async getReportData() {
503
- return {
504
- summary: await this.dbManager.getSummaryData(),
505
- trends: await this.dbManager.getTrends(),
506
- flakyTests: await this.dbManager.getFlakyTests(),
507
- slowTests: await this.dbManager.getSlowTests()
508
- };
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
+ }
509
409
  }
510
410
  async prepareReportData(filteredResults, totalDuration, results, projectSet) {
511
411
  const totalTests = filteredResults.length;
@@ -514,10 +414,10 @@ var HTMLGenerator = class {
514
414
  const failed = filteredResults.filter(
515
415
  (r) => r.status === "failed" || r.status === "timedOut"
516
416
  ).length;
517
- const successRate = ((passedTests + flakyTests) / totalTests * 100).toFixed(2);
417
+ const successRate = totalTests === 0 ? "0.00" : ((passedTests + flakyTests) / totalTests * 100).toFixed(2);
518
418
  const allTags = /* @__PURE__ */ new Set();
519
419
  results.forEach(
520
- (result) => result.testTags.forEach((tag) => allTags.add(tag))
420
+ (result) => (result.testTags || []).forEach((tag) => allTags.add(tag))
521
421
  );
522
422
  const projectResults = this.calculateProjectResults(
523
423
  filteredResults,
@@ -528,13 +428,31 @@ var HTMLGenerator = class {
528
428
  const testHistories = await Promise.all(
529
429
  results.map(async (result) => {
530
430
  const testId = `${result.filePath}:${result.projectName}:${result.title}`;
531
- const history = await this.dbManager.getTestHistory(testId);
532
- return {
533
- testId,
534
- history
535
- };
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
+ }
536
453
  })
537
454
  );
455
+ const reportData = await this.getReportData();
538
456
  return {
539
457
  summary: {
540
458
  overAllResult: {
@@ -570,7 +488,7 @@ var HTMLGenerator = class {
570
488
  showProject: this.ortoniConfig.showProject || false
571
489
  },
572
490
  analytics: {
573
- reportData: await this.getReportData()
491
+ reportData
574
492
  }
575
493
  };
576
494
  }
@@ -608,64 +526,357 @@ var HTMLGenerator = class {
608
526
  }
609
527
  };
610
528
 
611
- // src/cli/mergeData.ts
612
- async function mergerData(options) {
613
- const projectRoot = process.cwd();
614
- const folderPath = path3.resolve(projectRoot, options.dir);
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";
615
663
  console.info(`Ortoni Report: Merging shard files in folder: ${folderPath}`);
616
- const shardFiles = fs2.readdirSync(folderPath).filter((f) => f.startsWith("ortoni-shard-") && f.endsWith(".json"));
617
- if (shardFiles.length === 0) {
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) {
618
671
  console.error("Ortoni Report: \u274C No shard files found to merge.");
619
- process.exit(1);
672
+ process.exitCode = 1;
673
+ return;
620
674
  }
621
- let allResults = [];
622
- let projectSet = /* @__PURE__ */ new Set();
623
- let totalDuration = 0;
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 allResults = [];
686
+ const projectSet = /* @__PURE__ */ new Set();
687
+ let totalDurationSum = 0;
624
688
  let mergedUserConfig = null;
625
689
  let mergedUserMeta = null;
626
- for (const file of shardFiles) {
627
- const shardData = JSON.parse(
628
- fs2.readFileSync(path3.join(folderPath, file), "utf-8")
690
+ const badShards = [];
691
+ const shardCounts = {};
692
+ const shardDurMap = {};
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
+ shardData.results.forEach((r) => allResults.push(r));
706
+ shardCounts[file] = shardData.results.length;
707
+ if (Array.isArray(shardData.projectSet)) {
708
+ shardData.projectSet.forEach((p) => projectSet.add(p));
709
+ }
710
+ const rawShardDur = shardData.totalDuration;
711
+ let durToAdd = 0;
712
+ let perTestSum = 0;
713
+ if (typeof rawShardDur === "number") {
714
+ durToAdd = rawShardDur;
715
+ } else {
716
+ perTestSum = Array.isArray(shardData.results) ? shardData.results.reduce(
717
+ (acc, t) => acc + (Number(t?.duration) || 0),
718
+ 0
719
+ ) : 0;
720
+ durToAdd = perTestSum;
721
+ }
722
+ totalDurationSum += durToAdd;
723
+ shardDurMap[file] = durToAdd;
724
+ if (shardData.userConfig) {
725
+ if (!mergedUserConfig) mergedUserConfig = { ...shardData.userConfig };
726
+ else {
727
+ Object.keys(shardData.userConfig).forEach((k) => {
728
+ if (mergedUserConfig[k] === void 0 || mergedUserConfig[k] === null || mergedUserConfig[k] === "") {
729
+ mergedUserConfig[k] = shardData.userConfig[k];
730
+ } else if (shardData.userConfig[k] !== mergedUserConfig[k]) {
731
+ console.warn(
732
+ `Ortoni Report: userConfig mismatch for key "${k}" between shards. Using first value "${mergedUserConfig[k]}".`
733
+ );
734
+ }
735
+ });
736
+ }
737
+ }
738
+ if (shardData.userMeta) {
739
+ if (!mergedUserMeta) mergedUserMeta = { ...shardData.userMeta };
740
+ else {
741
+ mergedUserMeta.meta = {
742
+ ...mergedUserMeta.meta || {},
743
+ ...shardData.userMeta.meta || {}
744
+ };
745
+ }
746
+ }
747
+ } catch (err) {
748
+ console.error(`Ortoni Report: Failed to parse shard ${file}:`, err);
749
+ badShards.push(file);
750
+ continue;
751
+ }
752
+ }
753
+ if (badShards.length > 0) {
754
+ console.warn(
755
+ `Ortoni Report: Completed merge with ${badShards.length} bad shard(s) skipped:`,
756
+ badShards
629
757
  );
630
- allResults.push(...shardData.results);
631
- shardData.projectSet.forEach((p) => projectSet.add(p));
632
- totalDuration += shardData.duration;
633
- if (!mergedUserConfig && shardData.userConfig)
634
- mergedUserConfig = shardData.userConfig;
635
- if (!mergedUserMeta && shardData.userMeta)
636
- mergedUserMeta = shardData.userMeta;
637
- }
638
- const dbManager = new DatabaseManager();
639
- await dbManager.initialize(
640
- path3.join(folderPath, "ortoni-data-history.sqlite")
641
- );
642
- const runId = await dbManager.saveTestRun();
643
- if (typeof runId === "number") {
644
- await dbManager.saveTestResults(runId, allResults);
758
+ }
759
+ const totalDuration = totalDurationSum;
760
+ const saveHistoryFromOptions = typeof options.saveHistory === "boolean" ? options.saveHistory : void 0;
761
+ const saveHistoryFromShard = mergedUserConfig && typeof mergedUserConfig.saveHistory === "boolean" ? mergedUserConfig.saveHistory : void 0;
762
+ const saveHistory = saveHistoryFromOptions ?? saveHistoryFromShard ?? true;
763
+ let dbManager;
764
+ let runId;
765
+ if (saveHistory) {
766
+ try {
767
+ dbManager = new DatabaseManager();
768
+ const dbPath = path3.join(folderPath, "ortoni-data-history.sqlite");
769
+ await dbManager.initialize(dbPath);
770
+ runId = await dbManager.saveTestRun();
771
+ if (typeof runId === "number") {
772
+ await dbManager.saveTestResults(runId, allResults);
773
+ console.info(
774
+ `Ortoni Report: Saved ${allResults.length} results to DB (runId=${runId}).`
775
+ );
776
+ } else {
777
+ console.warn(
778
+ "Ortoni Report: Failed to create test run in DB; proceeding without saving results."
779
+ );
780
+ }
781
+ } catch (err) {
782
+ console.error(
783
+ "Ortoni Report: Error while saving history to DB. Proceeding without DB:",
784
+ err
785
+ );
786
+ dbManager = void 0;
787
+ runId = void 0;
788
+ }
645
789
  } else {
646
- console.error("Ortoni Report: \u274C Failed to save test run to database.");
790
+ console.info(
791
+ "Ortoni Report: Skipping history save (saveHistory=false). (Typical for CI runs)"
792
+ );
647
793
  }
648
794
  const htmlGenerator = new HTMLGenerator(
649
- { ...mergedUserConfig, meta: mergedUserMeta?.meta },
795
+ { ...mergedUserConfig || {}, meta: mergedUserMeta?.meta },
650
796
  dbManager
651
797
  );
652
798
  const finalReportData = await htmlGenerator.generateFinalReport(
799
+ // filteredResults: typically filter out skipped for display (keeps existing behavior)
653
800
  allResults.filter((r) => r.status !== "skipped"),
654
801
  totalDuration,
655
802
  allResults,
656
803
  projectSet
804
+ // pass Set<string> as original generateFinalReport expects
657
805
  );
658
806
  const fileManager = new FileManager(folderPath);
659
807
  const outputFileName = options.file || "ortoni-report.html";
660
- const outputFilenamePath = path3.join(folderPath, outputFileName);
661
808
  const outputPath = fileManager.writeReportFile(
662
- outputFilenamePath,
809
+ outputFileName,
663
810
  finalReportData
664
811
  );
665
- console.log(`\u2705 Final merged report generated at ${outputPath}`);
812
+ console.log(`\u2705 Final merged report generated at ${await outputPath}`);
813
+ console.log(`\u2705 Shards merged: ${sortedFiles.length}`);
814
+ console.log(`\u2705 Tests per shard:`, shardCounts);
815
+ console.log(`\u2705 Total tests merged ${allResults.length}`);
666
816
  }
667
817
 
668
- // src/cli/cli.ts
818
+ // src/utils/expressServer.ts
819
+ var import_express = __toESM(require("express"));
820
+ var import_path3 = __toESM(require("path"));
821
+ var import_child_process = require("child_process");
822
+ function startReportServer(reportFolder, reportFilename, port = 2004, open2) {
823
+ const app = (0, import_express.default)();
824
+ app.use(import_express.default.static(reportFolder, { index: false }));
825
+ app.get("/", (_req, res) => {
826
+ try {
827
+ res.sendFile(import_path3.default.resolve(reportFolder, reportFilename));
828
+ } catch (error) {
829
+ console.error("Ortoni Report: Error sending report file:", error);
830
+ res.status(500).send("Error loading report");
831
+ }
832
+ });
833
+ try {
834
+ const server = app.listen(port, () => {
835
+ console.log(
836
+ `Server is running at http://localhost:${port}
837
+ Press Ctrl+C to stop.`
838
+ );
839
+ if (open2 === "always" || open2 === "on-failure") {
840
+ try {
841
+ openBrowser(`http://localhost:${port}`);
842
+ } catch (error) {
843
+ console.error("Ortoni Report: Error opening browser:", error);
844
+ }
845
+ }
846
+ });
847
+ server.on("error", (error) => {
848
+ if (error.code === "EADDRINUSE") {
849
+ console.error(
850
+ `Ortoni Report: Port ${port} is already in use. Trying a different port...`
851
+ );
852
+ } else {
853
+ console.error("Ortoni Report: Server error:", error);
854
+ }
855
+ });
856
+ } catch (error) {
857
+ console.error("Ortoni Report: Error starting the server:", error);
858
+ }
859
+ }
860
+ function openBrowser(url) {
861
+ const platform = process.platform;
862
+ let command;
863
+ try {
864
+ if (platform === "win32") {
865
+ command = "cmd";
866
+ (0, import_child_process.spawn)(command, ["/c", "start", url]);
867
+ } else if (platform === "darwin") {
868
+ command = "open";
869
+ (0, import_child_process.spawn)(command, [url]);
870
+ } else {
871
+ command = "xdg-open";
872
+ (0, import_child_process.spawn)(command, [url]);
873
+ }
874
+ } catch (error) {
875
+ console.error("Ortoni Report: Error opening the browser:", error);
876
+ }
877
+ }
878
+
879
+ // src/cli.ts
669
880
  import_commander.program.version("4.0.1").description("Ortoni Report - CLI");
670
881
  import_commander.program.command("show-report").description("Open Ortoni Report").option(
671
882
  "-d, --dir <path>",
@@ -677,22 +888,26 @@ import_commander.program.command("show-report").description("Open Ortoni Report"
677
888
  "ortoni-report.html"
678
889
  ).option("-p, --port <port>", "Port to run the server", "2004").action((options) => {
679
890
  const projectRoot = process.cwd();
680
- const folderPath = path4.resolve(projectRoot, options.dir);
681
- const filePath = path4.resolve(folderPath, options.file);
891
+ const folderPath = path5.resolve(projectRoot, options.dir);
892
+ const filePath = path5.resolve(folderPath, options.file);
682
893
  const port = parseInt(options.port) || 2004;
683
- if (!fs3.existsSync(filePath)) {
894
+ if (!fs4.existsSync(filePath)) {
684
895
  console.error(
685
896
  `\u274C Error: The file "${filePath}" does not exist in "${folderPath}".`
686
897
  );
687
898
  process.exit(1);
688
899
  }
689
- startReportServer(folderPath, path4.basename(filePath), port, "always");
900
+ startReportServer(folderPath, path5.basename(filePath), port, "always");
690
901
  });
691
902
  import_commander.program.command("merge-report").description("Merge sharded reports into one final report").option(
692
903
  "-d, --dir <path>",
693
904
  "Path to the folder containing shard files",
694
905
  "ortoni-report"
695
906
  ).option("-f, --file <filename>", "Output report file", "ortoni-report.html").action(async (options) => {
696
- await mergerData(options);
907
+ await mergeAllData({
908
+ dir: options.dir,
909
+ file: options.file,
910
+ saveHistory: false
911
+ });
697
912
  });
698
913
  import_commander.program.parse(process.argv);