@arghajit/dummy 0.1.3 → 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.
package/README.md CHANGED
@@ -1,6 +1,11 @@
1
1
  # Playwright Pluse Report
2
2
 
3
- ![Playwright Pulse Report](https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images/image.png)
3
+ [![NPM Version](https://img.shields.io/npm/v/@arghajit/playwright-pulse-report.svg)](https://www.npmjs.com/package/@arghajit/playwright-pulse-report)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+ [![NPM Downloads](https://img.shields.io/npm/dm/@arghajit/playwright-pulse-report.svg)](https://www.npmjs.com/package/@arghajit/playwright-pulse-report)
6
+
7
+ ![Playwright Pulse Report](https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images/pulse-report/playwright-pulse-report.png)
8
+
4
9
  _The ultimate Playwright reporter — Interactive dashboard with historical trend analytics, CI/CD-ready standalone HTML reports, and sharding support for scalable test execution._
5
10
 
6
11
  ## [Live Demo](https://pulse-report.netlify.app/)
@@ -272,8 +277,7 @@ export default defineConfig({
272
277
 
273
278
  ---
274
279
 
275
- <img src="https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images//pulse-logo.png" alt="pulse dashboard" title="pulse dashboard" height="35px" width="60px" align="left" padding="5px"/>
276
- <h2>Pulse Dashboard</h2>
280
+ ![pulse dashboard](https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images/pulse-report/pulse_dashboard_full_icon.png)
277
281
 
278
282
  **Real-time Playwright Test Monitoring & Analysis**
279
283
 
@@ -197,7 +197,7 @@ class PlaywrightPulseReporter {
197
197
  };
198
198
  }
199
199
  async onTestEnd(test, result) {
200
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
200
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
201
201
  const project = (_a = test.parent) === null || _a === void 0 ? void 0 : _a.project();
202
202
  const browserDetails = this.getBrowserDetails(test);
203
203
  const testStatus = convertStatus(result.status, test);
@@ -261,6 +261,7 @@ class PlaywrightPulseReporter {
261
261
  attachments: [],
262
262
  stdout: stdoutMessages.length > 0 ? stdoutMessages : undefined,
263
263
  stderr: stderrMessages.length > 0 ? stderrMessages : undefined,
264
+ annotations: ((_k = test.annotations) === null || _k === void 0 ? void 0 : _k.length) > 0 ? test.annotations : undefined,
264
265
  ...testSpecificData,
265
266
  };
266
267
  for (const [index, attachment] of result.attachments.entries()) {
@@ -277,16 +278,16 @@ class PlaywrightPulseReporter {
277
278
  await this._ensureDirExists(path.dirname(absoluteDestPath));
278
279
  await fs.copyFile(attachment.path, absoluteDestPath);
279
280
  if (attachment.contentType.startsWith("image/")) {
280
- (_k = pulseResult.screenshots) === null || _k === void 0 ? void 0 : _k.push(relativeDestPath);
281
+ (_l = pulseResult.screenshots) === null || _l === void 0 ? void 0 : _l.push(relativeDestPath);
281
282
  }
282
283
  else if (attachment.contentType.startsWith("video/")) {
283
- (_l = pulseResult.videoPath) === null || _l === void 0 ? void 0 : _l.push(relativeDestPath);
284
+ (_m = pulseResult.videoPath) === null || _m === void 0 ? void 0 : _m.push(relativeDestPath);
284
285
  }
285
286
  else if (attachment.name === "trace") {
286
287
  pulseResult.tracePath = relativeDestPath;
287
288
  }
288
289
  else {
289
- (_m = pulseResult.attachments) === null || _m === void 0 ? void 0 : _m.push({
290
+ (_o = pulseResult.attachments) === null || _o === void 0 ? void 0 : _o.push({
290
291
  name: attachment.name,
291
292
  path: relativeDestPath,
292
293
  contentType: attachment.contentType,
@@ -46,6 +46,15 @@ export interface TestResult {
46
46
  totalWorkers?: number;
47
47
  configFile?: string;
48
48
  metadata?: string;
49
+ annotations?: {
50
+ type: string;
51
+ description?: string;
52
+ location?: {
53
+ file: string;
54
+ line: number;
55
+ column: number;
56
+ };
57
+ }[];
49
58
  }
50
59
  export interface TestRun {
51
60
  id: string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@arghajit/dummy",
3
3
  "author": "Arghajit Singha",
4
- "version": "0.1.3",
4
+ "version": "0.3.0",
5
5
  "description": "A Playwright reporter and dashboard for visualizing test results.",
6
6
  "homepage": "https://playwright-pulse-report.netlify.app/",
7
7
  "keywords": [
@@ -18,7 +18,8 @@
18
18
  "send-report",
19
19
  "email",
20
20
  "playwright-report",
21
- "pulse"
21
+ "pulse",
22
+ "ai-failure-analysis"
22
23
  ],
23
24
  "main": "dist/reporter/index.js",
24
25
  "types": "dist/reporter/index.d.ts",
@@ -1713,41 +1713,86 @@ function generateHTML(reportData, trendData = null) {
1713
1713
  };
1714
1714
 
1715
1715
  return `
1716
- <div class="test-case" data-status="${test.status
1717
- }" data-browser="${sanitizeHTML(browser)}" data-tags="${(test.tags || [])
1718
- .join(",")
1719
- .toLowerCase()}">
1716
+ <div class="test-case" data-status="${
1717
+ test.status
1718
+ }" data-browser="${sanitizeHTML(browser)}" data-tags="${(test.tags || [])
1719
+ .join(",")
1720
+ .toLowerCase()}">
1720
1721
  <div class="test-case-header" role="button" aria-expanded="false">
1721
1722
  <div class="test-case-summary">
1722
1723
  <span class="status-badge ${getStatusClass(test.status)}">${String(
1723
- test.status
1724
- ).toUpperCase()}</span>
1724
+ test.status
1725
+ ).toUpperCase()}</span>
1725
1726
  <span class="test-case-title" title="${sanitizeHTML(
1726
1727
  test.name
1727
1728
  )}">${sanitizeHTML(testTitle)}</span>
1728
1729
  <span class="test-case-browser">(${sanitizeHTML(browser)})</span>
1729
1730
  </div>
1730
1731
  <div class="test-case-meta">
1731
- ${test.tags && test.tags.length > 0
1732
- ? test.tags
1733
- .map((t) => `<span class="tag">${sanitizeHTML(t)}</span>`)
1734
- .join(" ")
1735
- : ""
1736
- }
1732
+ ${
1733
+ test.tags && test.tags.length > 0
1734
+ ? test.tags
1735
+ .map((t) => `<span class="tag">${sanitizeHTML(t)}</span>`)
1736
+ .join(" ")
1737
+ : ""
1738
+ }
1737
1739
  <span class="test-duration">${formatDuration(test.duration)}</span>
1738
1740
  </div>
1739
1741
  </div>
1740
1742
  <div class="test-case-content" style="display: none;">
1741
1743
  <p><strong>Full Path:</strong> ${sanitizeHTML(test.name)}</p>
1744
+ ${
1745
+ test.annotations && test.annotations.length > 0
1746
+ ? `<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;">
1747
+ <h4 style="margin-top: 0; margin-bottom: 10px; color: #8b5cf6; font-size: 1.1em;">📌 Annotations</h4>
1748
+ ${test.annotations
1749
+ .map((annotation, idx) => {
1750
+ const isIssueOrBug =
1751
+ annotation.type === "issue" ||
1752
+ annotation.type === "bug";
1753
+ const descriptionText = annotation.description || "";
1754
+ const typeLabel = sanitizeHTML(annotation.type);
1755
+ const descriptionHtml =
1756
+ isIssueOrBug && descriptionText.match(/^[A-Z]+-\d+$/)
1757
+ ? `<a href="#" class="annotation-link" data-annotation="${sanitizeHTML(
1758
+ descriptionText
1759
+ )}" style="color: #3b82f6; text-decoration: underline; cursor: pointer;">${sanitizeHTML(
1760
+ descriptionText
1761
+ )}</a>`
1762
+ : sanitizeHTML(descriptionText);
1763
+ const locationText = annotation.location
1764
+ ? `<div style="font-size: 0.85em; color: #6b7280; margin-top: 4px;">Location: ${sanitizeHTML(
1765
+ annotation.location.file
1766
+ )}:${annotation.location.line}:${
1767
+ annotation.location.column
1768
+ }</div>`
1769
+ : "";
1770
+ return `<div style="margin-bottom: ${
1771
+ idx < test.annotations.length - 1 ? "10px" : "0"
1772
+ };">
1773
+ <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>
1774
+ ${
1775
+ descriptionText
1776
+ ? `<br><strong style="color: #8b5cf6;">Description:</strong> ${descriptionHtml}`
1777
+ : ""
1778
+ }
1779
+ ${locationText}
1780
+ </div>`;
1781
+ })
1782
+ .join("")}
1783
+ </div>`
1784
+ : ""
1785
+ }
1742
1786
  <p><strong>Test run Worker ID:</strong> ${sanitizeHTML(
1743
1787
  test.workerId
1744
1788
  )} [<strong>Total No. of Workers:</strong> ${sanitizeHTML(
1745
- test.totalWorkers
1746
- )}]</p>
1747
- ${test.errorMessage
1748
- ? `<div class="test-error-summary">${formatPlaywrightError(
1749
- test.errorMessage
1750
- )}
1789
+ test.totalWorkers
1790
+ )}]</p>
1791
+ ${
1792
+ test.errorMessage
1793
+ ? `<div class="test-error-summary">${formatPlaywrightError(
1794
+ test.errorMessage
1795
+ )}
1751
1796
  <button
1752
1797
  class="copy-error-btn"
1753
1798
  onclick="copyErrorToClipboard(this)"
@@ -1768,13 +1813,14 @@ function generateHTML(reportData, trendData = null) {
1768
1813
  Copy Error Prompt
1769
1814
  </button>
1770
1815
  </div>`
1771
- : ""
1816
+ : ""
1772
1817
  }
1773
- ${test.snippet
1774
- ? `<div class="code-section"><h4>Error Snippet</h4><pre><code>${formatPlaywrightError(
1775
- test.snippet
1776
- )}</code></pre></div>`
1777
- : ""
1818
+ ${
1819
+ test.snippet
1820
+ ? `<div class="code-section"><h4>Error Snippet</h4><pre><code>${formatPlaywrightError(
1821
+ test.snippet
1822
+ )}</code></pre></div>`
1823
+ : ""
1778
1824
  }
1779
1825
  <h4>Steps</h4>
1780
1826
  <div class="steps-list">${generateStepsHTML(test.steps)}</div>
@@ -1793,75 +1839,86 @@ function generateHTML(reportData, trendData = null) {
1793
1839
  </div>
1794
1840
  </div>`;
1795
1841
  })()}
1796
- ${test.stderr && test.stderr.length > 0
1797
- ? `<div class="console-output-section"><h4>Console Output (stderr)</h4><pre class="console-log stderr-log" style="background-color: #2d2d2d; color: indianred; padding: 1.25em; border-radius: 0.85em; line-height: 1.2;">${formatPlaywrightError(
1798
- test.stderr.map((line) => sanitizeHTML(line)).join("\n")
1799
- )}</pre></div>`
1800
- : ""
1842
+ ${
1843
+ test.stderr && test.stderr.length > 0
1844
+ ? `<div class="console-output-section"><h4>Console Output (stderr)</h4><pre class="console-log stderr-log" style="background-color: #2d2d2d; color: indianred; padding: 1.25em; border-radius: 0.85em; line-height: 1.2;">${formatPlaywrightError(
1845
+ test.stderr.map((line) => sanitizeHTML(line)).join("\n")
1846
+ )}</pre></div>`
1847
+ : ""
1801
1848
  }
1802
- ${test.screenshots && test.screenshots.length > 0
1803
- ? `
1849
+ ${
1850
+ test.screenshots && test.screenshots.length > 0
1851
+ ? `
1804
1852
  <div class="attachments-section">
1805
1853
  <h4>Screenshots</h4>
1806
1854
  <div class="attachments-grid">
1807
1855
  ${test.screenshots
1808
- .map(
1809
- (screenshot, index) => `
1856
+ .map(
1857
+ (screenshot, index) => `
1810
1858
  <div class="attachment-item">
1811
- <img src="${fixPath(screenshot)}" alt="Screenshot ${index + 1}">
1859
+ <img src="${fixPath(screenshot)}" alt="Screenshot ${
1860
+ index + 1
1861
+ }">
1812
1862
  <div class="attachment-info">
1813
1863
  <div class="trace-actions">
1814
- <a href="${fixPath(screenshot)}" target="_blank" class="view-full">View Full Image</a>
1815
- <a href="${fixPath(screenshot)}" target="_blank" download="screenshot-${Date.now()}-${index}.png">Download</a>
1864
+ <a href="${fixPath(
1865
+ screenshot
1866
+ )}" target="_blank" class="view-full">View Full Image</a>
1867
+ <a href="${fixPath(
1868
+ screenshot
1869
+ )}" target="_blank" download="screenshot-${Date.now()}-${index}.png">Download</a>
1816
1870
  </div>
1817
1871
  </div>
1818
1872
  </div>
1819
1873
  `
1820
- )
1821
- .join("")}
1874
+ )
1875
+ .join("")}
1822
1876
  </div>
1823
1877
  </div>
1824
1878
  `
1825
- : ""
1879
+ : ""
1826
1880
  }
1827
- ${test.videoPath && test.videoPath.length > 0
1828
- ? `<div class="attachments-section"><h4>Videos</h4><div class="attachments-grid">${test.videoPath
1829
- .map((videoUrl, index) => {
1830
- const fixedVideoUrl = fixPath(videoUrl);
1831
- const fileExtension = String(fixedVideoUrl)
1832
- .split(".")
1833
- .pop()
1834
- .toLowerCase();
1835
- const mimeType =
1836
- {
1837
- mp4: "video/mp4",
1838
- webm: "video/webm",
1839
- ogg: "video/ogg",
1840
- mov: "video/quicktime",
1841
- avi: "video/x-msvideo",
1842
- }[fileExtension] || "video/mp4";
1843
- return `<div class="attachment-item video-item">
1844
- <video controls width="100%" height="auto" title="Video ${index + 1
1845
- }">
1881
+ ${
1882
+ test.videoPath && test.videoPath.length > 0
1883
+ ? `<div class="attachments-section"><h4>Videos</h4><div class="attachments-grid">${test.videoPath
1884
+ .map((videoUrl, index) => {
1885
+ const fixedVideoUrl = fixPath(videoUrl);
1886
+ const fileExtension = String(fixedVideoUrl)
1887
+ .split(".")
1888
+ .pop()
1889
+ .toLowerCase();
1890
+ const mimeType =
1891
+ {
1892
+ mp4: "video/mp4",
1893
+ webm: "video/webm",
1894
+ ogg: "video/ogg",
1895
+ mov: "video/quicktime",
1896
+ avi: "video/x-msvideo",
1897
+ }[fileExtension] || "video/mp4";
1898
+ return `<div class="attachment-item video-item">
1899
+ <video controls width="100%" height="auto" title="Video ${
1900
+ index + 1
1901
+ }">
1846
1902
  <source src="${sanitizeHTML(
1847
- fixedVideoUrl
1848
- )}" type="${mimeType}">
1903
+ fixedVideoUrl
1904
+ )}" type="${mimeType}">
1849
1905
  Your browser does not support the video tag.
1850
1906
  </video>
1851
1907
  <div class="attachment-info">
1852
1908
  <div class="trace-actions">
1853
1909
  <a href="${sanitizeHTML(
1854
- fixedVideoUrl
1855
- )}" target="_blank" download="video-${Date.now()}-${index}.${fileExtension}">Download</a>
1910
+ fixedVideoUrl
1911
+ )}" target="_blank" download="video-${Date.now()}-${index}.${fileExtension}">Download</a>
1856
1912
  </div>
1857
1913
  </div>
1858
1914
  </div>`;
1859
- })
1860
- .join("")}</div></div>`
1861
- : ""
1915
+ })
1916
+ .join("")}</div></div>`
1917
+ : ""
1862
1918
  }
1863
- ${test.tracePath
1864
- ? `
1919
+ ${
1920
+ test.tracePath
1921
+ ? `
1865
1922
  <div class="attachments-section">
1866
1923
  <h4>Trace Files</h4>
1867
1924
  <div class="attachments-grid">
@@ -1869,70 +1926,72 @@ function generateHTML(reportData, trendData = null) {
1869
1926
  <div class="trace-preview">
1870
1927
  <span class="trace-icon">📄</span>
1871
1928
  <span class="trace-name">${sanitizeHTML(
1872
- path.basename(test.tracePath)
1873
- )}</span>
1929
+ path.basename(test.tracePath)
1930
+ )}</span>
1874
1931
  </div>
1875
1932
  <div class="attachment-info">
1876
1933
  <div class="trace-actions">
1877
1934
  <a href="${sanitizeHTML(
1878
- fixPath(test.tracePath)
1879
- )}" target="_blank" download="${sanitizeHTML(
1880
- path.basename(test.tracePath)
1881
- )}" class="download-trace">Download Trace</a>
1935
+ fixPath(test.tracePath)
1936
+ )}" target="_blank" download="${sanitizeHTML(
1937
+ path.basename(test.tracePath)
1938
+ )}" class="download-trace">Download Trace</a>
1882
1939
  </div>
1883
1940
  </div>
1884
1941
  </div>
1885
1942
  </div>
1886
1943
  </div>
1887
1944
  `
1888
- : ""
1945
+ : ""
1889
1946
  }
1890
- ${test.attachments && test.attachments.length > 0
1891
- ? `
1947
+ ${
1948
+ test.attachments && test.attachments.length > 0
1949
+ ? `
1892
1950
  <div class="attachments-section">
1893
1951
  <h4>Other Attachments</h4>
1894
1952
  <div class="attachments-grid">
1895
1953
  ${test.attachments
1896
- .map(
1897
- (attachment) => `
1954
+ .map(
1955
+ (attachment) => `
1898
1956
  <div class="attachment-item generic-attachment">
1899
1957
  <div class="attachment-icon">${getAttachmentIcon(
1900
- attachment.contentType
1901
- )}</div>
1958
+ attachment.contentType
1959
+ )}</div>
1902
1960
  <div class="attachment-caption">
1903
1961
  <span class="attachment-name" title="${sanitizeHTML(
1904
- attachment.name
1905
- )}">${sanitizeHTML(attachment.name)}</span>
1962
+ attachment.name
1963
+ )}">${sanitizeHTML(attachment.name)}</span>
1906
1964
  <span class="attachment-type">${sanitizeHTML(
1907
- attachment.contentType
1908
- )}</span>
1965
+ attachment.contentType
1966
+ )}</span>
1909
1967
  </div>
1910
1968
  <div class="attachment-info">
1911
1969
  <div class="trace-actions">
1912
1970
  <a href="${sanitizeHTML(
1913
- fixPath(attachment.path)
1914
- )}" target="_blank" class="view-full">View</a>
1971
+ fixPath(attachment.path)
1972
+ )}" target="_blank" class="view-full">View</a>
1915
1973
  <a href="${sanitizeHTML(
1916
- fixPath(attachment.path)
1917
- )}" target="_blank" download="${sanitizeHTML(
1918
- attachment.name
1919
- )}" class="download-trace">Download</a>
1974
+ fixPath(attachment.path)
1975
+ )}" target="_blank" download="${sanitizeHTML(
1976
+ attachment.name
1977
+ )}" class="download-trace">Download</a>
1920
1978
  </div>
1921
1979
  </div>
1922
1980
  </div>
1923
1981
  `
1924
- )
1925
- .join("")}
1982
+ )
1983
+ .join("")}
1926
1984
  </div>
1927
1985
  </div>
1928
1986
  `
1929
- : ""
1987
+ : ""
1930
1988
  }
1931
- ${test.codeSnippet
1932
- ? `<div class="code-section"><h4>Code Snippet</h4><pre><code>${formatPlaywrightError(
1933
- sanitizeHTML(test.codeSnippet)
1934
- )}</code></pre></div>`
1935
- : ""
1989
+ ${
1990
+ test.codeSnippet
1991
+ ? `<div class="code-section"><h4>Code Snippet</h4><pre><code>${formatPlaywrightError(
1992
+ sanitizeHTML(test.codeSnippet)
1993
+ )}</code></pre></div>`
1994
+ : ""
1936
1995
  }
1937
1996
  </div>
1938
1997
  </div>`;
@@ -2345,8 +2404,8 @@ function generateHTML(reportData, trendData = null) {
2345
2404
  <h1>Playwright Pulse Report</h1>
2346
2405
  </div>
2347
2406
  <div class="run-info"><strong>Run Date:</strong> ${formatDate(
2348
- runSummary.timestamp
2349
- )}<br><strong>Total Duration:</strong> ${formatDuration(
2407
+ runSummary.timestamp
2408
+ )}<br><strong>Total Duration:</strong> ${formatDuration(
2350
2409
  runSummary.duration
2351
2410
  )}</div>
2352
2411
  </header>
@@ -2358,35 +2417,40 @@ function generateHTML(reportData, trendData = null) {
2358
2417
  </div>
2359
2418
  <div id="dashboard" class="tab-content active">
2360
2419
  <div class="dashboard-grid">
2361
- <div class="summary-card"><h3>Total Tests</h3><div class="value">${runSummary.totalTests
2362
- }</div></div>
2363
- <div class="summary-card status-passed"><h3>Passed</h3><div class="value">${runSummary.passed
2364
- }</div><div class="trend-percentage">${passPercentage}%</div></div>
2365
- <div class="summary-card status-failed"><h3>Failed</h3><div class="value">${runSummary.failed
2366
- }</div><div class="trend-percentage">${failPercentage}%</div></div>
2367
- <div class="summary-card status-skipped"><h3>Skipped</h3><div class="value">${runSummary.skipped || 0
2368
- }</div><div class="trend-percentage">${skipPercentage}%</div></div>
2420
+ <div class="summary-card"><h3>Total Tests</h3><div class="value">${
2421
+ runSummary.totalTests
2422
+ }</div></div>
2423
+ <div class="summary-card status-passed"><h3>Passed</h3><div class="value">${
2424
+ runSummary.passed
2425
+ }</div><div class="trend-percentage">${passPercentage}%</div></div>
2426
+ <div class="summary-card status-failed"><h3>Failed</h3><div class="value">${
2427
+ runSummary.failed
2428
+ }</div><div class="trend-percentage">${failPercentage}%</div></div>
2429
+ <div class="summary-card status-skipped"><h3>Skipped</h3><div class="value">${
2430
+ runSummary.skipped || 0
2431
+ }</div><div class="trend-percentage">${skipPercentage}%</div></div>
2369
2432
  <div class="summary-card"><h3>Avg. Test Time</h3><div class="value">${avgTestDuration}</div></div>
2370
2433
  <div class="summary-card"><h3>Run Duration</h3><div class="value">${formatDuration(
2371
- runSummary.duration
2372
- )}</div></div>
2434
+ runSummary.duration
2435
+ )}</div></div>
2373
2436
  </div>
2374
2437
  <div class="dashboard-bottom-row">
2375
2438
  <div style="display: grid; gap: 20px">
2376
2439
  ${generatePieChart(
2377
- [
2378
- { label: "Passed", value: runSummary.passed },
2379
- { label: "Failed", value: runSummary.failed },
2380
- { label: "Skipped", value: runSummary.skipped || 0 },
2381
- ],
2382
- 400,
2383
- 390
2384
- )}
2385
- ${runSummary.environment &&
2386
- Object.keys(runSummary.environment).length > 0
2387
- ? generateEnvironmentDashboard(runSummary.environment)
2388
- : '<div class="no-data">Environment data not available.</div>'
2389
- }
2440
+ [
2441
+ { label: "Passed", value: runSummary.passed },
2442
+ { label: "Failed", value: runSummary.failed },
2443
+ { label: "Skipped", value: runSummary.skipped || 0 },
2444
+ ],
2445
+ 400,
2446
+ 390
2447
+ )}
2448
+ ${
2449
+ runSummary.environment &&
2450
+ Object.keys(runSummary.environment).length > 0
2451
+ ? generateEnvironmentDashboard(runSummary.environment)
2452
+ : '<div class="no-data">Environment data not available.</div>'
2453
+ }
2390
2454
  </div>
2391
2455
  ${generateSuitesWidget(suitesData)}
2392
2456
  </div>
@@ -2396,17 +2460,17 @@ function generateHTML(reportData, trendData = null) {
2396
2460
  <input type="text" id="filter-name" placeholder="Filter by test name/path..." style="border-color: black; border-style: outset;">
2397
2461
  <select id="filter-status"><option value="">All Statuses</option><option value="passed">Passed</option><option value="failed">Failed</option><option value="skipped">Skipped</option></select>
2398
2462
  <select id="filter-browser"><option value="">All Browsers</option>${Array.from(
2399
- new Set(
2400
- (results || []).map((test) => test.browser || "unknown")
2401
- )
2402
- )
2403
- .map(
2404
- (browser) =>
2405
- `<option value="${sanitizeHTML(browser)}">${sanitizeHTML(
2406
- browser
2407
- )}</option>`
2408
- )
2409
- .join("")}</select>
2463
+ new Set(
2464
+ (results || []).map((test) => test.browser || "unknown")
2465
+ )
2466
+ )
2467
+ .map(
2468
+ (browser) =>
2469
+ `<option value="${sanitizeHTML(browser)}">${sanitizeHTML(
2470
+ browser
2471
+ )}</option>`
2472
+ )
2473
+ .join("")}</select>
2410
2474
  <button id="expand-all-tests">Expand All</button> <button id="collapse-all-tests">Collapse All</button> <button id="clear-run-summary-filters" class="clear-filters-btn">Clear Filters</button>
2411
2475
  </div>
2412
2476
  <div class="test-cases-list">${generateTestCasesHTML()}</div>
@@ -2415,16 +2479,18 @@ function generateHTML(reportData, trendData = null) {
2415
2479
  <h2 class="tab-main-title">Execution Trends</h2>
2416
2480
  <div class="trend-charts-row">
2417
2481
  <div class="trend-chart"><h3 class="chart-title-header">Test Volume & Outcome Trends</h3>
2418
- ${trendData && trendData.overall && trendData.overall.length > 0
2419
- ? generateTestTrendsChart(trendData)
2420
- : '<div class="no-data">Overall trend data not available for test counts.</div>'
2421
- }
2482
+ ${
2483
+ trendData && trendData.overall && trendData.overall.length > 0
2484
+ ? generateTestTrendsChart(trendData)
2485
+ : '<div class="no-data">Overall trend data not available for test counts.</div>'
2486
+ }
2422
2487
  </div>
2423
2488
  <div class="trend-chart"><h3 class="chart-title-header">Execution Duration Trends</h3>
2424
- ${trendData && trendData.overall && trendData.overall.length > 0
2425
- ? generateDurationTrendChart(trendData)
2426
- : '<div class="no-data">Overall trend data not available for durations.</div>'
2427
- }
2489
+ ${
2490
+ trendData && trendData.overall && trendData.overall.length > 0
2491
+ ? generateDurationTrendChart(trendData)
2492
+ : '<div class="no-data">Overall trend data not available for durations.</div>'
2493
+ }
2428
2494
  </div>
2429
2495
  </div>
2430
2496
  <h2 class="tab-main-title">Test Distribution by Worker ${infoTooltip}</h2>
@@ -2434,12 +2500,13 @@ function generateHTML(reportData, trendData = null) {
2434
2500
  </div>
2435
2501
  </div>
2436
2502
  <h2 class="tab-main-title">Individual Test History</h2>
2437
- ${trendData &&
2438
- trendData.testRuns &&
2439
- Object.keys(trendData.testRuns).length > 0
2440
- ? generateTestHistoryContent(trendData)
2441
- : '<div class="no-data">Individual test history data not available.</div>'
2442
- }
2503
+ ${
2504
+ trendData &&
2505
+ trendData.testRuns &&
2506
+ Object.keys(trendData.testRuns).length > 0
2507
+ ? generateTestHistoryContent(trendData)
2508
+ : '<div class="no-data">Individual test history data not available.</div>'
2509
+ }
2443
2510
  </div>
2444
2511
  <div id="ai-failure-analyzer" class="tab-content">
2445
2512
  ${generateAIFailureAnalyzerTab(results)}
@@ -2702,6 +2769,19 @@ function getAIFix(button) {
2702
2769
  }
2703
2770
  if (expandAllBtn) expandAllBtn.addEventListener('click', () => setAllTestRunDetailsVisibility('block', 'true'));
2704
2771
  if (collapseAllBtn) collapseAllBtn.addEventListener('click', () => setAllTestRunDetailsVisibility('none', 'false'));
2772
+ // --- Annotation Link Handler ---
2773
+ document.querySelectorAll('a.annotation-link').forEach(link => {
2774
+ link.addEventListener('click', (e) => {
2775
+ e.preventDefault();
2776
+ const annotationId = link.dataset.annotation;
2777
+ if (annotationId) {
2778
+ const jiraUrl = prompt('Enter your JIRA/Ticket system base URL (e.g., https://your-company.atlassian.net/browse/):', 'https://your-company.atlassian.net/browse/');
2779
+ if (jiraUrl) {
2780
+ window.open(jiraUrl + annotationId, '_blank');
2781
+ }
2782
+ }
2783
+ });
2784
+ });
2705
2785
  // --- Intersection Observer for Lazy Loading ---
2706
2786
  const lazyLoadElements = document.querySelectorAll('.lazy-load-chart');
2707
2787
  if ('IntersectionObserver' in window) {
@@ -1850,43 +1850,53 @@ function generateHTML(reportData, trendData = null) {
1850
1850
  : ""
1851
1851
  }<button class="copy-error-btn" onclick="copyErrorToClipboard(this)">Copy Error Prompt</button></div>`
1852
1852
  : ""
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>
1853
+ }${(() => {
1854
+ if (!step.attachments || step.attachments.length === 0)
1855
+ 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 =
1869
+ readFileSync(attachmentPath).toString("base64");
1870
+ const attachmentDataUri = `data:${attachment.contentType};base64,${attachmentBase64}`;
1871
+ return `<div class="attachment-item generic-attachment">
1872
+ <div class="attachment-icon">${getAttachmentIcon(
1873
+ attachment.contentType
1874
+ )}</div>
1872
1875
  <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>
1876
+ <span class="attachment-name" title="${sanitizeHTML(
1877
+ attachment.name
1878
+ )}">${sanitizeHTML(attachment.name)}</span>
1879
+ <span class="attachment-type">${sanitizeHTML(
1880
+ attachment.contentType
1881
+ )}</span>
1875
1882
  </div>
1876
1883
  <div class="attachment-info">
1877
1884
  <div class="trace-actions">
1878
1885
  <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>
1886
+ <a href="#" data-href="${attachmentDataUri}" class="lazy-load-attachment" download="${sanitizeHTML(
1887
+ attachment.name
1888
+ )}">Download</a>
1880
1889
  </div>
1881
1890
  </div>
1882
1891
  </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
- }${
1892
+ } catch (e) {
1893
+ return `<div class="attachment-item error">Failed to load attachment: ${sanitizeHTML(
1894
+ attachment.name
1895
+ )}</div>`;
1896
+ }
1897
+ })
1898
+ .join("")}</div></div>`;
1899
+ })()}${
1890
1900
  hasNestedSteps
1891
1901
  ? `<div class="nested-steps">${generateStepsHTML(
1892
1902
  step.steps,
@@ -1903,7 +1913,9 @@ function generateHTML(reportData, trendData = null) {
1903
1913
  test.tags || []
1904
1914
  )
1905
1915
  .join(",")
1906
- .toLowerCase()}" data-test-id="${sanitizeHTML(String(test.id || testIndex))}">
1916
+ .toLowerCase()}" data-test-id="${sanitizeHTML(
1917
+ String(test.id || testIndex)
1918
+ )}">
1907
1919
  <div class="test-case-header" role="button" aria-expanded="false"><div class="test-case-summary"><span class="status-badge ${getStatusClass(
1908
1920
  test.status
1909
1921
  )}">${String(
@@ -1927,18 +1939,66 @@ function generateHTML(reportData, trendData = null) {
1927
1939
  <p><strong>Full Path:</strong> ${sanitizeHTML(
1928
1940
  test.name
1929
1941
  )}</p>
1942
+ ${
1943
+ test.annotations && test.annotations.length > 0
1944
+ ? `<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;">
1945
+ <h4 style="margin-top: 0; margin-bottom: 10px; color: #8b5cf6; font-size: 1.1em;">📌 Annotations</h4>
1946
+ ${test.annotations
1947
+ .map((annotation, idx) => {
1948
+ const isIssueOrBug =
1949
+ annotation.type === "issue" ||
1950
+ annotation.type === "bug";
1951
+ const descriptionText =
1952
+ annotation.description || "";
1953
+ const typeLabel = sanitizeHTML(
1954
+ annotation.type
1955
+ );
1956
+ const descriptionHtml =
1957
+ isIssueOrBug &&
1958
+ descriptionText.match(/^[A-Z]+-\d+$/)
1959
+ ? `<a href="#" class="annotation-link" data-annotation="${sanitizeHTML(
1960
+ descriptionText
1961
+ )}" style="color: #3b82f6; text-decoration: underline; cursor: pointer;">${sanitizeHTML(
1962
+ descriptionText
1963
+ )}</a>`
1964
+ : sanitizeHTML(descriptionText);
1965
+ const locationText = annotation.location
1966
+ ? `<div style="font-size: 0.85em; color: #6b7280; margin-top: 4px;">Location: ${sanitizeHTML(
1967
+ annotation.location.file
1968
+ )}:${annotation.location.line}:${
1969
+ annotation.location.column
1970
+ }</div>`
1971
+ : "";
1972
+ return `<div style="margin-bottom: ${
1973
+ idx < test.annotations.length - 1
1974
+ ? "10px"
1975
+ : "0"
1976
+ };">
1977
+ <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>
1978
+ ${
1979
+ descriptionText
1980
+ ? `<br><strong style="color: #8b5cf6;">Description:</strong> ${descriptionHtml}`
1981
+ : ""
1982
+ }
1983
+ ${locationText}
1984
+ </div>`;
1985
+ })
1986
+ .join("")}
1987
+ </div>`
1988
+ : ""
1989
+ }
1930
1990
  <p><strong>Test run Worker ID:</strong> ${sanitizeHTML(
1931
1991
  test.workerId
1932
1992
  )} [<strong>Total No. of Workers:</strong> ${sanitizeHTML(
1933
1993
  test.totalWorkers
1934
1994
  )}]</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
- }
1995
+ ${
1996
+ test.errorMessage
1997
+ ? `<div class="test-error-summary">${formatPlaywrightError(
1998
+ test.errorMessage
1999
+ )}<button class="copy-error-btn" onclick="copyErrorToClipboard(this)">Copy Error Prompt</button></div>`
2000
+ : ""
2001
+ }
1942
2002
  ${
1943
2003
  test.snippet
1944
2004
  ? `<div class="code-section"><h4>Error Snippet</h4><pre><code>${formatPlaywrightError(
@@ -1970,7 +2030,9 @@ function generateHTML(reportData, trendData = null) {
1970
2030
  ${
1971
2031
  test.stderr && test.stderr.length > 0
1972
2032
  ? (() => {
1973
- const logId = `stderr-log-${test.id || testIndex}`;
2033
+ const logId = `stderr-log-${
2034
+ test.id || testIndex
2035
+ }`;
1974
2036
  return `<div class="console-output-section"><h4>Console Output (stderr)</h4><pre id="${logId}" class="console-log stderr-log">${test.stderr
1975
2037
  .map((line) => sanitizeHTML(line))
1976
2038
  .join("\\n")}</pre></div>`;
@@ -2384,7 +2446,7 @@ aspect-ratio: 16 / 9;
2384
2446
  @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
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; } }
2386
2448
  @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; }
2449
+ .trace-actions a { text-decoration: none; font-weight: 500; font-size: 0.9em; }
2388
2450
  .generic-attachment { text-align: center; padding: 1rem; justify-content: center; }
2389
2451
  .attachment-icon { font-size: 2.5rem; display: block; margin-bottom: 0.75rem; }
2390
2452
  .attachment-caption { display: flex; flex-direction: column; align-items: center; justify-content: center; flex-grow: 1; }
@@ -2826,6 +2888,18 @@ aspect-ratio: 16 / 9;
2826
2888
  }
2827
2889
  return;
2828
2890
  }
2891
+ const annotationLink = e.target.closest('a.annotation-link');
2892
+ if (annotationLink) {
2893
+ e.preventDefault();
2894
+ const annotationId = annotationLink.dataset.annotation;
2895
+ if (annotationId) {
2896
+ const jiraUrl = prompt('Enter your JIRA/Ticket system base URL (e.g., https://your-company.atlassian.net/browse/):', 'https://your-company.atlassian.net/browse/');
2897
+ if (jiraUrl) {
2898
+ window.open(jiraUrl + annotationId, '_blank');
2899
+ }
2900
+ }
2901
+ return;
2902
+ }
2829
2903
  const img = e.target.closest('img.lazy-load-image');
2830
2904
  if (img && img.dataset && img.dataset.src) {
2831
2905
  if (e.preventDefault) e.preventDefault();
@@ -2856,9 +2930,45 @@ aspect-ratio: 16 / 9;
2856
2930
  const a = e.target.closest('a.lazy-load-attachment');
2857
2931
  if (a && a.dataset && a.dataset.href) {
2858
2932
  e.preventDefault();
2859
- a.href = a.dataset.href;
2860
- a.removeAttribute('data-href');
2861
- a.click();
2933
+
2934
+ // Special handling for view-full links to avoid about:blank issue
2935
+ if (a.classList.contains('view-full')) {
2936
+ // Extract the data from the data URI
2937
+ const dataUri = a.dataset.href;
2938
+ const [header, base64Data] = dataUri.split(',');
2939
+ const mimeType = header.match(/data:([^;]+)/)[1];
2940
+
2941
+ try {
2942
+ // Convert base64 to blob
2943
+ const byteCharacters = atob(base64Data);
2944
+ const byteNumbers = new Array(byteCharacters.length);
2945
+ for (let i = 0; i < byteCharacters.length; i++) {
2946
+ byteNumbers[i] = byteCharacters.charCodeAt(i);
2947
+ }
2948
+ const byteArray = new Uint8Array(byteNumbers);
2949
+ const blob = new Blob([byteArray], { type: mimeType });
2950
+
2951
+ // Create a URL and open it
2952
+ const blobUrl = URL.createObjectURL(blob);
2953
+ const newWindow = window.open(blobUrl, '_blank');
2954
+
2955
+ // Clean up the URL after a delay
2956
+ setTimeout(() => {
2957
+ URL.revokeObjectURL(blobUrl);
2958
+ }, 1000);
2959
+ } catch (error) {
2960
+ console.error('Failed to open attachment:', error);
2961
+ // Fallback to original method
2962
+ a.href = a.dataset.href;
2963
+ a.removeAttribute('data-href');
2964
+ a.click();
2965
+ }
2966
+ } else {
2967
+ // For download links, use the original method
2968
+ a.href = a.dataset.href;
2969
+ a.removeAttribute('data-href');
2970
+ a.click();
2971
+ }
2862
2972
  return;
2863
2973
  }
2864
2974
  });
@@ -289,7 +289,7 @@ const sendEmail = async (credentials) => {
289
289
  }
290
290
  };
291
291
 
292
- async function fetchCredentials(retries = 6) {
292
+ async function fetchCredentials(retries = 10) {
293
293
  // Ensure fetch is initialized from the dynamic import before calling this
294
294
  if (!fetch) {
295
295
  try {
@@ -324,7 +324,7 @@ async function fetchCredentials(retries = 6) {
324
324
  });
325
325
 
326
326
  const fetchPromise = fetch(
327
- "https://test-dashboard-66zd.onrender.com/api/getcredentials",
327
+ "https://get-credentials.netlify.app/api/getcredentials",
328
328
  {
329
329
  method: "GET",
330
330
  headers: {