ortoni-report 3.0.5 → 4.0.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.
@@ -54,17 +54,17 @@ var FileManager = class {
54
54
  }
55
55
  }
56
56
  }
57
- writeReportFile(filename, content) {
57
+ writeReportFile(filename, data) {
58
+ const templatePath = import_path.default.resolve(__dirname, "index.html");
59
+ let html = import_fs.default.readFileSync(templatePath, "utf-8");
60
+ const reportJSON = JSON.stringify({
61
+ data
62
+ });
63
+ html = html.replace("__ORTONI_TEST_REPORTDATA__", reportJSON);
58
64
  const outputPath = import_path.default.join(process.cwd(), this.folderPath, filename);
59
- import_fs.default.writeFileSync(outputPath, content);
65
+ import_fs.default.writeFileSync(outputPath, html);
60
66
  return outputPath;
61
67
  }
62
- readCssContent() {
63
- return import_fs.default.readFileSync(
64
- import_path.default.resolve(__dirname, "style", "main.css"),
65
- "utf-8"
66
- );
67
- }
68
68
  copyTraceViewerAssets(skip) {
69
69
  if (skip) return;
70
70
  const traceViewerFolder = import_path.default.join(
@@ -99,157 +99,48 @@ var FileManager = class {
99
99
  }
100
100
  };
101
101
 
102
- // src/helpers/HTMLGenerator.ts
103
- var import_path3 = __toESM(require("path"));
104
-
105
102
  // src/utils/groupProjects.ts
106
103
  function groupResults(config, results) {
107
104
  if (config.showProject) {
108
105
  const groupedResults = results.reduce((acc, result, index) => {
109
106
  const testId = `${result.filePath}:${result.projectName}:${result.title}`;
107
+ const key = `${testId}-${result.key}-${result.retryAttemptCount}`;
110
108
  const { filePath, suite, projectName } = result;
111
109
  acc[filePath] = acc[filePath] || {};
112
110
  acc[filePath][suite] = acc[filePath][suite] || {};
113
111
  acc[filePath][suite][projectName] = acc[filePath][suite][projectName] || [];
114
- acc[filePath][suite][projectName].push({ ...result, index, testId });
112
+ acc[filePath][suite][projectName].push({ ...result, index, testId, key });
115
113
  return acc;
116
114
  }, {});
117
115
  return groupedResults;
118
116
  } else {
119
117
  const groupedResults = results.reduce((acc, result, index) => {
120
118
  const testId = `${result.filePath}:${result.projectName}:${result.title}`;
119
+ const key = `${testId}-${result.key}-${result.retryAttemptCount}`;
121
120
  const { filePath, suite } = result;
122
121
  acc[filePath] = acc[filePath] || {};
123
122
  acc[filePath][suite] = acc[filePath][suite] || [];
124
- acc[filePath][suite].push({ ...result, index, testId });
123
+ acc[filePath][suite].push({ ...result, index, testId, key });
125
124
  return acc;
126
125
  }, {});
127
126
  return groupedResults;
128
127
  }
129
128
  }
130
129
 
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
130
  // src/helpers/HTMLGenerator.ts
231
- var import_fs2 = __toESM(require("fs"));
232
- var import_handlebars = __toESM(require("handlebars"));
233
131
  var HTMLGenerator = class {
234
132
  constructor(ortoniConfig, dbManager) {
235
133
  this.ortoniConfig = ortoniConfig;
236
- this.registerHandlebarsHelpers();
237
- this.registerPartials();
238
134
  this.dbManager = dbManager;
239
135
  }
240
- async generateHTML(filteredResults, totalDuration, cssContent, results, projectSet) {
136
+ async generateFinalReport(filteredResults, totalDuration, results, projectSet) {
241
137
  const data = await this.prepareReportData(
242
138
  filteredResults,
243
139
  totalDuration,
244
140
  results,
245
141
  projectSet
246
142
  );
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 });
143
+ return data;
253
144
  }
254
145
  async getReportData() {
255
146
  return {
@@ -259,18 +150,6 @@ var HTMLGenerator = class {
259
150
  slowTests: await this.dbManager.getSlowTests()
260
151
  };
261
152
  }
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
153
  async prepareReportData(filteredResults, totalDuration, results, projectSet) {
275
154
  const totalTests = filteredResults.length;
276
155
  const passedTests = results.filter((r) => r.status === "passed").length;
@@ -288,8 +167,7 @@ var HTMLGenerator = class {
288
167
  results,
289
168
  projectSet
290
169
  );
291
- const utcRunDate = formatDateUTC(/* @__PURE__ */ new Date());
292
- const localRunDate = formatDateLocal(utcRunDate);
170
+ const lastRunDate = (/* @__PURE__ */ new Date()).toLocaleString();
293
171
  const testHistories = await Promise.all(
294
172
  results.map(async (result) => {
295
173
  const testId = `${result.filePath}:${result.projectName}:${result.title}`;
@@ -301,34 +179,44 @@ var HTMLGenerator = class {
301
179
  })
302
180
  );
303
181
  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)
182
+ summary: {
183
+ overAllResult: {
184
+ pass: passedTests,
185
+ fail: failed,
186
+ skip: results.filter((r) => r.status === "skipped").length,
187
+ retry: results.filter((r) => r.retryAttemptCount).length,
188
+ flaky: flakyTests,
189
+ total: filteredResults.length
190
+ },
191
+ successRate,
192
+ lastRunDate,
193
+ totalDuration,
194
+ stats: this.extractProjectStats(projectResults)
195
+ },
196
+ testResult: {
197
+ tests: groupResults(this.ortoniConfig, results),
198
+ testHistories,
199
+ allTags: Array.from(allTags),
200
+ set: projectSet
201
+ },
202
+ userConfig: {
203
+ projectName: this.ortoniConfig.projectName,
204
+ authorName: this.ortoniConfig.authorName,
205
+ type: this.ortoniConfig.testType,
206
+ title: this.ortoniConfig.title
207
+ },
208
+ userMeta: {
209
+ meta: this.ortoniConfig.meta
210
+ },
211
+ preferences: {
212
+ theme: this.ortoniConfig.preferredTheme,
213
+ logo: this.ortoniConfig.logo || void 0,
214
+ showProject: this.ortoniConfig.showProject || false
215
+ },
216
+ analytics: {
217
+ reportData: await this.getReportData()
218
+ // chartTrendData: await this.chartTrendData(),
219
+ }
332
220
  };
333
221
  }
334
222
  calculateProjectResults(filteredResults, results, projectSet) {
@@ -346,7 +234,7 @@ var HTMLGenerator = class {
346
234
  (r) => r.status === "failed" || r.status === "timedOut"
347
235
  ).length,
348
236
  skippedTests: allProjectTests.filter((r) => r.status === "skipped").length,
349
- retryTests: allProjectTests.filter((r) => r.isRetry).length,
237
+ retryTests: allProjectTests.filter((r) => r.retryAttemptCount).length,
350
238
  flakyTests: allProjectTests.filter((r) => r.status === "flaky").length,
351
239
  totalTests: projectTests.length
352
240
  };
@@ -363,59 +251,18 @@ var HTMLGenerator = class {
363
251
  flakyTests: projectResults.map((result) => result.flakyTests)
364
252
  };
365
253
  }
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
254
  };
408
255
 
409
256
  // src/helpers/resultProcessor .ts
410
257
  var import_ansi_to_html = __toESM(require("ansi-to-html"));
411
- var import_path5 = __toESM(require("path"));
258
+ var import_path4 = __toESM(require("path"));
412
259
 
413
260
  // src/utils/attachFiles.ts
414
- var import_path4 = __toESM(require("path"));
415
- var import_fs4 = __toESM(require("fs"));
261
+ var import_path2 = __toESM(require("path"));
262
+ var import_fs3 = __toESM(require("fs"));
416
263
 
417
264
  // src/helpers/markdownConverter.ts
418
- var import_fs3 = __toESM(require("fs"));
265
+ var import_fs2 = __toESM(require("fs"));
419
266
 
420
267
  // node_modules/marked/lib/marked.esm.js
421
268
  function M() {
@@ -1540,108 +1387,44 @@ var Ft = T.parse;
1540
1387
  var Qt = b.lex;
1541
1388
 
1542
1389
  // 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") : "";
1390
+ function convertMarkdownToHtml(markdownPath, htmlOutputPath) {
1391
+ const hasMarkdown = import_fs2.default.existsSync(markdownPath);
1392
+ const markdownContent = hasMarkdown ? import_fs2.default.readFileSync(markdownPath, "utf-8") : "";
1546
1393
  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");
1394
+ const drawerHtml = `${markdownHtml || ""}`;
1395
+ import_fs2.default.writeFileSync(htmlOutputPath, drawerHtml.trim(), "utf-8");
1614
1396
  if (hasMarkdown) {
1615
- import_fs3.default.unlinkSync(markdownPath);
1397
+ import_fs2.default.unlinkSync(markdownPath);
1616
1398
  }
1617
1399
  }
1618
1400
 
1619
1401
  // src/utils/attachFiles.ts
1620
1402
  function attachFiles(subFolder, result, testResult, config, steps, errors) {
1621
1403
  const folderPath = config.folderPath || "ortoni-report";
1622
- const attachmentsFolder = import_path4.default.join(
1404
+ const attachmentsFolder = import_path2.default.join(
1623
1405
  folderPath,
1624
1406
  "ortoni-data",
1625
1407
  "attachments",
1626
1408
  subFolder
1627
1409
  );
1628
- if (!import_fs4.default.existsSync(attachmentsFolder)) {
1629
- import_fs4.default.mkdirSync(attachmentsFolder, { recursive: true });
1410
+ if (!import_fs3.default.existsSync(attachmentsFolder)) {
1411
+ import_fs3.default.mkdirSync(attachmentsFolder, { recursive: true });
1630
1412
  }
1631
1413
  if (!result.attachments) return;
1632
1414
  const { base64Image } = config;
1633
1415
  testResult.screenshots = [];
1416
+ testResult.videoPath = [];
1634
1417
  result.attachments.forEach((attachment) => {
1635
1418
  const { contentType, name, path: attachmentPath, body } = attachment;
1636
1419
  if (!attachmentPath && !body) return;
1637
- const fileName = attachmentPath ? import_path4.default.basename(attachmentPath) : `${name}.${getFileExtension(contentType)}`;
1638
- const relativePath = import_path4.default.join(
1420
+ const fileName = attachmentPath ? import_path2.default.basename(attachmentPath) : `${name}.${getFileExtension(contentType)}`;
1421
+ const relativePath = import_path2.default.join(
1639
1422
  "ortoni-data",
1640
1423
  "attachments",
1641
1424
  subFolder,
1642
1425
  fileName
1643
1426
  );
1644
- const fullPath = import_path4.default.join(attachmentsFolder, fileName);
1427
+ const fullPath = import_path2.default.join(attachmentsFolder, fileName);
1645
1428
  if (contentType === "image/png") {
1646
1429
  handleImage(
1647
1430
  attachmentPath,
@@ -1684,13 +1467,13 @@ function handleImage(attachmentPath, body, base64Image, fullPath, relativePath,
1684
1467
  let screenshotPath = "";
1685
1468
  if (attachmentPath) {
1686
1469
  try {
1687
- const screenshotContent = import_fs4.default.readFileSync(
1470
+ const screenshotContent = import_fs3.default.readFileSync(
1688
1471
  attachmentPath,
1689
1472
  base64Image ? "base64" : void 0
1690
1473
  );
1691
1474
  screenshotPath = base64Image ? `data:image/png;base64,${screenshotContent}` : relativePath;
1692
1475
  if (!base64Image) {
1693
- import_fs4.default.copyFileSync(attachmentPath, fullPath);
1476
+ import_fs3.default.copyFileSync(attachmentPath, fullPath);
1694
1477
  }
1695
1478
  } catch (error) {
1696
1479
  console.error(
@@ -1707,13 +1490,17 @@ function handleImage(attachmentPath, body, base64Image, fullPath, relativePath,
1707
1490
  }
1708
1491
  function handleAttachment(attachmentPath, fullPath, relativePath, resultKey, testResult, steps, errors) {
1709
1492
  if (attachmentPath) {
1710
- import_fs4.default.copyFileSync(attachmentPath, fullPath);
1711
- testResult[resultKey] = relativePath;
1493
+ import_fs3.default.copyFileSync(attachmentPath, fullPath);
1494
+ if (resultKey === "videoPath") {
1495
+ testResult[resultKey]?.push(relativePath);
1496
+ } else if (resultKey === "tracePath") {
1497
+ testResult[resultKey] = relativePath;
1498
+ }
1712
1499
  }
1713
1500
  if (resultKey === "markdownPath" && errors) {
1714
1501
  const htmlPath = fullPath.replace(/\.md$/, ".html");
1715
1502
  const htmlRelativePath = relativePath.replace(/\.md$/, ".html");
1716
- convertMarkdownToHtml(fullPath, htmlPath, steps || [], errors || []);
1503
+ convertMarkdownToHtml(fullPath, htmlPath);
1717
1504
  testResult[resultKey] = htmlRelativePath;
1718
1505
  return;
1719
1506
  }
@@ -1728,6 +1515,61 @@ function getFileExtension(contentType) {
1728
1515
  return extensions[contentType] || "unknown";
1729
1516
  }
1730
1517
 
1518
+ // src/utils/utils.ts
1519
+ var import_path3 = __toESM(require("path"));
1520
+ function normalizeFilePath(filePath) {
1521
+ const normalizedPath = import_path3.default.normalize(filePath);
1522
+ return import_path3.default.basename(normalizedPath);
1523
+ }
1524
+ function ensureHtmlExtension(filename) {
1525
+ const ext = import_path3.default.extname(filename);
1526
+ if (ext && ext.toLowerCase() === ".html") {
1527
+ return filename;
1528
+ }
1529
+ return `${filename}.html`;
1530
+ }
1531
+ function escapeHtml(unsafe) {
1532
+ if (typeof unsafe !== "string") {
1533
+ return String(unsafe);
1534
+ }
1535
+ return unsafe.replace(/[&<"']/g, function(match) {
1536
+ const escapeMap = {
1537
+ "&": "&amp;",
1538
+ "<": "&lt;",
1539
+ ">": "&gt;",
1540
+ '"': "&quot;",
1541
+ "'": "&#039;"
1542
+ };
1543
+ return escapeMap[match] || match;
1544
+ });
1545
+ }
1546
+ function formatDateLocal(dateInput) {
1547
+ const date = typeof dateInput === "string" ? new Date(dateInput) : dateInput;
1548
+ const options = {
1549
+ year: "numeric",
1550
+ month: "short",
1551
+ day: "2-digit",
1552
+ hour: "2-digit",
1553
+ minute: "2-digit",
1554
+ hour12: true,
1555
+ timeZoneName: "short"
1556
+ // or "Asia/Kolkata"
1557
+ };
1558
+ return new Intl.DateTimeFormat(void 0, options).format(date);
1559
+ }
1560
+ function extractSuites(titlePath) {
1561
+ const tagPattern = /@[\w]+/g;
1562
+ const suiteParts = titlePath.slice(3, titlePath.length - 1).map((p) => p.replace(tagPattern, "").trim());
1563
+ return {
1564
+ hierarchy: suiteParts.join(" > "),
1565
+ // full hierarchy
1566
+ topLevelSuite: suiteParts[0] ?? "",
1567
+ // first suite
1568
+ parentSuite: suiteParts[suiteParts.length - 1] ?? ""
1569
+ // last suite
1570
+ };
1571
+ }
1572
+
1731
1573
  // src/helpers/resultProcessor .ts
1732
1574
  var TestResultProcessor = class {
1733
1575
  constructor(projectRoot) {
@@ -1743,19 +1585,21 @@ var TestResultProcessor = class {
1743
1585
  const tagPattern = /@[\w]+/g;
1744
1586
  const title = test.title.replace(tagPattern, "").trim();
1745
1587
  const suite = test.titlePath()[3].replace(tagPattern, "").trim();
1588
+ const suiteAndTitle = extractSuites(test.titlePath());
1589
+ const suiteHierarchy = suiteAndTitle.hierarchy;
1746
1590
  const testResult = {
1747
- port: ortoniConfig.port || 2004,
1591
+ suiteHierarchy,
1592
+ key: test.id,
1748
1593
  annotations: test.annotations,
1749
1594
  testTags: test.tags,
1750
1595
  location: `${filePath}:${location.line}:${location.column}`,
1751
- retry: result.retry > 0 ? "retry" : "",
1752
- isRetry: result.retry,
1596
+ retryAttemptCount: result.retry,
1753
1597
  projectName,
1754
1598
  suite,
1755
1599
  title,
1756
1600
  status,
1757
1601
  flaky: test.outcome(),
1758
- duration: msToTime(result.duration),
1602
+ duration: result.duration,
1759
1603
  errors: result.errors.map(
1760
1604
  (e) => this.ansiToHtml.toHtml(escapeHtml(e.stack || e.toString()))
1761
1605
  ),
@@ -1767,7 +1611,8 @@ var TestResultProcessor = class {
1767
1611
  ),
1768
1612
  filePath,
1769
1613
  filters: projectSet,
1770
- base64Image: ortoniConfig.base64Image
1614
+ base64Image: ortoniConfig.base64Image,
1615
+ testId: `${filePath}:${projectName}:${title}`
1771
1616
  };
1772
1617
  attachFiles(
1773
1618
  test.id,
@@ -1781,7 +1626,7 @@ var TestResultProcessor = class {
1781
1626
  }
1782
1627
  processSteps(steps) {
1783
1628
  return steps.map((step) => {
1784
- const stepLocation = step.location ? `${import_path5.default.relative(this.projectRoot, step.location.file)}:${step.location.line}:${step.location.column}` : "";
1629
+ const stepLocation = step.location ? `${import_path4.default.relative(this.projectRoot, step.location.file)}:${step.location.line}:${step.location.column}` : "";
1785
1630
  return {
1786
1631
  snippet: this.ansiToHtml.toHtml(escapeHtml(step.error?.snippet || "")),
1787
1632
  title: step.title,
@@ -1793,14 +1638,14 @@ var TestResultProcessor = class {
1793
1638
 
1794
1639
  // src/utils/expressServer.ts
1795
1640
  var import_express = __toESM(require("express"));
1796
- var import_path6 = __toESM(require("path"));
1641
+ var import_path5 = __toESM(require("path"));
1797
1642
  var import_child_process = require("child_process");
1798
1643
  function startReportServer(reportFolder, reportFilename, port = 2004, open2) {
1799
1644
  const app = (0, import_express.default)();
1800
1645
  app.use(import_express.default.static(reportFolder));
1801
1646
  app.get("/", (_req, res) => {
1802
1647
  try {
1803
- res.sendFile(import_path6.default.resolve(reportFolder, reportFilename));
1648
+ res.sendFile(import_path5.default.resolve(reportFolder, reportFilename));
1804
1649
  } catch (error) {
1805
1650
  console.error("Ortoni-Report: Error sending report file:", error);
1806
1651
  res.status(500).send("Error loading report");
@@ -1907,7 +1752,7 @@ var DatabaseManager = class {
1907
1752
  run_id INTEGER,
1908
1753
  test_id TEXT,
1909
1754
  status TEXT,
1910
- duration TEXT,
1755
+ duration INTEGER, -- store duration as raw ms
1911
1756
  error_message TEXT,
1912
1757
  FOREIGN KEY (run_id) REFERENCES test_runs (id)
1913
1758
  );
@@ -1967,6 +1812,7 @@ var DatabaseManager = class {
1967
1812
  `${result.filePath}:${result.projectName}:${result.title}`,
1968
1813
  result.status,
1969
1814
  result.duration,
1815
+ // store raw ms
1970
1816
  result.errors.join("\n")
1971
1817
  ]);
1972
1818
  }
@@ -2033,7 +1879,7 @@ var DatabaseManager = class {
2033
1879
  (SELECT COUNT(*) FROM test_results) as totalTests,
2034
1880
  (SELECT COUNT(*) FROM test_results WHERE status = 'passed') as passed,
2035
1881
  (SELECT COUNT(*) FROM test_results WHERE status = 'failed') as failed,
2036
- (SELECT AVG(CAST(duration AS FLOAT)) FROM test_results) as avgDuration
1882
+ (SELECT AVG(duration) FROM test_results) as avgDuration
2037
1883
  `);
2038
1884
  const passRate = summary.totalTests ? (summary.passed / summary.totalTests * 100).toFixed(2) : 0;
2039
1885
  return {
@@ -2043,6 +1889,7 @@ var DatabaseManager = class {
2043
1889
  failed: summary.failed,
2044
1890
  passRate: parseFloat(passRate.toString()),
2045
1891
  avgDuration: Math.round(summary.avgDuration || 0)
1892
+ // raw ms avg
2046
1893
  };
2047
1894
  } catch (error) {
2048
1895
  console.error("OrtoniReport: Error getting summary data:", error);
@@ -2067,7 +1914,7 @@ var DatabaseManager = class {
2067
1914
  SELECT trun.run_date,
2068
1915
  SUM(CASE WHEN tr.status = 'passed' THEN 1 ELSE 0 END) AS passed,
2069
1916
  SUM(CASE WHEN tr.status = 'failed' THEN 1 ELSE 0 END) AS failed,
2070
- AVG(CAST(tr.duration AS FLOAT)) AS avg_duration
1917
+ AVG(tr.duration) AS avg_duration
2071
1918
  FROM test_results tr
2072
1919
  JOIN test_runs trun ON tr.run_id = trun.id
2073
1920
  GROUP BY trun.run_date
@@ -2080,6 +1927,7 @@ var DatabaseManager = class {
2080
1927
  ...row,
2081
1928
  run_date: formatDateLocal(row.run_date),
2082
1929
  avg_duration: Math.round(row.avg_duration || 0)
1930
+ // raw ms avg
2083
1931
  }));
2084
1932
  } catch (error) {
2085
1933
  console.error("OrtoniReport: Error getting trends data:", error);
@@ -2097,7 +1945,8 @@ var DatabaseManager = class {
2097
1945
  SELECT
2098
1946
  test_id,
2099
1947
  COUNT(*) AS total,
2100
- SUM(CASE WHEN status = 'flaky' THEN 1 ELSE 0 END) AS flaky
1948
+ SUM(CASE WHEN status = 'flaky' THEN 1 ELSE 0 END) AS flaky,
1949
+ AVG(duration) AS avg_duration
2101
1950
  FROM test_results
2102
1951
  GROUP BY test_id
2103
1952
  HAVING flaky > 0
@@ -2121,7 +1970,7 @@ var DatabaseManager = class {
2121
1970
  `
2122
1971
  SELECT
2123
1972
  test_id,
2124
- AVG(CAST(duration AS FLOAT)) AS avg_duration
1973
+ AVG(duration) AS avg_duration
2125
1974
  FROM test_results
2126
1975
  GROUP BY test_id
2127
1976
  ORDER BY avg_duration DESC
@@ -2132,6 +1981,7 @@ var DatabaseManager = class {
2132
1981
  return rows.map((row) => ({
2133
1982
  test_id: row.test_id,
2134
1983
  avg_duration: Math.round(row.avg_duration || 0)
1984
+ // raw ms avg
2135
1985
  }));
2136
1986
  } catch (error) {
2137
1987
  console.error("OrtoniReport: Error getting slow tests:", error);
@@ -2141,7 +1991,7 @@ var DatabaseManager = class {
2141
1991
  };
2142
1992
 
2143
1993
  // src/ortoni-report.ts
2144
- var import_path7 = __toESM(require("path"));
1994
+ var import_path6 = __toESM(require("path"));
2145
1995
  var OrtoniReport = class {
2146
1996
  constructor(ortoniConfig = {}) {
2147
1997
  this.ortoniConfig = ortoniConfig;
@@ -2172,7 +2022,7 @@ var OrtoniReport = class {
2172
2022
  this.testResultProcessor = new TestResultProcessor(config.rootDir);
2173
2023
  this.fileManager.ensureReportDirectory();
2174
2024
  await this.dbManager.initialize(
2175
- import_path7.default.join(this.folderPath, "ortoni-data-history.sqlite")
2025
+ import_path6.default.join(this.folderPath, "ortoni-data-history.sqlite")
2176
2026
  );
2177
2027
  }
2178
2028
  onStdOut(chunk, _test, _result) {
@@ -2206,23 +2056,21 @@ var OrtoniReport = class {
2206
2056
  this.overAllStatus = result.status;
2207
2057
  if (this.shouldGenerateReport) {
2208
2058
  const filteredResults = this.results.filter(
2209
- (r) => r.status !== "skipped" && !r.isRetry
2059
+ (r) => r.status !== "skipped"
2210
2060
  );
2211
- const totalDuration = msToTime(result.duration);
2212
- const cssContent = this.fileManager.readCssContent();
2061
+ const totalDuration = result.duration;
2213
2062
  const runId = await this.dbManager.saveTestRun();
2214
2063
  if (runId !== null) {
2215
2064
  await this.dbManager.saveTestResults(runId, this.results);
2216
- const html = await this.htmlGenerator.generateHTML(
2065
+ const finalReportData = await this.htmlGenerator.generateFinalReport(
2217
2066
  filteredResults,
2218
2067
  totalDuration,
2219
- cssContent,
2220
2068
  this.results,
2221
2069
  this.projectSet
2222
2070
  );
2223
2071
  this.outputPath = this.fileManager.writeReportFile(
2224
2072
  this.outputFilename,
2225
- html
2073
+ finalReportData
2226
2074
  );
2227
2075
  } else {
2228
2076
  console.error("OrtoniReport: Error saving test run to database");