@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.
- package/README.md +40 -0
- package/dist/reporter/playwright-pulse-reporter.js +5 -4
- package/dist/types/index.d.ts +9 -0
- package/package.json +4 -3
- package/scripts/config-reader.mjs +132 -0
- package/scripts/generate-email-report.mjs +21 -1
- package/scripts/generate-report.mjs +525 -274
- package/scripts/generate-static-report.mjs +267 -51
- package/scripts/generate-trend.mjs +11 -1
- package/scripts/merge-pulse-report.js +46 -14
- package/scripts/sendReport.mjs +36 -21
|
@@ -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
|
|
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
|
-
<
|
|
1752
|
-
<
|
|
1753
|
-
|
|
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
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
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(
|
|
1874
|
-
|
|
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(
|
|
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
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
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(
|
|
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
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
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-${
|
|
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;
|
|
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
|
-
|
|
2860
|
-
|
|
2861
|
-
a.
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
80
|
-
|
|
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
|
+
})();
|