@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 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.0",
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 = path.resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
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: ${chartHeight - 40
619
- }, // Adjusted height to make space for legend if chartHeight is for the whole wrapper
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: ${chartHeight - 40
672
- }px;"></div>
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
- { length: Math.max(0, environment.cpu.cores || 0) },
901
- (_, i) =>
902
- `<div class="env-core-indicator ${i >=
903
- (environment.cpu.cores >= 8 ? 8 : environment.cpu.cores)
904
- ? "inactive"
905
- : ""
906
- }" title="Core ${i + 1}"></div>`
907
- ).join("")}
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">${environment.os.split(" ")[0] === "darwin"
928
- ? "darwin (macOS)"
929
- : environment.os.split(" ")[0] || "Unknown"
930
- }</span>
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">${environment.os.split(" ")[1] || "N/A"
935
- }</span>
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}">${environment.host
940
- }</span>
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}">${environment.cwd.length > 25
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
- }</span>
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 ${environment.os.includes("darwin") &&
979
- environment.cpu.model.toLowerCase().includes("apple")
980
- ? "env-chip-success"
981
- : "env-chip-warning"
982
- }">
983
- ${environment.os.includes("darwin") &&
984
- environment.cpu.model.toLowerCase().includes("apple")
985
- ? "Apple Silicon"
986
- : environment.cpu.model.toLowerCase().includes("arm") ||
987
- environment.cpu.model.toLowerCase().includes("aarch64")
988
- ? "ARM-based"
989
- : "x86/Other"
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">${environment.cpu.cores > 0
997
- ? (
998
- parseFloat(environment.memory) / environment.cpu.cores
999
- ).toFixed(2) + " GB"
1000
- : "N/A"
1001
- }</span>
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
- .map((test) => {
1334
- const latestRun =
1335
- test.history.length > 0
1336
- ? test.history[test.history.length - 1]
1337
- : { status: "unknown" };
1338
- return `
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
- test.testTitle.toLowerCase()
1341
- )}" data-latest-status="${latestRun.status}">
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
- sanitizeHTML(test.testTitle)
1345
- )}</p>
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
- .slice()
1361
- .reverse()
1362
- .map(
1363
- (run) => `
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
- run.status
1368
- )}">${String(run.status).toUpperCase()}</span></td>
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
- .join("")}
1383
+ )
1384
+ .join("")}
1374
1385
  </tbody>
1375
1386
  </table>
1376
1387
  </div>
1377
1388
  </details>
1378
1389
  </div>`;
1379
- })
1380
- .join("")}
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">${suitesData.length
1476
+ <span class="summary-badge">${
1477
+ suitesData.length
1466
1478
  } suites • ${suitesData.reduce(
1467
- (sum, suite) => sum + suite.count,
1468
- 0
1469
- )} tests</span>
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
- suite.browser
1483
- )}</span></div>
1494
+ suite.browser
1495
+ )}</span></div>
1484
1496
  <div class="suite-card-body">
1485
- <span class="test-count">${suite.count} test${suite.count !== 1 ? "s" : ""
1486
- }</span>
1497
+ <span class="test-count">${suite.count} test${
1498
+ suite.count !== 1 ? "s" : ""
1499
+ }</span>
1487
1500
  <div class="suite-stats">
1488
- ${suite.passed > 0
1489
- ? `<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>`
1490
- : ""
1491
- }
1492
- ${suite.failed > 0
1493
- ? `<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>`
1494
- : ""
1495
- }
1496
- ${suite.skipped > 0
1497
- ? `<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>`
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(test => test.status === 'failed');
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('base64');
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">${new Set(failedTests.map(t => t.browser)).size}</span>
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">${(Math.round(failedTests.reduce((sum, test) => sum + (test.duration || 0), 0) / 1000))}s</span>
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 specific failed test.
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.map(test => {
1556
- const testTitle = test.name.split(" > ").pop() || "Unnamed Test";
1557
- const testJson = btoa(JSON.stringify(test)); // Base64 encode the test object
1558
- const truncatedError = (test.errorMessage || "No error message").slice(0, 150) +
1559
- (test.errorMessage && test.errorMessage.length > 150 ? "..." : "");
1560
-
1561
- return `
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(test.name)}">${sanitizeHTML(testTitle)}</h3>
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(test.browser || 'unknown')}</span>
1568
- <span class="duration-indicator">${formatDuration(test.duration)}</span>
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
- <button class="compact-ai-btn" onclick="getAIFix(this)" data-test-json="${testJson}">
1572
- <span class="ai-text">AI Fix</span>
1573
- </button>
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(truncatedError)}</div>
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(test.errorMessage || "No detailed error message available")}
1622
+ ${formatPlaywrightError(
1623
+ test.errorMessage || "No detailed error message available"
1624
+ )}
1585
1625
  </div>
1586
1626
  </div>
1587
1627
  </div>
1588
- `
1589
- }).join('')}
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
- ${step.codeLocation
1707
+ ${
1708
+ step.codeLocation
1667
1709
  ? `<div class="step-info code-section"><strong>Location:</strong> ${sanitizeHTML(
1668
- step.codeLocation
1669
- )}</div>`
1710
+ step.codeLocation
1711
+ )}</div>`
1670
1712
  : ""
1671
- }
1672
- ${step.errorMessage
1713
+ }
1714
+ ${
1715
+ step.errorMessage
1673
1716
  ? `<div class="test-error-summary">
1674
- ${step.stackTrace
1675
- ? `<div class="stack-trace">${formatPlaywrightError(
1676
- step.stackTrace
1677
- )}</div>`
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
- ${hasNestedSteps
1745
+ }
1746
+ ${
1747
+ hasNestedSteps
1703
1748
  ? `<div class="nested-steps">${generateStepsHTML(
1704
- step.steps,
1705
- depth + 1
1706
- )}</div>`
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
- .compact-ai-btn {
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 = path.resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
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
- await runScript(archiveRunScriptPath); // This script now handles JSON history
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 specific failed test.
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
- <button class="compact-ai-btn" onclick="getAIFix(this)" data-test-json="${testJson}">
1752
- <span class="ai-text">AI Fix</span>
1753
- </button>
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 = path.resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
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
- await runScript(archiveRunScriptPath); // This script now handles JSON history
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 = path.resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
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 REPORT_DIR = "./pulse-report"; // Or change this to your reports directory
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(REPORT_DIR, file);
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
- const reportFiles = getReportFiles(REPORT_DIR);
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
- if (reportFiles.length === 0) {
72
- console.log("No matching JSON report files found.");
73
- process.exit(1);
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
- path.join(REPORT_DIR, OUTPUT_FILE),
80
- JSON.stringify(merged, null, 2)
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
+ })();
@@ -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 reportDir = "./pulse-report";
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")); // CHANGED
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
- await runScript(archiveRunScriptPath);
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 credentials = await fetchCredentials();
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
  }