ortoni-report 3.0.5 → 4.0.2-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/changelog.md +30 -0
  2. package/dist/chunk-45EJSEX2.mjs +632 -0
  3. package/dist/chunk-75EAJL2U.mjs +632 -0
  4. package/dist/chunk-FGIYOFIC.mjs +632 -0
  5. package/dist/chunk-FHKWBHU6.mjs +633 -0
  6. package/dist/chunk-GLICR3VS.mjs +637 -0
  7. package/dist/chunk-HFO6XSKC.mjs +633 -0
  8. package/dist/chunk-HOZD6YIV.mjs +634 -0
  9. package/dist/chunk-IJO2YIFE.mjs +637 -0
  10. package/dist/chunk-INS3E7E6.mjs +638 -0
  11. package/dist/chunk-JEIWNUQY.mjs +632 -0
  12. package/dist/chunk-JPLAGYR7.mjs +632 -0
  13. package/dist/chunk-NM6ULN2O.mjs +632 -0
  14. package/dist/chunk-OZS6QIJS.mjs +638 -0
  15. package/dist/chunk-P57227VN.mjs +633 -0
  16. package/dist/chunk-QMTRYN5N.js +635 -0
  17. package/dist/chunk-TI33PMMQ.mjs +639 -0
  18. package/dist/chunk-Z5NBP5TS.mjs +635 -0
  19. package/dist/cli/cli.cjs +678 -0
  20. package/dist/cli/cli.d.cts +1 -0
  21. package/dist/cli/cli.js +580 -11
  22. package/dist/cli/cli.mjs +62 -5
  23. package/dist/index.html +21 -0
  24. package/dist/ortoni-report.cjs +2134 -0
  25. package/dist/ortoni-report.d.cts +111 -0
  26. package/dist/ortoni-report.d.mts +3 -12
  27. package/dist/ortoni-report.d.ts +3 -12
  28. package/dist/ortoni-report.js +182 -314
  29. package/dist/ortoni-report.mjs +64 -740
  30. package/package.json +4 -5
  31. package/readme.md +26 -33
  32. package/dist/chunk-AY2PKDHU.mjs +0 -69
  33. package/dist/chunk-OOALU4XG.mjs +0 -72
  34. package/dist/chunk-ZSIRUQUA.mjs +0 -68
  35. package/dist/style/main.css +0 -80
  36. package/dist/views/analytics.hbs +0 -103
  37. package/dist/views/head.hbs +0 -11
  38. package/dist/views/main.hbs +0 -1295
  39. package/dist/views/project.hbs +0 -238
  40. package/dist/views/sidebar.hbs +0 -244
  41. package/dist/views/summaryCard.hbs +0 -15
  42. package/dist/views/testIcons.hbs +0 -13
  43. package/dist/views/testPanel.hbs +0 -45
  44. package/dist/views/testStatus.hbs +0 -9
  45. package/dist/views/userInfo.hbs +0 -260
@@ -54,17 +54,24 @@ var FileManager = class {
54
54
  }
55
55
  }
56
56
  }
57
- writeReportFile(filename, content) {
57
+ writeReportFile(filename, data) {
58
+ const templatePath = import_path.default.join(__dirname, "..", "index.html");
59
+ console.log("temp path - " + templatePath);
60
+ let html = import_fs.default.readFileSync(templatePath, "utf-8");
61
+ const reportJSON = JSON.stringify({
62
+ data
63
+ });
64
+ html = html.replace("__ORTONI_TEST_REPORTDATA__", reportJSON);
65
+ import_fs.default.writeFileSync(filename, html);
66
+ return filename;
67
+ }
68
+ writeRawFile(filename, data) {
58
69
  const outputPath = import_path.default.join(process.cwd(), this.folderPath, filename);
59
- import_fs.default.writeFileSync(outputPath, content);
70
+ import_fs.default.mkdirSync(import_path.default.dirname(outputPath), { recursive: true });
71
+ const content = typeof data === "string" ? data : JSON.stringify(data, null, 2);
72
+ import_fs.default.writeFileSync(outputPath, content, "utf-8");
60
73
  return outputPath;
61
74
  }
62
- readCssContent() {
63
- return import_fs.default.readFileSync(
64
- import_path.default.resolve(__dirname, "style", "main.css"),
65
- "utf-8"
66
- );
67
- }
68
75
  copyTraceViewerAssets(skip) {
69
76
  if (skip) return;
70
77
  const traceViewerFolder = import_path.default.join(
@@ -99,157 +106,48 @@ var FileManager = class {
99
106
  }
100
107
  };
101
108
 
102
- // src/helpers/HTMLGenerator.ts
103
- var import_path3 = __toESM(require("path"));
104
-
105
109
  // src/utils/groupProjects.ts
106
110
  function groupResults(config, results) {
107
111
  if (config.showProject) {
108
112
  const groupedResults = results.reduce((acc, result, index) => {
109
113
  const testId = `${result.filePath}:${result.projectName}:${result.title}`;
114
+ const key = `${testId}-${result.key}-${result.retryAttemptCount}`;
110
115
  const { filePath, suite, projectName } = result;
111
116
  acc[filePath] = acc[filePath] || {};
112
117
  acc[filePath][suite] = acc[filePath][suite] || {};
113
118
  acc[filePath][suite][projectName] = acc[filePath][suite][projectName] || [];
114
- acc[filePath][suite][projectName].push({ ...result, index, testId });
119
+ acc[filePath][suite][projectName].push({ ...result, index, testId, key });
115
120
  return acc;
116
121
  }, {});
117
122
  return groupedResults;
118
123
  } else {
119
124
  const groupedResults = results.reduce((acc, result, index) => {
120
125
  const testId = `${result.filePath}:${result.projectName}:${result.title}`;
126
+ const key = `${testId}-${result.key}-${result.retryAttemptCount}`;
121
127
  const { filePath, suite } = result;
122
128
  acc[filePath] = acc[filePath] || {};
123
129
  acc[filePath][suite] = acc[filePath][suite] || [];
124
- acc[filePath][suite].push({ ...result, index, testId });
130
+ acc[filePath][suite].push({ ...result, index, testId, key });
125
131
  return acc;
126
132
  }, {});
127
133
  return groupedResults;
128
134
  }
129
135
  }
130
136
 
131
- // src/utils/utils.ts
132
- var import_path2 = __toESM(require("path"));
133
- function msToTime(duration) {
134
- const milliseconds = Math.floor(duration % 1e3);
135
- const seconds = Math.floor(duration / 1e3 % 60);
136
- const minutes = Math.floor(duration / (1e3 * 60) % 60);
137
- const hours = Math.floor(duration / (1e3 * 60 * 60) % 24);
138
- let result = "";
139
- if (hours > 0) {
140
- result += `${hours}h:`;
141
- }
142
- if (minutes > 0 || hours > 0) {
143
- result += `${minutes < 10 ? "0" + minutes : minutes}m:`;
144
- }
145
- if (seconds > 0 || minutes > 0 || hours > 0) {
146
- result += `${seconds < 10 ? "0" + seconds : seconds}s`;
147
- }
148
- if (milliseconds > 0 && !(seconds > 0 || minutes > 0 || hours > 0)) {
149
- result += `${milliseconds}ms`;
150
- } else if (milliseconds > 0) {
151
- result += `:${milliseconds < 100 ? "0" + milliseconds : milliseconds}ms`;
152
- }
153
- return result;
154
- }
155
- function normalizeFilePath(filePath) {
156
- const normalizedPath = import_path2.default.normalize(filePath);
157
- return import_path2.default.basename(normalizedPath);
158
- }
159
- function formatDate(date) {
160
- const day = String(date.getDate()).padStart(2, "0");
161
- const month = date.toLocaleString("default", { month: "short" });
162
- const year = date.getFullYear();
163
- const time = date.toLocaleTimeString();
164
- return `${day}-${month}-${year} ${time}`;
165
- }
166
- function safeStringify(obj, indent = 2) {
167
- const cache = /* @__PURE__ */ new Set();
168
- const json = JSON.stringify(
169
- obj,
170
- (key, value) => {
171
- if (typeof value === "object" && value !== null) {
172
- if (cache.has(value)) {
173
- return;
174
- }
175
- cache.add(value);
176
- }
177
- return value;
178
- },
179
- indent
180
- );
181
- cache.clear();
182
- return json;
183
- }
184
- function ensureHtmlExtension(filename) {
185
- const ext = import_path2.default.extname(filename);
186
- if (ext && ext.toLowerCase() === ".html") {
187
- return filename;
188
- }
189
- return `${filename}.html`;
190
- }
191
- function escapeHtml(unsafe) {
192
- if (typeof unsafe !== "string") {
193
- return String(unsafe);
194
- }
195
- return unsafe.replace(/[&<"']/g, function(match) {
196
- const escapeMap = {
197
- "&": "&amp;",
198
- "<": "&lt;",
199
- ">": "&gt;",
200
- '"': "&quot;",
201
- "'": "&#039;"
202
- };
203
- return escapeMap[match] || match;
204
- });
205
- }
206
- function formatDateUTC(date) {
207
- return date.toISOString();
208
- }
209
- function formatDateLocal(isoString) {
210
- const date = new Date(isoString);
211
- const options = {
212
- year: "numeric",
213
- month: "short",
214
- day: "2-digit",
215
- hour: "2-digit",
216
- minute: "2-digit",
217
- hour12: true,
218
- timeZoneName: "shortOffset"
219
- };
220
- return new Intl.DateTimeFormat(void 0, options).format(date);
221
- }
222
- function formatDateNoTimezone(isoString) {
223
- const date = new Date(isoString);
224
- return date.toLocaleString("en-US", {
225
- dateStyle: "medium",
226
- timeStyle: "short"
227
- });
228
- }
229
-
230
137
  // src/helpers/HTMLGenerator.ts
231
- var import_fs2 = __toESM(require("fs"));
232
- var import_handlebars = __toESM(require("handlebars"));
233
138
  var HTMLGenerator = class {
234
139
  constructor(ortoniConfig, dbManager) {
235
140
  this.ortoniConfig = ortoniConfig;
236
- this.registerHandlebarsHelpers();
237
- this.registerPartials();
238
141
  this.dbManager = dbManager;
239
142
  }
240
- async generateHTML(filteredResults, totalDuration, cssContent, results, projectSet) {
143
+ async generateFinalReport(filteredResults, totalDuration, results, projectSet) {
241
144
  const data = await this.prepareReportData(
242
145
  filteredResults,
243
146
  totalDuration,
244
147
  results,
245
148
  projectSet
246
149
  );
247
- const templateSource = import_fs2.default.readFileSync(
248
- import_path3.default.resolve(__dirname, "views", "main.hbs"),
249
- "utf-8"
250
- );
251
- const template = import_handlebars.default.compile(templateSource);
252
- return template({ ...data, inlineCss: cssContent });
150
+ return data;
253
151
  }
254
152
  async getReportData() {
255
153
  return {
@@ -259,18 +157,6 @@ var HTMLGenerator = class {
259
157
  slowTests: await this.dbManager.getSlowTests()
260
158
  };
261
159
  }
262
- async chartTrendData() {
263
- return {
264
- labels: (await this.getReportData()).trends.map(
265
- (t) => formatDateNoTimezone(t.run_date)
266
- ),
267
- passed: (await this.getReportData()).trends.map((t) => t.passed),
268
- failed: (await this.getReportData()).trends.map((t) => t.failed),
269
- avgDuration: (await this.getReportData()).trends.map(
270
- (t) => t.avg_duration
271
- )
272
- };
273
- }
274
160
  async prepareReportData(filteredResults, totalDuration, results, projectSet) {
275
161
  const totalTests = filteredResults.length;
276
162
  const passedTests = results.filter((r) => r.status === "passed").length;
@@ -288,8 +174,7 @@ var HTMLGenerator = class {
288
174
  results,
289
175
  projectSet
290
176
  );
291
- const utcRunDate = formatDateUTC(/* @__PURE__ */ new Date());
292
- const localRunDate = formatDateLocal(utcRunDate);
177
+ const lastRunDate = (/* @__PURE__ */ new Date()).toLocaleString();
293
178
  const testHistories = await Promise.all(
294
179
  results.map(async (result) => {
295
180
  const testId = `${result.filePath}:${result.projectName}:${result.title}`;
@@ -301,34 +186,42 @@ var HTMLGenerator = class {
301
186
  })
302
187
  );
303
188
  return {
304
- utcRunDate,
305
- localRunDate,
306
- testHistories,
307
- logo: this.ortoniConfig.logo || void 0,
308
- totalDuration,
309
- results,
310
- retryCount: results.filter((r) => r.isRetry).length,
311
- passCount: passedTests,
312
- failCount: failed,
313
- skipCount: results.filter((r) => r.status === "skipped").length,
314
- flakyCount: flakyTests,
315
- totalCount: filteredResults.length,
316
- groupedResults: groupResults(this.ortoniConfig, results),
317
- projectName: this.ortoniConfig.projectName,
318
- authorName: this.ortoniConfig.authorName,
319
- meta: this.ortoniConfig.meta,
320
- testType: this.ortoniConfig.testType,
321
- preferredTheme: this.ortoniConfig.preferredTheme,
322
- successRate,
323
- lastRunDate: formatDate(/* @__PURE__ */ new Date()),
324
- projects: projectSet,
325
- allTags: Array.from(allTags),
326
- showProject: this.ortoniConfig.showProject || false,
327
- title: this.ortoniConfig.title || "Ortoni Playwright Test Report",
328
- chartType: this.ortoniConfig.chartType || "pie",
329
- reportAnalyticsData: await this.getReportData(),
330
- chartData: await this.chartTrendData(),
331
- ...this.extractProjectStats(projectResults)
189
+ summary: {
190
+ overAllResult: {
191
+ pass: passedTests,
192
+ fail: failed,
193
+ skip: results.filter((r) => r.status === "skipped").length,
194
+ retry: results.filter((r) => r.retryAttemptCount).length,
195
+ flaky: flakyTests,
196
+ total: filteredResults.length
197
+ },
198
+ successRate,
199
+ lastRunDate,
200
+ totalDuration,
201
+ stats: this.extractProjectStats(projectResults)
202
+ },
203
+ testResult: {
204
+ tests: groupResults(this.ortoniConfig, results),
205
+ testHistories,
206
+ allTags: Array.from(allTags),
207
+ set: projectSet
208
+ },
209
+ userConfig: {
210
+ projectName: this.ortoniConfig.projectName,
211
+ authorName: this.ortoniConfig.authorName,
212
+ type: this.ortoniConfig.testType,
213
+ title: this.ortoniConfig.title
214
+ },
215
+ userMeta: {
216
+ meta: this.ortoniConfig.meta
217
+ },
218
+ preferences: {
219
+ logo: this.ortoniConfig.logo || void 0,
220
+ showProject: this.ortoniConfig.showProject || false
221
+ },
222
+ analytics: {
223
+ reportData: await this.getReportData()
224
+ }
332
225
  };
333
226
  }
334
227
  calculateProjectResults(filteredResults, results, projectSet) {
@@ -346,7 +239,7 @@ var HTMLGenerator = class {
346
239
  (r) => r.status === "failed" || r.status === "timedOut"
347
240
  ).length,
348
241
  skippedTests: allProjectTests.filter((r) => r.status === "skipped").length,
349
- retryTests: allProjectTests.filter((r) => r.isRetry).length,
242
+ retryTests: allProjectTests.filter((r) => r.retryAttemptCount).length,
350
243
  flakyTests: allProjectTests.filter((r) => r.status === "flaky").length,
351
244
  totalTests: projectTests.length
352
245
  };
@@ -363,59 +256,18 @@ var HTMLGenerator = class {
363
256
  flakyTests: projectResults.map((result) => result.flakyTests)
364
257
  };
365
258
  }
366
- registerHandlebarsHelpers() {
367
- import_handlebars.default.registerHelper("joinWithSpace", (array) => array.join(" "));
368
- import_handlebars.default.registerHelper("json", (context) => safeStringify(context));
369
- import_handlebars.default.registerHelper(
370
- "eq",
371
- (actualStatus, expectedStatus) => actualStatus === expectedStatus
372
- );
373
- import_handlebars.default.registerHelper(
374
- "includes",
375
- (actualStatus, expectedStatus) => actualStatus.includes(expectedStatus)
376
- );
377
- import_handlebars.default.registerHelper("gr", (count) => count > 0);
378
- import_handlebars.default.registerHelper("or", function(a3, b2) {
379
- return a3 || b2;
380
- });
381
- import_handlebars.default.registerHelper("concat", function(...args) {
382
- args.pop();
383
- return args.join("");
384
- });
385
- }
386
- registerPartials() {
387
- [
388
- "head",
389
- "sidebar",
390
- "testPanel",
391
- "summaryCard",
392
- "userInfo",
393
- "project",
394
- "testStatus",
395
- "testIcons",
396
- "analytics"
397
- ].forEach((partialName) => {
398
- import_handlebars.default.registerPartial(
399
- partialName,
400
- import_fs2.default.readFileSync(
401
- import_path3.default.resolve(__dirname, "views", `${partialName}.hbs`),
402
- "utf-8"
403
- )
404
- );
405
- });
406
- }
407
259
  };
408
260
 
409
261
  // src/helpers/resultProcessor .ts
410
262
  var import_ansi_to_html = __toESM(require("ansi-to-html"));
411
- var import_path5 = __toESM(require("path"));
263
+ var import_path4 = __toESM(require("path"));
412
264
 
413
265
  // src/utils/attachFiles.ts
414
- var import_path4 = __toESM(require("path"));
415
- var import_fs4 = __toESM(require("fs"));
266
+ var import_path2 = __toESM(require("path"));
267
+ var import_fs3 = __toESM(require("fs"));
416
268
 
417
269
  // src/helpers/markdownConverter.ts
418
- var import_fs3 = __toESM(require("fs"));
270
+ var import_fs2 = __toESM(require("fs"));
419
271
 
420
272
  // node_modules/marked/lib/marked.esm.js
421
273
  function M() {
@@ -1540,108 +1392,44 @@ var Ft = T.parse;
1540
1392
  var Qt = b.lex;
1541
1393
 
1542
1394
  // src/helpers/markdownConverter.ts
1543
- function convertMarkdownToHtml(markdownPath, htmlOutputPath, stepsError, resultError) {
1544
- const hasMarkdown = import_fs3.default.existsSync(markdownPath);
1545
- const markdownContent = hasMarkdown ? import_fs3.default.readFileSync(markdownPath, "utf-8") : "";
1395
+ function convertMarkdownToHtml(markdownPath, htmlOutputPath) {
1396
+ const hasMarkdown = import_fs2.default.existsSync(markdownPath);
1397
+ const markdownContent = hasMarkdown ? import_fs2.default.readFileSync(markdownPath, "utf-8") : "";
1546
1398
  const markdownHtml = hasMarkdown ? k(markdownContent) : "";
1547
- const stepsHtml = stepsError.filter((step) => step.snippet?.trim()).map(
1548
- (step) => `
1549
- <div>
1550
- <pre><code>${step.snippet}</code></pre>
1551
- ${step.location ? `<p><em>Location: ${escapeHtml(step.location)}</em></p>` : ""}
1552
- </div>`
1553
- ).join("\n");
1554
- const errorHtml = resultError.map((error) => `<pre><code>${error}</code></pre>`).join("\n");
1555
- const fullHtml = `
1556
- <!DOCTYPE html>
1557
- <html lang="en">
1558
- <head>
1559
- <meta charset="UTF-8" />
1560
- <title>Ortoni Error Report</title>
1561
- <style>
1562
- body { font-family: sans-serif; padding: 2rem; line-height: 1.6; max-width: 900px; margin: auto; }
1563
- code, pre { background: #f4f4f4; padding: 0.5rem; border-radius: 5px; display: block; overflow-x: auto; }
1564
- h1, h2, h3 { color: #444; }
1565
- hr { margin: 2em 0; }
1566
- #copyBtn {
1567
- background-color: #007acc;
1568
- color: white;
1569
- border: none;
1570
- padding: 0.5rem 1rem;
1571
- margin-bottom: 1rem;
1572
- border-radius: 5px;
1573
- cursor: pointer;
1574
- }
1575
- #copyBtn:hover {
1576
- background-color: #005fa3;
1577
- }
1578
- </style>
1579
- </head>
1580
- <body>
1581
- <button id="copyBtn">\u{1F4CB} Copy All</button>
1582
- <script>
1583
- document.getElementById("copyBtn").addEventListener("click", () => {
1584
- const content = document.getElementById("markdownContent").innerText;
1585
- navigator.clipboard.writeText(content).then(() => {
1586
- // change button text to indicate success
1587
- const button = document.getElementById("copyBtn");
1588
- button.textContent = "\u2705 Copied!";
1589
- setTimeout(() => {
1590
- button.textContent = "\u{1F4CB} Copy All"
1591
- }, 2000);
1592
- }).catch(err => {
1593
- console.error("Failed to copy text: ", err);
1594
- alert("Failed to copy text. Please try manually.");
1595
- });
1596
- });
1597
- </script>
1598
- <div id="markdownContent">
1599
- <h1>Instructions</h1>
1600
- <ul>
1601
- <li>Following Playwright test failed.</li>
1602
- <li>Explain why, be concise, respect Playwright best practices.</li>
1603
- <li>Provide a snippet of code with the fix, if possible.</li>
1604
- </ul>
1605
- <h1>Error Details</h1>
1606
- ${errorHtml || "<p>No errors found.</p>"}
1607
- ${stepsHtml || "<p>No step data available.</p>"}
1608
- ${markdownHtml || ""}
1609
- </div>
1610
- </body>
1611
- </html>
1612
- `;
1613
- import_fs3.default.writeFileSync(htmlOutputPath, fullHtml, "utf-8");
1399
+ const drawerHtml = `${markdownHtml || ""}`;
1400
+ import_fs2.default.writeFileSync(htmlOutputPath, drawerHtml.trim(), "utf-8");
1614
1401
  if (hasMarkdown) {
1615
- import_fs3.default.unlinkSync(markdownPath);
1402
+ import_fs2.default.unlinkSync(markdownPath);
1616
1403
  }
1617
1404
  }
1618
1405
 
1619
1406
  // src/utils/attachFiles.ts
1620
1407
  function attachFiles(subFolder, result, testResult, config, steps, errors) {
1621
1408
  const folderPath = config.folderPath || "ortoni-report";
1622
- const attachmentsFolder = import_path4.default.join(
1409
+ const attachmentsFolder = import_path2.default.join(
1623
1410
  folderPath,
1624
1411
  "ortoni-data",
1625
1412
  "attachments",
1626
1413
  subFolder
1627
1414
  );
1628
- if (!import_fs4.default.existsSync(attachmentsFolder)) {
1629
- import_fs4.default.mkdirSync(attachmentsFolder, { recursive: true });
1415
+ if (!import_fs3.default.existsSync(attachmentsFolder)) {
1416
+ import_fs3.default.mkdirSync(attachmentsFolder, { recursive: true });
1630
1417
  }
1631
1418
  if (!result.attachments) return;
1632
1419
  const { base64Image } = config;
1633
1420
  testResult.screenshots = [];
1421
+ testResult.videoPath = [];
1634
1422
  result.attachments.forEach((attachment) => {
1635
1423
  const { contentType, name, path: attachmentPath, body } = attachment;
1636
1424
  if (!attachmentPath && !body) return;
1637
- const fileName = attachmentPath ? import_path4.default.basename(attachmentPath) : `${name}.${getFileExtension(contentType)}`;
1638
- const relativePath = import_path4.default.join(
1425
+ const fileName = attachmentPath ? import_path2.default.basename(attachmentPath) : `${name}.${getFileExtension(contentType)}`;
1426
+ const relativePath = import_path2.default.join(
1639
1427
  "ortoni-data",
1640
1428
  "attachments",
1641
1429
  subFolder,
1642
1430
  fileName
1643
1431
  );
1644
- const fullPath = import_path4.default.join(attachmentsFolder, fileName);
1432
+ const fullPath = import_path2.default.join(attachmentsFolder, fileName);
1645
1433
  if (contentType === "image/png") {
1646
1434
  handleImage(
1647
1435
  attachmentPath,
@@ -1684,13 +1472,13 @@ function handleImage(attachmentPath, body, base64Image, fullPath, relativePath,
1684
1472
  let screenshotPath = "";
1685
1473
  if (attachmentPath) {
1686
1474
  try {
1687
- const screenshotContent = import_fs4.default.readFileSync(
1475
+ const screenshotContent = import_fs3.default.readFileSync(
1688
1476
  attachmentPath,
1689
1477
  base64Image ? "base64" : void 0
1690
1478
  );
1691
1479
  screenshotPath = base64Image ? `data:image/png;base64,${screenshotContent}` : relativePath;
1692
1480
  if (!base64Image) {
1693
- import_fs4.default.copyFileSync(attachmentPath, fullPath);
1481
+ import_fs3.default.copyFileSync(attachmentPath, fullPath);
1694
1482
  }
1695
1483
  } catch (error) {
1696
1484
  console.error(
@@ -1707,13 +1495,17 @@ function handleImage(attachmentPath, body, base64Image, fullPath, relativePath,
1707
1495
  }
1708
1496
  function handleAttachment(attachmentPath, fullPath, relativePath, resultKey, testResult, steps, errors) {
1709
1497
  if (attachmentPath) {
1710
- import_fs4.default.copyFileSync(attachmentPath, fullPath);
1711
- testResult[resultKey] = relativePath;
1498
+ import_fs3.default.copyFileSync(attachmentPath, fullPath);
1499
+ if (resultKey === "videoPath") {
1500
+ testResult[resultKey]?.push(relativePath);
1501
+ } else if (resultKey === "tracePath") {
1502
+ testResult[resultKey] = relativePath;
1503
+ }
1712
1504
  }
1713
1505
  if (resultKey === "markdownPath" && errors) {
1714
1506
  const htmlPath = fullPath.replace(/\.md$/, ".html");
1715
1507
  const htmlRelativePath = relativePath.replace(/\.md$/, ".html");
1716
- convertMarkdownToHtml(fullPath, htmlPath, steps || [], errors || []);
1508
+ convertMarkdownToHtml(fullPath, htmlPath);
1717
1509
  testResult[resultKey] = htmlRelativePath;
1718
1510
  return;
1719
1511
  }
@@ -1728,6 +1520,61 @@ function getFileExtension(contentType) {
1728
1520
  return extensions[contentType] || "unknown";
1729
1521
  }
1730
1522
 
1523
+ // src/utils/utils.ts
1524
+ var import_path3 = __toESM(require("path"));
1525
+ function normalizeFilePath(filePath) {
1526
+ const normalizedPath = import_path3.default.normalize(filePath);
1527
+ return import_path3.default.basename(normalizedPath);
1528
+ }
1529
+ function ensureHtmlExtension(filename) {
1530
+ const ext = import_path3.default.extname(filename);
1531
+ if (ext && ext.toLowerCase() === ".html") {
1532
+ return filename;
1533
+ }
1534
+ return `${filename}.html`;
1535
+ }
1536
+ function escapeHtml(unsafe) {
1537
+ if (typeof unsafe !== "string") {
1538
+ return String(unsafe);
1539
+ }
1540
+ return unsafe.replace(/[&<"']/g, function(match) {
1541
+ const escapeMap = {
1542
+ "&": "&amp;",
1543
+ "<": "&lt;",
1544
+ ">": "&gt;",
1545
+ '"': "&quot;",
1546
+ "'": "&#039;"
1547
+ };
1548
+ return escapeMap[match] || match;
1549
+ });
1550
+ }
1551
+ function formatDateLocal(dateInput) {
1552
+ const date = typeof dateInput === "string" ? new Date(dateInput) : dateInput;
1553
+ const options = {
1554
+ year: "numeric",
1555
+ month: "short",
1556
+ day: "2-digit",
1557
+ hour: "2-digit",
1558
+ minute: "2-digit",
1559
+ hour12: true,
1560
+ timeZoneName: "short"
1561
+ // or "Asia/Kolkata"
1562
+ };
1563
+ return new Intl.DateTimeFormat(void 0, options).format(date);
1564
+ }
1565
+ function extractSuites(titlePath) {
1566
+ const tagPattern = /@[\w]+/g;
1567
+ const suiteParts = titlePath.slice(3, titlePath.length - 1).map((p) => p.replace(tagPattern, "").trim());
1568
+ return {
1569
+ hierarchy: suiteParts.join(" > "),
1570
+ // full hierarchy
1571
+ topLevelSuite: suiteParts[0] ?? "",
1572
+ // first suite
1573
+ parentSuite: suiteParts[suiteParts.length - 1] ?? ""
1574
+ // last suite
1575
+ };
1576
+ }
1577
+
1731
1578
  // src/helpers/resultProcessor .ts
1732
1579
  var TestResultProcessor = class {
1733
1580
  constructor(projectRoot) {
@@ -1743,19 +1590,21 @@ var TestResultProcessor = class {
1743
1590
  const tagPattern = /@[\w]+/g;
1744
1591
  const title = test.title.replace(tagPattern, "").trim();
1745
1592
  const suite = test.titlePath()[3].replace(tagPattern, "").trim();
1593
+ const suiteAndTitle = extractSuites(test.titlePath());
1594
+ const suiteHierarchy = suiteAndTitle.hierarchy;
1746
1595
  const testResult = {
1747
- port: ortoniConfig.port || 2004,
1596
+ suiteHierarchy,
1597
+ key: test.id,
1748
1598
  annotations: test.annotations,
1749
1599
  testTags: test.tags,
1750
1600
  location: `${filePath}:${location.line}:${location.column}`,
1751
- retry: result.retry > 0 ? "retry" : "",
1752
- isRetry: result.retry,
1601
+ retryAttemptCount: result.retry,
1753
1602
  projectName,
1754
1603
  suite,
1755
1604
  title,
1756
1605
  status,
1757
1606
  flaky: test.outcome(),
1758
- duration: msToTime(result.duration),
1607
+ duration: result.duration,
1759
1608
  errors: result.errors.map(
1760
1609
  (e) => this.ansiToHtml.toHtml(escapeHtml(e.stack || e.toString()))
1761
1610
  ),
@@ -1767,7 +1616,8 @@ var TestResultProcessor = class {
1767
1616
  ),
1768
1617
  filePath,
1769
1618
  filters: projectSet,
1770
- base64Image: ortoniConfig.base64Image
1619
+ base64Image: ortoniConfig.base64Image,
1620
+ testId: `${filePath}:${projectName}:${title}`
1771
1621
  };
1772
1622
  attachFiles(
1773
1623
  test.id,
@@ -1781,7 +1631,7 @@ var TestResultProcessor = class {
1781
1631
  }
1782
1632
  processSteps(steps) {
1783
1633
  return steps.map((step) => {
1784
- const stepLocation = step.location ? `${import_path5.default.relative(this.projectRoot, step.location.file)}:${step.location.line}:${step.location.column}` : "";
1634
+ const stepLocation = step.location ? `${import_path4.default.relative(this.projectRoot, step.location.file)}:${step.location.line}:${step.location.column}` : "";
1785
1635
  return {
1786
1636
  snippet: this.ansiToHtml.toHtml(escapeHtml(step.error?.snippet || "")),
1787
1637
  title: step.title,
@@ -1793,14 +1643,14 @@ var TestResultProcessor = class {
1793
1643
 
1794
1644
  // src/utils/expressServer.ts
1795
1645
  var import_express = __toESM(require("express"));
1796
- var import_path6 = __toESM(require("path"));
1646
+ var import_path5 = __toESM(require("path"));
1797
1647
  var import_child_process = require("child_process");
1798
1648
  function startReportServer(reportFolder, reportFilename, port = 2004, open2) {
1799
1649
  const app = (0, import_express.default)();
1800
1650
  app.use(import_express.default.static(reportFolder));
1801
1651
  app.get("/", (_req, res) => {
1802
1652
  try {
1803
- res.sendFile(import_path6.default.resolve(reportFolder, reportFilename));
1653
+ res.sendFile(import_path5.default.resolve(reportFolder, reportFilename));
1804
1654
  } catch (error) {
1805
1655
  console.error("Ortoni-Report: Error sending report file:", error);
1806
1656
  res.status(500).send("Error loading report");
@@ -1907,7 +1757,7 @@ var DatabaseManager = class {
1907
1757
  run_id INTEGER,
1908
1758
  test_id TEXT,
1909
1759
  status TEXT,
1910
- duration TEXT,
1760
+ duration INTEGER, -- store duration as raw ms
1911
1761
  error_message TEXT,
1912
1762
  FOREIGN KEY (run_id) REFERENCES test_runs (id)
1913
1763
  );
@@ -1967,6 +1817,7 @@ var DatabaseManager = class {
1967
1817
  `${result.filePath}:${result.projectName}:${result.title}`,
1968
1818
  result.status,
1969
1819
  result.duration,
1820
+ // store raw ms
1970
1821
  result.errors.join("\n")
1971
1822
  ]);
1972
1823
  }
@@ -2033,7 +1884,7 @@ var DatabaseManager = class {
2033
1884
  (SELECT COUNT(*) FROM test_results) as totalTests,
2034
1885
  (SELECT COUNT(*) FROM test_results WHERE status = 'passed') as passed,
2035
1886
  (SELECT COUNT(*) FROM test_results WHERE status = 'failed') as failed,
2036
- (SELECT AVG(CAST(duration AS FLOAT)) FROM test_results) as avgDuration
1887
+ (SELECT AVG(duration) FROM test_results) as avgDuration
2037
1888
  `);
2038
1889
  const passRate = summary.totalTests ? (summary.passed / summary.totalTests * 100).toFixed(2) : 0;
2039
1890
  return {
@@ -2043,6 +1894,7 @@ var DatabaseManager = class {
2043
1894
  failed: summary.failed,
2044
1895
  passRate: parseFloat(passRate.toString()),
2045
1896
  avgDuration: Math.round(summary.avgDuration || 0)
1897
+ // raw ms avg
2046
1898
  };
2047
1899
  } catch (error) {
2048
1900
  console.error("OrtoniReport: Error getting summary data:", error);
@@ -2067,7 +1919,7 @@ var DatabaseManager = class {
2067
1919
  SELECT trun.run_date,
2068
1920
  SUM(CASE WHEN tr.status = 'passed' THEN 1 ELSE 0 END) AS passed,
2069
1921
  SUM(CASE WHEN tr.status = 'failed' THEN 1 ELSE 0 END) AS failed,
2070
- AVG(CAST(tr.duration AS FLOAT)) AS avg_duration
1922
+ AVG(tr.duration) AS avg_duration
2071
1923
  FROM test_results tr
2072
1924
  JOIN test_runs trun ON tr.run_id = trun.id
2073
1925
  GROUP BY trun.run_date
@@ -2080,6 +1932,7 @@ var DatabaseManager = class {
2080
1932
  ...row,
2081
1933
  run_date: formatDateLocal(row.run_date),
2082
1934
  avg_duration: Math.round(row.avg_duration || 0)
1935
+ // raw ms avg
2083
1936
  }));
2084
1937
  } catch (error) {
2085
1938
  console.error("OrtoniReport: Error getting trends data:", error);
@@ -2097,7 +1950,8 @@ var DatabaseManager = class {
2097
1950
  SELECT
2098
1951
  test_id,
2099
1952
  COUNT(*) AS total,
2100
- SUM(CASE WHEN status = 'flaky' THEN 1 ELSE 0 END) AS flaky
1953
+ SUM(CASE WHEN status = 'flaky' THEN 1 ELSE 0 END) AS flaky,
1954
+ AVG(duration) AS avg_duration
2101
1955
  FROM test_results
2102
1956
  GROUP BY test_id
2103
1957
  HAVING flaky > 0
@@ -2121,7 +1975,7 @@ var DatabaseManager = class {
2121
1975
  `
2122
1976
  SELECT
2123
1977
  test_id,
2124
- AVG(CAST(duration AS FLOAT)) AS avg_duration
1978
+ AVG(duration) AS avg_duration
2125
1979
  FROM test_results
2126
1980
  GROUP BY test_id
2127
1981
  ORDER BY avg_duration DESC
@@ -2132,6 +1986,7 @@ var DatabaseManager = class {
2132
1986
  return rows.map((row) => ({
2133
1987
  test_id: row.test_id,
2134
1988
  avg_duration: Math.round(row.avg_duration || 0)
1989
+ // raw ms avg
2135
1990
  }));
2136
1991
  } catch (error) {
2137
1992
  console.error("OrtoniReport: Error getting slow tests:", error);
@@ -2141,7 +1996,7 @@ var DatabaseManager = class {
2141
1996
  };
2142
1997
 
2143
1998
  // src/ortoni-report.ts
2144
- var import_path7 = __toESM(require("path"));
1999
+ var import_path6 = __toESM(require("path"));
2145
2000
  var OrtoniReport = class {
2146
2001
  constructor(ortoniConfig = {}) {
2147
2002
  this.ortoniConfig = ortoniConfig;
@@ -2172,8 +2027,9 @@ var OrtoniReport = class {
2172
2027
  this.testResultProcessor = new TestResultProcessor(config.rootDir);
2173
2028
  this.fileManager.ensureReportDirectory();
2174
2029
  await this.dbManager.initialize(
2175
- import_path7.default.join(this.folderPath, "ortoni-data-history.sqlite")
2030
+ import_path6.default.join(this.folderPath, "ortoni-data-history.sqlite")
2176
2031
  );
2032
+ this.config = config?.shard;
2177
2033
  }
2178
2034
  onStdOut(chunk, _test, _result) {
2179
2035
  if (this.reportsCount == 1 && this.showConsoleLogs) {
@@ -2206,23 +2062,35 @@ var OrtoniReport = class {
2206
2062
  this.overAllStatus = result.status;
2207
2063
  if (this.shouldGenerateReport) {
2208
2064
  const filteredResults = this.results.filter(
2209
- (r) => r.status !== "skipped" && !r.isRetry
2065
+ (r) => r.status !== "skipped"
2210
2066
  );
2211
- const totalDuration = msToTime(result.duration);
2212
- const cssContent = this.fileManager.readCssContent();
2067
+ const totalDuration = result.duration;
2068
+ if (this.config && this.config.total > 1) {
2069
+ const shard = this.config;
2070
+ const shardFile = `ortoni-shard-${shard.current}-of-${shard.total}.json`;
2071
+ const shardData = {
2072
+ status: result.status,
2073
+ duration: totalDuration,
2074
+ results: this.results,
2075
+ projectSet: Array.from(this.projectSet)
2076
+ };
2077
+ this.fileManager.writeRawFile(shardFile, shardData);
2078
+ console.log(`\u{1F4E6} OrtoniReport wrote shard file: ${shardFile}`);
2079
+ this.shouldGenerateReport = false;
2080
+ return;
2081
+ }
2213
2082
  const runId = await this.dbManager.saveTestRun();
2214
2083
  if (runId !== null) {
2215
2084
  await this.dbManager.saveTestResults(runId, this.results);
2216
- const html = await this.htmlGenerator.generateHTML(
2085
+ const finalReportData = await this.htmlGenerator.generateFinalReport(
2217
2086
  filteredResults,
2218
2087
  totalDuration,
2219
- cssContent,
2220
2088
  this.results,
2221
2089
  this.projectSet
2222
2090
  );
2223
2091
  this.outputPath = this.fileManager.writeReportFile(
2224
2092
  this.outputFilename,
2225
- html
2093
+ finalReportData
2226
2094
  );
2227
2095
  } else {
2228
2096
  console.error("OrtoniReport: Error saving test run to database");