ortoni-report 3.0.4 → 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: "short"
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,77 +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
- </style>
1567
- </head>
1568
- <body>
1569
- <h1>Instructions</h1>
1570
- <ul>
1571
- <li>Following Playwright test failed.</li>
1572
- <li>Explain why, be concise, respect Playwright best practices.</li>
1573
- <li>Provide a snippet of code with the fix, if possible.</li>
1574
- </ul>
1575
- <h1>Error Details</h1>
1576
- ${errorHtml || "<p>No errors found.</p>"}
1577
- ${stepsHtml || "<p>No step data available.</p>"}
1578
- ${markdownHtml ? `${markdownHtml}` : ""}
1579
- </body>
1580
- </html>
1581
- `;
1582
- import_fs3.default.writeFileSync(htmlOutputPath, fullHtml, "utf-8");
1394
+ const drawerHtml = `${markdownHtml || ""}`;
1395
+ import_fs2.default.writeFileSync(htmlOutputPath, drawerHtml.trim(), "utf-8");
1583
1396
  if (hasMarkdown) {
1584
- import_fs3.default.unlinkSync(markdownPath);
1397
+ import_fs2.default.unlinkSync(markdownPath);
1585
1398
  }
1586
1399
  }
1587
1400
 
1588
1401
  // src/utils/attachFiles.ts
1589
1402
  function attachFiles(subFolder, result, testResult, config, steps, errors) {
1590
1403
  const folderPath = config.folderPath || "ortoni-report";
1591
- const attachmentsFolder = import_path4.default.join(
1404
+ const attachmentsFolder = import_path2.default.join(
1592
1405
  folderPath,
1593
1406
  "ortoni-data",
1594
1407
  "attachments",
1595
1408
  subFolder
1596
1409
  );
1597
- if (!import_fs4.default.existsSync(attachmentsFolder)) {
1598
- import_fs4.default.mkdirSync(attachmentsFolder, { recursive: true });
1410
+ if (!import_fs3.default.existsSync(attachmentsFolder)) {
1411
+ import_fs3.default.mkdirSync(attachmentsFolder, { recursive: true });
1599
1412
  }
1600
1413
  if (!result.attachments) return;
1601
1414
  const { base64Image } = config;
1602
1415
  testResult.screenshots = [];
1416
+ testResult.videoPath = [];
1603
1417
  result.attachments.forEach((attachment) => {
1604
1418
  const { contentType, name, path: attachmentPath, body } = attachment;
1605
1419
  if (!attachmentPath && !body) return;
1606
- const fileName = attachmentPath ? import_path4.default.basename(attachmentPath) : `${name}.${getFileExtension(contentType)}`;
1607
- 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(
1608
1422
  "ortoni-data",
1609
1423
  "attachments",
1610
1424
  subFolder,
1611
1425
  fileName
1612
1426
  );
1613
- const fullPath = import_path4.default.join(attachmentsFolder, fileName);
1427
+ const fullPath = import_path2.default.join(attachmentsFolder, fileName);
1614
1428
  if (contentType === "image/png") {
1615
1429
  handleImage(
1616
1430
  attachmentPath,
@@ -1653,13 +1467,13 @@ function handleImage(attachmentPath, body, base64Image, fullPath, relativePath,
1653
1467
  let screenshotPath = "";
1654
1468
  if (attachmentPath) {
1655
1469
  try {
1656
- const screenshotContent = import_fs4.default.readFileSync(
1470
+ const screenshotContent = import_fs3.default.readFileSync(
1657
1471
  attachmentPath,
1658
1472
  base64Image ? "base64" : void 0
1659
1473
  );
1660
1474
  screenshotPath = base64Image ? `data:image/png;base64,${screenshotContent}` : relativePath;
1661
1475
  if (!base64Image) {
1662
- import_fs4.default.copyFileSync(attachmentPath, fullPath);
1476
+ import_fs3.default.copyFileSync(attachmentPath, fullPath);
1663
1477
  }
1664
1478
  } catch (error) {
1665
1479
  console.error(
@@ -1676,13 +1490,17 @@ function handleImage(attachmentPath, body, base64Image, fullPath, relativePath,
1676
1490
  }
1677
1491
  function handleAttachment(attachmentPath, fullPath, relativePath, resultKey, testResult, steps, errors) {
1678
1492
  if (attachmentPath) {
1679
- import_fs4.default.copyFileSync(attachmentPath, fullPath);
1680
- 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
+ }
1681
1499
  }
1682
1500
  if (resultKey === "markdownPath" && errors) {
1683
1501
  const htmlPath = fullPath.replace(/\.md$/, ".html");
1684
1502
  const htmlRelativePath = relativePath.replace(/\.md$/, ".html");
1685
- convertMarkdownToHtml(fullPath, htmlPath, steps || [], errors || []);
1503
+ convertMarkdownToHtml(fullPath, htmlPath);
1686
1504
  testResult[resultKey] = htmlRelativePath;
1687
1505
  return;
1688
1506
  }
@@ -1697,6 +1515,61 @@ function getFileExtension(contentType) {
1697
1515
  return extensions[contentType] || "unknown";
1698
1516
  }
1699
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
+
1700
1573
  // src/helpers/resultProcessor .ts
1701
1574
  var TestResultProcessor = class {
1702
1575
  constructor(projectRoot) {
@@ -1712,19 +1585,21 @@ var TestResultProcessor = class {
1712
1585
  const tagPattern = /@[\w]+/g;
1713
1586
  const title = test.title.replace(tagPattern, "").trim();
1714
1587
  const suite = test.titlePath()[3].replace(tagPattern, "").trim();
1588
+ const suiteAndTitle = extractSuites(test.titlePath());
1589
+ const suiteHierarchy = suiteAndTitle.hierarchy;
1715
1590
  const testResult = {
1716
- port: ortoniConfig.port || 2004,
1591
+ suiteHierarchy,
1592
+ key: test.id,
1717
1593
  annotations: test.annotations,
1718
1594
  testTags: test.tags,
1719
1595
  location: `${filePath}:${location.line}:${location.column}`,
1720
- retry: result.retry > 0 ? "retry" : "",
1721
- isRetry: result.retry,
1596
+ retryAttemptCount: result.retry,
1722
1597
  projectName,
1723
1598
  suite,
1724
1599
  title,
1725
1600
  status,
1726
1601
  flaky: test.outcome(),
1727
- duration: msToTime(result.duration),
1602
+ duration: result.duration,
1728
1603
  errors: result.errors.map(
1729
1604
  (e) => this.ansiToHtml.toHtml(escapeHtml(e.stack || e.toString()))
1730
1605
  ),
@@ -1736,7 +1611,8 @@ var TestResultProcessor = class {
1736
1611
  ),
1737
1612
  filePath,
1738
1613
  filters: projectSet,
1739
- base64Image: ortoniConfig.base64Image
1614
+ base64Image: ortoniConfig.base64Image,
1615
+ testId: `${filePath}:${projectName}:${title}`
1740
1616
  };
1741
1617
  attachFiles(
1742
1618
  test.id,
@@ -1750,7 +1626,7 @@ var TestResultProcessor = class {
1750
1626
  }
1751
1627
  processSteps(steps) {
1752
1628
  return steps.map((step) => {
1753
- 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}` : "";
1754
1630
  return {
1755
1631
  snippet: this.ansiToHtml.toHtml(escapeHtml(step.error?.snippet || "")),
1756
1632
  title: step.title,
@@ -1762,14 +1638,14 @@ var TestResultProcessor = class {
1762
1638
 
1763
1639
  // src/utils/expressServer.ts
1764
1640
  var import_express = __toESM(require("express"));
1765
- var import_path6 = __toESM(require("path"));
1641
+ var import_path5 = __toESM(require("path"));
1766
1642
  var import_child_process = require("child_process");
1767
1643
  function startReportServer(reportFolder, reportFilename, port = 2004, open2) {
1768
1644
  const app = (0, import_express.default)();
1769
1645
  app.use(import_express.default.static(reportFolder));
1770
1646
  app.get("/", (_req, res) => {
1771
1647
  try {
1772
- res.sendFile(import_path6.default.resolve(reportFolder, reportFilename));
1648
+ res.sendFile(import_path5.default.resolve(reportFolder, reportFilename));
1773
1649
  } catch (error) {
1774
1650
  console.error("Ortoni-Report: Error sending report file:", error);
1775
1651
  res.status(500).send("Error loading report");
@@ -1876,7 +1752,7 @@ var DatabaseManager = class {
1876
1752
  run_id INTEGER,
1877
1753
  test_id TEXT,
1878
1754
  status TEXT,
1879
- duration TEXT,
1755
+ duration INTEGER, -- store duration as raw ms
1880
1756
  error_message TEXT,
1881
1757
  FOREIGN KEY (run_id) REFERENCES test_runs (id)
1882
1758
  );
@@ -1936,6 +1812,7 @@ var DatabaseManager = class {
1936
1812
  `${result.filePath}:${result.projectName}:${result.title}`,
1937
1813
  result.status,
1938
1814
  result.duration,
1815
+ // store raw ms
1939
1816
  result.errors.join("\n")
1940
1817
  ]);
1941
1818
  }
@@ -2002,7 +1879,7 @@ var DatabaseManager = class {
2002
1879
  (SELECT COUNT(*) FROM test_results) as totalTests,
2003
1880
  (SELECT COUNT(*) FROM test_results WHERE status = 'passed') as passed,
2004
1881
  (SELECT COUNT(*) FROM test_results WHERE status = 'failed') as failed,
2005
- (SELECT AVG(CAST(duration AS FLOAT)) FROM test_results) as avgDuration
1882
+ (SELECT AVG(duration) FROM test_results) as avgDuration
2006
1883
  `);
2007
1884
  const passRate = summary.totalTests ? (summary.passed / summary.totalTests * 100).toFixed(2) : 0;
2008
1885
  return {
@@ -2012,6 +1889,7 @@ var DatabaseManager = class {
2012
1889
  failed: summary.failed,
2013
1890
  passRate: parseFloat(passRate.toString()),
2014
1891
  avgDuration: Math.round(summary.avgDuration || 0)
1892
+ // raw ms avg
2015
1893
  };
2016
1894
  } catch (error) {
2017
1895
  console.error("OrtoniReport: Error getting summary data:", error);
@@ -2036,7 +1914,7 @@ var DatabaseManager = class {
2036
1914
  SELECT trun.run_date,
2037
1915
  SUM(CASE WHEN tr.status = 'passed' THEN 1 ELSE 0 END) AS passed,
2038
1916
  SUM(CASE WHEN tr.status = 'failed' THEN 1 ELSE 0 END) AS failed,
2039
- AVG(CAST(tr.duration AS FLOAT)) AS avg_duration
1917
+ AVG(tr.duration) AS avg_duration
2040
1918
  FROM test_results tr
2041
1919
  JOIN test_runs trun ON tr.run_id = trun.id
2042
1920
  GROUP BY trun.run_date
@@ -2049,6 +1927,7 @@ var DatabaseManager = class {
2049
1927
  ...row,
2050
1928
  run_date: formatDateLocal(row.run_date),
2051
1929
  avg_duration: Math.round(row.avg_duration || 0)
1930
+ // raw ms avg
2052
1931
  }));
2053
1932
  } catch (error) {
2054
1933
  console.error("OrtoniReport: Error getting trends data:", error);
@@ -2066,7 +1945,8 @@ var DatabaseManager = class {
2066
1945
  SELECT
2067
1946
  test_id,
2068
1947
  COUNT(*) AS total,
2069
- 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
2070
1950
  FROM test_results
2071
1951
  GROUP BY test_id
2072
1952
  HAVING flaky > 0
@@ -2090,7 +1970,7 @@ var DatabaseManager = class {
2090
1970
  `
2091
1971
  SELECT
2092
1972
  test_id,
2093
- AVG(CAST(duration AS FLOAT)) AS avg_duration
1973
+ AVG(duration) AS avg_duration
2094
1974
  FROM test_results
2095
1975
  GROUP BY test_id
2096
1976
  ORDER BY avg_duration DESC
@@ -2101,6 +1981,7 @@ var DatabaseManager = class {
2101
1981
  return rows.map((row) => ({
2102
1982
  test_id: row.test_id,
2103
1983
  avg_duration: Math.round(row.avg_duration || 0)
1984
+ // raw ms avg
2104
1985
  }));
2105
1986
  } catch (error) {
2106
1987
  console.error("OrtoniReport: Error getting slow tests:", error);
@@ -2110,7 +1991,7 @@ var DatabaseManager = class {
2110
1991
  };
2111
1992
 
2112
1993
  // src/ortoni-report.ts
2113
- var import_path7 = __toESM(require("path"));
1994
+ var import_path6 = __toESM(require("path"));
2114
1995
  var OrtoniReport = class {
2115
1996
  constructor(ortoniConfig = {}) {
2116
1997
  this.ortoniConfig = ortoniConfig;
@@ -2141,7 +2022,7 @@ var OrtoniReport = class {
2141
2022
  this.testResultProcessor = new TestResultProcessor(config.rootDir);
2142
2023
  this.fileManager.ensureReportDirectory();
2143
2024
  await this.dbManager.initialize(
2144
- import_path7.default.join(this.folderPath, "ortoni-data-history.sqlite")
2025
+ import_path6.default.join(this.folderPath, "ortoni-data-history.sqlite")
2145
2026
  );
2146
2027
  }
2147
2028
  onStdOut(chunk, _test, _result) {
@@ -2175,23 +2056,21 @@ var OrtoniReport = class {
2175
2056
  this.overAllStatus = result.status;
2176
2057
  if (this.shouldGenerateReport) {
2177
2058
  const filteredResults = this.results.filter(
2178
- (r) => r.status !== "skipped" && !r.isRetry
2059
+ (r) => r.status !== "skipped"
2179
2060
  );
2180
- const totalDuration = msToTime(result.duration);
2181
- const cssContent = this.fileManager.readCssContent();
2061
+ const totalDuration = result.duration;
2182
2062
  const runId = await this.dbManager.saveTestRun();
2183
2063
  if (runId !== null) {
2184
2064
  await this.dbManager.saveTestResults(runId, this.results);
2185
- const html = await this.htmlGenerator.generateHTML(
2065
+ const finalReportData = await this.htmlGenerator.generateFinalReport(
2186
2066
  filteredResults,
2187
2067
  totalDuration,
2188
- cssContent,
2189
2068
  this.results,
2190
2069
  this.projectSet
2191
2070
  );
2192
2071
  this.outputPath = this.fileManager.writeReportFile(
2193
2072
  this.outputFilename,
2194
- html
2073
+ finalReportData
2195
2074
  );
2196
2075
  } else {
2197
2076
  console.error("OrtoniReport: Error saving test run to database");