@arghajit/dummy 0.3.0 → 0.3.2
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.
- package/README.md +36 -0
- package/package.json +1 -1
- package/scripts/generate-email-report.mjs +21 -1
- package/scripts/generate-report.mjs +297 -126
- package/scripts/generate-static-report.mjs +115 -9
- package/scripts/generate-trend.mjs +11 -1
- package/scripts/merge-pulse-report.js +46 -14
- package/scripts/sendReport.mjs +36 -21
package/README.md
CHANGED
|
@@ -84,6 +84,42 @@ npx generate-pulse-report # Generates static HTML
|
|
|
84
84
|
npx send-email # Sends email report
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
+
### 4. Custom Output Directory (Optional)
|
|
88
|
+
|
|
89
|
+
All CLI scripts now support custom output directories, giving you full flexibility over where reports are generated:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# Using custom directory
|
|
93
|
+
npx generate-pulse-report --outputDir my-reports
|
|
94
|
+
npx generate-report -o test-results/e2e
|
|
95
|
+
npx send-email --outputDir custom-pulse-reports
|
|
96
|
+
|
|
97
|
+
# Using nested paths
|
|
98
|
+
npx generate-pulse-report --outputDir reports/integration
|
|
99
|
+
npx merge-pulse-report --outputDir my-test-reports
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**Important:** Make sure your `playwright.config.ts` custom directory matches the CLI script:
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
import { defineConfig } from "@playwright/test";
|
|
106
|
+
import * as path from "path";
|
|
107
|
+
|
|
108
|
+
const CUSTOM_REPORT_DIR = path.resolve(__dirname, "my-reports");
|
|
109
|
+
|
|
110
|
+
export default defineConfig({
|
|
111
|
+
reporter: [
|
|
112
|
+
["list"],
|
|
113
|
+
[
|
|
114
|
+
"@arghajit/playwright-pulse-report",
|
|
115
|
+
{
|
|
116
|
+
outputDir: CUSTOM_REPORT_DIR, // Must match CLI --outputDir
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
],
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
87
123
|
## 📊 Report Options
|
|
88
124
|
|
|
89
125
|
### Option 1: Static HTML Report (Embedded Attachments)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arghajit/dummy",
|
|
3
3
|
"author": "Arghajit Singha",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.2",
|
|
5
5
|
"description": "A Playwright reporter and dashboard for visualizing test results.",
|
|
6
6
|
"homepage": "https://playwright-pulse-report.netlify.app/",
|
|
7
7
|
"keywords": [
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as fs from "fs/promises";
|
|
4
4
|
import path from "path";
|
|
5
|
+
import { getOutputDir } from "./config-reader.mjs";
|
|
5
6
|
|
|
6
7
|
// Use dynamic import for chalk as it's ESM only
|
|
7
8
|
let chalk;
|
|
@@ -23,6 +24,15 @@ const DEFAULT_OUTPUT_DIR = "pulse-report";
|
|
|
23
24
|
const DEFAULT_JSON_FILE = "playwright-pulse-report.json";
|
|
24
25
|
const MINIFIED_HTML_FILE = "pulse-email-summary.html"; // New minified report
|
|
25
26
|
|
|
27
|
+
const args = process.argv.slice(2);
|
|
28
|
+
let customOutputDir = null;
|
|
29
|
+
for (let i = 0; i < args.length; i++) {
|
|
30
|
+
if (args[i] === "--outputDir" || args[i] === "-o") {
|
|
31
|
+
customOutputDir = args[i + 1];
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
26
36
|
function sanitizeHTML(str) {
|
|
27
37
|
if (str === null || str === undefined) return "";
|
|
28
38
|
return String(str).replace(/[&<>"']/g, (match) => {
|
|
@@ -652,10 +662,20 @@ function generateMinifiedHTML(reportData) {
|
|
|
652
662
|
`;
|
|
653
663
|
}
|
|
654
664
|
async function main() {
|
|
655
|
-
const outputDir =
|
|
665
|
+
const outputDir = await getOutputDir(customOutputDir);
|
|
656
666
|
const reportJsonPath = path.resolve(outputDir, DEFAULT_JSON_FILE);
|
|
657
667
|
const minifiedReportHtmlPath = path.resolve(outputDir, MINIFIED_HTML_FILE); // Path for the new minified HTML
|
|
658
668
|
|
|
669
|
+
console.log(chalk.blue(`Generating email report...`));
|
|
670
|
+
console.log(chalk.blue(`Output directory set to: ${outputDir}`));
|
|
671
|
+
if (customOutputDir) {
|
|
672
|
+
console.log(chalk.gray(` (from CLI argument)`));
|
|
673
|
+
} else {
|
|
674
|
+
console.log(
|
|
675
|
+
chalk.gray(` (auto-detected from playwright.config or using default)`)
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
|
|
659
679
|
// Step 2: Load current run's data
|
|
660
680
|
let currentRunReportData;
|
|
661
681
|
try {
|
|
@@ -5,6 +5,7 @@ import { readFileSync, existsSync as fsExistsSync } from "fs";
|
|
|
5
5
|
import path from "path";
|
|
6
6
|
import { fork } from "child_process";
|
|
7
7
|
import { fileURLToPath } from "url";
|
|
8
|
+
import { getOutputDir } from "./config-reader.mjs";
|
|
8
9
|
|
|
9
10
|
// Use dynamic import for chalk as it's ESM only
|
|
10
11
|
let chalk;
|
|
@@ -615,8 +616,9 @@ function generatePieChart(data, chartWidth = 300, chartHeight = 300) {
|
|
|
615
616
|
chart: {
|
|
616
617
|
type: 'pie',
|
|
617
618
|
width: ${chartWidth},
|
|
618
|
-
height: ${
|
|
619
|
-
|
|
619
|
+
height: ${
|
|
620
|
+
chartHeight - 40
|
|
621
|
+
}, // Adjusted height to make space for legend if chartHeight is for the whole wrapper
|
|
620
622
|
backgroundColor: 'transparent',
|
|
621
623
|
plotShadow: false,
|
|
622
624
|
spacingBottom: 40 // Ensure space for legend
|
|
@@ -668,8 +670,9 @@ function generatePieChart(data, chartWidth = 300, chartHeight = 300) {
|
|
|
668
670
|
return `
|
|
669
671
|
<div class="pie-chart-wrapper" style="align-items: center; max-height: 450px">
|
|
670
672
|
<div style="display: flex; align-items: start; width: 100%;"><h3>Test Distribution</h3></div>
|
|
671
|
-
<div id="${chartId}" style="width: ${chartWidth}px; height: ${
|
|
672
|
-
|
|
673
|
+
<div id="${chartId}" style="width: ${chartWidth}px; height: ${
|
|
674
|
+
chartHeight - 40
|
|
675
|
+
}px;"></div>
|
|
673
676
|
<script>
|
|
674
677
|
document.addEventListener('DOMContentLoaded', function() {
|
|
675
678
|
if (typeof Highcharts !== 'undefined') {
|
|
@@ -897,14 +900,15 @@ function generateEnvironmentDashboard(environment, dashboardHeight = 600) {
|
|
|
897
900
|
<span class="env-detail-value">
|
|
898
901
|
<div class="env-cpu-cores">
|
|
899
902
|
${Array.from(
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
903
|
+
{ length: Math.max(0, environment.cpu.cores || 0) },
|
|
904
|
+
(_, i) =>
|
|
905
|
+
`<div class="env-core-indicator ${
|
|
906
|
+
i >=
|
|
907
|
+
(environment.cpu.cores >= 8 ? 8 : environment.cpu.cores)
|
|
908
|
+
? "inactive"
|
|
909
|
+
: ""
|
|
910
|
+
}" title="Core ${i + 1}"></div>`
|
|
911
|
+
).join("")}
|
|
908
912
|
<span>${environment.cpu.cores || "N/A"} cores</span>
|
|
909
913
|
</div>
|
|
910
914
|
</span>
|
|
@@ -924,20 +928,23 @@ function generateEnvironmentDashboard(environment, dashboardHeight = 600) {
|
|
|
924
928
|
<div class="env-card-content">
|
|
925
929
|
<div class="env-detail-row">
|
|
926
930
|
<span class="env-detail-label">OS Type</span>
|
|
927
|
-
<span class="env-detail-value">${
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
+
<span class="env-detail-value">${
|
|
932
|
+
environment.os.split(" ")[0] === "darwin"
|
|
933
|
+
? "darwin (macOS)"
|
|
934
|
+
: environment.os.split(" ")[0] || "Unknown"
|
|
935
|
+
}</span>
|
|
931
936
|
</div>
|
|
932
937
|
<div class="env-detail-row">
|
|
933
938
|
<span class="env-detail-label">OS Version</span>
|
|
934
|
-
<span class="env-detail-value">${
|
|
935
|
-
|
|
939
|
+
<span class="env-detail-value">${
|
|
940
|
+
environment.os.split(" ")[1] || "N/A"
|
|
941
|
+
}</span>
|
|
936
942
|
</div>
|
|
937
943
|
<div class="env-detail-row">
|
|
938
944
|
<span class="env-detail-label">Hostname</span>
|
|
939
|
-
<span class="env-detail-value" title="${environment.host}">${
|
|
940
|
-
|
|
945
|
+
<span class="env-detail-value" title="${environment.host}">${
|
|
946
|
+
environment.host
|
|
947
|
+
}</span>
|
|
941
948
|
</div>
|
|
942
949
|
</div>
|
|
943
950
|
</div>
|
|
@@ -958,10 +965,11 @@ function generateEnvironmentDashboard(environment, dashboardHeight = 600) {
|
|
|
958
965
|
</div>
|
|
959
966
|
<div class="env-detail-row">
|
|
960
967
|
<span class="env-detail-label">Working Dir</span>
|
|
961
|
-
<span class="env-detail-value" title="${environment.cwd}">${
|
|
968
|
+
<span class="env-detail-value" title="${environment.cwd}">${
|
|
969
|
+
environment.cwd.length > 25
|
|
962
970
|
? "..." + environment.cwd.slice(-22)
|
|
963
971
|
: environment.cwd
|
|
964
|
-
|
|
972
|
+
}</span>
|
|
965
973
|
</div>
|
|
966
974
|
</div>
|
|
967
975
|
</div>
|
|
@@ -975,30 +983,33 @@ function generateEnvironmentDashboard(environment, dashboardHeight = 600) {
|
|
|
975
983
|
<div class="env-detail-row">
|
|
976
984
|
<span class="env-detail-label">Platform Arch</span>
|
|
977
985
|
<span class="env-detail-value">
|
|
978
|
-
<span class="env-chip ${
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
986
|
+
<span class="env-chip ${
|
|
987
|
+
environment.os.includes("darwin") &&
|
|
988
|
+
environment.cpu.model.toLowerCase().includes("apple")
|
|
989
|
+
? "env-chip-success"
|
|
990
|
+
: "env-chip-warning"
|
|
991
|
+
}">
|
|
992
|
+
${
|
|
993
|
+
environment.os.includes("darwin") &&
|
|
994
|
+
environment.cpu.model.toLowerCase().includes("apple")
|
|
995
|
+
? "Apple Silicon"
|
|
996
|
+
: environment.cpu.model.toLowerCase().includes("arm") ||
|
|
997
|
+
environment.cpu.model.toLowerCase().includes("aarch64")
|
|
998
|
+
? "ARM-based"
|
|
999
|
+
: "x86/Other"
|
|
1000
|
+
}
|
|
991
1001
|
</span>
|
|
992
1002
|
</span>
|
|
993
1003
|
</div>
|
|
994
1004
|
<div class="env-detail-row">
|
|
995
1005
|
<span class="env-detail-label">Memory per Core</span>
|
|
996
|
-
<span class="env-detail-value">${
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1006
|
+
<span class="env-detail-value">${
|
|
1007
|
+
environment.cpu.cores > 0
|
|
1008
|
+
? (
|
|
1009
|
+
parseFloat(environment.memory) / environment.cpu.cores
|
|
1010
|
+
).toFixed(2) + " GB"
|
|
1011
|
+
: "N/A"
|
|
1012
|
+
}</span>
|
|
1002
1013
|
</div>
|
|
1003
1014
|
<div class="env-detail-row">
|
|
1004
1015
|
<span class="env-detail-label">Run Context</span>
|
|
@@ -1330,19 +1341,19 @@ function generateTestHistoryContent(trendData) {
|
|
|
1330
1341
|
|
|
1331
1342
|
<div class="test-history-grid">
|
|
1332
1343
|
${testHistory
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1344
|
+
.map((test) => {
|
|
1345
|
+
const latestRun =
|
|
1346
|
+
test.history.length > 0
|
|
1347
|
+
? test.history[test.history.length - 1]
|
|
1348
|
+
: { status: "unknown" };
|
|
1349
|
+
return `
|
|
1339
1350
|
<div class="test-history-card" data-test-name="${sanitizeHTML(
|
|
1340
|
-
|
|
1341
|
-
|
|
1351
|
+
test.testTitle.toLowerCase()
|
|
1352
|
+
)}" data-latest-status="${latestRun.status}">
|
|
1342
1353
|
<div class="test-history-header">
|
|
1343
1354
|
<p title="${sanitizeHTML(test.testTitle)}">${capitalize(
|
|
1344
|
-
|
|
1345
|
-
|
|
1355
|
+
sanitizeHTML(test.testTitle)
|
|
1356
|
+
)}</p>
|
|
1346
1357
|
<span class="status-badge ${getStatusClass(latestRun.status)}">
|
|
1347
1358
|
${String(latestRun.status).toUpperCase()}
|
|
1348
1359
|
</span>
|
|
@@ -1357,27 +1368,27 @@ function generateTestHistoryContent(trendData) {
|
|
|
1357
1368
|
<thead><tr><th>Run</th><th>Status</th><th>Duration</th><th>Date</th></tr></thead>
|
|
1358
1369
|
<tbody>
|
|
1359
1370
|
${test.history
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1371
|
+
.slice()
|
|
1372
|
+
.reverse()
|
|
1373
|
+
.map(
|
|
1374
|
+
(run) => `
|
|
1364
1375
|
<tr>
|
|
1365
1376
|
<td>${run.runId}</td>
|
|
1366
1377
|
<td><span class="status-badge-small ${getStatusClass(
|
|
1367
|
-
|
|
1368
|
-
|
|
1378
|
+
run.status
|
|
1379
|
+
)}">${String(run.status).toUpperCase()}</span></td>
|
|
1369
1380
|
<td>${formatDuration(run.duration)}</td>
|
|
1370
1381
|
<td>${formatDate(run.timestamp)}</td>
|
|
1371
1382
|
</tr>`
|
|
1372
|
-
|
|
1373
|
-
|
|
1383
|
+
)
|
|
1384
|
+
.join("")}
|
|
1374
1385
|
</tbody>
|
|
1375
1386
|
</table>
|
|
1376
1387
|
</div>
|
|
1377
1388
|
</details>
|
|
1378
1389
|
</div>`;
|
|
1379
|
-
|
|
1380
|
-
|
|
1390
|
+
})
|
|
1391
|
+
.join("")}
|
|
1381
1392
|
</div>
|
|
1382
1393
|
</div>
|
|
1383
1394
|
`;
|
|
@@ -1462,11 +1473,12 @@ function generateSuitesWidget(suitesData) {
|
|
|
1462
1473
|
<div class="suites-widget">
|
|
1463
1474
|
<div class="suites-header">
|
|
1464
1475
|
<h2>Test Suites</h2>
|
|
1465
|
-
<span class="summary-badge">${
|
|
1476
|
+
<span class="summary-badge">${
|
|
1477
|
+
suitesData.length
|
|
1466
1478
|
} suites • ${suitesData.reduce(
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1479
|
+
(sum, suite) => sum + suite.count,
|
|
1480
|
+
0
|
|
1481
|
+
)} tests</span>
|
|
1470
1482
|
</div>
|
|
1471
1483
|
<div class="suites-grid">
|
|
1472
1484
|
${suitesData
|
|
@@ -1479,24 +1491,28 @@ function generateSuitesWidget(suitesData) {
|
|
|
1479
1491
|
)} (${sanitizeHTML(suite.browser)})">${sanitizeHTML(suite.name)}</h3>
|
|
1480
1492
|
</div>
|
|
1481
1493
|
<div>🖥️ <span class="browser-tag">${sanitizeHTML(
|
|
1482
|
-
|
|
1483
|
-
|
|
1494
|
+
suite.browser
|
|
1495
|
+
)}</span></div>
|
|
1484
1496
|
<div class="suite-card-body">
|
|
1485
|
-
<span class="test-count">${suite.count} test${
|
|
1486
|
-
|
|
1497
|
+
<span class="test-count">${suite.count} test${
|
|
1498
|
+
suite.count !== 1 ? "s" : ""
|
|
1499
|
+
}</span>
|
|
1487
1500
|
<div class="suite-stats">
|
|
1488
|
-
${
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1501
|
+
${
|
|
1502
|
+
suite.passed > 0
|
|
1503
|
+
? `<span class="stat-passed" title="Passed"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-check-circle-fill" viewBox="0 0 16 16"><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"/></svg> ${suite.passed}</span>`
|
|
1504
|
+
: ""
|
|
1505
|
+
}
|
|
1506
|
+
${
|
|
1507
|
+
suite.failed > 0
|
|
1508
|
+
? `<span class="stat-failed" title="Failed"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-x-circle-fill" viewBox="0 0 16 16"><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z"/></svg> ${suite.failed}</span>`
|
|
1509
|
+
: ""
|
|
1510
|
+
}
|
|
1511
|
+
${
|
|
1512
|
+
suite.skipped > 0
|
|
1513
|
+
? `<span class="stat-skipped" title="Skipped"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-exclamation-triangle-fill" viewBox="0 0 16 16"><path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/></svg> ${suite.skipped}</span>`
|
|
1514
|
+
: ""
|
|
1515
|
+
}
|
|
1500
1516
|
</div>
|
|
1501
1517
|
</div>
|
|
1502
1518
|
</div>`
|
|
@@ -1519,7 +1535,9 @@ function getAttachmentIcon(contentType) {
|
|
|
1519
1535
|
return "📎";
|
|
1520
1536
|
}
|
|
1521
1537
|
function generateAIFailureAnalyzerTab(results) {
|
|
1522
|
-
const failedTests = (results || []).filter(
|
|
1538
|
+
const failedTests = (results || []).filter(
|
|
1539
|
+
(test) => test.status === "failed"
|
|
1540
|
+
);
|
|
1523
1541
|
|
|
1524
1542
|
if (failedTests.length === 0) {
|
|
1525
1543
|
return `
|
|
@@ -1529,7 +1547,7 @@ function generateAIFailureAnalyzerTab(results) {
|
|
|
1529
1547
|
}
|
|
1530
1548
|
|
|
1531
1549
|
// btoa is not available in Node.js environment, so we define a simple polyfill for it.
|
|
1532
|
-
const btoa = (str) => Buffer.from(str).toString(
|
|
1550
|
+
const btoa = (str) => Buffer.from(str).toString("base64");
|
|
1533
1551
|
|
|
1534
1552
|
return `
|
|
1535
1553
|
<h2 class="tab-main-title">AI Failure Analysis</h2>
|
|
@@ -1539,41 +1557,61 @@ function generateAIFailureAnalyzerTab(results) {
|
|
|
1539
1557
|
<span class="stat-label">Failed Tests</span>
|
|
1540
1558
|
</div>
|
|
1541
1559
|
<div class="stat-item">
|
|
1542
|
-
<span class="stat-number">${
|
|
1560
|
+
<span class="stat-number">${
|
|
1561
|
+
new Set(failedTests.map((t) => t.browser)).size
|
|
1562
|
+
}</span>
|
|
1543
1563
|
<span class="stat-label">Browsers</span>
|
|
1544
1564
|
</div>
|
|
1545
1565
|
<div class="stat-item">
|
|
1546
|
-
<span class="stat-number">${
|
|
1566
|
+
<span class="stat-number">${Math.round(
|
|
1567
|
+
failedTests.reduce((sum, test) => sum + (test.duration || 0), 0) /
|
|
1568
|
+
1000
|
|
1569
|
+
)}s</span>
|
|
1547
1570
|
<span class="stat-label">Total Duration</span>
|
|
1548
1571
|
</div>
|
|
1549
1572
|
</div>
|
|
1550
1573
|
<p class="ai-analyzer-description">
|
|
1551
|
-
Analyze failed tests using AI to get suggestions and potential fixes. Click the AI Fix button for
|
|
1574
|
+
Analyze failed tests using AI to get suggestions and potential fixes. Click the AI Fix button for instant analysis or use Copy AI Prompt to analyze with your preferred AI tool.
|
|
1552
1575
|
</p>
|
|
1553
1576
|
|
|
1554
1577
|
<div class="compact-failure-list">
|
|
1555
|
-
${failedTests
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1578
|
+
${failedTests
|
|
1579
|
+
.map((test) => {
|
|
1580
|
+
const testTitle = test.name.split(" > ").pop() || "Unnamed Test";
|
|
1581
|
+
const testJson = btoa(JSON.stringify(test)); // Base64 encode the test object
|
|
1582
|
+
const truncatedError =
|
|
1583
|
+
(test.errorMessage || "No error message").slice(0, 150) +
|
|
1584
|
+
(test.errorMessage && test.errorMessage.length > 150 ? "..." : "");
|
|
1585
|
+
|
|
1586
|
+
return `
|
|
1562
1587
|
<div class="compact-failure-item">
|
|
1563
1588
|
<div class="failure-header">
|
|
1564
1589
|
<div class="failure-main-info">
|
|
1565
|
-
<h3 class="failure-title" title="${sanitizeHTML(
|
|
1590
|
+
<h3 class="failure-title" title="${sanitizeHTML(
|
|
1591
|
+
test.name
|
|
1592
|
+
)}">${sanitizeHTML(testTitle)}</h3>
|
|
1566
1593
|
<div class="failure-meta">
|
|
1567
|
-
<span class="browser-indicator">${sanitizeHTML(
|
|
1568
|
-
|
|
1594
|
+
<span class="browser-indicator">${sanitizeHTML(
|
|
1595
|
+
test.browser || "unknown"
|
|
1596
|
+
)}</span>
|
|
1597
|
+
<span class="duration-indicator">${formatDuration(
|
|
1598
|
+
test.duration
|
|
1599
|
+
)}</span>
|
|
1569
1600
|
</div>
|
|
1570
1601
|
</div>
|
|
1571
|
-
<
|
|
1572
|
-
<
|
|
1573
|
-
|
|
1602
|
+
<div class="ai-buttons-group">
|
|
1603
|
+
<button class="compact-ai-btn" onclick="getAIFix(this)" data-test-json="${testJson}">
|
|
1604
|
+
<span class="ai-text">AI Fix</span>
|
|
1605
|
+
</button>
|
|
1606
|
+
<button class="copy-prompt-btn" onclick="copyAIPrompt(this)" data-test-json="${testJson}" title="Copy AI Prompt">
|
|
1607
|
+
<span class="copy-prompt-text">Copy AI Prompt</span>
|
|
1608
|
+
</button>
|
|
1609
|
+
</div>
|
|
1574
1610
|
</div>
|
|
1575
1611
|
<div class="failure-error-preview">
|
|
1576
|
-
<div class="error-snippet">${formatPlaywrightError(
|
|
1612
|
+
<div class="error-snippet">${formatPlaywrightError(
|
|
1613
|
+
truncatedError
|
|
1614
|
+
)}</div>
|
|
1577
1615
|
<button class="expand-error-btn" onclick="toggleErrorDetails(this)">
|
|
1578
1616
|
<span class="expand-text">Show Full Error</span>
|
|
1579
1617
|
<span class="expand-icon">▼</span>
|
|
@@ -1581,12 +1619,15 @@ function generateAIFailureAnalyzerTab(results) {
|
|
|
1581
1619
|
</div>
|
|
1582
1620
|
<div class="full-error-details" style="display: none;">
|
|
1583
1621
|
<div class="full-error-content">
|
|
1584
|
-
${formatPlaywrightError(
|
|
1622
|
+
${formatPlaywrightError(
|
|
1623
|
+
test.errorMessage || "No detailed error message available"
|
|
1624
|
+
)}
|
|
1585
1625
|
</div>
|
|
1586
1626
|
</div>
|
|
1587
1627
|
</div>
|
|
1588
|
-
|
|
1589
|
-
|
|
1628
|
+
`;
|
|
1629
|
+
})
|
|
1630
|
+
.join("")}
|
|
1590
1631
|
</div>
|
|
1591
1632
|
|
|
1592
1633
|
<!-- AI Fix Modal -->
|
|
@@ -1618,7 +1659,7 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1618
1659
|
const fixPath = (p) => {
|
|
1619
1660
|
if (!p) return "";
|
|
1620
1661
|
// This regex handles both forward slashes and backslashes
|
|
1621
|
-
return p.replace(new RegExp(`^${DEFAULT_OUTPUT_DIR}[\\\\/]`),
|
|
1662
|
+
return p.replace(new RegExp(`^${DEFAULT_OUTPUT_DIR}[\\\\/]`), "");
|
|
1622
1663
|
};
|
|
1623
1664
|
|
|
1624
1665
|
const totalTestsOr1 = runSummary.totalTests || 1;
|
|
@@ -1663,20 +1704,23 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1663
1704
|
)}</span>
|
|
1664
1705
|
</div>
|
|
1665
1706
|
<div class="step-details" style="display: none;">
|
|
1666
|
-
${
|
|
1707
|
+
${
|
|
1708
|
+
step.codeLocation
|
|
1667
1709
|
? `<div class="step-info code-section"><strong>Location:</strong> ${sanitizeHTML(
|
|
1668
|
-
|
|
1669
|
-
|
|
1710
|
+
step.codeLocation
|
|
1711
|
+
)}</div>`
|
|
1670
1712
|
: ""
|
|
1671
|
-
|
|
1672
|
-
${
|
|
1713
|
+
}
|
|
1714
|
+
${
|
|
1715
|
+
step.errorMessage
|
|
1673
1716
|
? `<div class="test-error-summary">
|
|
1674
|
-
${
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1717
|
+
${
|
|
1718
|
+
step.stackTrace
|
|
1719
|
+
? `<div class="stack-trace">${formatPlaywrightError(
|
|
1720
|
+
step.stackTrace
|
|
1721
|
+
)}</div>`
|
|
1722
|
+
: ""
|
|
1723
|
+
}
|
|
1680
1724
|
<button
|
|
1681
1725
|
class="copy-error-btn"
|
|
1682
1726
|
onclick="copyErrorToClipboard(this)"
|
|
@@ -1698,14 +1742,15 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1698
1742
|
</button>
|
|
1699
1743
|
</div>`
|
|
1700
1744
|
: ""
|
|
1701
|
-
|
|
1702
|
-
${
|
|
1745
|
+
}
|
|
1746
|
+
${
|
|
1747
|
+
hasNestedSteps
|
|
1703
1748
|
? `<div class="nested-steps">${generateStepsHTML(
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1749
|
+
step.steps,
|
|
1750
|
+
depth + 1
|
|
1751
|
+
)}</div>`
|
|
1707
1752
|
: ""
|
|
1708
|
-
|
|
1753
|
+
}
|
|
1709
1754
|
</div>
|
|
1710
1755
|
</div>`;
|
|
1711
1756
|
})
|
|
@@ -2297,6 +2342,35 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2297
2342
|
.ai-text {
|
|
2298
2343
|
font-size: 0.95em;
|
|
2299
2344
|
}
|
|
2345
|
+
.ai-buttons-group {
|
|
2346
|
+
display: flex;
|
|
2347
|
+
gap: 10px;
|
|
2348
|
+
flex-wrap: wrap;
|
|
2349
|
+
}
|
|
2350
|
+
.copy-prompt-btn {
|
|
2351
|
+
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
|
|
2352
|
+
color: white;
|
|
2353
|
+
border: none;
|
|
2354
|
+
padding: 12px 18px;
|
|
2355
|
+
border-radius: 6px;
|
|
2356
|
+
cursor: pointer;
|
|
2357
|
+
font-weight: 600;
|
|
2358
|
+
display: flex;
|
|
2359
|
+
align-items: center;
|
|
2360
|
+
gap: 8px;
|
|
2361
|
+
transition: all 0.3s ease;
|
|
2362
|
+
white-space: nowrap;
|
|
2363
|
+
}
|
|
2364
|
+
.copy-prompt-btn:hover {
|
|
2365
|
+
transform: translateY(-2px);
|
|
2366
|
+
box-shadow: 0 6px 20px rgba(37, 99, 235, 0.4);
|
|
2367
|
+
}
|
|
2368
|
+
.copy-prompt-btn.copied {
|
|
2369
|
+
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
|
2370
|
+
}
|
|
2371
|
+
.copy-prompt-text {
|
|
2372
|
+
font-size: 0.95em;
|
|
2373
|
+
}
|
|
2300
2374
|
.failure-error-preview {
|
|
2301
2375
|
padding: 0 20px 18px 20px;
|
|
2302
2376
|
border-top: 1px solid var(--light-gray-color);
|
|
@@ -2372,9 +2446,14 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2372
2446
|
.failure-meta {
|
|
2373
2447
|
justify-content: center;
|
|
2374
2448
|
}
|
|
2375
|
-
.
|
|
2449
|
+
.ai-buttons-group {
|
|
2450
|
+
flex-direction: column;
|
|
2451
|
+
width: 100%;
|
|
2452
|
+
}
|
|
2453
|
+
.compact-ai-btn, .copy-prompt-btn {
|
|
2376
2454
|
justify-content: center;
|
|
2377
2455
|
padding: 12px 20px;
|
|
2456
|
+
width: 100%;
|
|
2378
2457
|
}
|
|
2379
2458
|
}
|
|
2380
2459
|
@media (max-width: 480px) {
|
|
@@ -2649,6 +2728,81 @@ function getAIFix(button) {
|
|
|
2649
2728
|
}
|
|
2650
2729
|
|
|
2651
2730
|
|
|
2731
|
+
function copyAIPrompt(button) {
|
|
2732
|
+
try {
|
|
2733
|
+
const testJson = button.dataset.testJson;
|
|
2734
|
+
const test = JSON.parse(atob(testJson));
|
|
2735
|
+
|
|
2736
|
+
const testName = test.name || 'Unknown Test';
|
|
2737
|
+
const failureLogsAndErrors = [
|
|
2738
|
+
'Error Message:',
|
|
2739
|
+
test.errorMessage || 'Not available.',
|
|
2740
|
+
'\\n\\n--- stdout ---',
|
|
2741
|
+
(test.stdout && test.stdout.length > 0) ? test.stdout.join('\\n') : 'Not available.',
|
|
2742
|
+
'\\n\\n--- stderr ---',
|
|
2743
|
+
(test.stderr && test.stderr.length > 0) ? test.stderr.join('\\n') : 'Not available.'
|
|
2744
|
+
].join('\\n');
|
|
2745
|
+
const codeSnippet = test.snippet || '';
|
|
2746
|
+
|
|
2747
|
+
const aiPrompt = \`You are an expert Playwright test automation engineer specializing in debugging test failures.
|
|
2748
|
+
|
|
2749
|
+
INSTRUCTIONS:
|
|
2750
|
+
1. Analyze the test failure carefully
|
|
2751
|
+
2. Provide a brief root cause analysis
|
|
2752
|
+
3. Provide EXACTLY 5 specific, actionable fixes
|
|
2753
|
+
4. Each fix MUST include a code snippet (codeSnippet field)
|
|
2754
|
+
5. Return ONLY valid JSON, no markdown or extra text
|
|
2755
|
+
|
|
2756
|
+
REQUIRED JSON FORMAT:
|
|
2757
|
+
{
|
|
2758
|
+
"rootCause": "Brief explanation of why the test failed",
|
|
2759
|
+
"suggestedFixes": [
|
|
2760
|
+
{
|
|
2761
|
+
"description": "Clear explanation of the fix",
|
|
2762
|
+
"codeSnippet": "await page.waitForSelector('.button', { timeout: 5000 });"
|
|
2763
|
+
}
|
|
2764
|
+
],
|
|
2765
|
+
"affectedTests": ["test1", "test2"]
|
|
2766
|
+
}
|
|
2767
|
+
|
|
2768
|
+
IMPORTANT:
|
|
2769
|
+
- Always return valid JSON only
|
|
2770
|
+
- Always provide exactly 5 fixes in suggestedFixes array
|
|
2771
|
+
- Each fix must have both description and codeSnippet fields
|
|
2772
|
+
- Make code snippets practical and Playwright-specific
|
|
2773
|
+
|
|
2774
|
+
---
|
|
2775
|
+
|
|
2776
|
+
Test Name: \${testName}
|
|
2777
|
+
|
|
2778
|
+
Failure Logs and Errors:
|
|
2779
|
+
\${failureLogsAndErrors}
|
|
2780
|
+
|
|
2781
|
+
Code Snippet:
|
|
2782
|
+
\${codeSnippet}\`;
|
|
2783
|
+
|
|
2784
|
+
navigator.clipboard.writeText(aiPrompt).then(() => {
|
|
2785
|
+
const originalText = button.querySelector('.copy-prompt-text').textContent;
|
|
2786
|
+
button.querySelector('.copy-prompt-text').textContent = 'Copied!';
|
|
2787
|
+
button.classList.add('copied');
|
|
2788
|
+
|
|
2789
|
+
const shortTestName = testName.split(' > ').pop() || testName;
|
|
2790
|
+
alert(\`AI prompt to generate a suggested fix for "\${shortTestName}" has been copied to your clipboard.\`);
|
|
2791
|
+
|
|
2792
|
+
setTimeout(() => {
|
|
2793
|
+
button.querySelector('.copy-prompt-text').textContent = originalText;
|
|
2794
|
+
button.classList.remove('copied');
|
|
2795
|
+
}, 2000);
|
|
2796
|
+
}).catch(err => {
|
|
2797
|
+
console.error('Failed to copy AI prompt:', err);
|
|
2798
|
+
alert('Failed to copy AI prompt to clipboard. Please try again.');
|
|
2799
|
+
});
|
|
2800
|
+
} catch (e) {
|
|
2801
|
+
console.error('Error processing test data for AI Prompt copy:', e);
|
|
2802
|
+
alert('Could not process test data. Please try again.');
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
|
|
2652
2806
|
function closeAiModal() {
|
|
2653
2807
|
const modal = document.getElementById('ai-fix-modal');
|
|
2654
2808
|
if(modal) modal.style.display = 'none';
|
|
@@ -2897,10 +3051,10 @@ function copyErrorToClipboard(button) {
|
|
|
2897
3051
|
</html>
|
|
2898
3052
|
`;
|
|
2899
3053
|
}
|
|
2900
|
-
async function runScript(scriptPath) {
|
|
3054
|
+
async function runScript(scriptPath, args = []) {
|
|
2901
3055
|
return new Promise((resolve, reject) => {
|
|
2902
3056
|
console.log(chalk.blue(`Executing script: ${scriptPath}...`));
|
|
2903
|
-
const process = fork(scriptPath,
|
|
3057
|
+
const process = fork(scriptPath, args, {
|
|
2904
3058
|
stdio: "inherit",
|
|
2905
3059
|
});
|
|
2906
3060
|
|
|
@@ -2925,13 +3079,22 @@ async function main() {
|
|
|
2925
3079
|
const __filename = fileURLToPath(import.meta.url);
|
|
2926
3080
|
const __dirname = path.dirname(__filename);
|
|
2927
3081
|
|
|
3082
|
+
const args = process.argv.slice(2);
|
|
3083
|
+
let customOutputDir = null;
|
|
3084
|
+
for (let i = 0; i < args.length; i++) {
|
|
3085
|
+
if (args[i] === "--outputDir" || args[i] === "-o") {
|
|
3086
|
+
customOutputDir = args[i + 1];
|
|
3087
|
+
break;
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
3090
|
+
|
|
2928
3091
|
// Script to archive current run to JSON history (this is your modified "generate-trend.mjs")
|
|
2929
3092
|
const archiveRunScriptPath = path.resolve(
|
|
2930
3093
|
__dirname,
|
|
2931
3094
|
"generate-trend.mjs" // Keeping the filename as per your request
|
|
2932
3095
|
);
|
|
2933
3096
|
|
|
2934
|
-
const outputDir =
|
|
3097
|
+
const outputDir = await getOutputDir(customOutputDir);
|
|
2935
3098
|
const reportJsonPath = path.resolve(outputDir, DEFAULT_JSON_FILE); // Current run's main JSON
|
|
2936
3099
|
const reportHtmlPath = path.resolve(outputDir, DEFAULT_HTML_FILE);
|
|
2937
3100
|
|
|
@@ -2941,10 +3104,18 @@ async function main() {
|
|
|
2941
3104
|
|
|
2942
3105
|
console.log(chalk.blue(`Starting static HTML report generation...`));
|
|
2943
3106
|
console.log(chalk.blue(`Output directory set to: ${outputDir}`));
|
|
3107
|
+
if (customOutputDir) {
|
|
3108
|
+
console.log(chalk.gray(` (from CLI argument)`));
|
|
3109
|
+
} else {
|
|
3110
|
+
console.log(
|
|
3111
|
+
chalk.gray(` (auto-detected from playwright.config or using default)`)
|
|
3112
|
+
);
|
|
3113
|
+
}
|
|
2944
3114
|
|
|
2945
3115
|
// Step 1: Ensure current run data is archived to the history folder
|
|
2946
3116
|
try {
|
|
2947
|
-
|
|
3117
|
+
const archiveArgs = customOutputDir ? ["--outputDir", customOutputDir] : [];
|
|
3118
|
+
await runScript(archiveRunScriptPath, archiveArgs);
|
|
2948
3119
|
console.log(
|
|
2949
3120
|
chalk.green("Current run data archiving to history completed.")
|
|
2950
3121
|
);
|
|
@@ -5,6 +5,7 @@ import { readFileSync, existsSync as fsExistsSync } from "fs";
|
|
|
5
5
|
import path from "path";
|
|
6
6
|
import { fork } from "child_process";
|
|
7
7
|
import { fileURLToPath } from "url";
|
|
8
|
+
import { getOutputDir } from "./config-reader.mjs";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Dynamically imports the 'chalk' library for terminal string styling.
|
|
@@ -1720,7 +1721,7 @@ function generateAIFailureAnalyzerTab(results) {
|
|
|
1720
1721
|
</div>
|
|
1721
1722
|
</div>
|
|
1722
1723
|
<p class="ai-analyzer-description">
|
|
1723
|
-
Analyze failed tests using AI to get suggestions and potential fixes. Click the AI Fix button for
|
|
1724
|
+
Analyze failed tests using AI to get suggestions and potential fixes. Click the AI Fix button for instant analysis or use Copy AI Prompt to analyze with your preferred AI tool.
|
|
1724
1725
|
</p>
|
|
1725
1726
|
|
|
1726
1727
|
<div class="compact-failure-list">
|
|
@@ -1748,9 +1749,14 @@ function generateAIFailureAnalyzerTab(results) {
|
|
|
1748
1749
|
)}</span>
|
|
1749
1750
|
</div>
|
|
1750
1751
|
</div>
|
|
1751
|
-
<
|
|
1752
|
-
<
|
|
1753
|
-
|
|
1752
|
+
<div class="ai-buttons-group">
|
|
1753
|
+
<button class="compact-ai-btn" onclick="getAIFix(this)" data-test-json="${testJson}">
|
|
1754
|
+
<span class="ai-text">AI Fix</span>
|
|
1755
|
+
</button>
|
|
1756
|
+
<button class="copy-prompt-btn" onclick="copyAIPrompt(this)" data-test-json="${testJson}" title="Copy AI Prompt">
|
|
1757
|
+
<span class="copy-prompt-text">Copy AI Prompt</span>
|
|
1758
|
+
</button>
|
|
1759
|
+
</div>
|
|
1754
1760
|
</div>
|
|
1755
1761
|
<div class="failure-error-preview">
|
|
1756
1762
|
<div class="error-snippet">${formatPlaywrightError(
|
|
@@ -2431,9 +2437,14 @@ aspect-ratio: 16 / 9;
|
|
|
2431
2437
|
.browser-indicator { background: var(--info-color); color: white; }
|
|
2432
2438
|
#load-more-tests { font-size: 16px; padding: 4px; background-color: var(--light-gray-color); border-radius: 4px; color: var(--text-color); }
|
|
2433
2439
|
.duration-indicator { background: var(--medium-gray-color); color: var(--text-color); }
|
|
2440
|
+
.ai-buttons-group { display: flex; gap: 10px; flex-wrap: wrap; }
|
|
2434
2441
|
.compact-ai-btn { background: linear-gradient(135deg, #374151 0%, #1f2937 100%); color: white; border: none; padding: 12px 18px; border-radius: 6px; cursor: pointer; font-weight: 600; display: flex; align-items: center; gap: 8px; transition: all 0.3s ease; white-space: nowrap;}
|
|
2435
2442
|
.compact-ai-btn:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(55, 65, 81, 0.4); }
|
|
2436
2443
|
.ai-text { font-size: 0.95em; }
|
|
2444
|
+
.copy-prompt-btn { background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%); color: white; border: none; padding: 12px 18px; border-radius: 6px; cursor: pointer; font-weight: 600; display: flex; align-items: center; gap: 8px; transition: all 0.3s ease; white-space: nowrap;}
|
|
2445
|
+
.copy-prompt-btn:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(37, 99, 235, 0.4); }
|
|
2446
|
+
.copy-prompt-btn.copied { background: linear-gradient(135deg, #10b981 0%, #059669 100%); }
|
|
2447
|
+
.copy-prompt-text { font-size: 0.95em; }
|
|
2437
2448
|
.failure-error-preview { padding: 0 20px 18px 20px; border-top: 1px solid var(--light-gray-color);}
|
|
2438
2449
|
.error-snippet { background: rgba(248, 113, 113, 0.1); border: 1px solid rgba(248, 113, 113, 0.3); border-radius: 6px; padding: 12px; margin-bottom: 12px; font-family: monospace; font-size: 0.9em; color: var(--danger-color); line-height: 1.4;}
|
|
2439
2450
|
.expand-error-btn { background: none; border: 1px solid var(--border-color); color: var(--text-color-secondary); padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 0.85em; display: flex; align-items: center; gap: 6px; transition: all 0.2s ease;}
|
|
@@ -2444,7 +2455,7 @@ aspect-ratio: 16 / 9;
|
|
|
2444
2455
|
.full-error-content { background: rgba(248, 113, 113, 0.1); border: 1px solid rgba(248, 113, 113, 0.3); border-radius: 6px; padding: 15px; font-family: monospace; font-size: 0.9em; color: var(--danger-color); line-height: 1.4; max-height: 300px; overflow-y: auto;}
|
|
2445
2456
|
@media (max-width: 1200px) { .trend-charts-row { grid-template-columns: 1fr; } }
|
|
2446
2457
|
@media (max-width: 992px) { .dashboard-bottom-row { grid-template-columns: 1fr; } .pie-chart-wrapper div[id^="pieChart-"] { max-width: 350px; margin: 0 auto; } .filters input { min-width: 180px; } .filters select { min-width: 150px; } }
|
|
2447
|
-
@media (max-width: 768px) { body { font-size: 15px; } .container { margin: 10px; padding: 20px; } .header { flex-direction: column; align-items: flex-start; gap: 15px; } .header h1 { font-size: 1.6em; } .run-info { text-align: left; font-size:0.9em; } .tabs { margin-bottom: 25px;} .tab-button { padding: 12px 20px; font-size: 1.05em;} .dashboard-grid { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 18px;} .summary-card .value {font-size: 2em;} .summary-card h3 {font-size: 0.95em;} .filters { flex-direction: column; padding: 18px; gap: 12px;} .filters input, .filters select, .filters button {width: 100%; box-sizing: border-box;} .test-case-header { flex-direction: column; align-items: flex-start; gap: 10px; padding: 14px; } .test-case-summary {gap: 10px;} .test-case-title {font-size: 1.05em;} .test-case-meta { flex-direction: row; flex-wrap: wrap; gap: 8px; margin-top: 8px;} .attachments-grid {grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 18px;} .test-history-grid {grid-template-columns: 1fr;} .pie-chart-wrapper {min-height: auto;} .ai-failure-cards-grid { grid-template-columns: 1fr; } .ai-analyzer-stats { flex-direction: column; gap: 15px; text-align: center; } .failure-header { flex-direction: column; align-items: stretch; gap: 15px; } .failure-main-info { text-align: center; } .failure-meta { justify-content: center; } .compact-ai-btn { justify-content: center; padding: 12px 20px; } }
|
|
2458
|
+
@media (max-width: 768px) { body { font-size: 15px; } .container { margin: 10px; padding: 20px; } .header { flex-direction: column; align-items: flex-start; gap: 15px; } .header h1 { font-size: 1.6em; } .run-info { text-align: left; font-size:0.9em; } .tabs { margin-bottom: 25px;} .tab-button { padding: 12px 20px; font-size: 1.05em;} .dashboard-grid { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 18px;} .summary-card .value {font-size: 2em;} .summary-card h3 {font-size: 0.95em;} .filters { flex-direction: column; padding: 18px; gap: 12px;} .filters input, .filters select, .filters button {width: 100%; box-sizing: border-box;} .test-case-header { flex-direction: column; align-items: flex-start; gap: 10px; padding: 14px; } .test-case-summary {gap: 10px;} .test-case-title {font-size: 1.05em;} .test-case-meta { flex-direction: row; flex-wrap: wrap; gap: 8px; margin-top: 8px;} .attachments-grid {grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 18px;} .test-history-grid {grid-template-columns: 1fr;} .pie-chart-wrapper {min-height: auto;} .ai-failure-cards-grid { grid-template-columns: 1fr; } .ai-analyzer-stats { flex-direction: column; gap: 15px; text-align: center; } .failure-header { flex-direction: column; align-items: stretch; gap: 15px; } .failure-main-info { text-align: center; } .failure-meta { justify-content: center; } .ai-buttons-group { flex-direction: column; width: 100%; } .compact-ai-btn, .copy-prompt-btn { justify-content: center; padding: 12px 20px; width: 100%; } }
|
|
2448
2459
|
@media (max-width: 480px) { body {font-size: 14px;} .container {padding: 15px;} .header h1 {font-size: 1.4em;} #report-logo { height: 35px; width: 45px; } .tab-button {padding: 10px 15px; font-size: 1em;} .summary-card .value {font-size: 1.8em;} .attachments-grid {grid-template-columns: 1fr;} .step-item {padding-left: calc(var(--depth, 0) * 18px);} .test-case-content, .step-details {padding: 15px;} .trend-charts-row {gap: 20px;} .trend-chart {padding: 20px;} .stat-item .stat-number { font-size: 1.5em; } .failure-header { padding: 15px; } .failure-error-preview, .full-error-details { padding-left: 15px; padding-right: 15px; } }
|
|
2449
2460
|
.trace-actions a { text-decoration: none; font-weight: 500; font-size: 0.9em; }
|
|
2450
2461
|
.generic-attachment { text-align: center; padding: 1rem; justify-content: center; }
|
|
@@ -2713,6 +2724,81 @@ aspect-ratio: 16 / 9;
|
|
|
2713
2724
|
}
|
|
2714
2725
|
}
|
|
2715
2726
|
|
|
2727
|
+
function copyAIPrompt(button) {
|
|
2728
|
+
try {
|
|
2729
|
+
const testJson = button.dataset.testJson;
|
|
2730
|
+
const test = JSON.parse(atob(testJson));
|
|
2731
|
+
|
|
2732
|
+
const testName = test.name || 'Unknown Test';
|
|
2733
|
+
const failureLogsAndErrors = [
|
|
2734
|
+
'Error Message:',
|
|
2735
|
+
test.errorMessage || 'Not available.',
|
|
2736
|
+
'\\n\\n--- stdout ---',
|
|
2737
|
+
(test.stdout && test.stdout.length > 0) ? test.stdout.join('\\n') : 'Not available.',
|
|
2738
|
+
'\\n\\n--- stderr ---',
|
|
2739
|
+
(test.stderr && test.stderr.length > 0) ? test.stderr.join('\\n') : 'Not available.'
|
|
2740
|
+
].join('\\n');
|
|
2741
|
+
const codeSnippet = test.snippet || '';
|
|
2742
|
+
|
|
2743
|
+
const aiPrompt = \`You are an expert Playwright test automation engineer specializing in debugging test failures.
|
|
2744
|
+
|
|
2745
|
+
INSTRUCTIONS:
|
|
2746
|
+
1. Analyze the test failure carefully
|
|
2747
|
+
2. Provide a brief root cause analysis
|
|
2748
|
+
3. Provide EXACTLY 5 specific, actionable fixes
|
|
2749
|
+
4. Each fix MUST include a code snippet (codeSnippet field)
|
|
2750
|
+
5. Return ONLY valid JSON, no markdown or extra text
|
|
2751
|
+
|
|
2752
|
+
REQUIRED JSON FORMAT:
|
|
2753
|
+
{
|
|
2754
|
+
"rootCause": "Brief explanation of why the test failed",
|
|
2755
|
+
"suggestedFixes": [
|
|
2756
|
+
{
|
|
2757
|
+
"description": "Clear explanation of the fix",
|
|
2758
|
+
"codeSnippet": "await page.waitForSelector('.button', { timeout: 5000 });"
|
|
2759
|
+
}
|
|
2760
|
+
],
|
|
2761
|
+
"affectedTests": ["test1", "test2"]
|
|
2762
|
+
}
|
|
2763
|
+
|
|
2764
|
+
IMPORTANT:
|
|
2765
|
+
- Always return valid JSON only
|
|
2766
|
+
- Always provide exactly 5 fixes in suggestedFixes array
|
|
2767
|
+
- Each fix must have both description and codeSnippet fields
|
|
2768
|
+
- Make code snippets practical and Playwright-specific
|
|
2769
|
+
|
|
2770
|
+
---
|
|
2771
|
+
|
|
2772
|
+
Test Name: \${testName}
|
|
2773
|
+
|
|
2774
|
+
Failure Logs and Errors:
|
|
2775
|
+
\${failureLogsAndErrors}
|
|
2776
|
+
|
|
2777
|
+
Code Snippet:
|
|
2778
|
+
\${codeSnippet}\`;
|
|
2779
|
+
|
|
2780
|
+
navigator.clipboard.writeText(aiPrompt).then(() => {
|
|
2781
|
+
const originalText = button.querySelector('.copy-prompt-text').textContent;
|
|
2782
|
+
button.querySelector('.copy-prompt-text').textContent = 'Copied!';
|
|
2783
|
+
button.classList.add('copied');
|
|
2784
|
+
|
|
2785
|
+
const shortTestName = testName.split(' > ').pop() || testName;
|
|
2786
|
+
alert(\`AI prompt to generate a suggested fix for "\${shortTestName}" has been copied to your clipboard.\`);
|
|
2787
|
+
|
|
2788
|
+
setTimeout(() => {
|
|
2789
|
+
button.querySelector('.copy-prompt-text').textContent = originalText;
|
|
2790
|
+
button.classList.remove('copied');
|
|
2791
|
+
}, 2000);
|
|
2792
|
+
}).catch(err => {
|
|
2793
|
+
console.error('Failed to copy AI prompt:', err);
|
|
2794
|
+
alert('Failed to copy AI prompt to clipboard. Please try again.');
|
|
2795
|
+
});
|
|
2796
|
+
} catch (e) {
|
|
2797
|
+
console.error('Error processing test data for AI Prompt copy:', e);
|
|
2798
|
+
alert('Could not process test data. Please try again.');
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
|
|
2716
2802
|
function closeAiModal() {
|
|
2717
2803
|
const modal = document.getElementById('ai-fix-modal');
|
|
2718
2804
|
if(modal) modal.style.display = 'none';
|
|
@@ -3068,10 +3154,10 @@ aspect-ratio: 16 / 9;
|
|
|
3068
3154
|
</html>
|
|
3069
3155
|
`;
|
|
3070
3156
|
}
|
|
3071
|
-
async function runScript(scriptPath) {
|
|
3157
|
+
async function runScript(scriptPath, args = []) {
|
|
3072
3158
|
return new Promise((resolve, reject) => {
|
|
3073
3159
|
console.log(chalk.blue(`Executing script: ${scriptPath}...`));
|
|
3074
|
-
const process = fork(scriptPath,
|
|
3160
|
+
const process = fork(scriptPath, args, {
|
|
3075
3161
|
stdio: "inherit",
|
|
3076
3162
|
});
|
|
3077
3163
|
|
|
@@ -3101,13 +3187,22 @@ async function main() {
|
|
|
3101
3187
|
const __filename = fileURLToPath(import.meta.url);
|
|
3102
3188
|
const __dirname = path.dirname(__filename);
|
|
3103
3189
|
|
|
3190
|
+
const args = process.argv.slice(2);
|
|
3191
|
+
let customOutputDir = null;
|
|
3192
|
+
for (let i = 0; i < args.length; i++) {
|
|
3193
|
+
if (args[i] === "--outputDir" || args[i] === "-o") {
|
|
3194
|
+
customOutputDir = args[i + 1];
|
|
3195
|
+
break;
|
|
3196
|
+
}
|
|
3197
|
+
}
|
|
3198
|
+
|
|
3104
3199
|
// Script to archive current run to JSON history (this is your modified "generate-trend.mjs")
|
|
3105
3200
|
const archiveRunScriptPath = path.resolve(
|
|
3106
3201
|
__dirname,
|
|
3107
3202
|
"generate-trend.mjs" // Keeping the filename as per your request
|
|
3108
3203
|
);
|
|
3109
3204
|
|
|
3110
|
-
const outputDir =
|
|
3205
|
+
const outputDir = await getOutputDir(customOutputDir);
|
|
3111
3206
|
const reportJsonPath = path.resolve(outputDir, DEFAULT_JSON_FILE); // Current run's main JSON
|
|
3112
3207
|
const reportHtmlPath = path.resolve(outputDir, DEFAULT_HTML_FILE);
|
|
3113
3208
|
|
|
@@ -3117,10 +3212,21 @@ async function main() {
|
|
|
3117
3212
|
|
|
3118
3213
|
console.log(chalk.blue(`Starting static HTML report generation...`));
|
|
3119
3214
|
console.log(chalk.blue(`Output directory set to: ${outputDir}`));
|
|
3215
|
+
if (customOutputDir) {
|
|
3216
|
+
console.log(chalk.gray(` (from CLI argument)`));
|
|
3217
|
+
} else {
|
|
3218
|
+
const { exists } = await import("./config-reader.mjs").then((m) => ({
|
|
3219
|
+
exists: true,
|
|
3220
|
+
}));
|
|
3221
|
+
console.log(
|
|
3222
|
+
chalk.gray(` (auto-detected from playwright.config or using default)`)
|
|
3223
|
+
);
|
|
3224
|
+
}
|
|
3120
3225
|
|
|
3121
3226
|
// Step 1: Ensure current run data is archived to the history folder
|
|
3122
3227
|
try {
|
|
3123
|
-
|
|
3228
|
+
const archiveArgs = customOutputDir ? ["--outputDir", customOutputDir] : [];
|
|
3229
|
+
await runScript(archiveRunScriptPath, archiveArgs);
|
|
3124
3230
|
console.log(
|
|
3125
3231
|
chalk.green("Current run data archiving to history completed.")
|
|
3126
3232
|
);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import * as fs from "fs/promises";
|
|
3
3
|
import path from "path";
|
|
4
|
+
import { getOutputDir } from "./config-reader.mjs";
|
|
4
5
|
|
|
5
6
|
// Use dynamic import for chalk as it's ESM only for prettier console logs
|
|
6
7
|
let chalk;
|
|
@@ -22,8 +23,17 @@ const HISTORY_SUBDIR = "history"; // Subdirectory for historical JSON files
|
|
|
22
23
|
const HISTORY_FILE_PREFIX = "trend-";
|
|
23
24
|
const MAX_HISTORY_FILES = 15; // Store last 15 runs
|
|
24
25
|
|
|
26
|
+
const args = process.argv.slice(2);
|
|
27
|
+
let customOutputDir = null;
|
|
28
|
+
for (let i = 0; i < args.length; i++) {
|
|
29
|
+
if (args[i] === "--outputDir" || args[i] === "-o") {
|
|
30
|
+
customOutputDir = args[i + 1];
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
25
35
|
async function archiveCurrentRunData() {
|
|
26
|
-
const outputDir =
|
|
36
|
+
const outputDir = await getOutputDir(customOutputDir);
|
|
27
37
|
const currentRunJsonPath = path.join(outputDir, CURRENT_RUN_JSON_FILE);
|
|
28
38
|
const historyDir = path.join(outputDir, HISTORY_SUBDIR);
|
|
29
39
|
|
|
@@ -3,9 +3,30 @@
|
|
|
3
3
|
const fs = require("fs");
|
|
4
4
|
const path = require("path");
|
|
5
5
|
|
|
6
|
-
const
|
|
6
|
+
const args = process.argv.slice(2);
|
|
7
|
+
let customOutputDir = null;
|
|
8
|
+
for (let i = 0; i < args.length; i++) {
|
|
9
|
+
if (args[i] === '--outputDir' || args[i] === '-o') {
|
|
10
|
+
customOutputDir = args[i + 1];
|
|
11
|
+
break;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
7
15
|
const OUTPUT_FILE = "playwright-pulse-report.json";
|
|
8
16
|
|
|
17
|
+
async function getReportDir() {
|
|
18
|
+
if (customOutputDir) {
|
|
19
|
+
return path.resolve(process.cwd(), customOutputDir);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const { getOutputDir } = await import("./config-reader.mjs");
|
|
24
|
+
return await getOutputDir();
|
|
25
|
+
} catch (error) {
|
|
26
|
+
return path.resolve(process.cwd(), "pulse-report");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
9
30
|
function getReportFiles(dir) {
|
|
10
31
|
return fs
|
|
11
32
|
.readdirSync(dir)
|
|
@@ -15,7 +36,7 @@ function getReportFiles(dir) {
|
|
|
15
36
|
);
|
|
16
37
|
}
|
|
17
38
|
|
|
18
|
-
function mergeReports(files) {
|
|
39
|
+
function mergeReports(files, reportDir) {
|
|
19
40
|
let combinedRun = {
|
|
20
41
|
totalTests: 0,
|
|
21
42
|
passed: 0,
|
|
@@ -30,7 +51,7 @@ function mergeReports(files) {
|
|
|
30
51
|
let latestGeneratedAt = "";
|
|
31
52
|
|
|
32
53
|
for (const file of files) {
|
|
33
|
-
const filePath = path.join(
|
|
54
|
+
const filePath = path.join(reportDir, file);
|
|
34
55
|
const json = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
35
56
|
|
|
36
57
|
const run = json.run || {};
|
|
@@ -66,17 +87,28 @@ function mergeReports(files) {
|
|
|
66
87
|
}
|
|
67
88
|
|
|
68
89
|
// Main execution
|
|
69
|
-
|
|
90
|
+
(async () => {
|
|
91
|
+
const REPORT_DIR = await getReportDir();
|
|
92
|
+
|
|
93
|
+
console.log(`Report directory set to: ${REPORT_DIR}`);
|
|
94
|
+
if (customOutputDir) {
|
|
95
|
+
console.log(` (from CLI argument)`);
|
|
96
|
+
} else {
|
|
97
|
+
console.log(` (auto-detected from playwright.config or using default)`);
|
|
98
|
+
}
|
|
70
99
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
100
|
+
const reportFiles = getReportFiles(REPORT_DIR);
|
|
101
|
+
|
|
102
|
+
if (reportFiles.length === 0) {
|
|
103
|
+
console.log("No matching JSON report files found.");
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
75
106
|
|
|
76
|
-
const merged = mergeReports(reportFiles);
|
|
107
|
+
const merged = mergeReports(reportFiles, REPORT_DIR);
|
|
77
108
|
|
|
78
|
-
fs.writeFileSync(
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
);
|
|
82
|
-
console.log(`✅ Merged report saved as ${OUTPUT_FILE}`);
|
|
109
|
+
fs.writeFileSync(
|
|
110
|
+
path.join(REPORT_DIR, OUTPUT_FILE),
|
|
111
|
+
JSON.stringify(merged, null, 2)
|
|
112
|
+
);
|
|
113
|
+
console.log(`✅ Merged report saved as ${OUTPUT_FILE}`);
|
|
114
|
+
})();
|
package/scripts/sendReport.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import { fileURLToPath } from "url";
|
|
11
11
|
import { fork } from "child_process"; // This was missing in your sendReport.js but present in generate-email-report.js and needed for runScript
|
|
12
12
|
import "dotenv/config"; // CHANGED for dotenv
|
|
13
|
+
import { getOutputDir } from "./config-reader.mjs";
|
|
13
14
|
|
|
14
15
|
// Import chalk using top-level await if your Node version supports it (14.8+)
|
|
15
16
|
// or keep the dynamic import if preferred, but ensure chalk is resolved before use.
|
|
@@ -28,7 +29,14 @@ try {
|
|
|
28
29
|
};
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
const
|
|
32
|
+
const args = process.argv.slice(2);
|
|
33
|
+
let customOutputDir = null;
|
|
34
|
+
for (let i = 0; i < args.length; i++) {
|
|
35
|
+
if (args[i] === "--outputDir" || args[i] === "-o") {
|
|
36
|
+
customOutputDir = args[i + 1];
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
32
40
|
|
|
33
41
|
let fetch;
|
|
34
42
|
// Ensure fetch is imported and available before it's used in fetchCredentials
|
|
@@ -40,11 +48,8 @@ let fetch;
|
|
|
40
48
|
|
|
41
49
|
let projectName;
|
|
42
50
|
|
|
43
|
-
function getUUID() {
|
|
44
|
-
const reportPath = path.join(
|
|
45
|
-
process.cwd(),
|
|
46
|
-
`${reportDir}/playwright-pulse-report.json`
|
|
47
|
-
);
|
|
51
|
+
function getUUID(reportDir) {
|
|
52
|
+
const reportPath = path.join(reportDir, "playwright-pulse-report.json");
|
|
48
53
|
console.log("Report path:", reportPath);
|
|
49
54
|
|
|
50
55
|
if (!fsExistsSync(reportPath)) {
|
|
@@ -71,18 +76,15 @@ const formatStartTime = (isoString) => {
|
|
|
71
76
|
return date.toLocaleString(); // Default locale
|
|
72
77
|
};
|
|
73
78
|
|
|
74
|
-
const getPulseReportSummary = () => {
|
|
75
|
-
const reportPath = path.join(
|
|
76
|
-
process.cwd(),
|
|
77
|
-
`${reportDir}/playwright-pulse-report.json`
|
|
78
|
-
);
|
|
79
|
+
const getPulseReportSummary = (reportDir) => {
|
|
80
|
+
const reportPath = path.join(reportDir, "playwright-pulse-report.json");
|
|
79
81
|
|
|
80
82
|
if (!fsExistsSync(reportPath)) {
|
|
81
83
|
// CHANGED
|
|
82
84
|
throw new Error("Pulse report file not found.");
|
|
83
85
|
}
|
|
84
86
|
|
|
85
|
-
const content = JSON.parse(fsReadFileSync(reportPath, "utf-8")); //
|
|
87
|
+
const content = JSON.parse(fsReadFileSync(reportPath, "utf-8")); // D
|
|
86
88
|
const run = content.run;
|
|
87
89
|
|
|
88
90
|
const total = run.totalTests || 0;
|
|
@@ -220,9 +222,9 @@ const archiveRunScriptPath = path.resolve(
|
|
|
220
222
|
"generate-email-report.mjs" // Or input_file_0.mjs if you rename it, or input_file_0.js if you configure package.json
|
|
221
223
|
);
|
|
222
224
|
|
|
223
|
-
async function runScript(scriptPath) {
|
|
225
|
+
async function runScript(scriptPath, args = []) {
|
|
224
226
|
return new Promise((resolve, reject) => {
|
|
225
|
-
const childProcess = fork(scriptPath,
|
|
227
|
+
const childProcess = fork(scriptPath, args, {
|
|
226
228
|
// Renamed variable
|
|
227
229
|
stdio: "inherit",
|
|
228
230
|
});
|
|
@@ -244,8 +246,9 @@ async function runScript(scriptPath) {
|
|
|
244
246
|
});
|
|
245
247
|
}
|
|
246
248
|
|
|
247
|
-
const sendEmail = async (credentials) => {
|
|
248
|
-
|
|
249
|
+
const sendEmail = async (credentials, reportDir) => {
|
|
250
|
+
const archiveArgs = customOutputDir ? ["--outputDir", customOutputDir] : [];
|
|
251
|
+
await runScript(archiveRunScriptPath, archiveArgs);
|
|
249
252
|
try {
|
|
250
253
|
console.log("Starting the sendEmail function...");
|
|
251
254
|
|
|
@@ -259,7 +262,7 @@ const sendEmail = async (credentials) => {
|
|
|
259
262
|
},
|
|
260
263
|
});
|
|
261
264
|
|
|
262
|
-
const reportData = getPulseReportSummary();
|
|
265
|
+
const reportData = getPulseReportSummary(reportDir);
|
|
263
266
|
const htmlContent = generateHtmlTable(reportData);
|
|
264
267
|
|
|
265
268
|
const mailOptions = {
|
|
@@ -289,7 +292,7 @@ const sendEmail = async (credentials) => {
|
|
|
289
292
|
}
|
|
290
293
|
};
|
|
291
294
|
|
|
292
|
-
async function fetchCredentials(retries = 10) {
|
|
295
|
+
async function fetchCredentials(reportDir, retries = 10) {
|
|
293
296
|
// Ensure fetch is initialized from the dynamic import before calling this
|
|
294
297
|
if (!fetch) {
|
|
295
298
|
try {
|
|
@@ -304,7 +307,7 @@ async function fetchCredentials(retries = 10) {
|
|
|
304
307
|
}
|
|
305
308
|
|
|
306
309
|
const timeout = 10000;
|
|
307
|
-
const key = getUUID();
|
|
310
|
+
const key = getUUID(reportDir);
|
|
308
311
|
|
|
309
312
|
if (!key) {
|
|
310
313
|
console.error(
|
|
@@ -384,7 +387,19 @@ const main = async () => {
|
|
|
384
387
|
}
|
|
385
388
|
}
|
|
386
389
|
|
|
387
|
-
const
|
|
390
|
+
const reportDir = await getOutputDir(customOutputDir);
|
|
391
|
+
|
|
392
|
+
console.log(chalk.blue(`Preparing to send email report...`));
|
|
393
|
+
console.log(chalk.blue(`Report directory set to: ${reportDir}`));
|
|
394
|
+
if (customOutputDir) {
|
|
395
|
+
console.log(chalk.gray(` (from CLI argument)`));
|
|
396
|
+
} else {
|
|
397
|
+
console.log(
|
|
398
|
+
chalk.gray(` (auto-detected from playwright.config or using default)`)
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const credentials = await fetchCredentials(reportDir);
|
|
388
403
|
if (!credentials) {
|
|
389
404
|
console.warn(
|
|
390
405
|
"Skipping email sending due to missing or failed credential fetch"
|
|
@@ -393,7 +408,7 @@ const main = async () => {
|
|
|
393
408
|
}
|
|
394
409
|
// Removed await delay(10000); // If not strictly needed, remove it.
|
|
395
410
|
try {
|
|
396
|
-
await sendEmail(credentials);
|
|
411
|
+
await sendEmail(credentials, reportDir);
|
|
397
412
|
} catch (error) {
|
|
398
413
|
console.error("Error in main function: ", error);
|
|
399
414
|
}
|