@arghajit/dummy 0.3.0 → 0.3.1
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/package.json +1 -1
- package/scripts/generate-email-report.mjs +12 -1
- package/scripts/generate-report.mjs +195 -45
- package/scripts/generate-static-report.mjs +106 -9
- package/scripts/generate-trend.mjs +12 -1
- package/scripts/merge-pulse-report.js +10 -1
- package/scripts/sendReport.mjs +14 -4
package/README.md
CHANGED
|
@@ -84,6 +84,42 @@ npx generate-pulse-report # Generates static HTML
|
|
|
84
84
|
npx send-email # Sends email report
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
+
### 4. Custom Output Directory (Optional)
|
|
88
|
+
|
|
89
|
+
All CLI scripts now support custom output directories, giving you full flexibility over where reports are generated:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# Using custom directory
|
|
93
|
+
npx generate-pulse-report --outputDir my-reports
|
|
94
|
+
npx generate-report -o test-results/e2e
|
|
95
|
+
npx send-email --outputDir custom-pulse-reports
|
|
96
|
+
|
|
97
|
+
# Using nested paths
|
|
98
|
+
npx generate-pulse-report --outputDir reports/integration
|
|
99
|
+
npx merge-pulse-report --outputDir my-test-reports
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**Important:** Make sure your `playwright.config.ts` custom directory matches the CLI script:
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
import { defineConfig } from "@playwright/test";
|
|
106
|
+
import * as path from "path";
|
|
107
|
+
|
|
108
|
+
const CUSTOM_REPORT_DIR = path.resolve(__dirname, "my-reports");
|
|
109
|
+
|
|
110
|
+
export default defineConfig({
|
|
111
|
+
reporter: [
|
|
112
|
+
["list"],
|
|
113
|
+
[
|
|
114
|
+
"@arghajit/playwright-pulse-report",
|
|
115
|
+
{
|
|
116
|
+
outputDir: CUSTOM_REPORT_DIR, // Must match CLI --outputDir
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
],
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
87
123
|
## 📊 Report Options
|
|
88
124
|
|
|
89
125
|
### Option 1: Static HTML Report (Embedded Attachments)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arghajit/dummy",
|
|
3
3
|
"author": "Arghajit Singha",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.1",
|
|
5
5
|
"description": "A Playwright reporter and dashboard for visualizing test results.",
|
|
6
6
|
"homepage": "https://playwright-pulse-report.netlify.app/",
|
|
7
7
|
"keywords": [
|
|
@@ -23,6 +23,15 @@ const DEFAULT_OUTPUT_DIR = "pulse-report";
|
|
|
23
23
|
const DEFAULT_JSON_FILE = "playwright-pulse-report.json";
|
|
24
24
|
const MINIFIED_HTML_FILE = "pulse-email-summary.html"; // New minified report
|
|
25
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
|
+
|
|
26
35
|
function sanitizeHTML(str) {
|
|
27
36
|
if (str === null || str === undefined) return "";
|
|
28
37
|
return String(str).replace(/[&<>"']/g, (match) => {
|
|
@@ -652,7 +661,9 @@ function generateMinifiedHTML(reportData) {
|
|
|
652
661
|
`;
|
|
653
662
|
}
|
|
654
663
|
async function main() {
|
|
655
|
-
const outputDir =
|
|
664
|
+
const outputDir = customOutputDir
|
|
665
|
+
? path.resolve(process.cwd(), customOutputDir)
|
|
666
|
+
: path.resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
|
|
656
667
|
const reportJsonPath = path.resolve(outputDir, DEFAULT_JSON_FILE);
|
|
657
668
|
const minifiedReportHtmlPath = path.resolve(outputDir, MINIFIED_HTML_FILE); // Path for the new minified HTML
|
|
658
669
|
|
|
@@ -1519,7 +1519,9 @@ function getAttachmentIcon(contentType) {
|
|
|
1519
1519
|
return "📎";
|
|
1520
1520
|
}
|
|
1521
1521
|
function generateAIFailureAnalyzerTab(results) {
|
|
1522
|
-
const failedTests = (results || []).filter(
|
|
1522
|
+
const failedTests = (results || []).filter(
|
|
1523
|
+
(test) => test.status === "failed"
|
|
1524
|
+
);
|
|
1523
1525
|
|
|
1524
1526
|
if (failedTests.length === 0) {
|
|
1525
1527
|
return `
|
|
@@ -1529,7 +1531,7 @@ function generateAIFailureAnalyzerTab(results) {
|
|
|
1529
1531
|
}
|
|
1530
1532
|
|
|
1531
1533
|
// btoa is not available in Node.js environment, so we define a simple polyfill for it.
|
|
1532
|
-
const btoa = (str) => Buffer.from(str).toString(
|
|
1534
|
+
const btoa = (str) => Buffer.from(str).toString("base64");
|
|
1533
1535
|
|
|
1534
1536
|
return `
|
|
1535
1537
|
<h2 class="tab-main-title">AI Failure Analysis</h2>
|
|
@@ -1539,41 +1541,61 @@ function generateAIFailureAnalyzerTab(results) {
|
|
|
1539
1541
|
<span class="stat-label">Failed Tests</span>
|
|
1540
1542
|
</div>
|
|
1541
1543
|
<div class="stat-item">
|
|
1542
|
-
<span class="stat-number">${
|
|
1544
|
+
<span class="stat-number">${
|
|
1545
|
+
new Set(failedTests.map((t) => t.browser)).size
|
|
1546
|
+
}</span>
|
|
1543
1547
|
<span class="stat-label">Browsers</span>
|
|
1544
1548
|
</div>
|
|
1545
1549
|
<div class="stat-item">
|
|
1546
|
-
<span class="stat-number">${
|
|
1550
|
+
<span class="stat-number">${Math.round(
|
|
1551
|
+
failedTests.reduce((sum, test) => sum + (test.duration || 0), 0) /
|
|
1552
|
+
1000
|
|
1553
|
+
)}s</span>
|
|
1547
1554
|
<span class="stat-label">Total Duration</span>
|
|
1548
1555
|
</div>
|
|
1549
1556
|
</div>
|
|
1550
1557
|
<p class="ai-analyzer-description">
|
|
1551
|
-
Analyze failed tests using AI to get suggestions and potential fixes. Click the AI Fix button for
|
|
1558
|
+
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.
|
|
1552
1559
|
</p>
|
|
1553
1560
|
|
|
1554
1561
|
<div class="compact-failure-list">
|
|
1555
|
-
${failedTests
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
+
${failedTests
|
|
1563
|
+
.map((test) => {
|
|
1564
|
+
const testTitle = test.name.split(" > ").pop() || "Unnamed Test";
|
|
1565
|
+
const testJson = btoa(JSON.stringify(test)); // Base64 encode the test object
|
|
1566
|
+
const truncatedError =
|
|
1567
|
+
(test.errorMessage || "No error message").slice(0, 150) +
|
|
1568
|
+
(test.errorMessage && test.errorMessage.length > 150 ? "..." : "");
|
|
1569
|
+
|
|
1570
|
+
return `
|
|
1562
1571
|
<div class="compact-failure-item">
|
|
1563
1572
|
<div class="failure-header">
|
|
1564
1573
|
<div class="failure-main-info">
|
|
1565
|
-
<h3 class="failure-title" title="${sanitizeHTML(
|
|
1574
|
+
<h3 class="failure-title" title="${sanitizeHTML(
|
|
1575
|
+
test.name
|
|
1576
|
+
)}">${sanitizeHTML(testTitle)}</h3>
|
|
1566
1577
|
<div class="failure-meta">
|
|
1567
|
-
<span class="browser-indicator">${sanitizeHTML(
|
|
1568
|
-
|
|
1578
|
+
<span class="browser-indicator">${sanitizeHTML(
|
|
1579
|
+
test.browser || "unknown"
|
|
1580
|
+
)}</span>
|
|
1581
|
+
<span class="duration-indicator">${formatDuration(
|
|
1582
|
+
test.duration
|
|
1583
|
+
)}</span>
|
|
1569
1584
|
</div>
|
|
1570
1585
|
</div>
|
|
1571
|
-
<
|
|
1572
|
-
<
|
|
1573
|
-
|
|
1586
|
+
<div class="ai-buttons-group">
|
|
1587
|
+
<button class="compact-ai-btn" onclick="getAIFix(this)" data-test-json="${testJson}">
|
|
1588
|
+
<span class="ai-text">AI Fix</span>
|
|
1589
|
+
</button>
|
|
1590
|
+
<button class="copy-prompt-btn" onclick="copyAIPrompt(this)" data-test-json="${testJson}" title="Copy AI Prompt">
|
|
1591
|
+
<span class="copy-prompt-text">Copy AI Prompt</span>
|
|
1592
|
+
</button>
|
|
1593
|
+
</div>
|
|
1574
1594
|
</div>
|
|
1575
1595
|
<div class="failure-error-preview">
|
|
1576
|
-
<div class="error-snippet">${formatPlaywrightError(
|
|
1596
|
+
<div class="error-snippet">${formatPlaywrightError(
|
|
1597
|
+
truncatedError
|
|
1598
|
+
)}</div>
|
|
1577
1599
|
<button class="expand-error-btn" onclick="toggleErrorDetails(this)">
|
|
1578
1600
|
<span class="expand-text">Show Full Error</span>
|
|
1579
1601
|
<span class="expand-icon">▼</span>
|
|
@@ -1581,12 +1603,15 @@ function generateAIFailureAnalyzerTab(results) {
|
|
|
1581
1603
|
</div>
|
|
1582
1604
|
<div class="full-error-details" style="display: none;">
|
|
1583
1605
|
<div class="full-error-content">
|
|
1584
|
-
${formatPlaywrightError(
|
|
1606
|
+
${formatPlaywrightError(
|
|
1607
|
+
test.errorMessage || "No detailed error message available"
|
|
1608
|
+
)}
|
|
1585
1609
|
</div>
|
|
1586
1610
|
</div>
|
|
1587
1611
|
</div>
|
|
1588
|
-
|
|
1589
|
-
|
|
1612
|
+
`;
|
|
1613
|
+
})
|
|
1614
|
+
.join("")}
|
|
1590
1615
|
</div>
|
|
1591
1616
|
|
|
1592
1617
|
<!-- AI Fix Modal -->
|
|
@@ -1618,7 +1643,7 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1618
1643
|
const fixPath = (p) => {
|
|
1619
1644
|
if (!p) return "";
|
|
1620
1645
|
// This regex handles both forward slashes and backslashes
|
|
1621
|
-
return p.replace(new RegExp(`^${DEFAULT_OUTPUT_DIR}[\\\\/]`),
|
|
1646
|
+
return p.replace(new RegExp(`^${DEFAULT_OUTPUT_DIR}[\\\\/]`), "");
|
|
1622
1647
|
};
|
|
1623
1648
|
|
|
1624
1649
|
const totalTestsOr1 = runSummary.totalTests || 1;
|
|
@@ -1663,20 +1688,23 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1663
1688
|
)}</span>
|
|
1664
1689
|
</div>
|
|
1665
1690
|
<div class="step-details" style="display: none;">
|
|
1666
|
-
${
|
|
1691
|
+
${
|
|
1692
|
+
step.codeLocation
|
|
1667
1693
|
? `<div class="step-info code-section"><strong>Location:</strong> ${sanitizeHTML(
|
|
1668
|
-
|
|
1669
|
-
|
|
1694
|
+
step.codeLocation
|
|
1695
|
+
)}</div>`
|
|
1670
1696
|
: ""
|
|
1671
|
-
|
|
1672
|
-
${
|
|
1697
|
+
}
|
|
1698
|
+
${
|
|
1699
|
+
step.errorMessage
|
|
1673
1700
|
? `<div class="test-error-summary">
|
|
1674
|
-
${
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1701
|
+
${
|
|
1702
|
+
step.stackTrace
|
|
1703
|
+
? `<div class="stack-trace">${formatPlaywrightError(
|
|
1704
|
+
step.stackTrace
|
|
1705
|
+
)}</div>`
|
|
1706
|
+
: ""
|
|
1707
|
+
}
|
|
1680
1708
|
<button
|
|
1681
1709
|
class="copy-error-btn"
|
|
1682
1710
|
onclick="copyErrorToClipboard(this)"
|
|
@@ -1698,14 +1726,15 @@ function generateHTML(reportData, trendData = null) {
|
|
|
1698
1726
|
</button>
|
|
1699
1727
|
</div>`
|
|
1700
1728
|
: ""
|
|
1701
|
-
|
|
1702
|
-
${
|
|
1729
|
+
}
|
|
1730
|
+
${
|
|
1731
|
+
hasNestedSteps
|
|
1703
1732
|
? `<div class="nested-steps">${generateStepsHTML(
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1733
|
+
step.steps,
|
|
1734
|
+
depth + 1
|
|
1735
|
+
)}</div>`
|
|
1707
1736
|
: ""
|
|
1708
|
-
|
|
1737
|
+
}
|
|
1709
1738
|
</div>
|
|
1710
1739
|
</div>`;
|
|
1711
1740
|
})
|
|
@@ -2297,6 +2326,35 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2297
2326
|
.ai-text {
|
|
2298
2327
|
font-size: 0.95em;
|
|
2299
2328
|
}
|
|
2329
|
+
.ai-buttons-group {
|
|
2330
|
+
display: flex;
|
|
2331
|
+
gap: 10px;
|
|
2332
|
+
flex-wrap: wrap;
|
|
2333
|
+
}
|
|
2334
|
+
.copy-prompt-btn {
|
|
2335
|
+
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
|
|
2336
|
+
color: white;
|
|
2337
|
+
border: none;
|
|
2338
|
+
padding: 12px 18px;
|
|
2339
|
+
border-radius: 6px;
|
|
2340
|
+
cursor: pointer;
|
|
2341
|
+
font-weight: 600;
|
|
2342
|
+
display: flex;
|
|
2343
|
+
align-items: center;
|
|
2344
|
+
gap: 8px;
|
|
2345
|
+
transition: all 0.3s ease;
|
|
2346
|
+
white-space: nowrap;
|
|
2347
|
+
}
|
|
2348
|
+
.copy-prompt-btn:hover {
|
|
2349
|
+
transform: translateY(-2px);
|
|
2350
|
+
box-shadow: 0 6px 20px rgba(37, 99, 235, 0.4);
|
|
2351
|
+
}
|
|
2352
|
+
.copy-prompt-btn.copied {
|
|
2353
|
+
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
|
2354
|
+
}
|
|
2355
|
+
.copy-prompt-text {
|
|
2356
|
+
font-size: 0.95em;
|
|
2357
|
+
}
|
|
2300
2358
|
.failure-error-preview {
|
|
2301
2359
|
padding: 0 20px 18px 20px;
|
|
2302
2360
|
border-top: 1px solid var(--light-gray-color);
|
|
@@ -2372,9 +2430,14 @@ function generateHTML(reportData, trendData = null) {
|
|
|
2372
2430
|
.failure-meta {
|
|
2373
2431
|
justify-content: center;
|
|
2374
2432
|
}
|
|
2375
|
-
.
|
|
2433
|
+
.ai-buttons-group {
|
|
2434
|
+
flex-direction: column;
|
|
2435
|
+
width: 100%;
|
|
2436
|
+
}
|
|
2437
|
+
.compact-ai-btn, .copy-prompt-btn {
|
|
2376
2438
|
justify-content: center;
|
|
2377
2439
|
padding: 12px 20px;
|
|
2440
|
+
width: 100%;
|
|
2378
2441
|
}
|
|
2379
2442
|
}
|
|
2380
2443
|
@media (max-width: 480px) {
|
|
@@ -2649,6 +2712,81 @@ function getAIFix(button) {
|
|
|
2649
2712
|
}
|
|
2650
2713
|
|
|
2651
2714
|
|
|
2715
|
+
function copyAIPrompt(button) {
|
|
2716
|
+
try {
|
|
2717
|
+
const testJson = button.dataset.testJson;
|
|
2718
|
+
const test = JSON.parse(atob(testJson));
|
|
2719
|
+
|
|
2720
|
+
const testName = test.name || 'Unknown Test';
|
|
2721
|
+
const failureLogsAndErrors = [
|
|
2722
|
+
'Error Message:',
|
|
2723
|
+
test.errorMessage || 'Not available.',
|
|
2724
|
+
'\\n\\n--- stdout ---',
|
|
2725
|
+
(test.stdout && test.stdout.length > 0) ? test.stdout.join('\\n') : 'Not available.',
|
|
2726
|
+
'\\n\\n--- stderr ---',
|
|
2727
|
+
(test.stderr && test.stderr.length > 0) ? test.stderr.join('\\n') : 'Not available.'
|
|
2728
|
+
].join('\\n');
|
|
2729
|
+
const codeSnippet = test.snippet || '';
|
|
2730
|
+
|
|
2731
|
+
const aiPrompt = \`You are an expert Playwright test automation engineer specializing in debugging test failures.
|
|
2732
|
+
|
|
2733
|
+
INSTRUCTIONS:
|
|
2734
|
+
1. Analyze the test failure carefully
|
|
2735
|
+
2. Provide a brief root cause analysis
|
|
2736
|
+
3. Provide EXACTLY 5 specific, actionable fixes
|
|
2737
|
+
4. Each fix MUST include a code snippet (codeSnippet field)
|
|
2738
|
+
5. Return ONLY valid JSON, no markdown or extra text
|
|
2739
|
+
|
|
2740
|
+
REQUIRED JSON FORMAT:
|
|
2741
|
+
{
|
|
2742
|
+
"rootCause": "Brief explanation of why the test failed",
|
|
2743
|
+
"suggestedFixes": [
|
|
2744
|
+
{
|
|
2745
|
+
"description": "Clear explanation of the fix",
|
|
2746
|
+
"codeSnippet": "await page.waitForSelector('.button', { timeout: 5000 });"
|
|
2747
|
+
}
|
|
2748
|
+
],
|
|
2749
|
+
"affectedTests": ["test1", "test2"]
|
|
2750
|
+
}
|
|
2751
|
+
|
|
2752
|
+
IMPORTANT:
|
|
2753
|
+
- Always return valid JSON only
|
|
2754
|
+
- Always provide exactly 5 fixes in suggestedFixes array
|
|
2755
|
+
- Each fix must have both description and codeSnippet fields
|
|
2756
|
+
- Make code snippets practical and Playwright-specific
|
|
2757
|
+
|
|
2758
|
+
---
|
|
2759
|
+
|
|
2760
|
+
Test Name: \${testName}
|
|
2761
|
+
|
|
2762
|
+
Failure Logs and Errors:
|
|
2763
|
+
\${failureLogsAndErrors}
|
|
2764
|
+
|
|
2765
|
+
Code Snippet:
|
|
2766
|
+
\${codeSnippet}\`;
|
|
2767
|
+
|
|
2768
|
+
navigator.clipboard.writeText(aiPrompt).then(() => {
|
|
2769
|
+
const originalText = button.querySelector('.copy-prompt-text').textContent;
|
|
2770
|
+
button.querySelector('.copy-prompt-text').textContent = 'Copied!';
|
|
2771
|
+
button.classList.add('copied');
|
|
2772
|
+
|
|
2773
|
+
const shortTestName = testName.split(' > ').pop() || testName;
|
|
2774
|
+
alert(\`AI prompt to generate a suggested fix for "\${shortTestName}" has been copied to your clipboard.\`);
|
|
2775
|
+
|
|
2776
|
+
setTimeout(() => {
|
|
2777
|
+
button.querySelector('.copy-prompt-text').textContent = originalText;
|
|
2778
|
+
button.classList.remove('copied');
|
|
2779
|
+
}, 2000);
|
|
2780
|
+
}).catch(err => {
|
|
2781
|
+
console.error('Failed to copy AI prompt:', err);
|
|
2782
|
+
alert('Failed to copy AI prompt to clipboard. Please try again.');
|
|
2783
|
+
});
|
|
2784
|
+
} catch (e) {
|
|
2785
|
+
console.error('Error processing test data for AI Prompt copy:', e);
|
|
2786
|
+
alert('Could not process test data. Please try again.');
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
|
|
2652
2790
|
function closeAiModal() {
|
|
2653
2791
|
const modal = document.getElementById('ai-fix-modal');
|
|
2654
2792
|
if(modal) modal.style.display = 'none';
|
|
@@ -2897,10 +3035,10 @@ function copyErrorToClipboard(button) {
|
|
|
2897
3035
|
</html>
|
|
2898
3036
|
`;
|
|
2899
3037
|
}
|
|
2900
|
-
async function runScript(scriptPath) {
|
|
3038
|
+
async function runScript(scriptPath, args = []) {
|
|
2901
3039
|
return new Promise((resolve, reject) => {
|
|
2902
3040
|
console.log(chalk.blue(`Executing script: ${scriptPath}...`));
|
|
2903
|
-
const process = fork(scriptPath,
|
|
3041
|
+
const process = fork(scriptPath, args, {
|
|
2904
3042
|
stdio: "inherit",
|
|
2905
3043
|
});
|
|
2906
3044
|
|
|
@@ -2925,13 +3063,24 @@ async function main() {
|
|
|
2925
3063
|
const __filename = fileURLToPath(import.meta.url);
|
|
2926
3064
|
const __dirname = path.dirname(__filename);
|
|
2927
3065
|
|
|
3066
|
+
const args = process.argv.slice(2);
|
|
3067
|
+
let customOutputDir = null;
|
|
3068
|
+
for (let i = 0; i < args.length; i++) {
|
|
3069
|
+
if (args[i] === "--outputDir" || args[i] === "-o") {
|
|
3070
|
+
customOutputDir = args[i + 1];
|
|
3071
|
+
break;
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
3074
|
+
|
|
2928
3075
|
// Script to archive current run to JSON history (this is your modified "generate-trend.mjs")
|
|
2929
3076
|
const archiveRunScriptPath = path.resolve(
|
|
2930
3077
|
__dirname,
|
|
2931
3078
|
"generate-trend.mjs" // Keeping the filename as per your request
|
|
2932
3079
|
);
|
|
2933
3080
|
|
|
2934
|
-
const outputDir =
|
|
3081
|
+
const outputDir = customOutputDir
|
|
3082
|
+
? path.resolve(process.cwd(), customOutputDir)
|
|
3083
|
+
: path.resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
|
|
2935
3084
|
const reportJsonPath = path.resolve(outputDir, DEFAULT_JSON_FILE); // Current run's main JSON
|
|
2936
3085
|
const reportHtmlPath = path.resolve(outputDir, DEFAULT_HTML_FILE);
|
|
2937
3086
|
|
|
@@ -2944,7 +3093,8 @@ async function main() {
|
|
|
2944
3093
|
|
|
2945
3094
|
// Step 1: Ensure current run data is archived to the history folder
|
|
2946
3095
|
try {
|
|
2947
|
-
|
|
3096
|
+
const archiveArgs = customOutputDir ? ["--outputDir", customOutputDir] : [];
|
|
3097
|
+
await runScript(archiveRunScriptPath, archiveArgs);
|
|
2948
3098
|
console.log(
|
|
2949
3099
|
chalk.green("Current run data archiving to history completed.")
|
|
2950
3100
|
);
|
|
@@ -1720,7 +1720,7 @@ function generateAIFailureAnalyzerTab(results) {
|
|
|
1720
1720
|
</div>
|
|
1721
1721
|
</div>
|
|
1722
1722
|
<p class="ai-analyzer-description">
|
|
1723
|
-
Analyze failed tests using AI to get suggestions and potential fixes. Click the AI Fix button for
|
|
1723
|
+
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
1724
|
</p>
|
|
1725
1725
|
|
|
1726
1726
|
<div class="compact-failure-list">
|
|
@@ -1748,9 +1748,14 @@ function generateAIFailureAnalyzerTab(results) {
|
|
|
1748
1748
|
)}</span>
|
|
1749
1749
|
</div>
|
|
1750
1750
|
</div>
|
|
1751
|
-
<
|
|
1752
|
-
<
|
|
1753
|
-
|
|
1751
|
+
<div class="ai-buttons-group">
|
|
1752
|
+
<button class="compact-ai-btn" onclick="getAIFix(this)" data-test-json="${testJson}">
|
|
1753
|
+
<span class="ai-text">AI Fix</span>
|
|
1754
|
+
</button>
|
|
1755
|
+
<button class="copy-prompt-btn" onclick="copyAIPrompt(this)" data-test-json="${testJson}" title="Copy AI Prompt">
|
|
1756
|
+
<span class="copy-prompt-text">Copy AI Prompt</span>
|
|
1757
|
+
</button>
|
|
1758
|
+
</div>
|
|
1754
1759
|
</div>
|
|
1755
1760
|
<div class="failure-error-preview">
|
|
1756
1761
|
<div class="error-snippet">${formatPlaywrightError(
|
|
@@ -2431,9 +2436,14 @@ aspect-ratio: 16 / 9;
|
|
|
2431
2436
|
.browser-indicator { background: var(--info-color); color: white; }
|
|
2432
2437
|
#load-more-tests { font-size: 16px; padding: 4px; background-color: var(--light-gray-color); border-radius: 4px; color: var(--text-color); }
|
|
2433
2438
|
.duration-indicator { background: var(--medium-gray-color); color: var(--text-color); }
|
|
2439
|
+
.ai-buttons-group { display: flex; gap: 10px; flex-wrap: wrap; }
|
|
2434
2440
|
.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;}
|
|
2435
2441
|
.compact-ai-btn:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(55, 65, 81, 0.4); }
|
|
2436
2442
|
.ai-text { font-size: 0.95em; }
|
|
2443
|
+
.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;}
|
|
2444
|
+
.copy-prompt-btn:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(37, 99, 235, 0.4); }
|
|
2445
|
+
.copy-prompt-btn.copied { background: linear-gradient(135deg, #10b981 0%, #059669 100%); }
|
|
2446
|
+
.copy-prompt-text { font-size: 0.95em; }
|
|
2437
2447
|
.failure-error-preview { padding: 0 20px 18px 20px; border-top: 1px solid var(--light-gray-color);}
|
|
2438
2448
|
.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;}
|
|
2439
2449
|
.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;}
|
|
@@ -2444,7 +2454,7 @@ aspect-ratio: 16 / 9;
|
|
|
2444
2454
|
.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;}
|
|
2445
2455
|
@media (max-width: 1200px) { .trend-charts-row { grid-template-columns: 1fr; } }
|
|
2446
2456
|
@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; } }
|
|
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; } }
|
|
2457
|
+
@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%; } }
|
|
2448
2458
|
@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; } }
|
|
2449
2459
|
.trace-actions a { text-decoration: none; font-weight: 500; font-size: 0.9em; }
|
|
2450
2460
|
.generic-attachment { text-align: center; padding: 1rem; justify-content: center; }
|
|
@@ -2713,6 +2723,81 @@ aspect-ratio: 16 / 9;
|
|
|
2713
2723
|
}
|
|
2714
2724
|
}
|
|
2715
2725
|
|
|
2726
|
+
function copyAIPrompt(button) {
|
|
2727
|
+
try {
|
|
2728
|
+
const testJson = button.dataset.testJson;
|
|
2729
|
+
const test = JSON.parse(atob(testJson));
|
|
2730
|
+
|
|
2731
|
+
const testName = test.name || 'Unknown Test';
|
|
2732
|
+
const failureLogsAndErrors = [
|
|
2733
|
+
'Error Message:',
|
|
2734
|
+
test.errorMessage || 'Not available.',
|
|
2735
|
+
'\\n\\n--- stdout ---',
|
|
2736
|
+
(test.stdout && test.stdout.length > 0) ? test.stdout.join('\\n') : 'Not available.',
|
|
2737
|
+
'\\n\\n--- stderr ---',
|
|
2738
|
+
(test.stderr && test.stderr.length > 0) ? test.stderr.join('\\n') : 'Not available.'
|
|
2739
|
+
].join('\\n');
|
|
2740
|
+
const codeSnippet = test.snippet || '';
|
|
2741
|
+
|
|
2742
|
+
const aiPrompt = \`You are an expert Playwright test automation engineer specializing in debugging test failures.
|
|
2743
|
+
|
|
2744
|
+
INSTRUCTIONS:
|
|
2745
|
+
1. Analyze the test failure carefully
|
|
2746
|
+
2. Provide a brief root cause analysis
|
|
2747
|
+
3. Provide EXACTLY 5 specific, actionable fixes
|
|
2748
|
+
4. Each fix MUST include a code snippet (codeSnippet field)
|
|
2749
|
+
5. Return ONLY valid JSON, no markdown or extra text
|
|
2750
|
+
|
|
2751
|
+
REQUIRED JSON FORMAT:
|
|
2752
|
+
{
|
|
2753
|
+
"rootCause": "Brief explanation of why the test failed",
|
|
2754
|
+
"suggestedFixes": [
|
|
2755
|
+
{
|
|
2756
|
+
"description": "Clear explanation of the fix",
|
|
2757
|
+
"codeSnippet": "await page.waitForSelector('.button', { timeout: 5000 });"
|
|
2758
|
+
}
|
|
2759
|
+
],
|
|
2760
|
+
"affectedTests": ["test1", "test2"]
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2763
|
+
IMPORTANT:
|
|
2764
|
+
- Always return valid JSON only
|
|
2765
|
+
- Always provide exactly 5 fixes in suggestedFixes array
|
|
2766
|
+
- Each fix must have both description and codeSnippet fields
|
|
2767
|
+
- Make code snippets practical and Playwright-specific
|
|
2768
|
+
|
|
2769
|
+
---
|
|
2770
|
+
|
|
2771
|
+
Test Name: \${testName}
|
|
2772
|
+
|
|
2773
|
+
Failure Logs and Errors:
|
|
2774
|
+
\${failureLogsAndErrors}
|
|
2775
|
+
|
|
2776
|
+
Code Snippet:
|
|
2777
|
+
\${codeSnippet}\`;
|
|
2778
|
+
|
|
2779
|
+
navigator.clipboard.writeText(aiPrompt).then(() => {
|
|
2780
|
+
const originalText = button.querySelector('.copy-prompt-text').textContent;
|
|
2781
|
+
button.querySelector('.copy-prompt-text').textContent = 'Copied!';
|
|
2782
|
+
button.classList.add('copied');
|
|
2783
|
+
|
|
2784
|
+
const shortTestName = testName.split(' > ').pop() || testName;
|
|
2785
|
+
alert(\`AI prompt to generate a suggested fix for "\${shortTestName}" has been copied to your clipboard.\`);
|
|
2786
|
+
|
|
2787
|
+
setTimeout(() => {
|
|
2788
|
+
button.querySelector('.copy-prompt-text').textContent = originalText;
|
|
2789
|
+
button.classList.remove('copied');
|
|
2790
|
+
}, 2000);
|
|
2791
|
+
}).catch(err => {
|
|
2792
|
+
console.error('Failed to copy AI prompt:', err);
|
|
2793
|
+
alert('Failed to copy AI prompt to clipboard. Please try again.');
|
|
2794
|
+
});
|
|
2795
|
+
} catch (e) {
|
|
2796
|
+
console.error('Error processing test data for AI Prompt copy:', e);
|
|
2797
|
+
alert('Could not process test data. Please try again.');
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
|
|
2716
2801
|
function closeAiModal() {
|
|
2717
2802
|
const modal = document.getElementById('ai-fix-modal');
|
|
2718
2803
|
if(modal) modal.style.display = 'none';
|
|
@@ -3068,10 +3153,10 @@ aspect-ratio: 16 / 9;
|
|
|
3068
3153
|
</html>
|
|
3069
3154
|
`;
|
|
3070
3155
|
}
|
|
3071
|
-
async function runScript(scriptPath) {
|
|
3156
|
+
async function runScript(scriptPath, args = []) {
|
|
3072
3157
|
return new Promise((resolve, reject) => {
|
|
3073
3158
|
console.log(chalk.blue(`Executing script: ${scriptPath}...`));
|
|
3074
|
-
const process = fork(scriptPath,
|
|
3159
|
+
const process = fork(scriptPath, args, {
|
|
3075
3160
|
stdio: "inherit",
|
|
3076
3161
|
});
|
|
3077
3162
|
|
|
@@ -3101,13 +3186,24 @@ async function main() {
|
|
|
3101
3186
|
const __filename = fileURLToPath(import.meta.url);
|
|
3102
3187
|
const __dirname = path.dirname(__filename);
|
|
3103
3188
|
|
|
3189
|
+
const args = process.argv.slice(2);
|
|
3190
|
+
let customOutputDir = null;
|
|
3191
|
+
for (let i = 0; i < args.length; i++) {
|
|
3192
|
+
if (args[i] === "--outputDir" || args[i] === "-o") {
|
|
3193
|
+
customOutputDir = args[i + 1];
|
|
3194
|
+
break;
|
|
3195
|
+
}
|
|
3196
|
+
}
|
|
3197
|
+
|
|
3104
3198
|
// Script to archive current run to JSON history (this is your modified "generate-trend.mjs")
|
|
3105
3199
|
const archiveRunScriptPath = path.resolve(
|
|
3106
3200
|
__dirname,
|
|
3107
3201
|
"generate-trend.mjs" // Keeping the filename as per your request
|
|
3108
3202
|
);
|
|
3109
3203
|
|
|
3110
|
-
const outputDir =
|
|
3204
|
+
const outputDir = customOutputDir
|
|
3205
|
+
? path.resolve(process.cwd(), customOutputDir)
|
|
3206
|
+
: path.resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
|
|
3111
3207
|
const reportJsonPath = path.resolve(outputDir, DEFAULT_JSON_FILE); // Current run's main JSON
|
|
3112
3208
|
const reportHtmlPath = path.resolve(outputDir, DEFAULT_HTML_FILE);
|
|
3113
3209
|
|
|
@@ -3120,7 +3216,8 @@ async function main() {
|
|
|
3120
3216
|
|
|
3121
3217
|
// Step 1: Ensure current run data is archived to the history folder
|
|
3122
3218
|
try {
|
|
3123
|
-
|
|
3219
|
+
const archiveArgs = customOutputDir ? ["--outputDir", customOutputDir] : [];
|
|
3220
|
+
await runScript(archiveRunScriptPath, archiveArgs);
|
|
3124
3221
|
console.log(
|
|
3125
3222
|
chalk.green("Current run data archiving to history completed.")
|
|
3126
3223
|
);
|
|
@@ -22,8 +22,19 @@ const HISTORY_SUBDIR = "history"; // Subdirectory for historical JSON files
|
|
|
22
22
|
const HISTORY_FILE_PREFIX = "trend-";
|
|
23
23
|
const MAX_HISTORY_FILES = 15; // Store last 15 runs
|
|
24
24
|
|
|
25
|
+
const args = process.argv.slice(2);
|
|
26
|
+
let customOutputDir = null;
|
|
27
|
+
for (let i = 0; i < args.length; i++) {
|
|
28
|
+
if (args[i] === '--outputDir' || args[i] === '-o') {
|
|
29
|
+
customOutputDir = args[i + 1];
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
25
34
|
async function archiveCurrentRunData() {
|
|
26
|
-
const outputDir =
|
|
35
|
+
const outputDir = customOutputDir
|
|
36
|
+
? path.resolve(process.cwd(), customOutputDir)
|
|
37
|
+
: path.resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
|
|
27
38
|
const currentRunJsonPath = path.join(outputDir, CURRENT_RUN_JSON_FILE);
|
|
28
39
|
const historyDir = path.join(outputDir, HISTORY_SUBDIR);
|
|
29
40
|
|
|
@@ -3,7 +3,16 @@
|
|
|
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
|
+
|
|
15
|
+
const REPORT_DIR = customOutputDir || "./pulse-report";
|
|
7
16
|
const OUTPUT_FILE = "playwright-pulse-report.json";
|
|
8
17
|
|
|
9
18
|
function getReportFiles(dir) {
|
package/scripts/sendReport.mjs
CHANGED
|
@@ -28,7 +28,16 @@ try {
|
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
const
|
|
31
|
+
const args = process.argv.slice(2);
|
|
32
|
+
let customOutputDir = null;
|
|
33
|
+
for (let i = 0; i < args.length; i++) {
|
|
34
|
+
if (args[i] === "--outputDir" || args[i] === "-o") {
|
|
35
|
+
customOutputDir = args[i + 1];
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const reportDir = customOutputDir || "./pulse-report";
|
|
32
41
|
|
|
33
42
|
let fetch;
|
|
34
43
|
// Ensure fetch is imported and available before it's used in fetchCredentials
|
|
@@ -220,9 +229,9 @@ const archiveRunScriptPath = path.resolve(
|
|
|
220
229
|
"generate-email-report.mjs" // Or input_file_0.mjs if you rename it, or input_file_0.js if you configure package.json
|
|
221
230
|
);
|
|
222
231
|
|
|
223
|
-
async function runScript(scriptPath) {
|
|
232
|
+
async function runScript(scriptPath, args = []) {
|
|
224
233
|
return new Promise((resolve, reject) => {
|
|
225
|
-
const childProcess = fork(scriptPath,
|
|
234
|
+
const childProcess = fork(scriptPath, args, {
|
|
226
235
|
// Renamed variable
|
|
227
236
|
stdio: "inherit",
|
|
228
237
|
});
|
|
@@ -245,7 +254,8 @@ async function runScript(scriptPath) {
|
|
|
245
254
|
}
|
|
246
255
|
|
|
247
256
|
const sendEmail = async (credentials) => {
|
|
248
|
-
|
|
257
|
+
const archiveArgs = customOutputDir ? ["--outputDir", customOutputDir] : [];
|
|
258
|
+
await runScript(archiveRunScriptPath, archiveArgs);
|
|
249
259
|
try {
|
|
250
260
|
console.log("Starting the sendEmail function...");
|
|
251
261
|
|