ortoni-report 3.0.5 → 4.0.1-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 (46) 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-MPZLDOCN.mjs +631 -0
  14. package/dist/chunk-NM6ULN2O.mjs +632 -0
  15. package/dist/chunk-OZS6QIJS.mjs +638 -0
  16. package/dist/chunk-P57227VN.mjs +633 -0
  17. package/dist/chunk-QMTRYN5N.js +635 -0
  18. package/dist/chunk-TI33PMMQ.mjs +639 -0
  19. package/dist/chunk-Z5NBP5TS.mjs +635 -0
  20. package/dist/cli/cli.cjs +678 -0
  21. package/dist/cli/cli.d.cts +1 -0
  22. package/dist/cli/cli.js +609 -18
  23. package/dist/cli/cli.mjs +89 -9
  24. package/dist/index.html +21 -0
  25. package/dist/ortoni-report.cjs +2134 -0
  26. package/dist/ortoni-report.d.cts +111 -0
  27. package/dist/ortoni-report.d.mts +3 -12
  28. package/dist/ortoni-report.d.ts +3 -12
  29. package/dist/ortoni-report.js +201 -326
  30. package/dist/ortoni-report.mjs +78 -746
  31. package/package.json +4 -5
  32. package/readme.md +26 -33
  33. package/dist/chunk-AY2PKDHU.mjs +0 -69
  34. package/dist/chunk-OOALU4XG.mjs +0 -72
  35. package/dist/chunk-ZSIRUQUA.mjs +0 -68
  36. package/dist/style/main.css +0 -80
  37. package/dist/views/analytics.hbs +0 -103
  38. package/dist/views/head.hbs +0 -11
  39. package/dist/views/main.hbs +0 -1295
  40. package/dist/views/project.hbs +0 -238
  41. package/dist/views/sidebar.hbs +0 -244
  42. package/dist/views/summaryCard.hbs +0 -15
  43. package/dist/views/testIcons.hbs +0 -13
  44. package/dist/views/testPanel.hbs +0 -45
  45. package/dist/views/testStatus.hbs +0 -9
  46. 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-MPZLDOCN.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.shardConfig = config?.shard;
2083
1395
  }
2084
1396
  onStdOut(chunk, _test, _result) {
2085
1397
  if (this.reportsCount == 1 && this.showConsoleLogs) {
@@ -2096,7 +1408,7 @@ var OrtoniReport = class {
2096
1408
  );
2097
1409
  this.results.push(testResult);
2098
1410
  } catch (error) {
2099
- console.error("OrtoniReport: Error processing test end:", error);
1411
+ console.error("Ortoni Report: Error processing test end:", error);
2100
1412
  }
2101
1413
  }
2102
1414
  printsToStdio() {
@@ -2112,35 +1424,55 @@ 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.shardConfig && this.shardConfig.total > 1) {
1431
+ const shard = this.shardConfig;
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
+ userConfig: {
1439
+ projectName: this.ortoniConfig.projectName,
1440
+ authorName: this.ortoniConfig.authorName,
1441
+ type: this.ortoniConfig.testType,
1442
+ title: this.ortoniConfig.title
1443
+ },
1444
+ userMeta: {
1445
+ meta: this.ortoniConfig.meta
1446
+ }
1447
+ };
1448
+ this.fileManager.writeRawFile(shardFile, shardData);
1449
+ this.shouldGenerateReport = false;
1450
+ return;
1451
+ }
2119
1452
  const runId = await this.dbManager.saveTestRun();
2120
1453
  if (runId !== null) {
2121
1454
  await this.dbManager.saveTestResults(runId, this.results);
2122
- const html = await this.htmlGenerator.generateHTML(
1455
+ const finalReportData = await this.htmlGenerator.generateFinalReport(
2123
1456
  filteredResults,
2124
1457
  totalDuration,
2125
- cssContent,
2126
1458
  this.results,
2127
1459
  this.projectSet
2128
1460
  );
2129
1461
  this.outputPath = this.fileManager.writeReportFile(
2130
1462
  this.outputFilename,
2131
- html
1463
+ finalReportData
2132
1464
  );
2133
1465
  } else {
2134
- console.error("OrtoniReport: Error saving test run to database");
1466
+ console.error("Ortoni Report: Error saving test run to database");
2135
1467
  }
2136
1468
  } else {
2137
1469
  console.error(
2138
- "OrtoniReport: Report generation skipped due to error in Playwright worker!"
1470
+ "Ortoni Report: Report generation skipped due to error in Playwright worker!"
2139
1471
  );
2140
1472
  }
2141
1473
  } catch (error) {
2142
1474
  this.shouldGenerateReport = false;
2143
- console.error("OrtoniReport: Error generating report:", error);
1475
+ console.error("Ortoni Report: Error generating report:", error);
2144
1476
  }
2145
1477
  }
2146
1478
  async onExit() {
@@ -2148,7 +1480,7 @@ var OrtoniReport = class {
2148
1480
  await this.dbManager.close();
2149
1481
  if (this.shouldGenerateReport) {
2150
1482
  this.fileManager.copyTraceViewerAssets(this.skipTraceViewer);
2151
- console.info(`Ortoni HTML report generated at ${this.outputPath}`);
1483
+ console.info(`Ortoni Report generated at ${this.outputPath}`);
2152
1484
  this.serverManager.startServer(
2153
1485
  this.folderPath,
2154
1486
  this.outputFilename,
@@ -2158,7 +1490,7 @@ var OrtoniReport = class {
2158
1490
  });
2159
1491
  }
2160
1492
  } catch (error) {
2161
- console.error("OrtoniReport: Error in onExit:", error);
1493
+ console.error("Ortoni Report: Error in onExit:", error);
2162
1494
  }
2163
1495
  }
2164
1496
  };