@arghajit/playwright-pulse-report 0.2.9 → 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,9 +2455,9 @@ 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
- .trace-actions a { text-decoration: none; color: var(--primary-color); font-weight: 500; font-size: 0.9em; }
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; }
2389
2462
  .attachment-icon { font-size: 2.5rem; display: block; margin-bottom: 0.75rem; }
2390
2463
  .attachment-caption { display: flex; flex-direction: column; align-items: center; justify-content: center; flex-grow: 1; }
@@ -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();
@@ -2856,9 +3016,45 @@ aspect-ratio: 16 / 9;
2856
3016
  const a = e.target.closest('a.lazy-load-attachment');
2857
3017
  if (a && a.dataset && a.dataset.href) {
2858
3018
  e.preventDefault();
2859
- a.href = a.dataset.href;
2860
- a.removeAttribute('data-href');
2861
- a.click();
3019
+
3020
+ // Special handling for view-full links to avoid about:blank issue
3021
+ if (a.classList.contains('view-full')) {
3022
+ // Extract the data from the data URI
3023
+ const dataUri = a.dataset.href;
3024
+ const [header, base64Data] = dataUri.split(',');
3025
+ const mimeType = header.match(/data:([^;]+)/)[1];
3026
+
3027
+ try {
3028
+ // Convert base64 to blob
3029
+ const byteCharacters = atob(base64Data);
3030
+ const byteNumbers = new Array(byteCharacters.length);
3031
+ for (let i = 0; i < byteCharacters.length; i++) {
3032
+ byteNumbers[i] = byteCharacters.charCodeAt(i);
3033
+ }
3034
+ const byteArray = new Uint8Array(byteNumbers);
3035
+ const blob = new Blob([byteArray], { type: mimeType });
3036
+
3037
+ // Create a URL and open it
3038
+ const blobUrl = URL.createObjectURL(blob);
3039
+ const newWindow = window.open(blobUrl, '_blank');
3040
+
3041
+ // Clean up the URL after a delay
3042
+ setTimeout(() => {
3043
+ URL.revokeObjectURL(blobUrl);
3044
+ }, 1000);
3045
+ } catch (error) {
3046
+ console.error('Failed to open attachment:', error);
3047
+ // Fallback to original method
3048
+ a.href = a.dataset.href;
3049
+ a.removeAttribute('data-href');
3050
+ a.click();
3051
+ }
3052
+ } else {
3053
+ // For download links, use the original method
3054
+ a.href = a.dataset.href;
3055
+ a.removeAttribute('data-href');
3056
+ a.click();
3057
+ }
2862
3058
  return;
2863
3059
  }
2864
3060
  });
@@ -2958,10 +3154,10 @@ aspect-ratio: 16 / 9;
2958
3154
  </html>
2959
3155
  `;
2960
3156
  }
2961
- async function runScript(scriptPath) {
3157
+ async function runScript(scriptPath, args = []) {
2962
3158
  return new Promise((resolve, reject) => {
2963
3159
  console.log(chalk.blue(`Executing script: ${scriptPath}...`));
2964
- const process = fork(scriptPath, [], {
3160
+ const process = fork(scriptPath, args, {
2965
3161
  stdio: "inherit",
2966
3162
  });
2967
3163
 
@@ -2991,13 +3187,22 @@ async function main() {
2991
3187
  const __filename = fileURLToPath(import.meta.url);
2992
3188
  const __dirname = path.dirname(__filename);
2993
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
+
2994
3199
  // Script to archive current run to JSON history (this is your modified "generate-trend.mjs")
2995
3200
  const archiveRunScriptPath = path.resolve(
2996
3201
  __dirname,
2997
3202
  "generate-trend.mjs" // Keeping the filename as per your request
2998
3203
  );
2999
3204
 
3000
- const outputDir = path.resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
3205
+ const outputDir = await getOutputDir(customOutputDir);
3001
3206
  const reportJsonPath = path.resolve(outputDir, DEFAULT_JSON_FILE); // Current run's main JSON
3002
3207
  const reportHtmlPath = path.resolve(outputDir, DEFAULT_HTML_FILE);
3003
3208
 
@@ -3007,10 +3212,21 @@ async function main() {
3007
3212
 
3008
3213
  console.log(chalk.blue(`Starting static HTML report generation...`));
3009
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
+ }
3010
3225
 
3011
3226
  // Step 1: Ensure current run data is archived to the history folder
3012
3227
  try {
3013
- await runScript(archiveRunScriptPath); // This script now handles JSON history
3228
+ const archiveArgs = customOutputDir ? ["--outputDir", customOutputDir] : [];
3229
+ await runScript(archiveRunScriptPath, archiveArgs);
3014
3230
  console.log(
3015
3231
  chalk.green("Current run data archiving to history completed.")
3016
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
+ })();