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.
@@ -21,17 +21,17 @@ var FileManager = class {
21
21
  }
22
22
  }
23
23
  }
24
- writeReportFile(filename, content) {
24
+ writeReportFile(filename, data) {
25
+ const templatePath = path.resolve(__dirname, "index.html");
26
+ let html = fs.readFileSync(templatePath, "utf-8");
27
+ const reportJSON = JSON.stringify({
28
+ data
29
+ });
30
+ html = html.replace("__ORTONI_TEST_REPORTDATA__", reportJSON);
25
31
  const outputPath = path.join(process.cwd(), this.folderPath, filename);
26
- fs.writeFileSync(outputPath, content);
32
+ fs.writeFileSync(outputPath, html);
27
33
  return outputPath;
28
34
  }
29
- readCssContent() {
30
- return fs.readFileSync(
31
- path.resolve(__dirname, "style", "main.css"),
32
- "utf-8"
33
- );
34
- }
35
35
  copyTraceViewerAssets(skip) {
36
36
  if (skip) return;
37
37
  const traceViewerFolder = path.join(
@@ -66,157 +66,48 @@ var FileManager = class {
66
66
  }
67
67
  };
68
68
 
69
- // src/helpers/HTMLGenerator.ts
70
- import path3 from "path";
71
-
72
69
  // src/utils/groupProjects.ts
73
70
  function groupResults(config, results) {
74
71
  if (config.showProject) {
75
72
  const groupedResults = results.reduce((acc, result, index) => {
76
73
  const testId = `${result.filePath}:${result.projectName}:${result.title}`;
74
+ const key = `${testId}-${result.key}-${result.retryAttemptCount}`;
77
75
  const { filePath, suite, projectName } = result;
78
76
  acc[filePath] = acc[filePath] || {};
79
77
  acc[filePath][suite] = acc[filePath][suite] || {};
80
78
  acc[filePath][suite][projectName] = acc[filePath][suite][projectName] || [];
81
- acc[filePath][suite][projectName].push({ ...result, index, testId });
79
+ acc[filePath][suite][projectName].push({ ...result, index, testId, key });
82
80
  return acc;
83
81
  }, {});
84
82
  return groupedResults;
85
83
  } else {
86
84
  const groupedResults = results.reduce((acc, result, index) => {
87
85
  const testId = `${result.filePath}:${result.projectName}:${result.title}`;
86
+ const key = `${testId}-${result.key}-${result.retryAttemptCount}`;
88
87
  const { filePath, suite } = result;
89
88
  acc[filePath] = acc[filePath] || {};
90
89
  acc[filePath][suite] = acc[filePath][suite] || [];
91
- acc[filePath][suite].push({ ...result, index, testId });
90
+ acc[filePath][suite].push({ ...result, index, testId, key });
92
91
  return acc;
93
92
  }, {});
94
93
  return groupedResults;
95
94
  }
96
95
  }
97
96
 
98
- // src/utils/utils.ts
99
- import path2 from "path";
100
- function msToTime(duration) {
101
- const milliseconds = Math.floor(duration % 1e3);
102
- const seconds = Math.floor(duration / 1e3 % 60);
103
- const minutes = Math.floor(duration / (1e3 * 60) % 60);
104
- const hours = Math.floor(duration / (1e3 * 60 * 60) % 24);
105
- let result = "";
106
- if (hours > 0) {
107
- result += `${hours}h:`;
108
- }
109
- if (minutes > 0 || hours > 0) {
110
- result += `${minutes < 10 ? "0" + minutes : minutes}m:`;
111
- }
112
- if (seconds > 0 || minutes > 0 || hours > 0) {
113
- result += `${seconds < 10 ? "0" + seconds : seconds}s`;
114
- }
115
- if (milliseconds > 0 && !(seconds > 0 || minutes > 0 || hours > 0)) {
116
- result += `${milliseconds}ms`;
117
- } else if (milliseconds > 0) {
118
- result += `:${milliseconds < 100 ? "0" + milliseconds : milliseconds}ms`;
119
- }
120
- return result;
121
- }
122
- function normalizeFilePath(filePath) {
123
- const normalizedPath = path2.normalize(filePath);
124
- return path2.basename(normalizedPath);
125
- }
126
- function formatDate(date) {
127
- const day = String(date.getDate()).padStart(2, "0");
128
- const month = date.toLocaleString("default", { month: "short" });
129
- const year = date.getFullYear();
130
- const time = date.toLocaleTimeString();
131
- return `${day}-${month}-${year} ${time}`;
132
- }
133
- function safeStringify(obj, indent = 2) {
134
- const cache = /* @__PURE__ */ new Set();
135
- const json = JSON.stringify(
136
- obj,
137
- (key, value) => {
138
- if (typeof value === "object" && value !== null) {
139
- if (cache.has(value)) {
140
- return;
141
- }
142
- cache.add(value);
143
- }
144
- return value;
145
- },
146
- indent
147
- );
148
- cache.clear();
149
- return json;
150
- }
151
- function ensureHtmlExtension(filename) {
152
- const ext = path2.extname(filename);
153
- if (ext && ext.toLowerCase() === ".html") {
154
- return filename;
155
- }
156
- return `${filename}.html`;
157
- }
158
- function escapeHtml(unsafe) {
159
- if (typeof unsafe !== "string") {
160
- return String(unsafe);
161
- }
162
- return unsafe.replace(/[&<"']/g, function(match) {
163
- const escapeMap = {
164
- "&": "&amp;",
165
- "<": "&lt;",
166
- ">": "&gt;",
167
- '"': "&quot;",
168
- "'": "&#039;"
169
- };
170
- return escapeMap[match] || match;
171
- });
172
- }
173
- function formatDateUTC(date) {
174
- return date.toISOString();
175
- }
176
- function formatDateLocal(isoString) {
177
- const date = new Date(isoString);
178
- const options = {
179
- year: "numeric",
180
- month: "short",
181
- day: "2-digit",
182
- hour: "2-digit",
183
- minute: "2-digit",
184
- hour12: true,
185
- timeZoneName: "shortOffset"
186
- };
187
- return new Intl.DateTimeFormat(void 0, options).format(date);
188
- }
189
- function formatDateNoTimezone(isoString) {
190
- const date = new Date(isoString);
191
- return date.toLocaleString("en-US", {
192
- dateStyle: "medium",
193
- timeStyle: "short"
194
- });
195
- }
196
-
197
97
  // src/helpers/HTMLGenerator.ts
198
- import fs2 from "fs";
199
- import Handlebars from "handlebars";
200
98
  var HTMLGenerator = class {
201
99
  constructor(ortoniConfig, dbManager) {
202
100
  this.ortoniConfig = ortoniConfig;
203
- this.registerHandlebarsHelpers();
204
- this.registerPartials();
205
101
  this.dbManager = dbManager;
206
102
  }
207
- async generateHTML(filteredResults, totalDuration, cssContent, results, projectSet) {
103
+ async generateFinalReport(filteredResults, totalDuration, results, projectSet) {
208
104
  const data = await this.prepareReportData(
209
105
  filteredResults,
210
106
  totalDuration,
211
107
  results,
212
108
  projectSet
213
109
  );
214
- const templateSource = fs2.readFileSync(
215
- path3.resolve(__dirname, "views", "main.hbs"),
216
- "utf-8"
217
- );
218
- const template = Handlebars.compile(templateSource);
219
- return template({ ...data, inlineCss: cssContent });
110
+ return data;
220
111
  }
221
112
  async getReportData() {
222
113
  return {
@@ -226,18 +117,6 @@ var HTMLGenerator = class {
226
117
  slowTests: await this.dbManager.getSlowTests()
227
118
  };
228
119
  }
229
- async chartTrendData() {
230
- return {
231
- labels: (await this.getReportData()).trends.map(
232
- (t) => formatDateNoTimezone(t.run_date)
233
- ),
234
- passed: (await this.getReportData()).trends.map((t) => t.passed),
235
- failed: (await this.getReportData()).trends.map((t) => t.failed),
236
- avgDuration: (await this.getReportData()).trends.map(
237
- (t) => t.avg_duration
238
- )
239
- };
240
- }
241
120
  async prepareReportData(filteredResults, totalDuration, results, projectSet) {
242
121
  const totalTests = filteredResults.length;
243
122
  const passedTests = results.filter((r) => r.status === "passed").length;
@@ -255,8 +134,7 @@ var HTMLGenerator = class {
255
134
  results,
256
135
  projectSet
257
136
  );
258
- const utcRunDate = formatDateUTC(/* @__PURE__ */ new Date());
259
- const localRunDate = formatDateLocal(utcRunDate);
137
+ const lastRunDate = (/* @__PURE__ */ new Date()).toLocaleString();
260
138
  const testHistories = await Promise.all(
261
139
  results.map(async (result) => {
262
140
  const testId = `${result.filePath}:${result.projectName}:${result.title}`;
@@ -268,34 +146,44 @@ var HTMLGenerator = class {
268
146
  })
269
147
  );
270
148
  return {
271
- utcRunDate,
272
- localRunDate,
273
- testHistories,
274
- logo: this.ortoniConfig.logo || void 0,
275
- totalDuration,
276
- results,
277
- retryCount: results.filter((r) => r.isRetry).length,
278
- passCount: passedTests,
279
- failCount: failed,
280
- skipCount: results.filter((r) => r.status === "skipped").length,
281
- flakyCount: flakyTests,
282
- totalCount: filteredResults.length,
283
- groupedResults: groupResults(this.ortoniConfig, results),
284
- projectName: this.ortoniConfig.projectName,
285
- authorName: this.ortoniConfig.authorName,
286
- meta: this.ortoniConfig.meta,
287
- testType: this.ortoniConfig.testType,
288
- preferredTheme: this.ortoniConfig.preferredTheme,
289
- successRate,
290
- lastRunDate: formatDate(/* @__PURE__ */ new Date()),
291
- projects: projectSet,
292
- allTags: Array.from(allTags),
293
- showProject: this.ortoniConfig.showProject || false,
294
- title: this.ortoniConfig.title || "Ortoni Playwright Test Report",
295
- chartType: this.ortoniConfig.chartType || "pie",
296
- reportAnalyticsData: await this.getReportData(),
297
- chartData: await this.chartTrendData(),
298
- ...this.extractProjectStats(projectResults)
149
+ summary: {
150
+ overAllResult: {
151
+ pass: passedTests,
152
+ fail: failed,
153
+ skip: results.filter((r) => r.status === "skipped").length,
154
+ retry: results.filter((r) => r.retryAttemptCount).length,
155
+ flaky: flakyTests,
156
+ total: filteredResults.length
157
+ },
158
+ successRate,
159
+ lastRunDate,
160
+ totalDuration,
161
+ stats: this.extractProjectStats(projectResults)
162
+ },
163
+ testResult: {
164
+ tests: groupResults(this.ortoniConfig, results),
165
+ testHistories,
166
+ allTags: Array.from(allTags),
167
+ set: projectSet
168
+ },
169
+ userConfig: {
170
+ projectName: this.ortoniConfig.projectName,
171
+ authorName: this.ortoniConfig.authorName,
172
+ type: this.ortoniConfig.testType,
173
+ title: this.ortoniConfig.title
174
+ },
175
+ userMeta: {
176
+ meta: this.ortoniConfig.meta
177
+ },
178
+ preferences: {
179
+ theme: this.ortoniConfig.preferredTheme,
180
+ logo: this.ortoniConfig.logo || void 0,
181
+ showProject: this.ortoniConfig.showProject || false
182
+ },
183
+ analytics: {
184
+ reportData: await this.getReportData()
185
+ // chartTrendData: await this.chartTrendData(),
186
+ }
299
187
  };
300
188
  }
301
189
  calculateProjectResults(filteredResults, results, projectSet) {
@@ -313,7 +201,7 @@ var HTMLGenerator = class {
313
201
  (r) => r.status === "failed" || r.status === "timedOut"
314
202
  ).length,
315
203
  skippedTests: allProjectTests.filter((r) => r.status === "skipped").length,
316
- retryTests: allProjectTests.filter((r) => r.isRetry).length,
204
+ retryTests: allProjectTests.filter((r) => r.retryAttemptCount).length,
317
205
  flakyTests: allProjectTests.filter((r) => r.status === "flaky").length,
318
206
  totalTests: projectTests.length
319
207
  };
@@ -330,59 +218,18 @@ var HTMLGenerator = class {
330
218
  flakyTests: projectResults.map((result) => result.flakyTests)
331
219
  };
332
220
  }
333
- registerHandlebarsHelpers() {
334
- Handlebars.registerHelper("joinWithSpace", (array) => array.join(" "));
335
- Handlebars.registerHelper("json", (context) => safeStringify(context));
336
- Handlebars.registerHelper(
337
- "eq",
338
- (actualStatus, expectedStatus) => actualStatus === expectedStatus
339
- );
340
- Handlebars.registerHelper(
341
- "includes",
342
- (actualStatus, expectedStatus) => actualStatus.includes(expectedStatus)
343
- );
344
- Handlebars.registerHelper("gr", (count) => count > 0);
345
- Handlebars.registerHelper("or", function(a3, b2) {
346
- return a3 || b2;
347
- });
348
- Handlebars.registerHelper("concat", function(...args) {
349
- args.pop();
350
- return args.join("");
351
- });
352
- }
353
- registerPartials() {
354
- [
355
- "head",
356
- "sidebar",
357
- "testPanel",
358
- "summaryCard",
359
- "userInfo",
360
- "project",
361
- "testStatus",
362
- "testIcons",
363
- "analytics"
364
- ].forEach((partialName) => {
365
- Handlebars.registerPartial(
366
- partialName,
367
- fs2.readFileSync(
368
- path3.resolve(__dirname, "views", `${partialName}.hbs`),
369
- "utf-8"
370
- )
371
- );
372
- });
373
- }
374
221
  };
375
222
 
376
223
  // src/helpers/resultProcessor .ts
377
224
  import AnsiToHtml from "ansi-to-html";
378
- import path5 from "path";
225
+ import path4 from "path";
379
226
 
380
227
  // src/utils/attachFiles.ts
381
- import path4 from "path";
382
- import fs4 from "fs";
228
+ import path2 from "path";
229
+ import fs3 from "fs";
383
230
 
384
231
  // src/helpers/markdownConverter.ts
385
- import fs3 from "fs";
232
+ import fs2 from "fs";
386
233
 
387
234
  // node_modules/marked/lib/marked.esm.js
388
235
  function M() {
@@ -1507,108 +1354,44 @@ var Ft = T.parse;
1507
1354
  var Qt = b.lex;
1508
1355
 
1509
1356
  // src/helpers/markdownConverter.ts
1510
- function convertMarkdownToHtml(markdownPath, htmlOutputPath, stepsError, resultError) {
1511
- const hasMarkdown = fs3.existsSync(markdownPath);
1512
- const markdownContent = hasMarkdown ? fs3.readFileSync(markdownPath, "utf-8") : "";
1357
+ function convertMarkdownToHtml(markdownPath, htmlOutputPath) {
1358
+ const hasMarkdown = fs2.existsSync(markdownPath);
1359
+ const markdownContent = hasMarkdown ? fs2.readFileSync(markdownPath, "utf-8") : "";
1513
1360
  const markdownHtml = hasMarkdown ? k(markdownContent) : "";
1514
- const stepsHtml = stepsError.filter((step) => step.snippet?.trim()).map(
1515
- (step) => `
1516
- <div>
1517
- <pre><code>${step.snippet}</code></pre>
1518
- ${step.location ? `<p><em>Location: ${escapeHtml(step.location)}</em></p>` : ""}
1519
- </div>`
1520
- ).join("\n");
1521
- const errorHtml = resultError.map((error) => `<pre><code>${error}</code></pre>`).join("\n");
1522
- const fullHtml = `
1523
- <!DOCTYPE html>
1524
- <html lang="en">
1525
- <head>
1526
- <meta charset="UTF-8" />
1527
- <title>Ortoni Error Report</title>
1528
- <style>
1529
- body { font-family: sans-serif; padding: 2rem; line-height: 1.6; max-width: 900px; margin: auto; }
1530
- code, pre { background: #f4f4f4; padding: 0.5rem; border-radius: 5px; display: block; overflow-x: auto; }
1531
- h1, h2, h3 { color: #444; }
1532
- hr { margin: 2em 0; }
1533
- #copyBtn {
1534
- background-color: #007acc;
1535
- color: white;
1536
- border: none;
1537
- padding: 0.5rem 1rem;
1538
- margin-bottom: 1rem;
1539
- border-radius: 5px;
1540
- cursor: pointer;
1541
- }
1542
- #copyBtn:hover {
1543
- background-color: #005fa3;
1544
- }
1545
- </style>
1546
- </head>
1547
- <body>
1548
- <button id="copyBtn">\u{1F4CB} Copy All</button>
1549
- <script>
1550
- document.getElementById("copyBtn").addEventListener("click", () => {
1551
- const content = document.getElementById("markdownContent").innerText;
1552
- navigator.clipboard.writeText(content).then(() => {
1553
- // change button text to indicate success
1554
- const button = document.getElementById("copyBtn");
1555
- button.textContent = "\u2705 Copied!";
1556
- setTimeout(() => {
1557
- button.textContent = "\u{1F4CB} Copy All"
1558
- }, 2000);
1559
- }).catch(err => {
1560
- console.error("Failed to copy text: ", err);
1561
- alert("Failed to copy text. Please try manually.");
1562
- });
1563
- });
1564
- </script>
1565
- <div id="markdownContent">
1566
- <h1>Instructions</h1>
1567
- <ul>
1568
- <li>Following Playwright test failed.</li>
1569
- <li>Explain why, be concise, respect Playwright best practices.</li>
1570
- <li>Provide a snippet of code with the fix, if possible.</li>
1571
- </ul>
1572
- <h1>Error Details</h1>
1573
- ${errorHtml || "<p>No errors found.</p>"}
1574
- ${stepsHtml || "<p>No step data available.</p>"}
1575
- ${markdownHtml || ""}
1576
- </div>
1577
- </body>
1578
- </html>
1579
- `;
1580
- fs3.writeFileSync(htmlOutputPath, fullHtml, "utf-8");
1361
+ const drawerHtml = `${markdownHtml || ""}`;
1362
+ fs2.writeFileSync(htmlOutputPath, drawerHtml.trim(), "utf-8");
1581
1363
  if (hasMarkdown) {
1582
- fs3.unlinkSync(markdownPath);
1364
+ fs2.unlinkSync(markdownPath);
1583
1365
  }
1584
1366
  }
1585
1367
 
1586
1368
  // src/utils/attachFiles.ts
1587
1369
  function attachFiles(subFolder, result, testResult, config, steps, errors) {
1588
1370
  const folderPath = config.folderPath || "ortoni-report";
1589
- const attachmentsFolder = path4.join(
1371
+ const attachmentsFolder = path2.join(
1590
1372
  folderPath,
1591
1373
  "ortoni-data",
1592
1374
  "attachments",
1593
1375
  subFolder
1594
1376
  );
1595
- if (!fs4.existsSync(attachmentsFolder)) {
1596
- fs4.mkdirSync(attachmentsFolder, { recursive: true });
1377
+ if (!fs3.existsSync(attachmentsFolder)) {
1378
+ fs3.mkdirSync(attachmentsFolder, { recursive: true });
1597
1379
  }
1598
1380
  if (!result.attachments) return;
1599
1381
  const { base64Image } = config;
1600
1382
  testResult.screenshots = [];
1383
+ testResult.videoPath = [];
1601
1384
  result.attachments.forEach((attachment) => {
1602
1385
  const { contentType, name, path: attachmentPath, body } = attachment;
1603
1386
  if (!attachmentPath && !body) return;
1604
- const fileName = attachmentPath ? path4.basename(attachmentPath) : `${name}.${getFileExtension(contentType)}`;
1605
- const relativePath = path4.join(
1387
+ const fileName = attachmentPath ? path2.basename(attachmentPath) : `${name}.${getFileExtension(contentType)}`;
1388
+ const relativePath = path2.join(
1606
1389
  "ortoni-data",
1607
1390
  "attachments",
1608
1391
  subFolder,
1609
1392
  fileName
1610
1393
  );
1611
- const fullPath = path4.join(attachmentsFolder, fileName);
1394
+ const fullPath = path2.join(attachmentsFolder, fileName);
1612
1395
  if (contentType === "image/png") {
1613
1396
  handleImage(
1614
1397
  attachmentPath,
@@ -1651,13 +1434,13 @@ function handleImage(attachmentPath, body, base64Image, fullPath, relativePath,
1651
1434
  let screenshotPath = "";
1652
1435
  if (attachmentPath) {
1653
1436
  try {
1654
- const screenshotContent = fs4.readFileSync(
1437
+ const screenshotContent = fs3.readFileSync(
1655
1438
  attachmentPath,
1656
1439
  base64Image ? "base64" : void 0
1657
1440
  );
1658
1441
  screenshotPath = base64Image ? `data:image/png;base64,${screenshotContent}` : relativePath;
1659
1442
  if (!base64Image) {
1660
- fs4.copyFileSync(attachmentPath, fullPath);
1443
+ fs3.copyFileSync(attachmentPath, fullPath);
1661
1444
  }
1662
1445
  } catch (error) {
1663
1446
  console.error(
@@ -1674,13 +1457,17 @@ function handleImage(attachmentPath, body, base64Image, fullPath, relativePath,
1674
1457
  }
1675
1458
  function handleAttachment(attachmentPath, fullPath, relativePath, resultKey, testResult, steps, errors) {
1676
1459
  if (attachmentPath) {
1677
- fs4.copyFileSync(attachmentPath, fullPath);
1678
- testResult[resultKey] = relativePath;
1460
+ fs3.copyFileSync(attachmentPath, fullPath);
1461
+ if (resultKey === "videoPath") {
1462
+ testResult[resultKey]?.push(relativePath);
1463
+ } else if (resultKey === "tracePath") {
1464
+ testResult[resultKey] = relativePath;
1465
+ }
1679
1466
  }
1680
1467
  if (resultKey === "markdownPath" && errors) {
1681
1468
  const htmlPath = fullPath.replace(/\.md$/, ".html");
1682
1469
  const htmlRelativePath = relativePath.replace(/\.md$/, ".html");
1683
- convertMarkdownToHtml(fullPath, htmlPath, steps || [], errors || []);
1470
+ convertMarkdownToHtml(fullPath, htmlPath);
1684
1471
  testResult[resultKey] = htmlRelativePath;
1685
1472
  return;
1686
1473
  }
@@ -1695,6 +1482,61 @@ function getFileExtension(contentType) {
1695
1482
  return extensions[contentType] || "unknown";
1696
1483
  }
1697
1484
 
1485
+ // src/utils/utils.ts
1486
+ import path3 from "path";
1487
+ function normalizeFilePath(filePath) {
1488
+ const normalizedPath = path3.normalize(filePath);
1489
+ return path3.basename(normalizedPath);
1490
+ }
1491
+ function ensureHtmlExtension(filename) {
1492
+ const ext = path3.extname(filename);
1493
+ if (ext && ext.toLowerCase() === ".html") {
1494
+ return filename;
1495
+ }
1496
+ return `${filename}.html`;
1497
+ }
1498
+ function escapeHtml(unsafe) {
1499
+ if (typeof unsafe !== "string") {
1500
+ return String(unsafe);
1501
+ }
1502
+ return unsafe.replace(/[&<"']/g, function(match) {
1503
+ const escapeMap = {
1504
+ "&": "&amp;",
1505
+ "<": "&lt;",
1506
+ ">": "&gt;",
1507
+ '"': "&quot;",
1508
+ "'": "&#039;"
1509
+ };
1510
+ return escapeMap[match] || match;
1511
+ });
1512
+ }
1513
+ function formatDateLocal(dateInput) {
1514
+ const date = typeof dateInput === "string" ? new Date(dateInput) : dateInput;
1515
+ const options = {
1516
+ year: "numeric",
1517
+ month: "short",
1518
+ day: "2-digit",
1519
+ hour: "2-digit",
1520
+ minute: "2-digit",
1521
+ hour12: true,
1522
+ timeZoneName: "short"
1523
+ // or "Asia/Kolkata"
1524
+ };
1525
+ return new Intl.DateTimeFormat(void 0, options).format(date);
1526
+ }
1527
+ function extractSuites(titlePath) {
1528
+ const tagPattern = /@[\w]+/g;
1529
+ const suiteParts = titlePath.slice(3, titlePath.length - 1).map((p) => p.replace(tagPattern, "").trim());
1530
+ return {
1531
+ hierarchy: suiteParts.join(" > "),
1532
+ // full hierarchy
1533
+ topLevelSuite: suiteParts[0] ?? "",
1534
+ // first suite
1535
+ parentSuite: suiteParts[suiteParts.length - 1] ?? ""
1536
+ // last suite
1537
+ };
1538
+ }
1539
+
1698
1540
  // src/helpers/resultProcessor .ts
1699
1541
  var TestResultProcessor = class {
1700
1542
  constructor(projectRoot) {
@@ -1710,19 +1552,21 @@ var TestResultProcessor = class {
1710
1552
  const tagPattern = /@[\w]+/g;
1711
1553
  const title = test.title.replace(tagPattern, "").trim();
1712
1554
  const suite = test.titlePath()[3].replace(tagPattern, "").trim();
1555
+ const suiteAndTitle = extractSuites(test.titlePath());
1556
+ const suiteHierarchy = suiteAndTitle.hierarchy;
1713
1557
  const testResult = {
1714
- port: ortoniConfig.port || 2004,
1558
+ suiteHierarchy,
1559
+ key: test.id,
1715
1560
  annotations: test.annotations,
1716
1561
  testTags: test.tags,
1717
1562
  location: `${filePath}:${location.line}:${location.column}`,
1718
- retry: result.retry > 0 ? "retry" : "",
1719
- isRetry: result.retry,
1563
+ retryAttemptCount: result.retry,
1720
1564
  projectName,
1721
1565
  suite,
1722
1566
  title,
1723
1567
  status,
1724
1568
  flaky: test.outcome(),
1725
- duration: msToTime(result.duration),
1569
+ duration: result.duration,
1726
1570
  errors: result.errors.map(
1727
1571
  (e) => this.ansiToHtml.toHtml(escapeHtml(e.stack || e.toString()))
1728
1572
  ),
@@ -1734,7 +1578,8 @@ var TestResultProcessor = class {
1734
1578
  ),
1735
1579
  filePath,
1736
1580
  filters: projectSet,
1737
- base64Image: ortoniConfig.base64Image
1581
+ base64Image: ortoniConfig.base64Image,
1582
+ testId: `${filePath}:${projectName}:${title}`
1738
1583
  };
1739
1584
  attachFiles(
1740
1585
  test.id,
@@ -1748,7 +1593,7 @@ var TestResultProcessor = class {
1748
1593
  }
1749
1594
  processSteps(steps) {
1750
1595
  return steps.map((step) => {
1751
- const stepLocation = step.location ? `${path5.relative(this.projectRoot, step.location.file)}:${step.location.line}:${step.location.column}` : "";
1596
+ const stepLocation = step.location ? `${path4.relative(this.projectRoot, step.location.file)}:${step.location.line}:${step.location.column}` : "";
1752
1597
  return {
1753
1598
  snippet: this.ansiToHtml.toHtml(escapeHtml(step.error?.snippet || "")),
1754
1599
  title: step.title,
@@ -1813,7 +1658,7 @@ var DatabaseManager = class {
1813
1658
  run_id INTEGER,
1814
1659
  test_id TEXT,
1815
1660
  status TEXT,
1816
- duration TEXT,
1661
+ duration INTEGER, -- store duration as raw ms
1817
1662
  error_message TEXT,
1818
1663
  FOREIGN KEY (run_id) REFERENCES test_runs (id)
1819
1664
  );
@@ -1873,6 +1718,7 @@ var DatabaseManager = class {
1873
1718
  `${result.filePath}:${result.projectName}:${result.title}`,
1874
1719
  result.status,
1875
1720
  result.duration,
1721
+ // store raw ms
1876
1722
  result.errors.join("\n")
1877
1723
  ]);
1878
1724
  }
@@ -1939,7 +1785,7 @@ var DatabaseManager = class {
1939
1785
  (SELECT COUNT(*) FROM test_results) as totalTests,
1940
1786
  (SELECT COUNT(*) FROM test_results WHERE status = 'passed') as passed,
1941
1787
  (SELECT COUNT(*) FROM test_results WHERE status = 'failed') as failed,
1942
- (SELECT AVG(CAST(duration AS FLOAT)) FROM test_results) as avgDuration
1788
+ (SELECT AVG(duration) FROM test_results) as avgDuration
1943
1789
  `);
1944
1790
  const passRate = summary.totalTests ? (summary.passed / summary.totalTests * 100).toFixed(2) : 0;
1945
1791
  return {
@@ -1949,6 +1795,7 @@ var DatabaseManager = class {
1949
1795
  failed: summary.failed,
1950
1796
  passRate: parseFloat(passRate.toString()),
1951
1797
  avgDuration: Math.round(summary.avgDuration || 0)
1798
+ // raw ms avg
1952
1799
  };
1953
1800
  } catch (error) {
1954
1801
  console.error("OrtoniReport: Error getting summary data:", error);
@@ -1973,7 +1820,7 @@ var DatabaseManager = class {
1973
1820
  SELECT trun.run_date,
1974
1821
  SUM(CASE WHEN tr.status = 'passed' THEN 1 ELSE 0 END) AS passed,
1975
1822
  SUM(CASE WHEN tr.status = 'failed' THEN 1 ELSE 0 END) AS failed,
1976
- AVG(CAST(tr.duration AS FLOAT)) AS avg_duration
1823
+ AVG(tr.duration) AS avg_duration
1977
1824
  FROM test_results tr
1978
1825
  JOIN test_runs trun ON tr.run_id = trun.id
1979
1826
  GROUP BY trun.run_date
@@ -1986,6 +1833,7 @@ var DatabaseManager = class {
1986
1833
  ...row,
1987
1834
  run_date: formatDateLocal(row.run_date),
1988
1835
  avg_duration: Math.round(row.avg_duration || 0)
1836
+ // raw ms avg
1989
1837
  }));
1990
1838
  } catch (error) {
1991
1839
  console.error("OrtoniReport: Error getting trends data:", error);
@@ -2003,7 +1851,8 @@ var DatabaseManager = class {
2003
1851
  SELECT
2004
1852
  test_id,
2005
1853
  COUNT(*) AS total,
2006
- SUM(CASE WHEN status = 'flaky' THEN 1 ELSE 0 END) AS flaky
1854
+ SUM(CASE WHEN status = 'flaky' THEN 1 ELSE 0 END) AS flaky,
1855
+ AVG(duration) AS avg_duration
2007
1856
  FROM test_results
2008
1857
  GROUP BY test_id
2009
1858
  HAVING flaky > 0
@@ -2027,7 +1876,7 @@ var DatabaseManager = class {
2027
1876
  `
2028
1877
  SELECT
2029
1878
  test_id,
2030
- AVG(CAST(duration AS FLOAT)) AS avg_duration
1879
+ AVG(duration) AS avg_duration
2031
1880
  FROM test_results
2032
1881
  GROUP BY test_id
2033
1882
  ORDER BY avg_duration DESC
@@ -2038,6 +1887,7 @@ var DatabaseManager = class {
2038
1887
  return rows.map((row) => ({
2039
1888
  test_id: row.test_id,
2040
1889
  avg_duration: Math.round(row.avg_duration || 0)
1890
+ // raw ms avg
2041
1891
  }));
2042
1892
  } catch (error) {
2043
1893
  console.error("OrtoniReport: Error getting slow tests:", error);
@@ -2047,7 +1897,7 @@ var DatabaseManager = class {
2047
1897
  };
2048
1898
 
2049
1899
  // src/ortoni-report.ts
2050
- import path6 from "path";
1900
+ import path5 from "path";
2051
1901
  var OrtoniReport = class {
2052
1902
  constructor(ortoniConfig = {}) {
2053
1903
  this.ortoniConfig = ortoniConfig;
@@ -2078,7 +1928,7 @@ var OrtoniReport = class {
2078
1928
  this.testResultProcessor = new TestResultProcessor(config.rootDir);
2079
1929
  this.fileManager.ensureReportDirectory();
2080
1930
  await this.dbManager.initialize(
2081
- path6.join(this.folderPath, "ortoni-data-history.sqlite")
1931
+ path5.join(this.folderPath, "ortoni-data-history.sqlite")
2082
1932
  );
2083
1933
  }
2084
1934
  onStdOut(chunk, _test, _result) {
@@ -2112,23 +1962,21 @@ var OrtoniReport = class {
2112
1962
  this.overAllStatus = result.status;
2113
1963
  if (this.shouldGenerateReport) {
2114
1964
  const filteredResults = this.results.filter(
2115
- (r) => r.status !== "skipped" && !r.isRetry
1965
+ (r) => r.status !== "skipped"
2116
1966
  );
2117
- const totalDuration = msToTime(result.duration);
2118
- const cssContent = this.fileManager.readCssContent();
1967
+ const totalDuration = result.duration;
2119
1968
  const runId = await this.dbManager.saveTestRun();
2120
1969
  if (runId !== null) {
2121
1970
  await this.dbManager.saveTestResults(runId, this.results);
2122
- const html = await this.htmlGenerator.generateHTML(
1971
+ const finalReportData = await this.htmlGenerator.generateFinalReport(
2123
1972
  filteredResults,
2124
1973
  totalDuration,
2125
- cssContent,
2126
1974
  this.results,
2127
1975
  this.projectSet
2128
1976
  );
2129
1977
  this.outputPath = this.fileManager.writeReportFile(
2130
1978
  this.outputFilename,
2131
- html
1979
+ finalReportData
2132
1980
  );
2133
1981
  } else {
2134
1982
  console.error("OrtoniReport: Error saving test run to database");