ortoni-report 3.0.5 → 4.0.2-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/changelog.md +30 -0
  2. package/dist/chunk-45EJSEX2.mjs +632 -0
  3. package/dist/chunk-75EAJL2U.mjs +632 -0
  4. package/dist/chunk-FGIYOFIC.mjs +632 -0
  5. package/dist/chunk-FHKWBHU6.mjs +633 -0
  6. package/dist/chunk-GLICR3VS.mjs +637 -0
  7. package/dist/chunk-HFO6XSKC.mjs +633 -0
  8. package/dist/chunk-HOZD6YIV.mjs +634 -0
  9. package/dist/chunk-IJO2YIFE.mjs +637 -0
  10. package/dist/chunk-INS3E7E6.mjs +638 -0
  11. package/dist/chunk-JEIWNUQY.mjs +632 -0
  12. package/dist/chunk-JPLAGYR7.mjs +632 -0
  13. package/dist/chunk-NM6ULN2O.mjs +632 -0
  14. package/dist/chunk-OZS6QIJS.mjs +638 -0
  15. package/dist/chunk-P57227VN.mjs +633 -0
  16. package/dist/chunk-QMTRYN5N.js +635 -0
  17. package/dist/chunk-TI33PMMQ.mjs +639 -0
  18. package/dist/chunk-Z5NBP5TS.mjs +635 -0
  19. package/dist/cli/cli.cjs +678 -0
  20. package/dist/cli/cli.d.cts +1 -0
  21. package/dist/cli/cli.js +580 -11
  22. package/dist/cli/cli.mjs +62 -5
  23. package/dist/index.html +21 -0
  24. package/dist/ortoni-report.cjs +2134 -0
  25. package/dist/ortoni-report.d.cts +111 -0
  26. package/dist/ortoni-report.d.mts +3 -12
  27. package/dist/ortoni-report.d.ts +3 -12
  28. package/dist/ortoni-report.js +182 -314
  29. package/dist/ortoni-report.mjs +64 -740
  30. package/package.json +4 -5
  31. package/readme.md +26 -33
  32. package/dist/chunk-AY2PKDHU.mjs +0 -69
  33. package/dist/chunk-OOALU4XG.mjs +0 -72
  34. package/dist/chunk-ZSIRUQUA.mjs +0 -68
  35. package/dist/style/main.css +0 -80
  36. package/dist/views/analytics.hbs +0 -103
  37. package/dist/views/head.hbs +0 -11
  38. package/dist/views/main.hbs +0 -1295
  39. package/dist/views/project.hbs +0 -238
  40. package/dist/views/sidebar.hbs +0 -244
  41. package/dist/views/summaryCard.hbs +0 -15
  42. package/dist/views/testIcons.hbs +0 -13
  43. package/dist/views/testPanel.hbs +0 -45
  44. package/dist/views/testStatus.hbs +0 -9
  45. package/dist/views/userInfo.hbs +0 -260
@@ -1,388 +1,25 @@
1
1
  import {
2
+ DatabaseManager,
3
+ FileManager,
4
+ HTMLGenerator,
2
5
  __publicField,
3
- __require,
6
+ ensureHtmlExtension,
7
+ escapeHtml,
8
+ extractSuites,
9
+ normalizeFilePath,
4
10
  startReportServer
5
- } from "./chunk-A6HCKATU.mjs";
6
-
7
- // src/helpers/fileManager.ts
8
- import fs from "fs";
9
- import path from "path";
10
- var FileManager = class {
11
- constructor(folderPath) {
12
- this.folderPath = folderPath;
13
- }
14
- ensureReportDirectory() {
15
- const ortoniDataFolder = path.join(this.folderPath, "ortoni-data");
16
- if (!fs.existsSync(this.folderPath)) {
17
- fs.mkdirSync(this.folderPath, { recursive: true });
18
- } else {
19
- if (fs.existsSync(ortoniDataFolder)) {
20
- fs.rmSync(ortoniDataFolder, { recursive: true, force: true });
21
- }
22
- }
23
- }
24
- writeReportFile(filename, content) {
25
- const outputPath = path.join(process.cwd(), this.folderPath, filename);
26
- fs.writeFileSync(outputPath, content);
27
- return outputPath;
28
- }
29
- readCssContent() {
30
- return fs.readFileSync(
31
- path.resolve(__dirname, "style", "main.css"),
32
- "utf-8"
33
- );
34
- }
35
- copyTraceViewerAssets(skip) {
36
- if (skip) return;
37
- const traceViewerFolder = path.join(
38
- __require.resolve("playwright-core"),
39
- "..",
40
- "lib",
41
- "vite",
42
- "traceViewer"
43
- );
44
- const traceViewerTargetFolder = path.join(this.folderPath, "trace");
45
- const traceViewerAssetsTargetFolder = path.join(
46
- traceViewerTargetFolder,
47
- "assets"
48
- );
49
- fs.mkdirSync(traceViewerAssetsTargetFolder, { recursive: true });
50
- for (const file of fs.readdirSync(traceViewerFolder)) {
51
- if (file.endsWith(".map") || file.includes("watch") || file.includes("assets"))
52
- continue;
53
- fs.copyFileSync(
54
- path.join(traceViewerFolder, file),
55
- path.join(traceViewerTargetFolder, file)
56
- );
57
- }
58
- const assetsFolder = path.join(traceViewerFolder, "assets");
59
- for (const file of fs.readdirSync(assetsFolder)) {
60
- if (file.endsWith(".map") || file.includes("xtermModule")) continue;
61
- fs.copyFileSync(
62
- path.join(assetsFolder, file),
63
- path.join(traceViewerAssetsTargetFolder, file)
64
- );
65
- }
66
- }
67
- };
68
-
69
- // src/helpers/HTMLGenerator.ts
70
- import path3 from "path";
71
-
72
- // src/utils/groupProjects.ts
73
- function groupResults(config, results) {
74
- if (config.showProject) {
75
- const groupedResults = results.reduce((acc, result, index) => {
76
- const testId = `${result.filePath}:${result.projectName}:${result.title}`;
77
- const { filePath, suite, projectName } = result;
78
- acc[filePath] = acc[filePath] || {};
79
- acc[filePath][suite] = acc[filePath][suite] || {};
80
- acc[filePath][suite][projectName] = acc[filePath][suite][projectName] || [];
81
- acc[filePath][suite][projectName].push({ ...result, index, testId });
82
- return acc;
83
- }, {});
84
- return groupedResults;
85
- } else {
86
- const groupedResults = results.reduce((acc, result, index) => {
87
- const testId = `${result.filePath}:${result.projectName}:${result.title}`;
88
- const { filePath, suite } = result;
89
- acc[filePath] = acc[filePath] || {};
90
- acc[filePath][suite] = acc[filePath][suite] || [];
91
- acc[filePath][suite].push({ ...result, index, testId });
92
- return acc;
93
- }, {});
94
- return groupedResults;
95
- }
96
- }
97
-
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
- // src/helpers/HTMLGenerator.ts
198
- import fs2 from "fs";
199
- import Handlebars from "handlebars";
200
- var HTMLGenerator = class {
201
- constructor(ortoniConfig, dbManager) {
202
- this.ortoniConfig = ortoniConfig;
203
- this.registerHandlebarsHelpers();
204
- this.registerPartials();
205
- this.dbManager = dbManager;
206
- }
207
- async generateHTML(filteredResults, totalDuration, cssContent, results, projectSet) {
208
- const data = await this.prepareReportData(
209
- filteredResults,
210
- totalDuration,
211
- results,
212
- projectSet
213
- );
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 });
220
- }
221
- async getReportData() {
222
- return {
223
- summary: await this.dbManager.getSummaryData(),
224
- trends: await this.dbManager.getTrends(),
225
- flakyTests: await this.dbManager.getFlakyTests(),
226
- slowTests: await this.dbManager.getSlowTests()
227
- };
228
- }
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
- async prepareReportData(filteredResults, totalDuration, results, projectSet) {
242
- const totalTests = filteredResults.length;
243
- const passedTests = results.filter((r) => r.status === "passed").length;
244
- const flakyTests = results.filter((r) => r.status === "flaky").length;
245
- const failed = filteredResults.filter(
246
- (r) => r.status === "failed" || r.status === "timedOut"
247
- ).length;
248
- const successRate = ((passedTests + flakyTests) / totalTests * 100).toFixed(2);
249
- const allTags = /* @__PURE__ */ new Set();
250
- results.forEach(
251
- (result) => result.testTags.forEach((tag) => allTags.add(tag))
252
- );
253
- const projectResults = this.calculateProjectResults(
254
- filteredResults,
255
- results,
256
- projectSet
257
- );
258
- const utcRunDate = formatDateUTC(/* @__PURE__ */ new Date());
259
- const localRunDate = formatDateLocal(utcRunDate);
260
- const testHistories = await Promise.all(
261
- results.map(async (result) => {
262
- const testId = `${result.filePath}:${result.projectName}:${result.title}`;
263
- const history = await this.dbManager.getTestHistory(testId);
264
- return {
265
- testId,
266
- history
267
- };
268
- })
269
- );
270
- 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)
299
- };
300
- }
301
- calculateProjectResults(filteredResults, results, projectSet) {
302
- return Array.from(projectSet).map((projectName) => {
303
- const projectTests = filteredResults.filter(
304
- (r) => r.projectName === projectName
305
- );
306
- const allProjectTests = results.filter(
307
- (r) => r.projectName === projectName
308
- );
309
- return {
310
- projectName,
311
- passedTests: projectTests.filter((r) => r.status === "passed").length,
312
- failedTests: projectTests.filter(
313
- (r) => r.status === "failed" || r.status === "timedOut"
314
- ).length,
315
- skippedTests: allProjectTests.filter((r) => r.status === "skipped").length,
316
- retryTests: allProjectTests.filter((r) => r.isRetry).length,
317
- flakyTests: allProjectTests.filter((r) => r.status === "flaky").length,
318
- totalTests: projectTests.length
319
- };
320
- });
321
- }
322
- extractProjectStats(projectResults) {
323
- return {
324
- projectNames: projectResults.map((result) => result.projectName),
325
- totalTests: projectResults.map((result) => result.totalTests),
326
- passedTests: projectResults.map((result) => result.passedTests),
327
- failedTests: projectResults.map((result) => result.failedTests),
328
- skippedTests: projectResults.map((result) => result.skippedTests),
329
- retryTests: projectResults.map((result) => result.retryTests),
330
- flakyTests: projectResults.map((result) => result.flakyTests)
331
- };
332
- }
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
- };
11
+ } from "./chunk-JEIWNUQY.mjs";
375
12
 
376
13
  // src/helpers/resultProcessor .ts
377
14
  import AnsiToHtml from "ansi-to-html";
378
- import path5 from "path";
15
+ import path2 from "path";
379
16
 
380
17
  // src/utils/attachFiles.ts
381
- import path4 from "path";
382
- import fs4 from "fs";
18
+ import path from "path";
19
+ import fs2 from "fs";
383
20
 
384
21
  // src/helpers/markdownConverter.ts
385
- import fs3 from "fs";
22
+ import fs from "fs";
386
23
 
387
24
  // node_modules/marked/lib/marked.esm.js
388
25
  function M() {
@@ -1507,108 +1144,44 @@ var Ft = T.parse;
1507
1144
  var Qt = b.lex;
1508
1145
 
1509
1146
  // 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") : "";
1147
+ function convertMarkdownToHtml(markdownPath, htmlOutputPath) {
1148
+ const hasMarkdown = fs.existsSync(markdownPath);
1149
+ const markdownContent = hasMarkdown ? fs.readFileSync(markdownPath, "utf-8") : "";
1513
1150
  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");
1151
+ const drawerHtml = `${markdownHtml || ""}`;
1152
+ fs.writeFileSync(htmlOutputPath, drawerHtml.trim(), "utf-8");
1581
1153
  if (hasMarkdown) {
1582
- fs3.unlinkSync(markdownPath);
1154
+ fs.unlinkSync(markdownPath);
1583
1155
  }
1584
1156
  }
1585
1157
 
1586
1158
  // src/utils/attachFiles.ts
1587
1159
  function attachFiles(subFolder, result, testResult, config, steps, errors) {
1588
1160
  const folderPath = config.folderPath || "ortoni-report";
1589
- const attachmentsFolder = path4.join(
1161
+ const attachmentsFolder = path.join(
1590
1162
  folderPath,
1591
1163
  "ortoni-data",
1592
1164
  "attachments",
1593
1165
  subFolder
1594
1166
  );
1595
- if (!fs4.existsSync(attachmentsFolder)) {
1596
- fs4.mkdirSync(attachmentsFolder, { recursive: true });
1167
+ if (!fs2.existsSync(attachmentsFolder)) {
1168
+ fs2.mkdirSync(attachmentsFolder, { recursive: true });
1597
1169
  }
1598
1170
  if (!result.attachments) return;
1599
1171
  const { base64Image } = config;
1600
1172
  testResult.screenshots = [];
1173
+ testResult.videoPath = [];
1601
1174
  result.attachments.forEach((attachment) => {
1602
1175
  const { contentType, name, path: attachmentPath, body } = attachment;
1603
1176
  if (!attachmentPath && !body) return;
1604
- const fileName = attachmentPath ? path4.basename(attachmentPath) : `${name}.${getFileExtension(contentType)}`;
1605
- const relativePath = path4.join(
1177
+ const fileName = attachmentPath ? path.basename(attachmentPath) : `${name}.${getFileExtension(contentType)}`;
1178
+ const relativePath = path.join(
1606
1179
  "ortoni-data",
1607
1180
  "attachments",
1608
1181
  subFolder,
1609
1182
  fileName
1610
1183
  );
1611
- const fullPath = path4.join(attachmentsFolder, fileName);
1184
+ const fullPath = path.join(attachmentsFolder, fileName);
1612
1185
  if (contentType === "image/png") {
1613
1186
  handleImage(
1614
1187
  attachmentPath,
@@ -1651,13 +1224,13 @@ function handleImage(attachmentPath, body, base64Image, fullPath, relativePath,
1651
1224
  let screenshotPath = "";
1652
1225
  if (attachmentPath) {
1653
1226
  try {
1654
- const screenshotContent = fs4.readFileSync(
1227
+ const screenshotContent = fs2.readFileSync(
1655
1228
  attachmentPath,
1656
1229
  base64Image ? "base64" : void 0
1657
1230
  );
1658
1231
  screenshotPath = base64Image ? `data:image/png;base64,${screenshotContent}` : relativePath;
1659
1232
  if (!base64Image) {
1660
- fs4.copyFileSync(attachmentPath, fullPath);
1233
+ fs2.copyFileSync(attachmentPath, fullPath);
1661
1234
  }
1662
1235
  } catch (error) {
1663
1236
  console.error(
@@ -1674,13 +1247,17 @@ function handleImage(attachmentPath, body, base64Image, fullPath, relativePath,
1674
1247
  }
1675
1248
  function handleAttachment(attachmentPath, fullPath, relativePath, resultKey, testResult, steps, errors) {
1676
1249
  if (attachmentPath) {
1677
- fs4.copyFileSync(attachmentPath, fullPath);
1678
- testResult[resultKey] = relativePath;
1250
+ fs2.copyFileSync(attachmentPath, fullPath);
1251
+ if (resultKey === "videoPath") {
1252
+ testResult[resultKey]?.push(relativePath);
1253
+ } else if (resultKey === "tracePath") {
1254
+ testResult[resultKey] = relativePath;
1255
+ }
1679
1256
  }
1680
1257
  if (resultKey === "markdownPath" && errors) {
1681
1258
  const htmlPath = fullPath.replace(/\.md$/, ".html");
1682
1259
  const htmlRelativePath = relativePath.replace(/\.md$/, ".html");
1683
- convertMarkdownToHtml(fullPath, htmlPath, steps || [], errors || []);
1260
+ convertMarkdownToHtml(fullPath, htmlPath);
1684
1261
  testResult[resultKey] = htmlRelativePath;
1685
1262
  return;
1686
1263
  }
@@ -1710,19 +1287,21 @@ var TestResultProcessor = class {
1710
1287
  const tagPattern = /@[\w]+/g;
1711
1288
  const title = test.title.replace(tagPattern, "").trim();
1712
1289
  const suite = test.titlePath()[3].replace(tagPattern, "").trim();
1290
+ const suiteAndTitle = extractSuites(test.titlePath());
1291
+ const suiteHierarchy = suiteAndTitle.hierarchy;
1713
1292
  const testResult = {
1714
- port: ortoniConfig.port || 2004,
1293
+ suiteHierarchy,
1294
+ key: test.id,
1715
1295
  annotations: test.annotations,
1716
1296
  testTags: test.tags,
1717
1297
  location: `${filePath}:${location.line}:${location.column}`,
1718
- retry: result.retry > 0 ? "retry" : "",
1719
- isRetry: result.retry,
1298
+ retryAttemptCount: result.retry,
1720
1299
  projectName,
1721
1300
  suite,
1722
1301
  title,
1723
1302
  status,
1724
1303
  flaky: test.outcome(),
1725
- duration: msToTime(result.duration),
1304
+ duration: result.duration,
1726
1305
  errors: result.errors.map(
1727
1306
  (e) => this.ansiToHtml.toHtml(escapeHtml(e.stack || e.toString()))
1728
1307
  ),
@@ -1734,7 +1313,8 @@ var TestResultProcessor = class {
1734
1313
  ),
1735
1314
  filePath,
1736
1315
  filters: projectSet,
1737
- base64Image: ortoniConfig.base64Image
1316
+ base64Image: ortoniConfig.base64Image,
1317
+ testId: `${filePath}:${projectName}:${title}`
1738
1318
  };
1739
1319
  attachFiles(
1740
1320
  test.id,
@@ -1748,7 +1328,7 @@ var TestResultProcessor = class {
1748
1328
  }
1749
1329
  processSteps(steps) {
1750
1330
  return steps.map((step) => {
1751
- const stepLocation = step.location ? `${path5.relative(this.projectRoot, step.location.file)}:${step.location.line}:${step.location.column}` : "";
1331
+ const stepLocation = step.location ? `${path2.relative(this.projectRoot, step.location.file)}:${step.location.line}:${step.location.column}` : "";
1752
1332
  return {
1753
1333
  snippet: this.ansiToHtml.toHtml(escapeHtml(step.error?.snippet || "")),
1754
1334
  title: step.title,
@@ -1777,277 +1357,8 @@ var ServerManager = class {
1777
1357
  }
1778
1358
  };
1779
1359
 
1780
- // src/helpers/databaseManager.ts
1781
- import { open } from "sqlite";
1782
- import sqlite3 from "sqlite3";
1783
- var DatabaseManager = class {
1784
- constructor() {
1785
- this.db = null;
1786
- }
1787
- async initialize(dbPath) {
1788
- try {
1789
- this.db = await open({
1790
- filename: dbPath,
1791
- driver: sqlite3.Database
1792
- });
1793
- await this.createTables();
1794
- await this.createIndexes();
1795
- } catch (error) {
1796
- console.error("OrtoniReport: Error initializing database:", error);
1797
- }
1798
- }
1799
- async createTables() {
1800
- if (!this.db) {
1801
- console.error("OrtoniReport: Database not initialized");
1802
- return;
1803
- }
1804
- try {
1805
- await this.db.exec(`
1806
- CREATE TABLE IF NOT EXISTS test_runs (
1807
- id INTEGER PRIMARY KEY AUTOINCREMENT,
1808
- run_date TEXT
1809
- );
1810
-
1811
- CREATE TABLE IF NOT EXISTS test_results (
1812
- id INTEGER PRIMARY KEY AUTOINCREMENT,
1813
- run_id INTEGER,
1814
- test_id TEXT,
1815
- status TEXT,
1816
- duration TEXT,
1817
- error_message TEXT,
1818
- FOREIGN KEY (run_id) REFERENCES test_runs (id)
1819
- );
1820
- `);
1821
- } catch (error) {
1822
- console.error("OrtoniReport: Error creating tables:", error);
1823
- }
1824
- }
1825
- async createIndexes() {
1826
- if (!this.db) {
1827
- console.error("OrtoniReport: Database not initialized");
1828
- return;
1829
- }
1830
- try {
1831
- await this.db.exec(`
1832
- CREATE INDEX IF NOT EXISTS idx_test_id ON test_results (test_id);
1833
- CREATE INDEX IF NOT EXISTS idx_run_id ON test_results (run_id);
1834
- `);
1835
- } catch (error) {
1836
- console.error("OrtoniReport: Error creating indexes:", error);
1837
- }
1838
- }
1839
- async saveTestRun() {
1840
- if (!this.db) {
1841
- console.error("OrtoniReport: Database not initialized");
1842
- return null;
1843
- }
1844
- try {
1845
- const runDate = (/* @__PURE__ */ new Date()).toISOString();
1846
- const { lastID } = await this.db.run(
1847
- `
1848
- INSERT INTO test_runs (run_date)
1849
- VALUES (?)
1850
- `,
1851
- [runDate]
1852
- );
1853
- return lastID;
1854
- } catch (error) {
1855
- console.error("OrtoniReport: Error saving test run:", error);
1856
- return null;
1857
- }
1858
- }
1859
- async saveTestResults(runId, results) {
1860
- if (!this.db) {
1861
- console.error("OrtoniReport: Database not initialized");
1862
- return;
1863
- }
1864
- try {
1865
- await this.db.exec("BEGIN TRANSACTION;");
1866
- const stmt = await this.db.prepare(`
1867
- INSERT INTO test_results (run_id, test_id, status, duration, error_message)
1868
- VALUES (?, ?, ?, ?, ?)
1869
- `);
1870
- for (const result of results) {
1871
- await stmt.run([
1872
- runId,
1873
- `${result.filePath}:${result.projectName}:${result.title}`,
1874
- result.status,
1875
- result.duration,
1876
- result.errors.join("\n")
1877
- ]);
1878
- }
1879
- await stmt.finalize();
1880
- await this.db.exec("COMMIT;");
1881
- } catch (error) {
1882
- await this.db.exec("ROLLBACK;");
1883
- console.error("OrtoniReport: Error saving test results:", error);
1884
- }
1885
- }
1886
- async getTestHistory(testId, limit = 10) {
1887
- if (!this.db) {
1888
- console.error("OrtoniReport: Database not initialized");
1889
- return [];
1890
- }
1891
- try {
1892
- const results = await this.db.all(
1893
- `
1894
- SELECT tr.status, tr.duration, tr.error_message, trun.run_date
1895
- FROM test_results tr
1896
- JOIN test_runs trun ON tr.run_id = trun.id
1897
- WHERE tr.test_id = ?
1898
- ORDER BY trun.run_date DESC
1899
- LIMIT ?
1900
- `,
1901
- [testId, limit]
1902
- );
1903
- return results.map((result) => ({
1904
- ...result,
1905
- run_date: formatDateLocal(result.run_date)
1906
- }));
1907
- } catch (error) {
1908
- console.error("OrtoniReport: Error getting test history:", error);
1909
- return [];
1910
- }
1911
- }
1912
- async close() {
1913
- if (this.db) {
1914
- try {
1915
- await this.db.close();
1916
- } catch (error) {
1917
- console.error("OrtoniReport: Error closing database:", error);
1918
- } finally {
1919
- this.db = null;
1920
- }
1921
- }
1922
- }
1923
- async getSummaryData() {
1924
- if (!this.db) {
1925
- console.error("OrtoniReport: Database not initialized");
1926
- return {
1927
- totalRuns: 0,
1928
- totalTests: 0,
1929
- passed: 0,
1930
- failed: 0,
1931
- passRate: 0,
1932
- avgDuration: 0
1933
- };
1934
- }
1935
- try {
1936
- const summary = await this.db.get(`
1937
- SELECT
1938
- (SELECT COUNT(*) FROM test_runs) as totalRuns,
1939
- (SELECT COUNT(*) FROM test_results) as totalTests,
1940
- (SELECT COUNT(*) FROM test_results WHERE status = 'passed') as passed,
1941
- (SELECT COUNT(*) FROM test_results WHERE status = 'failed') as failed,
1942
- (SELECT AVG(CAST(duration AS FLOAT)) FROM test_results) as avgDuration
1943
- `);
1944
- const passRate = summary.totalTests ? (summary.passed / summary.totalTests * 100).toFixed(2) : 0;
1945
- return {
1946
- totalRuns: summary.totalRuns,
1947
- totalTests: summary.totalTests,
1948
- passed: summary.passed,
1949
- failed: summary.failed,
1950
- passRate: parseFloat(passRate.toString()),
1951
- avgDuration: Math.round(summary.avgDuration || 0)
1952
- };
1953
- } catch (error) {
1954
- console.error("OrtoniReport: Error getting summary data:", error);
1955
- return {
1956
- totalRuns: 0,
1957
- totalTests: 0,
1958
- passed: 0,
1959
- failed: 0,
1960
- passRate: 0,
1961
- avgDuration: 0
1962
- };
1963
- }
1964
- }
1965
- async getTrends(limit = 100) {
1966
- if (!this.db) {
1967
- console.error("OrtoniReport: Database not initialized");
1968
- return [];
1969
- }
1970
- try {
1971
- const rows = await this.db.all(
1972
- `
1973
- SELECT trun.run_date,
1974
- SUM(CASE WHEN tr.status = 'passed' THEN 1 ELSE 0 END) AS passed,
1975
- SUM(CASE WHEN tr.status = 'failed' THEN 1 ELSE 0 END) AS failed,
1976
- AVG(CAST(tr.duration AS FLOAT)) AS avg_duration
1977
- FROM test_results tr
1978
- JOIN test_runs trun ON tr.run_id = trun.id
1979
- GROUP BY trun.run_date
1980
- ORDER BY trun.run_date DESC
1981
- LIMIT ?
1982
- `,
1983
- [limit]
1984
- );
1985
- return rows.reverse().map((row) => ({
1986
- ...row,
1987
- run_date: formatDateLocal(row.run_date),
1988
- avg_duration: Math.round(row.avg_duration || 0)
1989
- }));
1990
- } catch (error) {
1991
- console.error("OrtoniReport: Error getting trends data:", error);
1992
- return [];
1993
- }
1994
- }
1995
- async getFlakyTests(limit = 10) {
1996
- if (!this.db) {
1997
- console.error("OrtoniReport: Database not initialized");
1998
- return [];
1999
- }
2000
- try {
2001
- return await this.db.all(
2002
- `
2003
- SELECT
2004
- test_id,
2005
- COUNT(*) AS total,
2006
- SUM(CASE WHEN status = 'flaky' THEN 1 ELSE 0 END) AS flaky
2007
- FROM test_results
2008
- GROUP BY test_id
2009
- HAVING flaky > 0
2010
- ORDER BY flaky DESC
2011
- LIMIT ?
2012
- `,
2013
- [limit]
2014
- );
2015
- } catch (error) {
2016
- console.error("OrtoniReport: Error getting flaky tests:", error);
2017
- return [];
2018
- }
2019
- }
2020
- async getSlowTests(limit = 10) {
2021
- if (!this.db) {
2022
- console.error("OrtoniReport: Database not initialized");
2023
- return [];
2024
- }
2025
- try {
2026
- const rows = await this.db.all(
2027
- `
2028
- SELECT
2029
- test_id,
2030
- AVG(CAST(duration AS FLOAT)) AS avg_duration
2031
- FROM test_results
2032
- GROUP BY test_id
2033
- ORDER BY avg_duration DESC
2034
- LIMIT ?
2035
- `,
2036
- [limit]
2037
- );
2038
- return rows.map((row) => ({
2039
- test_id: row.test_id,
2040
- avg_duration: Math.round(row.avg_duration || 0)
2041
- }));
2042
- } catch (error) {
2043
- console.error("OrtoniReport: Error getting slow tests:", error);
2044
- return [];
2045
- }
2046
- }
2047
- };
2048
-
2049
1360
  // src/ortoni-report.ts
2050
- import path6 from "path";
1361
+ import path3 from "path";
2051
1362
  var OrtoniReport = class {
2052
1363
  constructor(ortoniConfig = {}) {
2053
1364
  this.ortoniConfig = ortoniConfig;
@@ -2078,8 +1389,9 @@ var OrtoniReport = class {
2078
1389
  this.testResultProcessor = new TestResultProcessor(config.rootDir);
2079
1390
  this.fileManager.ensureReportDirectory();
2080
1391
  await this.dbManager.initialize(
2081
- path6.join(this.folderPath, "ortoni-data-history.sqlite")
1392
+ path3.join(this.folderPath, "ortoni-data-history.sqlite")
2082
1393
  );
1394
+ this.config = config?.shard;
2083
1395
  }
2084
1396
  onStdOut(chunk, _test, _result) {
2085
1397
  if (this.reportsCount == 1 && this.showConsoleLogs) {
@@ -2112,23 +1424,35 @@ var OrtoniReport = class {
2112
1424
  this.overAllStatus = result.status;
2113
1425
  if (this.shouldGenerateReport) {
2114
1426
  const filteredResults = this.results.filter(
2115
- (r) => r.status !== "skipped" && !r.isRetry
1427
+ (r) => r.status !== "skipped"
2116
1428
  );
2117
- const totalDuration = msToTime(result.duration);
2118
- const cssContent = this.fileManager.readCssContent();
1429
+ const totalDuration = result.duration;
1430
+ if (this.config && this.config.total > 1) {
1431
+ const shard = this.config;
1432
+ const shardFile = `ortoni-shard-${shard.current}-of-${shard.total}.json`;
1433
+ const shardData = {
1434
+ status: result.status,
1435
+ duration: totalDuration,
1436
+ results: this.results,
1437
+ projectSet: Array.from(this.projectSet)
1438
+ };
1439
+ this.fileManager.writeRawFile(shardFile, shardData);
1440
+ console.log(`\u{1F4E6} OrtoniReport wrote shard file: ${shardFile}`);
1441
+ this.shouldGenerateReport = false;
1442
+ return;
1443
+ }
2119
1444
  const runId = await this.dbManager.saveTestRun();
2120
1445
  if (runId !== null) {
2121
1446
  await this.dbManager.saveTestResults(runId, this.results);
2122
- const html = await this.htmlGenerator.generateHTML(
1447
+ const finalReportData = await this.htmlGenerator.generateFinalReport(
2123
1448
  filteredResults,
2124
1449
  totalDuration,
2125
- cssContent,
2126
1450
  this.results,
2127
1451
  this.projectSet
2128
1452
  );
2129
1453
  this.outputPath = this.fileManager.writeReportFile(
2130
1454
  this.outputFilename,
2131
- html
1455
+ finalReportData
2132
1456
  );
2133
1457
  } else {
2134
1458
  console.error("OrtoniReport: Error saving test run to database");