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.
@@ -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: "short"
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,77 +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
- </style>
1534
- </head>
1535
- <body>
1536
- <h1>Instructions</h1>
1537
- <ul>
1538
- <li>Following Playwright test failed.</li>
1539
- <li>Explain why, be concise, respect Playwright best practices.</li>
1540
- <li>Provide a snippet of code with the fix, if possible.</li>
1541
- </ul>
1542
- <h1>Error Details</h1>
1543
- ${errorHtml || "<p>No errors found.</p>"}
1544
- ${stepsHtml || "<p>No step data available.</p>"}
1545
- ${markdownHtml ? `${markdownHtml}` : ""}
1546
- </body>
1547
- </html>
1548
- `;
1549
- fs3.writeFileSync(htmlOutputPath, fullHtml, "utf-8");
1361
+ const drawerHtml = `${markdownHtml || ""}`;
1362
+ fs2.writeFileSync(htmlOutputPath, drawerHtml.trim(), "utf-8");
1550
1363
  if (hasMarkdown) {
1551
- fs3.unlinkSync(markdownPath);
1364
+ fs2.unlinkSync(markdownPath);
1552
1365
  }
1553
1366
  }
1554
1367
 
1555
1368
  // src/utils/attachFiles.ts
1556
1369
  function attachFiles(subFolder, result, testResult, config, steps, errors) {
1557
1370
  const folderPath = config.folderPath || "ortoni-report";
1558
- const attachmentsFolder = path4.join(
1371
+ const attachmentsFolder = path2.join(
1559
1372
  folderPath,
1560
1373
  "ortoni-data",
1561
1374
  "attachments",
1562
1375
  subFolder
1563
1376
  );
1564
- if (!fs4.existsSync(attachmentsFolder)) {
1565
- fs4.mkdirSync(attachmentsFolder, { recursive: true });
1377
+ if (!fs3.existsSync(attachmentsFolder)) {
1378
+ fs3.mkdirSync(attachmentsFolder, { recursive: true });
1566
1379
  }
1567
1380
  if (!result.attachments) return;
1568
1381
  const { base64Image } = config;
1569
1382
  testResult.screenshots = [];
1383
+ testResult.videoPath = [];
1570
1384
  result.attachments.forEach((attachment) => {
1571
1385
  const { contentType, name, path: attachmentPath, body } = attachment;
1572
1386
  if (!attachmentPath && !body) return;
1573
- const fileName = attachmentPath ? path4.basename(attachmentPath) : `${name}.${getFileExtension(contentType)}`;
1574
- const relativePath = path4.join(
1387
+ const fileName = attachmentPath ? path2.basename(attachmentPath) : `${name}.${getFileExtension(contentType)}`;
1388
+ const relativePath = path2.join(
1575
1389
  "ortoni-data",
1576
1390
  "attachments",
1577
1391
  subFolder,
1578
1392
  fileName
1579
1393
  );
1580
- const fullPath = path4.join(attachmentsFolder, fileName);
1394
+ const fullPath = path2.join(attachmentsFolder, fileName);
1581
1395
  if (contentType === "image/png") {
1582
1396
  handleImage(
1583
1397
  attachmentPath,
@@ -1620,13 +1434,13 @@ function handleImage(attachmentPath, body, base64Image, fullPath, relativePath,
1620
1434
  let screenshotPath = "";
1621
1435
  if (attachmentPath) {
1622
1436
  try {
1623
- const screenshotContent = fs4.readFileSync(
1437
+ const screenshotContent = fs3.readFileSync(
1624
1438
  attachmentPath,
1625
1439
  base64Image ? "base64" : void 0
1626
1440
  );
1627
1441
  screenshotPath = base64Image ? `data:image/png;base64,${screenshotContent}` : relativePath;
1628
1442
  if (!base64Image) {
1629
- fs4.copyFileSync(attachmentPath, fullPath);
1443
+ fs3.copyFileSync(attachmentPath, fullPath);
1630
1444
  }
1631
1445
  } catch (error) {
1632
1446
  console.error(
@@ -1643,13 +1457,17 @@ function handleImage(attachmentPath, body, base64Image, fullPath, relativePath,
1643
1457
  }
1644
1458
  function handleAttachment(attachmentPath, fullPath, relativePath, resultKey, testResult, steps, errors) {
1645
1459
  if (attachmentPath) {
1646
- fs4.copyFileSync(attachmentPath, fullPath);
1647
- 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
+ }
1648
1466
  }
1649
1467
  if (resultKey === "markdownPath" && errors) {
1650
1468
  const htmlPath = fullPath.replace(/\.md$/, ".html");
1651
1469
  const htmlRelativePath = relativePath.replace(/\.md$/, ".html");
1652
- convertMarkdownToHtml(fullPath, htmlPath, steps || [], errors || []);
1470
+ convertMarkdownToHtml(fullPath, htmlPath);
1653
1471
  testResult[resultKey] = htmlRelativePath;
1654
1472
  return;
1655
1473
  }
@@ -1664,6 +1482,61 @@ function getFileExtension(contentType) {
1664
1482
  return extensions[contentType] || "unknown";
1665
1483
  }
1666
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
+
1667
1540
  // src/helpers/resultProcessor .ts
1668
1541
  var TestResultProcessor = class {
1669
1542
  constructor(projectRoot) {
@@ -1679,19 +1552,21 @@ var TestResultProcessor = class {
1679
1552
  const tagPattern = /@[\w]+/g;
1680
1553
  const title = test.title.replace(tagPattern, "").trim();
1681
1554
  const suite = test.titlePath()[3].replace(tagPattern, "").trim();
1555
+ const suiteAndTitle = extractSuites(test.titlePath());
1556
+ const suiteHierarchy = suiteAndTitle.hierarchy;
1682
1557
  const testResult = {
1683
- port: ortoniConfig.port || 2004,
1558
+ suiteHierarchy,
1559
+ key: test.id,
1684
1560
  annotations: test.annotations,
1685
1561
  testTags: test.tags,
1686
1562
  location: `${filePath}:${location.line}:${location.column}`,
1687
- retry: result.retry > 0 ? "retry" : "",
1688
- isRetry: result.retry,
1563
+ retryAttemptCount: result.retry,
1689
1564
  projectName,
1690
1565
  suite,
1691
1566
  title,
1692
1567
  status,
1693
1568
  flaky: test.outcome(),
1694
- duration: msToTime(result.duration),
1569
+ duration: result.duration,
1695
1570
  errors: result.errors.map(
1696
1571
  (e) => this.ansiToHtml.toHtml(escapeHtml(e.stack || e.toString()))
1697
1572
  ),
@@ -1703,7 +1578,8 @@ var TestResultProcessor = class {
1703
1578
  ),
1704
1579
  filePath,
1705
1580
  filters: projectSet,
1706
- base64Image: ortoniConfig.base64Image
1581
+ base64Image: ortoniConfig.base64Image,
1582
+ testId: `${filePath}:${projectName}:${title}`
1707
1583
  };
1708
1584
  attachFiles(
1709
1585
  test.id,
@@ -1717,7 +1593,7 @@ var TestResultProcessor = class {
1717
1593
  }
1718
1594
  processSteps(steps) {
1719
1595
  return steps.map((step) => {
1720
- 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}` : "";
1721
1597
  return {
1722
1598
  snippet: this.ansiToHtml.toHtml(escapeHtml(step.error?.snippet || "")),
1723
1599
  title: step.title,
@@ -1782,7 +1658,7 @@ var DatabaseManager = class {
1782
1658
  run_id INTEGER,
1783
1659
  test_id TEXT,
1784
1660
  status TEXT,
1785
- duration TEXT,
1661
+ duration INTEGER, -- store duration as raw ms
1786
1662
  error_message TEXT,
1787
1663
  FOREIGN KEY (run_id) REFERENCES test_runs (id)
1788
1664
  );
@@ -1842,6 +1718,7 @@ var DatabaseManager = class {
1842
1718
  `${result.filePath}:${result.projectName}:${result.title}`,
1843
1719
  result.status,
1844
1720
  result.duration,
1721
+ // store raw ms
1845
1722
  result.errors.join("\n")
1846
1723
  ]);
1847
1724
  }
@@ -1908,7 +1785,7 @@ var DatabaseManager = class {
1908
1785
  (SELECT COUNT(*) FROM test_results) as totalTests,
1909
1786
  (SELECT COUNT(*) FROM test_results WHERE status = 'passed') as passed,
1910
1787
  (SELECT COUNT(*) FROM test_results WHERE status = 'failed') as failed,
1911
- (SELECT AVG(CAST(duration AS FLOAT)) FROM test_results) as avgDuration
1788
+ (SELECT AVG(duration) FROM test_results) as avgDuration
1912
1789
  `);
1913
1790
  const passRate = summary.totalTests ? (summary.passed / summary.totalTests * 100).toFixed(2) : 0;
1914
1791
  return {
@@ -1918,6 +1795,7 @@ var DatabaseManager = class {
1918
1795
  failed: summary.failed,
1919
1796
  passRate: parseFloat(passRate.toString()),
1920
1797
  avgDuration: Math.round(summary.avgDuration || 0)
1798
+ // raw ms avg
1921
1799
  };
1922
1800
  } catch (error) {
1923
1801
  console.error("OrtoniReport: Error getting summary data:", error);
@@ -1942,7 +1820,7 @@ var DatabaseManager = class {
1942
1820
  SELECT trun.run_date,
1943
1821
  SUM(CASE WHEN tr.status = 'passed' THEN 1 ELSE 0 END) AS passed,
1944
1822
  SUM(CASE WHEN tr.status = 'failed' THEN 1 ELSE 0 END) AS failed,
1945
- AVG(CAST(tr.duration AS FLOAT)) AS avg_duration
1823
+ AVG(tr.duration) AS avg_duration
1946
1824
  FROM test_results tr
1947
1825
  JOIN test_runs trun ON tr.run_id = trun.id
1948
1826
  GROUP BY trun.run_date
@@ -1955,6 +1833,7 @@ var DatabaseManager = class {
1955
1833
  ...row,
1956
1834
  run_date: formatDateLocal(row.run_date),
1957
1835
  avg_duration: Math.round(row.avg_duration || 0)
1836
+ // raw ms avg
1958
1837
  }));
1959
1838
  } catch (error) {
1960
1839
  console.error("OrtoniReport: Error getting trends data:", error);
@@ -1972,7 +1851,8 @@ var DatabaseManager = class {
1972
1851
  SELECT
1973
1852
  test_id,
1974
1853
  COUNT(*) AS total,
1975
- 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
1976
1856
  FROM test_results
1977
1857
  GROUP BY test_id
1978
1858
  HAVING flaky > 0
@@ -1996,7 +1876,7 @@ var DatabaseManager = class {
1996
1876
  `
1997
1877
  SELECT
1998
1878
  test_id,
1999
- AVG(CAST(duration AS FLOAT)) AS avg_duration
1879
+ AVG(duration) AS avg_duration
2000
1880
  FROM test_results
2001
1881
  GROUP BY test_id
2002
1882
  ORDER BY avg_duration DESC
@@ -2007,6 +1887,7 @@ var DatabaseManager = class {
2007
1887
  return rows.map((row) => ({
2008
1888
  test_id: row.test_id,
2009
1889
  avg_duration: Math.round(row.avg_duration || 0)
1890
+ // raw ms avg
2010
1891
  }));
2011
1892
  } catch (error) {
2012
1893
  console.error("OrtoniReport: Error getting slow tests:", error);
@@ -2016,7 +1897,7 @@ var DatabaseManager = class {
2016
1897
  };
2017
1898
 
2018
1899
  // src/ortoni-report.ts
2019
- import path6 from "path";
1900
+ import path5 from "path";
2020
1901
  var OrtoniReport = class {
2021
1902
  constructor(ortoniConfig = {}) {
2022
1903
  this.ortoniConfig = ortoniConfig;
@@ -2047,7 +1928,7 @@ var OrtoniReport = class {
2047
1928
  this.testResultProcessor = new TestResultProcessor(config.rootDir);
2048
1929
  this.fileManager.ensureReportDirectory();
2049
1930
  await this.dbManager.initialize(
2050
- path6.join(this.folderPath, "ortoni-data-history.sqlite")
1931
+ path5.join(this.folderPath, "ortoni-data-history.sqlite")
2051
1932
  );
2052
1933
  }
2053
1934
  onStdOut(chunk, _test, _result) {
@@ -2081,23 +1962,21 @@ var OrtoniReport = class {
2081
1962
  this.overAllStatus = result.status;
2082
1963
  if (this.shouldGenerateReport) {
2083
1964
  const filteredResults = this.results.filter(
2084
- (r) => r.status !== "skipped" && !r.isRetry
1965
+ (r) => r.status !== "skipped"
2085
1966
  );
2086
- const totalDuration = msToTime(result.duration);
2087
- const cssContent = this.fileManager.readCssContent();
1967
+ const totalDuration = result.duration;
2088
1968
  const runId = await this.dbManager.saveTestRun();
2089
1969
  if (runId !== null) {
2090
1970
  await this.dbManager.saveTestResults(runId, this.results);
2091
- const html = await this.htmlGenerator.generateHTML(
1971
+ const finalReportData = await this.htmlGenerator.generateFinalReport(
2092
1972
  filteredResults,
2093
1973
  totalDuration,
2094
- cssContent,
2095
1974
  this.results,
2096
1975
  this.projectSet
2097
1976
  );
2098
1977
  this.outputPath = this.fileManager.writeReportFile(
2099
1978
  this.outputFilename,
2100
- html
1979
+ finalReportData
2101
1980
  );
2102
1981
  } else {
2103
1982
  console.error("OrtoniReport: Error saving test run to database");