ortoni-report 4.0.2 → 4.0.4

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 (37) hide show
  1. package/changelog.md +10 -0
  2. package/dist/chunk-L6VOLEP2.mjs +752 -0
  3. package/dist/cli.js +27 -21
  4. package/dist/cli.mjs +1 -1
  5. package/dist/helpers/HTMLGenerator.d.ts +89 -0
  6. package/dist/helpers/HTMLGenerator.js +164 -0
  7. package/dist/helpers/databaseManager.d.ts +35 -0
  8. package/dist/helpers/databaseManager.js +267 -0
  9. package/dist/helpers/fileManager.d.ts +8 -0
  10. package/dist/helpers/fileManager.js +60 -0
  11. package/dist/helpers/markdownConverter.d.ts +1 -0
  12. package/dist/helpers/markdownConverter.js +14 -0
  13. package/dist/helpers/resultProcessor.d.ts +10 -0
  14. package/dist/helpers/resultProcessor.js +60 -0
  15. package/dist/helpers/serverManager.d.ts +6 -0
  16. package/dist/helpers/serverManager.js +15 -0
  17. package/dist/helpers/templateLoader.d.ts +15 -0
  18. package/dist/helpers/templateLoader.js +88 -0
  19. package/dist/index.html +2 -2
  20. package/dist/mergeData.d.ts +13 -0
  21. package/dist/mergeData.js +182 -0
  22. package/dist/ortoni-report.js +72 -64
  23. package/dist/ortoni-report.mjs +2 -2
  24. package/dist/types/reporterConfig.d.ts +86 -0
  25. package/dist/types/reporterConfig.js +1 -0
  26. package/dist/types/testResults.d.ts +31 -0
  27. package/dist/types/testResults.js +1 -0
  28. package/dist/utils/attachFiles.d.ts +4 -0
  29. package/dist/utils/attachFiles.js +87 -0
  30. package/dist/utils/expressServer.d.ts +1 -0
  31. package/dist/utils/expressServer.js +61 -0
  32. package/dist/utils/groupProjects.d.ts +3 -0
  33. package/dist/utils/groupProjects.js +30 -0
  34. package/dist/utils/utils.d.ts +15 -0
  35. package/dist/utils/utils.js +93 -0
  36. package/package.json +1 -1
  37. package/readme.md +1 -1
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Merge shard JSON files into a single report while keeping ALL test results.
3
+ *
4
+ * options:
5
+ * - dir?: folder where shard files exist (default: "ortoni-report")
6
+ * - file?: output file name for final HTML (default: "ortoni-report.html")
7
+ * - saveHistory?: boolean | undefined -> if provided, overrides shard/userConfig
8
+ */
9
+ export declare function mergeAllData(options?: {
10
+ dir?: string;
11
+ file?: string;
12
+ saveHistory?: boolean;
13
+ }): Promise<void>;
@@ -0,0 +1,182 @@
1
+ // src/mergeAllData.ts
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ import { DatabaseManager } from "./helpers/databaseManager";
5
+ import { HTMLGenerator } from "./helpers/HTMLGenerator";
6
+ import { FileManager } from "./helpers/fileManager";
7
+ /**
8
+ * Merge shard JSON files into a single report while keeping ALL test results.
9
+ *
10
+ * options:
11
+ * - dir?: folder where shard files exist (default: "ortoni-report")
12
+ * - file?: output file name for final HTML (default: "ortoni-report.html")
13
+ * - saveHistory?: boolean | undefined -> if provided, overrides shard/userConfig
14
+ */
15
+ export async function mergeAllData(options = {}) {
16
+ const folderPath = options.dir || "ortoni-report";
17
+ console.info(`Ortoni Report: Merging shard files in folder: ${folderPath}`);
18
+ if (!fs.existsSync(folderPath)) {
19
+ console.error(`Ortoni Report: folder "${folderPath}" does not exist.`);
20
+ process.exitCode = 1;
21
+ return;
22
+ }
23
+ const filenames = fs
24
+ .readdirSync(folderPath)
25
+ .filter((f) => f.startsWith("ortoni-shard-") && f.endsWith(".json"));
26
+ if (filenames.length === 0) {
27
+ console.error("Ortoni Report: ❌ No shard files found to merge.");
28
+ process.exitCode = 1;
29
+ return;
30
+ }
31
+ // deterministic sort by numeric shard index if available (ortoni-shard-<current>-of-<total>.json)
32
+ const shardFileIndex = (name) => {
33
+ const m = name.match(/ortoni-shard-(\d+)-of-(\d+)\.json$/);
34
+ return m ? parseInt(m[1], 10) : null;
35
+ };
36
+ const sortedFiles = filenames
37
+ .map((f) => ({ f, idx: shardFileIndex(f) }))
38
+ .sort((a, b) => {
39
+ if (a.idx === null && b.idx === null)
40
+ return a.f.localeCompare(b.f);
41
+ if (a.idx === null)
42
+ return 1;
43
+ if (b.idx === null)
44
+ return -1;
45
+ return a.idx - b.idx;
46
+ })
47
+ .map((x) => x.f);
48
+ // Merge state
49
+ const allResults = []; // keep every test entry from every shard
50
+ const projectSet = new Set();
51
+ let totalDurationSum = 0; // sum of shard durations (ms)
52
+ let mergedUserConfig = null;
53
+ let mergedUserMeta = null;
54
+ const badShards = [];
55
+ const shardCounts = {};
56
+ const shardDurMap = {}; // used duration per shard (ms)
57
+ for (const file of sortedFiles) {
58
+ const fullPath = path.join(folderPath, file);
59
+ try {
60
+ const shardRaw = fs.readFileSync(fullPath, "utf-8");
61
+ const shardData = JSON.parse(shardRaw);
62
+ // Validate results array
63
+ if (!Array.isArray(shardData.results)) {
64
+ console.warn(`Ortoni Report: Shard ${file} missing results array — skipping.`);
65
+ badShards.push(file);
66
+ continue;
67
+ }
68
+ // Append all results (keep duplicates)
69
+ shardData.results.forEach((r) => allResults.push(r));
70
+ shardCounts[file] = shardData.results.length;
71
+ // Merge project sets
72
+ if (Array.isArray(shardData.projectSet)) {
73
+ shardData.projectSet.forEach((p) => projectSet.add(p));
74
+ }
75
+ // Duration handling:
76
+ // Accept shardData.totalDuration when it is a number (including 0).
77
+ // Fallback: if shardData.totalDuration is missing or not a number, sum per-test durations inside the shard.
78
+ const rawShardDur = shardData.totalDuration;
79
+ let durToAdd = 0;
80
+ let perTestSum = 0;
81
+ if (typeof rawShardDur === "number") {
82
+ // Accept numeric durations (including 0)
83
+ durToAdd = rawShardDur;
84
+ }
85
+ else {
86
+ // fallback: sum per-test durations (coerce to Number)
87
+ perTestSum = Array.isArray(shardData.results)
88
+ ? shardData.results.reduce((acc, t) => acc + (Number(t?.duration) || 0), 0)
89
+ : 0;
90
+ durToAdd = perTestSum;
91
+ }
92
+ // accumulate
93
+ totalDurationSum += durToAdd;
94
+ shardDurMap[file] = durToAdd;
95
+ // Merge userConfig/userMeta conservatively (prefer first non-empty value)
96
+ if (shardData.userConfig) {
97
+ if (!mergedUserConfig)
98
+ mergedUserConfig = { ...shardData.userConfig };
99
+ else {
100
+ Object.keys(shardData.userConfig).forEach((k) => {
101
+ if (mergedUserConfig[k] === undefined ||
102
+ mergedUserConfig[k] === null ||
103
+ mergedUserConfig[k] === "") {
104
+ mergedUserConfig[k] = shardData.userConfig[k];
105
+ }
106
+ else if (shardData.userConfig[k] !== mergedUserConfig[k]) {
107
+ console.warn(`Ortoni Report: userConfig mismatch for key "${k}" between shards. Using first value "${mergedUserConfig[k]}".`);
108
+ }
109
+ });
110
+ }
111
+ }
112
+ if (shardData.userMeta) {
113
+ if (!mergedUserMeta)
114
+ mergedUserMeta = { ...shardData.userMeta };
115
+ else {
116
+ mergedUserMeta.meta = {
117
+ ...(mergedUserMeta.meta || {}),
118
+ ...(shardData.userMeta.meta || {}),
119
+ };
120
+ }
121
+ }
122
+ }
123
+ catch (err) {
124
+ console.error(`Ortoni Report: Failed to parse shard ${file}:`, err);
125
+ badShards.push(file);
126
+ continue;
127
+ }
128
+ } // end for each shard
129
+ if (badShards.length > 0) {
130
+ console.warn(`Ortoni Report: Completed merge with ${badShards.length} bad shard(s) skipped:`, badShards);
131
+ }
132
+ // final results preserved with duplicates
133
+ const totalDuration = totalDurationSum; // in ms
134
+ // Determine whether to persist history:
135
+ // Priority: explicit options.saveHistory -> mergedUserConfig.saveHistory -> default true
136
+ const saveHistoryFromOptions = typeof options.saveHistory === "boolean" ? options.saveHistory : undefined;
137
+ const saveHistoryFromShard = mergedUserConfig && typeof mergedUserConfig.saveHistory === "boolean"
138
+ ? mergedUserConfig.saveHistory
139
+ : undefined;
140
+ const saveHistory = saveHistoryFromOptions ?? saveHistoryFromShard ?? true;
141
+ let dbManager;
142
+ let runId;
143
+ if (saveHistory) {
144
+ try {
145
+ dbManager = new DatabaseManager();
146
+ const dbPath = path.join(folderPath, "ortoni-data-history.sqlite");
147
+ await dbManager.initialize(dbPath);
148
+ runId = await dbManager.saveTestRun();
149
+ if (typeof runId === "number") {
150
+ // Save all results (your saveTestResults may batch internally)
151
+ await dbManager.saveTestResults(runId, allResults);
152
+ console.info(`Ortoni Report: Saved ${allResults.length} results to DB (runId=${runId}).`);
153
+ }
154
+ else {
155
+ console.warn("Ortoni Report: Failed to create test run in DB; proceeding without saving results.");
156
+ }
157
+ }
158
+ catch (err) {
159
+ console.error("Ortoni Report: Error while saving history to DB. Proceeding without DB:", err);
160
+ dbManager = undefined;
161
+ runId = undefined;
162
+ }
163
+ }
164
+ else {
165
+ console.info("Ortoni Report: Skipping history save (saveHistory=false). (Typical for CI runs)");
166
+ }
167
+ // Generate final report
168
+ const htmlGenerator = new HTMLGenerator({ ...(mergedUserConfig || {}), meta: mergedUserMeta?.meta }, dbManager);
169
+ const finalReportData = await htmlGenerator.generateFinalReport(
170
+ // filteredResults: typically filter out skipped for display (keeps existing behavior)
171
+ allResults.filter((r) => r.status !== "skipped"), totalDuration, allResults, projectSet // pass Set<string> as original generateFinalReport expects
172
+ );
173
+ // Write final HTML file
174
+ const fileManager = new FileManager(folderPath);
175
+ const outputFileName = options.file || "ortoni-report.html";
176
+ const outputPath = fileManager.writeReportFile(outputFileName, finalReportData);
177
+ // Logs & debugging summary
178
+ console.log(`✅ Final merged report generated at ${await outputPath}`);
179
+ console.log(`✅ Shards merged: ${sortedFiles.length}`);
180
+ console.log(`✅ Tests per shard:`, shardCounts);
181
+ console.log(`✅ Total tests merged ${allResults.length}`);
182
+ }
@@ -195,6 +195,67 @@ function groupResults(config, results) {
195
195
  }
196
196
  }
197
197
 
198
+ // src/utils/utils.ts
199
+ var import_path3 = __toESM(require("path"));
200
+ function normalizeFilePath(filePath) {
201
+ const normalizedPath = import_path3.default.normalize(filePath);
202
+ return import_path3.default.basename(normalizedPath);
203
+ }
204
+ function ensureHtmlExtension(filename) {
205
+ const ext = import_path3.default.extname(filename);
206
+ if (ext && ext.toLowerCase() === ".html") {
207
+ return filename;
208
+ }
209
+ return `${filename}.html`;
210
+ }
211
+ function escapeHtml(unsafe) {
212
+ if (typeof unsafe !== "string") {
213
+ return String(unsafe);
214
+ }
215
+ return unsafe.replace(/[&<"']/g, function(match) {
216
+ const escapeMap = {
217
+ "&": "&amp;",
218
+ "<": "&lt;",
219
+ ">": "&gt;",
220
+ '"': "&quot;",
221
+ "'": "&#039;"
222
+ };
223
+ return escapeMap[match] || match;
224
+ });
225
+ }
226
+ function formatDateLocal(dateInput) {
227
+ if (!dateInput) return "N/A";
228
+ try {
229
+ const date = typeof dateInput === "string" ? new Date(dateInput) : dateInput;
230
+ if (isNaN(date.getTime())) {
231
+ return "N/A";
232
+ }
233
+ const options = {
234
+ year: "numeric",
235
+ month: "short",
236
+ day: "2-digit",
237
+ hour: "2-digit",
238
+ minute: "2-digit",
239
+ hour12: true
240
+ };
241
+ return new Intl.DateTimeFormat("en-US", options).format(date);
242
+ } catch (e) {
243
+ return "N/A";
244
+ }
245
+ }
246
+ function extractSuites(titlePath) {
247
+ const tagPattern = /@[\w]+/g;
248
+ const suiteParts = titlePath.slice(3, titlePath.length - 1).map((p) => p.replace(tagPattern, "").trim());
249
+ return {
250
+ hierarchy: suiteParts.join(" > "),
251
+ // full hierarchy
252
+ topLevelSuite: suiteParts[0] ?? "",
253
+ // first suite
254
+ parentSuite: suiteParts[suiteParts.length - 1] ?? ""
255
+ // last suite
256
+ };
257
+ }
258
+
198
259
  // src/helpers/HTMLGenerator.ts
199
260
  var HTMLGenerator = class {
200
261
  constructor(ortoniConfig, dbManager) {
@@ -268,7 +329,7 @@ var HTMLGenerator = class {
268
329
  results,
269
330
  projectSet
270
331
  );
271
- const lastRunDate = (/* @__PURE__ */ new Date()).toLocaleString();
332
+ const lastRunDate = formatDateLocal(/* @__PURE__ */ new Date());
272
333
  const testHistories = await Promise.all(
273
334
  results.map(async (result) => {
274
335
  const testId = `${result.filePath}:${result.projectName}:${result.title}`;
@@ -375,7 +436,7 @@ var import_ansi_to_html = __toESM(require("ansi-to-html"));
375
436
  var import_path5 = __toESM(require("path"));
376
437
 
377
438
  // src/utils/attachFiles.ts
378
- var import_path3 = __toESM(require("path"));
439
+ var import_path4 = __toESM(require("path"));
379
440
  var import_fs4 = __toESM(require("fs"));
380
441
 
381
442
  // src/helpers/markdownConverter.ts
@@ -1518,7 +1579,7 @@ function convertMarkdownToHtml(markdownPath, htmlOutputPath) {
1518
1579
  // src/utils/attachFiles.ts
1519
1580
  function attachFiles(subFolder, result, testResult, config, steps, errors) {
1520
1581
  const folderPath = config.folderPath || "ortoni-report";
1521
- const attachmentsFolder = import_path3.default.join(
1582
+ const attachmentsFolder = import_path4.default.join(
1522
1583
  folderPath,
1523
1584
  "ortoni-data",
1524
1585
  "attachments",
@@ -1534,14 +1595,14 @@ function attachFiles(subFolder, result, testResult, config, steps, errors) {
1534
1595
  result.attachments.forEach((attachment) => {
1535
1596
  const { contentType, name, path: attachmentPath, body } = attachment;
1536
1597
  if (!attachmentPath && !body) return;
1537
- const fileName = attachmentPath ? import_path3.default.basename(attachmentPath) : `${name}.${getFileExtension(contentType)}`;
1538
- const relativePath = import_path3.default.join(
1598
+ const fileName = attachmentPath ? import_path4.default.basename(attachmentPath) : `${name}.${getFileExtension(contentType)}`;
1599
+ const relativePath = import_path4.default.join(
1539
1600
  "ortoni-data",
1540
1601
  "attachments",
1541
1602
  subFolder,
1542
1603
  fileName
1543
1604
  );
1544
- const fullPath = import_path3.default.join(attachmentsFolder, fileName);
1605
+ const fullPath = import_path4.default.join(attachmentsFolder, fileName);
1545
1606
  if (contentType === "image/png") {
1546
1607
  handleImage(
1547
1608
  attachmentPath,
@@ -1632,61 +1693,6 @@ function getFileExtension(contentType) {
1632
1693
  return extensions[contentType] || "unknown";
1633
1694
  }
1634
1695
 
1635
- // src/utils/utils.ts
1636
- var import_path4 = __toESM(require("path"));
1637
- function normalizeFilePath(filePath) {
1638
- const normalizedPath = import_path4.default.normalize(filePath);
1639
- return import_path4.default.basename(normalizedPath);
1640
- }
1641
- function ensureHtmlExtension(filename) {
1642
- const ext = import_path4.default.extname(filename);
1643
- if (ext && ext.toLowerCase() === ".html") {
1644
- return filename;
1645
- }
1646
- return `${filename}.html`;
1647
- }
1648
- function escapeHtml(unsafe) {
1649
- if (typeof unsafe !== "string") {
1650
- return String(unsafe);
1651
- }
1652
- return unsafe.replace(/[&<"']/g, function(match) {
1653
- const escapeMap = {
1654
- "&": "&amp;",
1655
- "<": "&lt;",
1656
- ">": "&gt;",
1657
- '"': "&quot;",
1658
- "'": "&#039;"
1659
- };
1660
- return escapeMap[match] || match;
1661
- });
1662
- }
1663
- function formatDateLocal(dateInput) {
1664
- const date = typeof dateInput === "string" ? new Date(dateInput) : dateInput;
1665
- const options = {
1666
- year: "numeric",
1667
- month: "short",
1668
- day: "2-digit",
1669
- hour: "2-digit",
1670
- minute: "2-digit",
1671
- hour12: true,
1672
- timeZoneName: "short"
1673
- // or "Asia/Kolkata"
1674
- };
1675
- return new Intl.DateTimeFormat(void 0, options).format(date);
1676
- }
1677
- function extractSuites(titlePath) {
1678
- const tagPattern = /@[\w]+/g;
1679
- const suiteParts = titlePath.slice(3, titlePath.length - 1).map((p) => p.replace(tagPattern, "").trim());
1680
- return {
1681
- hierarchy: suiteParts.join(" > "),
1682
- // full hierarchy
1683
- topLevelSuite: suiteParts[0] ?? "",
1684
- // first suite
1685
- parentSuite: suiteParts[suiteParts.length - 1] ?? ""
1686
- // last suite
1687
- };
1688
- }
1689
-
1690
1696
  // src/helpers/resultProcessor.ts
1691
1697
  var TestResultProcessor = class {
1692
1698
  constructor(projectRoot) {
@@ -1732,7 +1738,7 @@ var TestResultProcessor = class {
1732
1738
  testId: `${filePath}:${projectName}:${title}`
1733
1739
  };
1734
1740
  attachFiles(
1735
- test.id,
1741
+ import_path5.default.join(test.id, `retry-${result.retry}`),
1736
1742
  result,
1737
1743
  testResult,
1738
1744
  ortoniConfig,
@@ -1961,7 +1967,8 @@ var DatabaseManager = class {
1961
1967
  );
1962
1968
  return results.map((result) => ({
1963
1969
  ...result,
1964
- run_date: formatDateLocal(result.run_date)
1970
+ run_date: result.run_date
1971
+ // Return raw ISO string to avoid parsing issues in browser
1965
1972
  }));
1966
1973
  } catch (error) {
1967
1974
  console.error("OrtoniReport: Error getting test history:", error);
@@ -2044,7 +2051,8 @@ var DatabaseManager = class {
2044
2051
  );
2045
2052
  return rows.reverse().map((row) => ({
2046
2053
  ...row,
2047
- run_date: formatDateLocal(row.run_date),
2054
+ run_date: row.run_date,
2055
+ // Return raw ISO string for chart compatibility
2048
2056
  avg_duration: Math.round(row.avg_duration || 0)
2049
2057
  // raw ms avg
2050
2058
  }));
@@ -8,7 +8,7 @@ import {
8
8
  extractSuites,
9
9
  normalizeFilePath,
10
10
  startReportServer
11
- } from "./chunk-4RZ5C7KY.mjs";
11
+ } from "./chunk-L6VOLEP2.mjs";
12
12
 
13
13
  // src/helpers/resultProcessor.ts
14
14
  import AnsiToHtml from "ansi-to-html";
@@ -1317,7 +1317,7 @@ var TestResultProcessor = class {
1317
1317
  testId: `${filePath}:${projectName}:${title}`
1318
1318
  };
1319
1319
  attachFiles(
1320
- test.id,
1320
+ path2.join(test.id, `retry-${result.retry}`),
1321
1321
  result,
1322
1322
  testResult,
1323
1323
  ortoniConfig,
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Configuration options for OrtoniReport.
3
+ */
4
+ export interface OrtoniReportConfig {
5
+ /**
6
+ * Open the report in local host (Trace viewer is accessible only in localhost)
7
+ * @example "always"| "never"| "on-failure";
8
+ * @default "never"
9
+ */
10
+ open?: "always" | "never" | "on-failure";
11
+ /**
12
+ * The title of the HTML report.
13
+ * @example "Ortoni Playwright Test Report"
14
+ */
15
+ title?: string;
16
+ /**
17
+ * Add project on the list of the tests? (Filtering projects still works if hidden)
18
+ * @example true to display, false to hide.
19
+ * @default false
20
+ */
21
+ showProject?: boolean;
22
+ /**
23
+ * The name of the project.
24
+ * @example "Ortoni Project"
25
+ */
26
+ projectName?: string;
27
+ /**
28
+ * The name of the author.
29
+ * @example "Koushik Chatterjee"
30
+ */
31
+ authorName?: string;
32
+ /**
33
+ * The type of tests being run.
34
+ * @example "Regression"
35
+ */
36
+ testType?: string;
37
+ /**
38
+ * If true, images will be encoded in base64.
39
+ * Not recommended to use if many screenshots are present.
40
+ * @default false
41
+ * @example true
42
+ */
43
+ base64Image?: boolean;
44
+ /**
45
+ * The local relative of the logo image.
46
+ * Recommended to keep within the report genrated folder.
47
+ * @default "ortoni-report/logo.png"
48
+ * @example "logo.png"
49
+ */
50
+ logo?: string;
51
+ /**
52
+ * The filename to the html report.
53
+ * @example "index.html"
54
+ * @default "ortoni-report.html"
55
+ */
56
+ filename?: string;
57
+ /**
58
+ * The folder name.
59
+ * @example "report"
60
+ * @default "ortoni-report"
61
+ */
62
+ folderPath?: string;
63
+ /**
64
+ * Port to connect
65
+ * @example 3600
66
+ */
67
+ port?: number;
68
+ /**
69
+ * Display console logs?
70
+ * @example boolean
71
+ * @default true
72
+ */
73
+ stdIO?: boolean;
74
+ /**
75
+ * Metadata for the report. ['Test Cycle': 'Cycle 1', 'Test Environment':'QA', etc]
76
+ * @example { "key": "value" } as string
77
+ */
78
+ meta?: Record<string, string>;
79
+ /**
80
+ * Save the history of the reports in a SQL file to be used in future reports.
81
+ * The history file will be saved in the report folder.
82
+ * @default true
83
+ * @example false (to disable)
84
+ */
85
+ saveHistory?: boolean;
86
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,31 @@
1
+ export interface Steps {
2
+ snippet: string | undefined;
3
+ title: string;
4
+ location: string;
5
+ }
6
+ export interface TestResultData {
7
+ suiteHierarchy: string;
8
+ key: string;
9
+ annotations: any[];
10
+ testTags: string[];
11
+ location: string;
12
+ retryAttemptCount: number;
13
+ projectName: any;
14
+ suite: any;
15
+ title: string;
16
+ status: "passed" | "failed" | "timedOut" | "skipped" | "interrupted" | "expected" | "unexpected" | "flaky";
17
+ flaky: string;
18
+ duration: number;
19
+ errors: any[];
20
+ steps: Steps[];
21
+ logs: string;
22
+ screenshotPath?: string | null | undefined;
23
+ screenshots?: string[];
24
+ filePath: string;
25
+ filters: Set<string>;
26
+ tracePath?: string;
27
+ videoPath?: string[];
28
+ markdownPath?: string;
29
+ base64Image: boolean | undefined;
30
+ testId: string;
31
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ import { TestResult } from "@playwright/test/reporter";
2
+ import { Steps, TestResultData } from "../types/testResults";
3
+ import { OrtoniReportConfig } from "../types/reporterConfig";
4
+ export declare function attachFiles(subFolder: string, result: TestResult, testResult: TestResultData, config: OrtoniReportConfig, steps?: Steps[], errors?: string[]): void;
@@ -0,0 +1,87 @@
1
+ import path from "path";
2
+ import fs from "fs";
3
+ import { convertMarkdownToHtml } from "../helpers/markdownConverter";
4
+ export function attachFiles(subFolder, result, testResult, config, steps, errors) {
5
+ const folderPath = config.folderPath || "ortoni-report";
6
+ const attachmentsFolder = path.join(folderPath, "ortoni-data", "attachments", subFolder);
7
+ if (!fs.existsSync(attachmentsFolder)) {
8
+ fs.mkdirSync(attachmentsFolder, { recursive: true });
9
+ }
10
+ if (!result.attachments)
11
+ return;
12
+ const { base64Image } = config;
13
+ testResult.screenshots = [];
14
+ testResult.videoPath = [];
15
+ result.attachments.forEach((attachment) => {
16
+ const { contentType, name, path: attachmentPath, body } = attachment;
17
+ if (!attachmentPath && !body)
18
+ return;
19
+ const fileName = attachmentPath
20
+ ? path.basename(attachmentPath)
21
+ : `${name}.${getFileExtension(contentType)}`;
22
+ const relativePath = path.join("ortoni-data", "attachments", subFolder, fileName);
23
+ const fullPath = path.join(attachmentsFolder, fileName);
24
+ if (contentType === "image/png") {
25
+ handleImage(attachmentPath, body, base64Image, fullPath, relativePath, testResult);
26
+ }
27
+ else if (name === "video") {
28
+ handleAttachment(attachmentPath, fullPath, relativePath, "videoPath", testResult);
29
+ }
30
+ else if (name === "trace") {
31
+ handleAttachment(attachmentPath, fullPath, relativePath, "tracePath", testResult);
32
+ }
33
+ else if (name === "error-context") {
34
+ handleAttachment(attachmentPath, fullPath, relativePath, "markdownPath", testResult, steps, errors);
35
+ }
36
+ });
37
+ }
38
+ function handleImage(attachmentPath, body, base64Image, fullPath, relativePath, testResult) {
39
+ let screenshotPath = "";
40
+ if (attachmentPath) {
41
+ try {
42
+ const screenshotContent = fs.readFileSync(attachmentPath, base64Image ? "base64" : undefined);
43
+ screenshotPath = base64Image
44
+ ? `data:image/png;base64,${screenshotContent}`
45
+ : relativePath;
46
+ if (!base64Image) {
47
+ fs.copyFileSync(attachmentPath, fullPath);
48
+ }
49
+ }
50
+ catch (error) {
51
+ console.error(`OrtoniReport: Failed to read screenshot file: ${attachmentPath}`, error);
52
+ }
53
+ }
54
+ else if (body) {
55
+ screenshotPath = `data:image/png;base64,${body.toString("base64")}`;
56
+ }
57
+ if (screenshotPath) {
58
+ testResult.screenshots?.push(screenshotPath);
59
+ }
60
+ }
61
+ function handleAttachment(attachmentPath, fullPath, relativePath, resultKey, testResult, steps, errors) {
62
+ if (attachmentPath) {
63
+ fs.copyFileSync(attachmentPath, fullPath);
64
+ if (resultKey === "videoPath") {
65
+ testResult[resultKey]?.push(relativePath);
66
+ }
67
+ else if (resultKey === "tracePath") {
68
+ testResult[resultKey] = relativePath;
69
+ }
70
+ }
71
+ if (resultKey === "markdownPath" && errors) {
72
+ const htmlPath = fullPath.replace(/\.md$/, ".html");
73
+ const htmlRelativePath = relativePath.replace(/\.md$/, ".html");
74
+ convertMarkdownToHtml(fullPath, htmlPath);
75
+ testResult[resultKey] = htmlRelativePath;
76
+ return;
77
+ }
78
+ }
79
+ function getFileExtension(contentType) {
80
+ const extensions = {
81
+ "image/png": "png",
82
+ "video/webm": "webm",
83
+ "application/zip": "zip",
84
+ "text/markdown": "md",
85
+ };
86
+ return extensions[contentType] || "unknown";
87
+ }
@@ -0,0 +1 @@
1
+ export declare function startReportServer(reportFolder: string, reportFilename: string, port: number, open: string | undefined): void;