@arghajit/playwright-pulse-report 0.2.10 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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(
@@ -1850,43 +1856,53 @@ function generateHTML(reportData, trendData = null) {
1850
1856
  : ""
1851
1857
  }<button class="copy-error-btn" onclick="copyErrorToClipboard(this)">Copy Error Prompt</button></div>`
1852
1858
  : ""
1853
- }${
1854
- (() => {
1855
- if (!step.attachments || step.attachments.length === 0) return "";
1856
- return `<div class="attachments-section"><h4>Step Attachments</h4><div class="attachments-grid">${step.attachments
1857
- .map((attachment) => {
1858
- try {
1859
- const attachmentPath = path.resolve(
1860
- DEFAULT_OUTPUT_DIR,
1861
- attachment.path
1862
- );
1863
- if (!fsExistsSync(attachmentPath)) {
1864
- return `<div class="attachment-item error">Attachment not found: ${sanitizeHTML(
1865
- attachment.name
1866
- )}</div>`;
1867
- }
1868
- const attachmentBase64 = readFileSync(attachmentPath).toString("base64");
1869
- const attachmentDataUri = `data:${attachment.contentType};base64,${attachmentBase64}`;
1870
- return `<div class="attachment-item generic-attachment">
1871
- <div class="attachment-icon">${getAttachmentIcon(attachment.contentType)}</div>
1859
+ }${(() => {
1860
+ if (!step.attachments || step.attachments.length === 0)
1861
+ return "";
1862
+ return `<div class="attachments-section"><h4>Step Attachments</h4><div class="attachments-grid">${step.attachments
1863
+ .map((attachment) => {
1864
+ try {
1865
+ const attachmentPath = path.resolve(
1866
+ DEFAULT_OUTPUT_DIR,
1867
+ attachment.path
1868
+ );
1869
+ if (!fsExistsSync(attachmentPath)) {
1870
+ return `<div class="attachment-item error">Attachment not found: ${sanitizeHTML(
1871
+ attachment.name
1872
+ )}</div>`;
1873
+ }
1874
+ const attachmentBase64 =
1875
+ readFileSync(attachmentPath).toString("base64");
1876
+ const attachmentDataUri = `data:${attachment.contentType};base64,${attachmentBase64}`;
1877
+ return `<div class="attachment-item generic-attachment">
1878
+ <div class="attachment-icon">${getAttachmentIcon(
1879
+ attachment.contentType
1880
+ )}</div>
1872
1881
  <div class="attachment-caption">
1873
- <span class="attachment-name" title="${sanitizeHTML(attachment.name)}">${sanitizeHTML(attachment.name)}</span>
1874
- <span class="attachment-type">${sanitizeHTML(attachment.contentType)}</span>
1882
+ <span class="attachment-name" title="${sanitizeHTML(
1883
+ attachment.name
1884
+ )}">${sanitizeHTML(attachment.name)}</span>
1885
+ <span class="attachment-type">${sanitizeHTML(
1886
+ attachment.contentType
1887
+ )}</span>
1875
1888
  </div>
1876
1889
  <div class="attachment-info">
1877
1890
  <div class="trace-actions">
1878
1891
  <a href="#" data-href="${attachmentDataUri}" class="view-full lazy-load-attachment" target="_blank">View</a>
1879
- <a href="#" data-href="${attachmentDataUri}" class="lazy-load-attachment" download="${sanitizeHTML(attachment.name)}">Download</a>
1892
+ <a href="#" data-href="${attachmentDataUri}" class="lazy-load-attachment" download="${sanitizeHTML(
1893
+ attachment.name
1894
+ )}">Download</a>
1880
1895
  </div>
1881
1896
  </div>
1882
1897
  </div>`;
1883
- } catch (e) {
1884
- return `<div class="attachment-item error">Failed to load attachment: ${sanitizeHTML(attachment.name)}</div>`;
1885
- }
1886
- })
1887
- .join("")}</div></div>`;
1888
- })()
1889
- }${
1898
+ } catch (e) {
1899
+ return `<div class="attachment-item error">Failed to load attachment: ${sanitizeHTML(
1900
+ attachment.name
1901
+ )}</div>`;
1902
+ }
1903
+ })
1904
+ .join("")}</div></div>`;
1905
+ })()}${
1890
1906
  hasNestedSteps
1891
1907
  ? `<div class="nested-steps">${generateStepsHTML(
1892
1908
  step.steps,
@@ -1903,7 +1919,9 @@ function generateHTML(reportData, trendData = null) {
1903
1919
  test.tags || []
1904
1920
  )
1905
1921
  .join(",")
1906
- .toLowerCase()}" data-test-id="${sanitizeHTML(String(test.id || testIndex))}">
1922
+ .toLowerCase()}" data-test-id="${sanitizeHTML(
1923
+ String(test.id || testIndex)
1924
+ )}">
1907
1925
  <div class="test-case-header" role="button" aria-expanded="false"><div class="test-case-summary"><span class="status-badge ${getStatusClass(
1908
1926
  test.status
1909
1927
  )}">${String(
@@ -1927,18 +1945,66 @@ function generateHTML(reportData, trendData = null) {
1927
1945
  <p><strong>Full Path:</strong> ${sanitizeHTML(
1928
1946
  test.name
1929
1947
  )}</p>
1948
+ ${
1949
+ test.annotations && test.annotations.length > 0
1950
+ ? `<div class="annotations-section" style="margin: 12px 0; padding: 12px; background-color: rgba(139, 92, 246, 0.1); border: 1px solid rgba(139, 92, 246, 0.3); border-left: 4px solid #8b5cf6; border-radius: 4px;">
1951
+ <h4 style="margin-top: 0; margin-bottom: 10px; color: #8b5cf6; font-size: 1.1em;">📌 Annotations</h4>
1952
+ ${test.annotations
1953
+ .map((annotation, idx) => {
1954
+ const isIssueOrBug =
1955
+ annotation.type === "issue" ||
1956
+ annotation.type === "bug";
1957
+ const descriptionText =
1958
+ annotation.description || "";
1959
+ const typeLabel = sanitizeHTML(
1960
+ annotation.type
1961
+ );
1962
+ const descriptionHtml =
1963
+ isIssueOrBug &&
1964
+ descriptionText.match(/^[A-Z]+-\d+$/)
1965
+ ? `<a href="#" class="annotation-link" data-annotation="${sanitizeHTML(
1966
+ descriptionText
1967
+ )}" style="color: #3b82f6; text-decoration: underline; cursor: pointer;">${sanitizeHTML(
1968
+ descriptionText
1969
+ )}</a>`
1970
+ : sanitizeHTML(descriptionText);
1971
+ const locationText = annotation.location
1972
+ ? `<div style="font-size: 0.85em; color: #6b7280; margin-top: 4px;">Location: ${sanitizeHTML(
1973
+ annotation.location.file
1974
+ )}:${annotation.location.line}:${
1975
+ annotation.location.column
1976
+ }</div>`
1977
+ : "";
1978
+ return `<div style="margin-bottom: ${
1979
+ idx < test.annotations.length - 1
1980
+ ? "10px"
1981
+ : "0"
1982
+ };">
1983
+ <strong style="color: #8b5cf6;">Type:</strong> <span style="background-color: rgba(139, 92, 246, 0.2); padding: 2px 8px; border-radius: 4px; font-size: 0.9em;">${typeLabel}</span>
1984
+ ${
1985
+ descriptionText
1986
+ ? `<br><strong style="color: #8b5cf6;">Description:</strong> ${descriptionHtml}`
1987
+ : ""
1988
+ }
1989
+ ${locationText}
1990
+ </div>`;
1991
+ })
1992
+ .join("")}
1993
+ </div>`
1994
+ : ""
1995
+ }
1930
1996
  <p><strong>Test run Worker ID:</strong> ${sanitizeHTML(
1931
1997
  test.workerId
1932
1998
  )} [<strong>Total No. of Workers:</strong> ${sanitizeHTML(
1933
1999
  test.totalWorkers
1934
2000
  )}]</p>
1935
- ${
1936
- test.errorMessage
1937
- ? `<div class="test-error-summary">${formatPlaywrightError(
1938
- test.errorMessage
1939
- )}<button class="copy-error-btn" onclick="copyErrorToClipboard(this)">Copy Error Prompt</button></div>`
1940
- : ""
1941
- }
2001
+ ${
2002
+ test.errorMessage
2003
+ ? `<div class="test-error-summary">${formatPlaywrightError(
2004
+ test.errorMessage
2005
+ )}<button class="copy-error-btn" onclick="copyErrorToClipboard(this)">Copy Error Prompt</button></div>`
2006
+ : ""
2007
+ }
1942
2008
  ${
1943
2009
  test.snippet
1944
2010
  ? `<div class="code-section"><h4>Error Snippet</h4><pre><code>${formatPlaywrightError(
@@ -1970,7 +2036,9 @@ function generateHTML(reportData, trendData = null) {
1970
2036
  ${
1971
2037
  test.stderr && test.stderr.length > 0
1972
2038
  ? (() => {
1973
- const logId = `stderr-log-${test.id || testIndex}`;
2039
+ const logId = `stderr-log-${
2040
+ test.id || testIndex
2041
+ }`;
1974
2042
  return `<div class="console-output-section"><h4>Console Output (stderr)</h4><pre id="${logId}" class="console-log stderr-log">${test.stderr
1975
2043
  .map((line) => sanitizeHTML(line))
1976
2044
  .join("\\n")}</pre></div>`;
@@ -2369,9 +2437,14 @@ aspect-ratio: 16 / 9;
2369
2437
  .browser-indicator { background: var(--info-color); color: white; }
2370
2438
  #load-more-tests { font-size: 16px; padding: 4px; background-color: var(--light-gray-color); border-radius: 4px; color: var(--text-color); }
2371
2439
  .duration-indicator { background: var(--medium-gray-color); color: var(--text-color); }
2440
+ .ai-buttons-group { display: flex; gap: 10px; flex-wrap: wrap; }
2372
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;}
2373
2442
  .compact-ai-btn:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(55, 65, 81, 0.4); }
2374
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; }
2375
2448
  .failure-error-preview { padding: 0 20px 18px 20px; border-top: 1px solid var(--light-gray-color);}
2376
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;}
2377
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;}
@@ -2382,7 +2455,7 @@ aspect-ratio: 16 / 9;
2382
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;}
2383
2456
  @media (max-width: 1200px) { .trend-charts-row { grid-template-columns: 1fr; } }
2384
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; } }
2385
- @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%; } }
2386
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; } }
2387
2460
  .trace-actions a { text-decoration: none; font-weight: 500; font-size: 0.9em; }
2388
2461
  .generic-attachment { text-align: center; padding: 1rem; justify-content: center; }
@@ -2651,6 +2724,81 @@ aspect-ratio: 16 / 9;
2651
2724
  }
2652
2725
  }
2653
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
+
2654
2802
  function closeAiModal() {
2655
2803
  const modal = document.getElementById('ai-fix-modal');
2656
2804
  if(modal) modal.style.display = 'none';
@@ -2826,6 +2974,18 @@ aspect-ratio: 16 / 9;
2826
2974
  }
2827
2975
  return;
2828
2976
  }
2977
+ const annotationLink = e.target.closest('a.annotation-link');
2978
+ if (annotationLink) {
2979
+ e.preventDefault();
2980
+ const annotationId = annotationLink.dataset.annotation;
2981
+ if (annotationId) {
2982
+ const jiraUrl = prompt('Enter your JIRA/Ticket system base URL (e.g., https://your-company.atlassian.net/browse/):', 'https://your-company.atlassian.net/browse/');
2983
+ if (jiraUrl) {
2984
+ window.open(jiraUrl + annotationId, '_blank');
2985
+ }
2986
+ }
2987
+ return;
2988
+ }
2829
2989
  const img = e.target.closest('img.lazy-load-image');
2830
2990
  if (img && img.dataset && img.dataset.src) {
2831
2991
  if (e.preventDefault) e.preventDefault();
@@ -2994,10 +3154,10 @@ aspect-ratio: 16 / 9;
2994
3154
  </html>
2995
3155
  `;
2996
3156
  }
2997
- async function runScript(scriptPath) {
3157
+ async function runScript(scriptPath, args = []) {
2998
3158
  return new Promise((resolve, reject) => {
2999
3159
  console.log(chalk.blue(`Executing script: ${scriptPath}...`));
3000
- const process = fork(scriptPath, [], {
3160
+ const process = fork(scriptPath, args, {
3001
3161
  stdio: "inherit",
3002
3162
  });
3003
3163
 
@@ -3027,13 +3187,22 @@ async function main() {
3027
3187
  const __filename = fileURLToPath(import.meta.url);
3028
3188
  const __dirname = path.dirname(__filename);
3029
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
+
3030
3199
  // Script to archive current run to JSON history (this is your modified "generate-trend.mjs")
3031
3200
  const archiveRunScriptPath = path.resolve(
3032
3201
  __dirname,
3033
3202
  "generate-trend.mjs" // Keeping the filename as per your request
3034
3203
  );
3035
3204
 
3036
- const outputDir = path.resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
3205
+ const outputDir = await getOutputDir(customOutputDir);
3037
3206
  const reportJsonPath = path.resolve(outputDir, DEFAULT_JSON_FILE); // Current run's main JSON
3038
3207
  const reportHtmlPath = path.resolve(outputDir, DEFAULT_HTML_FILE);
3039
3208
 
@@ -3043,10 +3212,21 @@ async function main() {
3043
3212
 
3044
3213
  console.log(chalk.blue(`Starting static HTML report generation...`));
3045
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
+ }
3046
3225
 
3047
3226
  // Step 1: Ensure current run data is archived to the history folder
3048
3227
  try {
3049
- await runScript(archiveRunScriptPath); // This script now handles JSON history
3228
+ const archiveArgs = customOutputDir ? ["--outputDir", customOutputDir] : [];
3229
+ await runScript(archiveRunScriptPath, archiveArgs);
3050
3230
  console.log(
3051
3231
  chalk.green("Current run data archiving to history completed.")
3052
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
  }