@arghajit/playwright-pulse-report 0.2.10 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -0
- package/dist/reporter/playwright-pulse-reporter.js +5 -4
- package/dist/types/index.d.ts +9 -0
- package/package.json +2 -2
- 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 +227 -47
- 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,7 +2455,7 @@ aspect-ratio: 16 / 9;
|
|
|
2382
2455
|
.full-error-content { background: rgba(248, 113, 113, 0.1); border: 1px solid rgba(248, 113, 113, 0.3); border-radius: 6px; padding: 15px; font-family: monospace; font-size: 0.9em; color: var(--danger-color); line-height: 1.4; max-height: 300px; overflow-y: auto;}
|
|
2383
2456
|
@media (max-width: 1200px) { .trend-charts-row { grid-template-columns: 1fr; } }
|
|
2384
2457
|
@media (max-width: 992px) { .dashboard-bottom-row { grid-template-columns: 1fr; } .pie-chart-wrapper div[id^="pieChart-"] { max-width: 350px; margin: 0 auto; } .filters input { min-width: 180px; } .filters select { min-width: 150px; } }
|
|
2385
|
-
@media (max-width: 768px) { body { font-size: 15px; } .container { margin: 10px; padding: 20px; } .header { flex-direction: column; align-items: flex-start; gap: 15px; } .header h1 { font-size: 1.6em; } .run-info { text-align: left; font-size:0.9em; } .tabs { margin-bottom: 25px;} .tab-button { padding: 12px 20px; font-size: 1.05em;} .dashboard-grid { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 18px;} .summary-card .value {font-size: 2em;} .summary-card h3 {font-size: 0.95em;} .filters { flex-direction: column; padding: 18px; gap: 12px;} .filters input, .filters select, .filters button {width: 100%; box-sizing: border-box;} .test-case-header { flex-direction: column; align-items: flex-start; gap: 10px; padding: 14px; } .test-case-summary {gap: 10px;} .test-case-title {font-size: 1.05em;} .test-case-meta { flex-direction: row; flex-wrap: wrap; gap: 8px; margin-top: 8px;} .attachments-grid {grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 18px;} .test-history-grid {grid-template-columns: 1fr;} .pie-chart-wrapper {min-height: auto;} .ai-failure-cards-grid { grid-template-columns: 1fr; } .ai-analyzer-stats { flex-direction: column; gap: 15px; text-align: center; } .failure-header { flex-direction: column; align-items: stretch; gap: 15px; } .failure-main-info { text-align: center; } .failure-meta { justify-content: center; } .compact-ai-btn { justify-content: center; padding: 12px 20px; } }
|
|
2458
|
+
@media (max-width: 768px) { body { font-size: 15px; } .container { margin: 10px; padding: 20px; } .header { flex-direction: column; align-items: flex-start; gap: 15px; } .header h1 { font-size: 1.6em; } .run-info { text-align: left; font-size:0.9em; } .tabs { margin-bottom: 25px;} .tab-button { padding: 12px 20px; font-size: 1.05em;} .dashboard-grid { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 18px;} .summary-card .value {font-size: 2em;} .summary-card h3 {font-size: 0.95em;} .filters { flex-direction: column; padding: 18px; gap: 12px;} .filters input, .filters select, .filters button {width: 100%; box-sizing: border-box;} .test-case-header { flex-direction: column; align-items: flex-start; gap: 10px; padding: 14px; } .test-case-summary {gap: 10px;} .test-case-title {font-size: 1.05em;} .test-case-meta { flex-direction: row; flex-wrap: wrap; gap: 8px; margin-top: 8px;} .attachments-grid {grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 18px;} .test-history-grid {grid-template-columns: 1fr;} .pie-chart-wrapper {min-height: auto;} .ai-failure-cards-grid { grid-template-columns: 1fr; } .ai-analyzer-stats { flex-direction: column; gap: 15px; text-align: center; } .failure-header { flex-direction: column; align-items: stretch; gap: 15px; } .failure-main-info { text-align: center; } .failure-meta { justify-content: center; } .ai-buttons-group { flex-direction: column; width: 100%; } .compact-ai-btn, .copy-prompt-btn { justify-content: center; padding: 12px 20px; width: 100%; } }
|
|
2386
2459
|
@media (max-width: 480px) { body {font-size: 14px;} .container {padding: 15px;} .header h1 {font-size: 1.4em;} #report-logo { height: 35px; width: 45px; } .tab-button {padding: 10px 15px; font-size: 1em;} .summary-card .value {font-size: 1.8em;} .attachments-grid {grid-template-columns: 1fr;} .step-item {padding-left: calc(var(--depth, 0) * 18px);} .test-case-content, .step-details {padding: 15px;} .trend-charts-row {gap: 20px;} .trend-chart {padding: 20px;} .stat-item .stat-number { font-size: 1.5em; } .failure-header { padding: 15px; } .failure-error-preview, .full-error-details { padding-left: 15px; padding-right: 15px; } }
|
|
2387
2460
|
.trace-actions a { text-decoration: none; font-weight: 500; font-size: 0.9em; }
|
|
2388
2461
|
.generic-attachment { text-align: center; padding: 1rem; justify-content: center; }
|
|
@@ -2651,6 +2724,81 @@ aspect-ratio: 16 / 9;
|
|
|
2651
2724
|
}
|
|
2652
2725
|
}
|
|
2653
2726
|
|
|
2727
|
+
function copyAIPrompt(button) {
|
|
2728
|
+
try {
|
|
2729
|
+
const testJson = button.dataset.testJson;
|
|
2730
|
+
const test = JSON.parse(atob(testJson));
|
|
2731
|
+
|
|
2732
|
+
const testName = test.name || 'Unknown Test';
|
|
2733
|
+
const failureLogsAndErrors = [
|
|
2734
|
+
'Error Message:',
|
|
2735
|
+
test.errorMessage || 'Not available.',
|
|
2736
|
+
'\\n\\n--- stdout ---',
|
|
2737
|
+
(test.stdout && test.stdout.length > 0) ? test.stdout.join('\\n') : 'Not available.',
|
|
2738
|
+
'\\n\\n--- stderr ---',
|
|
2739
|
+
(test.stderr && test.stderr.length > 0) ? test.stderr.join('\\n') : 'Not available.'
|
|
2740
|
+
].join('\\n');
|
|
2741
|
+
const codeSnippet = test.snippet || '';
|
|
2742
|
+
|
|
2743
|
+
const aiPrompt = \`You are an expert Playwright test automation engineer specializing in debugging test failures.
|
|
2744
|
+
|
|
2745
|
+
INSTRUCTIONS:
|
|
2746
|
+
1. Analyze the test failure carefully
|
|
2747
|
+
2. Provide a brief root cause analysis
|
|
2748
|
+
3. Provide EXACTLY 5 specific, actionable fixes
|
|
2749
|
+
4. Each fix MUST include a code snippet (codeSnippet field)
|
|
2750
|
+
5. Return ONLY valid JSON, no markdown or extra text
|
|
2751
|
+
|
|
2752
|
+
REQUIRED JSON FORMAT:
|
|
2753
|
+
{
|
|
2754
|
+
"rootCause": "Brief explanation of why the test failed",
|
|
2755
|
+
"suggestedFixes": [
|
|
2756
|
+
{
|
|
2757
|
+
"description": "Clear explanation of the fix",
|
|
2758
|
+
"codeSnippet": "await page.waitForSelector('.button', { timeout: 5000 });"
|
|
2759
|
+
}
|
|
2760
|
+
],
|
|
2761
|
+
"affectedTests": ["test1", "test2"]
|
|
2762
|
+
}
|
|
2763
|
+
|
|
2764
|
+
IMPORTANT:
|
|
2765
|
+
- Always return valid JSON only
|
|
2766
|
+
- Always provide exactly 5 fixes in suggestedFixes array
|
|
2767
|
+
- Each fix must have both description and codeSnippet fields
|
|
2768
|
+
- Make code snippets practical and Playwright-specific
|
|
2769
|
+
|
|
2770
|
+
---
|
|
2771
|
+
|
|
2772
|
+
Test Name: \${testName}
|
|
2773
|
+
|
|
2774
|
+
Failure Logs and Errors:
|
|
2775
|
+
\${failureLogsAndErrors}
|
|
2776
|
+
|
|
2777
|
+
Code Snippet:
|
|
2778
|
+
\${codeSnippet}\`;
|
|
2779
|
+
|
|
2780
|
+
navigator.clipboard.writeText(aiPrompt).then(() => {
|
|
2781
|
+
const originalText = button.querySelector('.copy-prompt-text').textContent;
|
|
2782
|
+
button.querySelector('.copy-prompt-text').textContent = 'Copied!';
|
|
2783
|
+
button.classList.add('copied');
|
|
2784
|
+
|
|
2785
|
+
const shortTestName = testName.split(' > ').pop() || testName;
|
|
2786
|
+
alert(\`AI prompt to generate a suggested fix for "\${shortTestName}" has been copied to your clipboard.\`);
|
|
2787
|
+
|
|
2788
|
+
setTimeout(() => {
|
|
2789
|
+
button.querySelector('.copy-prompt-text').textContent = originalText;
|
|
2790
|
+
button.classList.remove('copied');
|
|
2791
|
+
}, 2000);
|
|
2792
|
+
}).catch(err => {
|
|
2793
|
+
console.error('Failed to copy AI prompt:', err);
|
|
2794
|
+
alert('Failed to copy AI prompt to clipboard. Please try again.');
|
|
2795
|
+
});
|
|
2796
|
+
} catch (e) {
|
|
2797
|
+
console.error('Error processing test data for AI Prompt copy:', e);
|
|
2798
|
+
alert('Could not process test data. Please try again.');
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
|
|
2654
2802
|
function closeAiModal() {
|
|
2655
2803
|
const modal = document.getElementById('ai-fix-modal');
|
|
2656
2804
|
if(modal) modal.style.display = 'none';
|
|
@@ -2826,6 +2974,18 @@ aspect-ratio: 16 / 9;
|
|
|
2826
2974
|
}
|
|
2827
2975
|
return;
|
|
2828
2976
|
}
|
|
2977
|
+
const annotationLink = e.target.closest('a.annotation-link');
|
|
2978
|
+
if (annotationLink) {
|
|
2979
|
+
e.preventDefault();
|
|
2980
|
+
const annotationId = annotationLink.dataset.annotation;
|
|
2981
|
+
if (annotationId) {
|
|
2982
|
+
const jiraUrl = prompt('Enter your JIRA/Ticket system base URL (e.g., https://your-company.atlassian.net/browse/):', 'https://your-company.atlassian.net/browse/');
|
|
2983
|
+
if (jiraUrl) {
|
|
2984
|
+
window.open(jiraUrl + annotationId, '_blank');
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2987
|
+
return;
|
|
2988
|
+
}
|
|
2829
2989
|
const img = e.target.closest('img.lazy-load-image');
|
|
2830
2990
|
if (img && img.dataset && img.dataset.src) {
|
|
2831
2991
|
if (e.preventDefault) e.preventDefault();
|
|
@@ -2994,10 +3154,10 @@ aspect-ratio: 16 / 9;
|
|
|
2994
3154
|
</html>
|
|
2995
3155
|
`;
|
|
2996
3156
|
}
|
|
2997
|
-
async function runScript(scriptPath) {
|
|
3157
|
+
async function runScript(scriptPath, args = []) {
|
|
2998
3158
|
return new Promise((resolve, reject) => {
|
|
2999
3159
|
console.log(chalk.blue(`Executing script: ${scriptPath}...`));
|
|
3000
|
-
const process = fork(scriptPath,
|
|
3160
|
+
const process = fork(scriptPath, args, {
|
|
3001
3161
|
stdio: "inherit",
|
|
3002
3162
|
});
|
|
3003
3163
|
|
|
@@ -3027,13 +3187,22 @@ async function main() {
|
|
|
3027
3187
|
const __filename = fileURLToPath(import.meta.url);
|
|
3028
3188
|
const __dirname = path.dirname(__filename);
|
|
3029
3189
|
|
|
3190
|
+
const args = process.argv.slice(2);
|
|
3191
|
+
let customOutputDir = null;
|
|
3192
|
+
for (let i = 0; i < args.length; i++) {
|
|
3193
|
+
if (args[i] === "--outputDir" || args[i] === "-o") {
|
|
3194
|
+
customOutputDir = args[i + 1];
|
|
3195
|
+
break;
|
|
3196
|
+
}
|
|
3197
|
+
}
|
|
3198
|
+
|
|
3030
3199
|
// Script to archive current run to JSON history (this is your modified "generate-trend.mjs")
|
|
3031
3200
|
const archiveRunScriptPath = path.resolve(
|
|
3032
3201
|
__dirname,
|
|
3033
3202
|
"generate-trend.mjs" // Keeping the filename as per your request
|
|
3034
3203
|
);
|
|
3035
3204
|
|
|
3036
|
-
const outputDir =
|
|
3205
|
+
const outputDir = await getOutputDir(customOutputDir);
|
|
3037
3206
|
const reportJsonPath = path.resolve(outputDir, DEFAULT_JSON_FILE); // Current run's main JSON
|
|
3038
3207
|
const reportHtmlPath = path.resolve(outputDir, DEFAULT_HTML_FILE);
|
|
3039
3208
|
|
|
@@ -3043,10 +3212,21 @@ async function main() {
|
|
|
3043
3212
|
|
|
3044
3213
|
console.log(chalk.blue(`Starting static HTML report generation...`));
|
|
3045
3214
|
console.log(chalk.blue(`Output directory set to: ${outputDir}`));
|
|
3215
|
+
if (customOutputDir) {
|
|
3216
|
+
console.log(chalk.gray(` (from CLI argument)`));
|
|
3217
|
+
} else {
|
|
3218
|
+
const { exists } = await import("./config-reader.mjs").then((m) => ({
|
|
3219
|
+
exists: true,
|
|
3220
|
+
}));
|
|
3221
|
+
console.log(
|
|
3222
|
+
chalk.gray(` (auto-detected from playwright.config or using default)`)
|
|
3223
|
+
);
|
|
3224
|
+
}
|
|
3046
3225
|
|
|
3047
3226
|
// Step 1: Ensure current run data is archived to the history folder
|
|
3048
3227
|
try {
|
|
3049
|
-
|
|
3228
|
+
const archiveArgs = customOutputDir ? ["--outputDir", customOutputDir] : [];
|
|
3229
|
+
await runScript(archiveRunScriptPath, archiveArgs);
|
|
3050
3230
|
console.log(
|
|
3051
3231
|
chalk.green("Current run data archiving to history completed.")
|
|
3052
3232
|
);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import * as fs from "fs/promises";
|
|
3
3
|
import path from "path";
|
|
4
|
+
import { getOutputDir } from "./config-reader.mjs";
|
|
4
5
|
|
|
5
6
|
// Use dynamic import for chalk as it's ESM only for prettier console logs
|
|
6
7
|
let chalk;
|
|
@@ -22,8 +23,17 @@ const HISTORY_SUBDIR = "history"; // Subdirectory for historical JSON files
|
|
|
22
23
|
const HISTORY_FILE_PREFIX = "trend-";
|
|
23
24
|
const MAX_HISTORY_FILES = 15; // Store last 15 runs
|
|
24
25
|
|
|
26
|
+
const args = process.argv.slice(2);
|
|
27
|
+
let customOutputDir = null;
|
|
28
|
+
for (let i = 0; i < args.length; i++) {
|
|
29
|
+
if (args[i] === "--outputDir" || args[i] === "-o") {
|
|
30
|
+
customOutputDir = args[i + 1];
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
25
35
|
async function archiveCurrentRunData() {
|
|
26
|
-
const outputDir =
|
|
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
|
+
})();
|
package/scripts/sendReport.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import { fileURLToPath } from "url";
|
|
11
11
|
import { fork } from "child_process"; // This was missing in your sendReport.js but present in generate-email-report.js and needed for runScript
|
|
12
12
|
import "dotenv/config"; // CHANGED for dotenv
|
|
13
|
+
import { getOutputDir } from "./config-reader.mjs";
|
|
13
14
|
|
|
14
15
|
// Import chalk using top-level await if your Node version supports it (14.8+)
|
|
15
16
|
// or keep the dynamic import if preferred, but ensure chalk is resolved before use.
|
|
@@ -28,7 +29,14 @@ try {
|
|
|
28
29
|
};
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
const
|
|
32
|
+
const args = process.argv.slice(2);
|
|
33
|
+
let customOutputDir = null;
|
|
34
|
+
for (let i = 0; i < args.length; i++) {
|
|
35
|
+
if (args[i] === "--outputDir" || args[i] === "-o") {
|
|
36
|
+
customOutputDir = args[i + 1];
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
32
40
|
|
|
33
41
|
let fetch;
|
|
34
42
|
// Ensure fetch is imported and available before it's used in fetchCredentials
|
|
@@ -40,11 +48,8 @@ let fetch;
|
|
|
40
48
|
|
|
41
49
|
let projectName;
|
|
42
50
|
|
|
43
|
-
function getUUID() {
|
|
44
|
-
const reportPath = path.join(
|
|
45
|
-
process.cwd(),
|
|
46
|
-
`${reportDir}/playwright-pulse-report.json`
|
|
47
|
-
);
|
|
51
|
+
function getUUID(reportDir) {
|
|
52
|
+
const reportPath = path.join(reportDir, "playwright-pulse-report.json");
|
|
48
53
|
console.log("Report path:", reportPath);
|
|
49
54
|
|
|
50
55
|
if (!fsExistsSync(reportPath)) {
|
|
@@ -71,18 +76,15 @@ const formatStartTime = (isoString) => {
|
|
|
71
76
|
return date.toLocaleString(); // Default locale
|
|
72
77
|
};
|
|
73
78
|
|
|
74
|
-
const getPulseReportSummary = () => {
|
|
75
|
-
const reportPath = path.join(
|
|
76
|
-
process.cwd(),
|
|
77
|
-
`${reportDir}/playwright-pulse-report.json`
|
|
78
|
-
);
|
|
79
|
+
const getPulseReportSummary = (reportDir) => {
|
|
80
|
+
const reportPath = path.join(reportDir, "playwright-pulse-report.json");
|
|
79
81
|
|
|
80
82
|
if (!fsExistsSync(reportPath)) {
|
|
81
83
|
// CHANGED
|
|
82
84
|
throw new Error("Pulse report file not found.");
|
|
83
85
|
}
|
|
84
86
|
|
|
85
|
-
const content = JSON.parse(fsReadFileSync(reportPath, "utf-8")); //
|
|
87
|
+
const content = JSON.parse(fsReadFileSync(reportPath, "utf-8")); // D
|
|
86
88
|
const run = content.run;
|
|
87
89
|
|
|
88
90
|
const total = run.totalTests || 0;
|
|
@@ -220,9 +222,9 @@ const archiveRunScriptPath = path.resolve(
|
|
|
220
222
|
"generate-email-report.mjs" // Or input_file_0.mjs if you rename it, or input_file_0.js if you configure package.json
|
|
221
223
|
);
|
|
222
224
|
|
|
223
|
-
async function runScript(scriptPath) {
|
|
225
|
+
async function runScript(scriptPath, args = []) {
|
|
224
226
|
return new Promise((resolve, reject) => {
|
|
225
|
-
const childProcess = fork(scriptPath,
|
|
227
|
+
const childProcess = fork(scriptPath, args, {
|
|
226
228
|
// Renamed variable
|
|
227
229
|
stdio: "inherit",
|
|
228
230
|
});
|
|
@@ -244,8 +246,9 @@ async function runScript(scriptPath) {
|
|
|
244
246
|
});
|
|
245
247
|
}
|
|
246
248
|
|
|
247
|
-
const sendEmail = async (credentials) => {
|
|
248
|
-
|
|
249
|
+
const sendEmail = async (credentials, reportDir) => {
|
|
250
|
+
const archiveArgs = customOutputDir ? ["--outputDir", customOutputDir] : [];
|
|
251
|
+
await runScript(archiveRunScriptPath, archiveArgs);
|
|
249
252
|
try {
|
|
250
253
|
console.log("Starting the sendEmail function...");
|
|
251
254
|
|
|
@@ -259,7 +262,7 @@ const sendEmail = async (credentials) => {
|
|
|
259
262
|
},
|
|
260
263
|
});
|
|
261
264
|
|
|
262
|
-
const reportData = getPulseReportSummary();
|
|
265
|
+
const reportData = getPulseReportSummary(reportDir);
|
|
263
266
|
const htmlContent = generateHtmlTable(reportData);
|
|
264
267
|
|
|
265
268
|
const mailOptions = {
|
|
@@ -289,7 +292,7 @@ const sendEmail = async (credentials) => {
|
|
|
289
292
|
}
|
|
290
293
|
};
|
|
291
294
|
|
|
292
|
-
async function fetchCredentials(retries = 10) {
|
|
295
|
+
async function fetchCredentials(reportDir, retries = 10) {
|
|
293
296
|
// Ensure fetch is initialized from the dynamic import before calling this
|
|
294
297
|
if (!fetch) {
|
|
295
298
|
try {
|
|
@@ -304,7 +307,7 @@ async function fetchCredentials(retries = 10) {
|
|
|
304
307
|
}
|
|
305
308
|
|
|
306
309
|
const timeout = 10000;
|
|
307
|
-
const key = getUUID();
|
|
310
|
+
const key = getUUID(reportDir);
|
|
308
311
|
|
|
309
312
|
if (!key) {
|
|
310
313
|
console.error(
|
|
@@ -384,7 +387,19 @@ const main = async () => {
|
|
|
384
387
|
}
|
|
385
388
|
}
|
|
386
389
|
|
|
387
|
-
const
|
|
390
|
+
const reportDir = await getOutputDir(customOutputDir);
|
|
391
|
+
|
|
392
|
+
console.log(chalk.blue(`Preparing to send email report...`));
|
|
393
|
+
console.log(chalk.blue(`Report directory set to: ${reportDir}`));
|
|
394
|
+
if (customOutputDir) {
|
|
395
|
+
console.log(chalk.gray(` (from CLI argument)`));
|
|
396
|
+
} else {
|
|
397
|
+
console.log(
|
|
398
|
+
chalk.gray(` (auto-detected from playwright.config or using default)`)
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const credentials = await fetchCredentials(reportDir);
|
|
388
403
|
if (!credentials) {
|
|
389
404
|
console.warn(
|
|
390
405
|
"Skipping email sending due to missing or failed credential fetch"
|
|
@@ -393,7 +408,7 @@ const main = async () => {
|
|
|
393
408
|
}
|
|
394
409
|
// Removed await delay(10000); // If not strictly needed, remove it.
|
|
395
410
|
try {
|
|
396
|
-
await sendEmail(credentials);
|
|
411
|
+
await sendEmail(credentials, reportDir);
|
|
397
412
|
} catch (error) {
|
|
398
413
|
console.error("Error in main function: ", error);
|
|
399
414
|
}
|