@arghajit/dummy 0.3.0 → 0.3.1

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.1",
5
5
  "description": "A Playwright reporter and dashboard for visualizing test results.",
6
6
  "homepage": "https://playwright-pulse-report.netlify.app/",
7
7
  "keywords": [
@@ -23,6 +23,15 @@ const DEFAULT_OUTPUT_DIR = "pulse-report";
23
23
  const DEFAULT_JSON_FILE = "playwright-pulse-report.json";
24
24
  const MINIFIED_HTML_FILE = "pulse-email-summary.html"; // New minified report
25
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
+
26
35
  function sanitizeHTML(str) {
27
36
  if (str === null || str === undefined) return "";
28
37
  return String(str).replace(/[&<>"']/g, (match) => {
@@ -652,7 +661,9 @@ function generateMinifiedHTML(reportData) {
652
661
  `;
653
662
  }
654
663
  async function main() {
655
- const outputDir = path.resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
664
+ const outputDir = customOutputDir
665
+ ? path.resolve(process.cwd(), customOutputDir)
666
+ : path.resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
656
667
  const reportJsonPath = path.resolve(outputDir, DEFAULT_JSON_FILE);
657
668
  const minifiedReportHtmlPath = path.resolve(outputDir, MINIFIED_HTML_FILE); // Path for the new minified HTML
658
669
 
@@ -1519,7 +1519,9 @@ function getAttachmentIcon(contentType) {
1519
1519
  return "📎";
1520
1520
  }
1521
1521
  function generateAIFailureAnalyzerTab(results) {
1522
- const failedTests = (results || []).filter(test => test.status === 'failed');
1522
+ const failedTests = (results || []).filter(
1523
+ (test) => test.status === "failed"
1524
+ );
1523
1525
 
1524
1526
  if (failedTests.length === 0) {
1525
1527
  return `
@@ -1529,7 +1531,7 @@ function generateAIFailureAnalyzerTab(results) {
1529
1531
  }
1530
1532
 
1531
1533
  // 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');
1534
+ const btoa = (str) => Buffer.from(str).toString("base64");
1533
1535
 
1534
1536
  return `
1535
1537
  <h2 class="tab-main-title">AI Failure Analysis</h2>
@@ -1539,41 +1541,61 @@ function generateAIFailureAnalyzerTab(results) {
1539
1541
  <span class="stat-label">Failed Tests</span>
1540
1542
  </div>
1541
1543
  <div class="stat-item">
1542
- <span class="stat-number">${new Set(failedTests.map(t => t.browser)).size}</span>
1544
+ <span class="stat-number">${
1545
+ new Set(failedTests.map((t) => t.browser)).size
1546
+ }</span>
1543
1547
  <span class="stat-label">Browsers</span>
1544
1548
  </div>
1545
1549
  <div class="stat-item">
1546
- <span class="stat-number">${(Math.round(failedTests.reduce((sum, test) => sum + (test.duration || 0), 0) / 1000))}s</span>
1550
+ <span class="stat-number">${Math.round(
1551
+ failedTests.reduce((sum, test) => sum + (test.duration || 0), 0) /
1552
+ 1000
1553
+ )}s</span>
1547
1554
  <span class="stat-label">Total Duration</span>
1548
1555
  </div>
1549
1556
  </div>
1550
1557
  <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.
1558
+ 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
1559
  </p>
1553
1560
 
1554
1561
  <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 `
1562
+ ${failedTests
1563
+ .map((test) => {
1564
+ const testTitle = test.name.split(" > ").pop() || "Unnamed Test";
1565
+ const testJson = btoa(JSON.stringify(test)); // Base64 encode the test object
1566
+ const truncatedError =
1567
+ (test.errorMessage || "No error message").slice(0, 150) +
1568
+ (test.errorMessage && test.errorMessage.length > 150 ? "..." : "");
1569
+
1570
+ return `
1562
1571
  <div class="compact-failure-item">
1563
1572
  <div class="failure-header">
1564
1573
  <div class="failure-main-info">
1565
- <h3 class="failure-title" title="${sanitizeHTML(test.name)}">${sanitizeHTML(testTitle)}</h3>
1574
+ <h3 class="failure-title" title="${sanitizeHTML(
1575
+ test.name
1576
+ )}">${sanitizeHTML(testTitle)}</h3>
1566
1577
  <div class="failure-meta">
1567
- <span class="browser-indicator">${sanitizeHTML(test.browser || 'unknown')}</span>
1568
- <span class="duration-indicator">${formatDuration(test.duration)}</span>
1578
+ <span class="browser-indicator">${sanitizeHTML(
1579
+ test.browser || "unknown"
1580
+ )}</span>
1581
+ <span class="duration-indicator">${formatDuration(
1582
+ test.duration
1583
+ )}</span>
1569
1584
  </div>
1570
1585
  </div>
1571
- <button class="compact-ai-btn" onclick="getAIFix(this)" data-test-json="${testJson}">
1572
- <span class="ai-text">AI Fix</span>
1573
- </button>
1586
+ <div class="ai-buttons-group">
1587
+ <button class="compact-ai-btn" onclick="getAIFix(this)" data-test-json="${testJson}">
1588
+ <span class="ai-text">AI Fix</span>
1589
+ </button>
1590
+ <button class="copy-prompt-btn" onclick="copyAIPrompt(this)" data-test-json="${testJson}" title="Copy AI Prompt">
1591
+ <span class="copy-prompt-text">Copy AI Prompt</span>
1592
+ </button>
1593
+ </div>
1574
1594
  </div>
1575
1595
  <div class="failure-error-preview">
1576
- <div class="error-snippet">${formatPlaywrightError(truncatedError)}</div>
1596
+ <div class="error-snippet">${formatPlaywrightError(
1597
+ truncatedError
1598
+ )}</div>
1577
1599
  <button class="expand-error-btn" onclick="toggleErrorDetails(this)">
1578
1600
  <span class="expand-text">Show Full Error</span>
1579
1601
  <span class="expand-icon">▼</span>
@@ -1581,12 +1603,15 @@ function generateAIFailureAnalyzerTab(results) {
1581
1603
  </div>
1582
1604
  <div class="full-error-details" style="display: none;">
1583
1605
  <div class="full-error-content">
1584
- ${formatPlaywrightError(test.errorMessage || "No detailed error message available")}
1606
+ ${formatPlaywrightError(
1607
+ test.errorMessage || "No detailed error message available"
1608
+ )}
1585
1609
  </div>
1586
1610
  </div>
1587
1611
  </div>
1588
- `
1589
- }).join('')}
1612
+ `;
1613
+ })
1614
+ .join("")}
1590
1615
  </div>
1591
1616
 
1592
1617
  <!-- AI Fix Modal -->
@@ -1618,7 +1643,7 @@ function generateHTML(reportData, trendData = null) {
1618
1643
  const fixPath = (p) => {
1619
1644
  if (!p) return "";
1620
1645
  // This regex handles both forward slashes and backslashes
1621
- return p.replace(new RegExp(`^${DEFAULT_OUTPUT_DIR}[\\\\/]`), '');
1646
+ return p.replace(new RegExp(`^${DEFAULT_OUTPUT_DIR}[\\\\/]`), "");
1622
1647
  };
1623
1648
 
1624
1649
  const totalTestsOr1 = runSummary.totalTests || 1;
@@ -1663,20 +1688,23 @@ function generateHTML(reportData, trendData = null) {
1663
1688
  )}</span>
1664
1689
  </div>
1665
1690
  <div class="step-details" style="display: none;">
1666
- ${step.codeLocation
1691
+ ${
1692
+ step.codeLocation
1667
1693
  ? `<div class="step-info code-section"><strong>Location:</strong> ${sanitizeHTML(
1668
- step.codeLocation
1669
- )}</div>`
1694
+ step.codeLocation
1695
+ )}</div>`
1670
1696
  : ""
1671
- }
1672
- ${step.errorMessage
1697
+ }
1698
+ ${
1699
+ step.errorMessage
1673
1700
  ? `<div class="test-error-summary">
1674
- ${step.stackTrace
1675
- ? `<div class="stack-trace">${formatPlaywrightError(
1676
- step.stackTrace
1677
- )}</div>`
1678
- : ""
1679
- }
1701
+ ${
1702
+ step.stackTrace
1703
+ ? `<div class="stack-trace">${formatPlaywrightError(
1704
+ step.stackTrace
1705
+ )}</div>`
1706
+ : ""
1707
+ }
1680
1708
  <button
1681
1709
  class="copy-error-btn"
1682
1710
  onclick="copyErrorToClipboard(this)"
@@ -1698,14 +1726,15 @@ function generateHTML(reportData, trendData = null) {
1698
1726
  </button>
1699
1727
  </div>`
1700
1728
  : ""
1701
- }
1702
- ${hasNestedSteps
1729
+ }
1730
+ ${
1731
+ hasNestedSteps
1703
1732
  ? `<div class="nested-steps">${generateStepsHTML(
1704
- step.steps,
1705
- depth + 1
1706
- )}</div>`
1733
+ step.steps,
1734
+ depth + 1
1735
+ )}</div>`
1707
1736
  : ""
1708
- }
1737
+ }
1709
1738
  </div>
1710
1739
  </div>`;
1711
1740
  })
@@ -2297,6 +2326,35 @@ function generateHTML(reportData, trendData = null) {
2297
2326
  .ai-text {
2298
2327
  font-size: 0.95em;
2299
2328
  }
2329
+ .ai-buttons-group {
2330
+ display: flex;
2331
+ gap: 10px;
2332
+ flex-wrap: wrap;
2333
+ }
2334
+ .copy-prompt-btn {
2335
+ background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
2336
+ color: white;
2337
+ border: none;
2338
+ padding: 12px 18px;
2339
+ border-radius: 6px;
2340
+ cursor: pointer;
2341
+ font-weight: 600;
2342
+ display: flex;
2343
+ align-items: center;
2344
+ gap: 8px;
2345
+ transition: all 0.3s ease;
2346
+ white-space: nowrap;
2347
+ }
2348
+ .copy-prompt-btn:hover {
2349
+ transform: translateY(-2px);
2350
+ box-shadow: 0 6px 20px rgba(37, 99, 235, 0.4);
2351
+ }
2352
+ .copy-prompt-btn.copied {
2353
+ background: linear-gradient(135deg, #10b981 0%, #059669 100%);
2354
+ }
2355
+ .copy-prompt-text {
2356
+ font-size: 0.95em;
2357
+ }
2300
2358
  .failure-error-preview {
2301
2359
  padding: 0 20px 18px 20px;
2302
2360
  border-top: 1px solid var(--light-gray-color);
@@ -2372,9 +2430,14 @@ function generateHTML(reportData, trendData = null) {
2372
2430
  .failure-meta {
2373
2431
  justify-content: center;
2374
2432
  }
2375
- .compact-ai-btn {
2433
+ .ai-buttons-group {
2434
+ flex-direction: column;
2435
+ width: 100%;
2436
+ }
2437
+ .compact-ai-btn, .copy-prompt-btn {
2376
2438
  justify-content: center;
2377
2439
  padding: 12px 20px;
2440
+ width: 100%;
2378
2441
  }
2379
2442
  }
2380
2443
  @media (max-width: 480px) {
@@ -2649,6 +2712,81 @@ function getAIFix(button) {
2649
2712
  }
2650
2713
 
2651
2714
 
2715
+ function copyAIPrompt(button) {
2716
+ try {
2717
+ const testJson = button.dataset.testJson;
2718
+ const test = JSON.parse(atob(testJson));
2719
+
2720
+ const testName = test.name || 'Unknown Test';
2721
+ const failureLogsAndErrors = [
2722
+ 'Error Message:',
2723
+ test.errorMessage || 'Not available.',
2724
+ '\\n\\n--- stdout ---',
2725
+ (test.stdout && test.stdout.length > 0) ? test.stdout.join('\\n') : 'Not available.',
2726
+ '\\n\\n--- stderr ---',
2727
+ (test.stderr && test.stderr.length > 0) ? test.stderr.join('\\n') : 'Not available.'
2728
+ ].join('\\n');
2729
+ const codeSnippet = test.snippet || '';
2730
+
2731
+ const aiPrompt = \`You are an expert Playwright test automation engineer specializing in debugging test failures.
2732
+
2733
+ INSTRUCTIONS:
2734
+ 1. Analyze the test failure carefully
2735
+ 2. Provide a brief root cause analysis
2736
+ 3. Provide EXACTLY 5 specific, actionable fixes
2737
+ 4. Each fix MUST include a code snippet (codeSnippet field)
2738
+ 5. Return ONLY valid JSON, no markdown or extra text
2739
+
2740
+ REQUIRED JSON FORMAT:
2741
+ {
2742
+ "rootCause": "Brief explanation of why the test failed",
2743
+ "suggestedFixes": [
2744
+ {
2745
+ "description": "Clear explanation of the fix",
2746
+ "codeSnippet": "await page.waitForSelector('.button', { timeout: 5000 });"
2747
+ }
2748
+ ],
2749
+ "affectedTests": ["test1", "test2"]
2750
+ }
2751
+
2752
+ IMPORTANT:
2753
+ - Always return valid JSON only
2754
+ - Always provide exactly 5 fixes in suggestedFixes array
2755
+ - Each fix must have both description and codeSnippet fields
2756
+ - Make code snippets practical and Playwright-specific
2757
+
2758
+ ---
2759
+
2760
+ Test Name: \${testName}
2761
+
2762
+ Failure Logs and Errors:
2763
+ \${failureLogsAndErrors}
2764
+
2765
+ Code Snippet:
2766
+ \${codeSnippet}\`;
2767
+
2768
+ navigator.clipboard.writeText(aiPrompt).then(() => {
2769
+ const originalText = button.querySelector('.copy-prompt-text').textContent;
2770
+ button.querySelector('.copy-prompt-text').textContent = 'Copied!';
2771
+ button.classList.add('copied');
2772
+
2773
+ const shortTestName = testName.split(' > ').pop() || testName;
2774
+ alert(\`AI prompt to generate a suggested fix for "\${shortTestName}" has been copied to your clipboard.\`);
2775
+
2776
+ setTimeout(() => {
2777
+ button.querySelector('.copy-prompt-text').textContent = originalText;
2778
+ button.classList.remove('copied');
2779
+ }, 2000);
2780
+ }).catch(err => {
2781
+ console.error('Failed to copy AI prompt:', err);
2782
+ alert('Failed to copy AI prompt to clipboard. Please try again.');
2783
+ });
2784
+ } catch (e) {
2785
+ console.error('Error processing test data for AI Prompt copy:', e);
2786
+ alert('Could not process test data. Please try again.');
2787
+ }
2788
+ }
2789
+
2652
2790
  function closeAiModal() {
2653
2791
  const modal = document.getElementById('ai-fix-modal');
2654
2792
  if(modal) modal.style.display = 'none';
@@ -2897,10 +3035,10 @@ function copyErrorToClipboard(button) {
2897
3035
  </html>
2898
3036
  `;
2899
3037
  }
2900
- async function runScript(scriptPath) {
3038
+ async function runScript(scriptPath, args = []) {
2901
3039
  return new Promise((resolve, reject) => {
2902
3040
  console.log(chalk.blue(`Executing script: ${scriptPath}...`));
2903
- const process = fork(scriptPath, [], {
3041
+ const process = fork(scriptPath, args, {
2904
3042
  stdio: "inherit",
2905
3043
  });
2906
3044
 
@@ -2925,13 +3063,24 @@ async function main() {
2925
3063
  const __filename = fileURLToPath(import.meta.url);
2926
3064
  const __dirname = path.dirname(__filename);
2927
3065
 
3066
+ const args = process.argv.slice(2);
3067
+ let customOutputDir = null;
3068
+ for (let i = 0; i < args.length; i++) {
3069
+ if (args[i] === "--outputDir" || args[i] === "-o") {
3070
+ customOutputDir = args[i + 1];
3071
+ break;
3072
+ }
3073
+ }
3074
+
2928
3075
  // Script to archive current run to JSON history (this is your modified "generate-trend.mjs")
2929
3076
  const archiveRunScriptPath = path.resolve(
2930
3077
  __dirname,
2931
3078
  "generate-trend.mjs" // Keeping the filename as per your request
2932
3079
  );
2933
3080
 
2934
- const outputDir = path.resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
3081
+ const outputDir = customOutputDir
3082
+ ? path.resolve(process.cwd(), customOutputDir)
3083
+ : path.resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
2935
3084
  const reportJsonPath = path.resolve(outputDir, DEFAULT_JSON_FILE); // Current run's main JSON
2936
3085
  const reportHtmlPath = path.resolve(outputDir, DEFAULT_HTML_FILE);
2937
3086
 
@@ -2944,7 +3093,8 @@ async function main() {
2944
3093
 
2945
3094
  // Step 1: Ensure current run data is archived to the history folder
2946
3095
  try {
2947
- await runScript(archiveRunScriptPath); // This script now handles JSON history
3096
+ const archiveArgs = customOutputDir ? ["--outputDir", customOutputDir] : [];
3097
+ await runScript(archiveRunScriptPath, archiveArgs);
2948
3098
  console.log(
2949
3099
  chalk.green("Current run data archiving to history completed.")
2950
3100
  );
@@ -1720,7 +1720,7 @@ function generateAIFailureAnalyzerTab(results) {
1720
1720
  </div>
1721
1721
  </div>
1722
1722
  <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.
1723
+ 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
1724
  </p>
1725
1725
 
1726
1726
  <div class="compact-failure-list">
@@ -1748,9 +1748,14 @@ function generateAIFailureAnalyzerTab(results) {
1748
1748
  )}</span>
1749
1749
  </div>
1750
1750
  </div>
1751
- <button class="compact-ai-btn" onclick="getAIFix(this)" data-test-json="${testJson}">
1752
- <span class="ai-text">AI Fix</span>
1753
- </button>
1751
+ <div class="ai-buttons-group">
1752
+ <button class="compact-ai-btn" onclick="getAIFix(this)" data-test-json="${testJson}">
1753
+ <span class="ai-text">AI Fix</span>
1754
+ </button>
1755
+ <button class="copy-prompt-btn" onclick="copyAIPrompt(this)" data-test-json="${testJson}" title="Copy AI Prompt">
1756
+ <span class="copy-prompt-text">Copy AI Prompt</span>
1757
+ </button>
1758
+ </div>
1754
1759
  </div>
1755
1760
  <div class="failure-error-preview">
1756
1761
  <div class="error-snippet">${formatPlaywrightError(
@@ -2431,9 +2436,14 @@ aspect-ratio: 16 / 9;
2431
2436
  .browser-indicator { background: var(--info-color); color: white; }
2432
2437
  #load-more-tests { font-size: 16px; padding: 4px; background-color: var(--light-gray-color); border-radius: 4px; color: var(--text-color); }
2433
2438
  .duration-indicator { background: var(--medium-gray-color); color: var(--text-color); }
2439
+ .ai-buttons-group { display: flex; gap: 10px; flex-wrap: wrap; }
2434
2440
  .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
2441
  .compact-ai-btn:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(55, 65, 81, 0.4); }
2436
2442
  .ai-text { font-size: 0.95em; }
2443
+ .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;}
2444
+ .copy-prompt-btn:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(37, 99, 235, 0.4); }
2445
+ .copy-prompt-btn.copied { background: linear-gradient(135deg, #10b981 0%, #059669 100%); }
2446
+ .copy-prompt-text { font-size: 0.95em; }
2437
2447
  .failure-error-preview { padding: 0 20px 18px 20px; border-top: 1px solid var(--light-gray-color);}
2438
2448
  .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
2449
  .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 +2454,7 @@ aspect-ratio: 16 / 9;
2444
2454
  .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
2455
  @media (max-width: 1200px) { .trend-charts-row { grid-template-columns: 1fr; } }
2446
2456
  @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; } }
2457
+ @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
2458
  @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
2459
  .trace-actions a { text-decoration: none; font-weight: 500; font-size: 0.9em; }
2450
2460
  .generic-attachment { text-align: center; padding: 1rem; justify-content: center; }
@@ -2713,6 +2723,81 @@ aspect-ratio: 16 / 9;
2713
2723
  }
2714
2724
  }
2715
2725
 
2726
+ function copyAIPrompt(button) {
2727
+ try {
2728
+ const testJson = button.dataset.testJson;
2729
+ const test = JSON.parse(atob(testJson));
2730
+
2731
+ const testName = test.name || 'Unknown Test';
2732
+ const failureLogsAndErrors = [
2733
+ 'Error Message:',
2734
+ test.errorMessage || 'Not available.',
2735
+ '\\n\\n--- stdout ---',
2736
+ (test.stdout && test.stdout.length > 0) ? test.stdout.join('\\n') : 'Not available.',
2737
+ '\\n\\n--- stderr ---',
2738
+ (test.stderr && test.stderr.length > 0) ? test.stderr.join('\\n') : 'Not available.'
2739
+ ].join('\\n');
2740
+ const codeSnippet = test.snippet || '';
2741
+
2742
+ const aiPrompt = \`You are an expert Playwright test automation engineer specializing in debugging test failures.
2743
+
2744
+ INSTRUCTIONS:
2745
+ 1. Analyze the test failure carefully
2746
+ 2. Provide a brief root cause analysis
2747
+ 3. Provide EXACTLY 5 specific, actionable fixes
2748
+ 4. Each fix MUST include a code snippet (codeSnippet field)
2749
+ 5. Return ONLY valid JSON, no markdown or extra text
2750
+
2751
+ REQUIRED JSON FORMAT:
2752
+ {
2753
+ "rootCause": "Brief explanation of why the test failed",
2754
+ "suggestedFixes": [
2755
+ {
2756
+ "description": "Clear explanation of the fix",
2757
+ "codeSnippet": "await page.waitForSelector('.button', { timeout: 5000 });"
2758
+ }
2759
+ ],
2760
+ "affectedTests": ["test1", "test2"]
2761
+ }
2762
+
2763
+ IMPORTANT:
2764
+ - Always return valid JSON only
2765
+ - Always provide exactly 5 fixes in suggestedFixes array
2766
+ - Each fix must have both description and codeSnippet fields
2767
+ - Make code snippets practical and Playwright-specific
2768
+
2769
+ ---
2770
+
2771
+ Test Name: \${testName}
2772
+
2773
+ Failure Logs and Errors:
2774
+ \${failureLogsAndErrors}
2775
+
2776
+ Code Snippet:
2777
+ \${codeSnippet}\`;
2778
+
2779
+ navigator.clipboard.writeText(aiPrompt).then(() => {
2780
+ const originalText = button.querySelector('.copy-prompt-text').textContent;
2781
+ button.querySelector('.copy-prompt-text').textContent = 'Copied!';
2782
+ button.classList.add('copied');
2783
+
2784
+ const shortTestName = testName.split(' > ').pop() || testName;
2785
+ alert(\`AI prompt to generate a suggested fix for "\${shortTestName}" has been copied to your clipboard.\`);
2786
+
2787
+ setTimeout(() => {
2788
+ button.querySelector('.copy-prompt-text').textContent = originalText;
2789
+ button.classList.remove('copied');
2790
+ }, 2000);
2791
+ }).catch(err => {
2792
+ console.error('Failed to copy AI prompt:', err);
2793
+ alert('Failed to copy AI prompt to clipboard. Please try again.');
2794
+ });
2795
+ } catch (e) {
2796
+ console.error('Error processing test data for AI Prompt copy:', e);
2797
+ alert('Could not process test data. Please try again.');
2798
+ }
2799
+ }
2800
+
2716
2801
  function closeAiModal() {
2717
2802
  const modal = document.getElementById('ai-fix-modal');
2718
2803
  if(modal) modal.style.display = 'none';
@@ -3068,10 +3153,10 @@ aspect-ratio: 16 / 9;
3068
3153
  </html>
3069
3154
  `;
3070
3155
  }
3071
- async function runScript(scriptPath) {
3156
+ async function runScript(scriptPath, args = []) {
3072
3157
  return new Promise((resolve, reject) => {
3073
3158
  console.log(chalk.blue(`Executing script: ${scriptPath}...`));
3074
- const process = fork(scriptPath, [], {
3159
+ const process = fork(scriptPath, args, {
3075
3160
  stdio: "inherit",
3076
3161
  });
3077
3162
 
@@ -3101,13 +3186,24 @@ async function main() {
3101
3186
  const __filename = fileURLToPath(import.meta.url);
3102
3187
  const __dirname = path.dirname(__filename);
3103
3188
 
3189
+ const args = process.argv.slice(2);
3190
+ let customOutputDir = null;
3191
+ for (let i = 0; i < args.length; i++) {
3192
+ if (args[i] === "--outputDir" || args[i] === "-o") {
3193
+ customOutputDir = args[i + 1];
3194
+ break;
3195
+ }
3196
+ }
3197
+
3104
3198
  // Script to archive current run to JSON history (this is your modified "generate-trend.mjs")
3105
3199
  const archiveRunScriptPath = path.resolve(
3106
3200
  __dirname,
3107
3201
  "generate-trend.mjs" // Keeping the filename as per your request
3108
3202
  );
3109
3203
 
3110
- const outputDir = path.resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
3204
+ const outputDir = customOutputDir
3205
+ ? path.resolve(process.cwd(), customOutputDir)
3206
+ : path.resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
3111
3207
  const reportJsonPath = path.resolve(outputDir, DEFAULT_JSON_FILE); // Current run's main JSON
3112
3208
  const reportHtmlPath = path.resolve(outputDir, DEFAULT_HTML_FILE);
3113
3209
 
@@ -3120,7 +3216,8 @@ async function main() {
3120
3216
 
3121
3217
  // Step 1: Ensure current run data is archived to the history folder
3122
3218
  try {
3123
- await runScript(archiveRunScriptPath); // This script now handles JSON history
3219
+ const archiveArgs = customOutputDir ? ["--outputDir", customOutputDir] : [];
3220
+ await runScript(archiveRunScriptPath, archiveArgs);
3124
3221
  console.log(
3125
3222
  chalk.green("Current run data archiving to history completed.")
3126
3223
  );
@@ -22,8 +22,19 @@ const HISTORY_SUBDIR = "history"; // Subdirectory for historical JSON files
22
22
  const HISTORY_FILE_PREFIX = "trend-";
23
23
  const MAX_HISTORY_FILES = 15; // Store last 15 runs
24
24
 
25
+ const args = process.argv.slice(2);
26
+ let customOutputDir = null;
27
+ for (let i = 0; i < args.length; i++) {
28
+ if (args[i] === '--outputDir' || args[i] === '-o') {
29
+ customOutputDir = args[i + 1];
30
+ break;
31
+ }
32
+ }
33
+
25
34
  async function archiveCurrentRunData() {
26
- const outputDir = path.resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
35
+ const outputDir = customOutputDir
36
+ ? path.resolve(process.cwd(), customOutputDir)
37
+ : path.resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
27
38
  const currentRunJsonPath = path.join(outputDir, CURRENT_RUN_JSON_FILE);
28
39
  const historyDir = path.join(outputDir, HISTORY_SUBDIR);
29
40
 
@@ -3,7 +3,16 @@
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
+
15
+ const REPORT_DIR = customOutputDir || "./pulse-report";
7
16
  const OUTPUT_FILE = "playwright-pulse-report.json";
8
17
 
9
18
  function getReportFiles(dir) {
@@ -28,7 +28,16 @@ try {
28
28
  };
29
29
  }
30
30
 
31
- const reportDir = "./pulse-report";
31
+ const args = process.argv.slice(2);
32
+ let customOutputDir = null;
33
+ for (let i = 0; i < args.length; i++) {
34
+ if (args[i] === "--outputDir" || args[i] === "-o") {
35
+ customOutputDir = args[i + 1];
36
+ break;
37
+ }
38
+ }
39
+
40
+ const reportDir = customOutputDir || "./pulse-report";
32
41
 
33
42
  let fetch;
34
43
  // Ensure fetch is imported and available before it's used in fetchCredentials
@@ -220,9 +229,9 @@ const archiveRunScriptPath = path.resolve(
220
229
  "generate-email-report.mjs" // Or input_file_0.mjs if you rename it, or input_file_0.js if you configure package.json
221
230
  );
222
231
 
223
- async function runScript(scriptPath) {
232
+ async function runScript(scriptPath, args = []) {
224
233
  return new Promise((resolve, reject) => {
225
- const childProcess = fork(scriptPath, [], {
234
+ const childProcess = fork(scriptPath, args, {
226
235
  // Renamed variable
227
236
  stdio: "inherit",
228
237
  });
@@ -245,7 +254,8 @@ async function runScript(scriptPath) {
245
254
  }
246
255
 
247
256
  const sendEmail = async (credentials) => {
248
- await runScript(archiveRunScriptPath);
257
+ const archiveArgs = customOutputDir ? ["--outputDir", customOutputDir] : [];
258
+ await runScript(archiveRunScriptPath, archiveArgs);
249
259
  try {
250
260
  console.log("Starting the sendEmail function...");
251
261