ci(cypress): create cypress dashboard (#8401)

Co-authored-by: PiX <69745008+pixincreate@users.noreply.github.com>
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
likhinbopanna
2025-07-15 18:13:07 +05:30
committed by GitHub
parent a6f4d7267f
commit dc4eccb85c
10 changed files with 2339 additions and 0 deletions

View File

@ -8,3 +8,6 @@ videos/
creds.json
run_all.sh
# Cypress Dashboard build
**/dist

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,164 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hyperswitch Cypress Test Dashboard</title>
<link rel="stylesheet" href="styles-minimal.css" />
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body>
<div class="container">
<header>
<h1>🚀 Hyperswitch Cypress Test Dashboard</h1>
<div class="header-info">
<span id="lastUpdated"></span>
<!-- Report loader for hosted environment -->
<div
id="reportLoader"
style="display: none; gap: 8px; align-items: center"
>
<input
type="text"
id="reportNameInput"
placeholder="Enter report name (e.g., report_2024_01_15)"
style="
padding: 6px 10px;
border: 1px solid #ccc;
border-radius: 4px;
min-width: 250px;
"
/>
<button
id="loadReportBtn"
class="btn btn-secondary"
title="Load Specific Report"
>
📂 Load
</button>
</div>
<button
id="loadLatestBtn"
class="btn btn-primary"
title="Load Latest Report"
>
📊 Latest Report
</button>
<button
id="themeToggle"
class="btn btn-secondary"
title="Toggle theme"
>
🌓
</button>
<button id="refreshBtn" class="btn btn-primary">
🔄 Refresh Data
</button>
</div>
</header>
<!-- Summary Cards -->
<section class="summary-cards">
<div class="card">
<h3>Total Connectors</h3>
<div class="metric" id="totalConnectors">0</div>
</div>
<div class="card">
<h3>Total Tests</h3>
<div class="metric" id="totalTests">0</div>
</div>
<div class="card">
<h3>Passed</h3>
<div class="metric success" id="totalPassed">0</div>
</div>
<div class="card">
<h3>Failed</h3>
<div class="metric error" id="totalFailed">0</div>
</div>
<div class="card">
<h3>Skipped</h3>
<div class="metric warning" id="totalSkipped">0</div>
</div>
<div class="card">
<h3>Success Rate</h3>
<div class="metric success" id="successRate">0%</div>
</div>
<div class="card">
<h3>Failure Rate</h3>
<div class="metric error" id="failureRate">0%</div>
</div>
<div class="card">
<h3>Execution Time</h3>
<div class="metric" id="executionTime">0s</div>
</div>
</section>
<!-- Charts Section -->
<section class="charts-section">
<div class="chart-container">
<h3>Test Results & Execution Time by Connector</h3>
<canvas id="connectorChart"></canvas>
</div>
<div class="chart-container">
<h3>Overall Test Distribution</h3>
<canvas id="distributionChart"></canvas>
</div>
<div class="chart-container">
<h3>Average Test Duration by Connector</h3>
<canvas id="avgDurationChart"></canvas>
</div>
</section>
<!-- Connector Details -->
<section class="connector-details">
<h2>Connector Details</h2>
<div class="filters">
<select id="connectorFilter" class="filter-select">
<option value="">All Connectors</option>
</select>
<select id="statusFilter" class="filter-select">
<option value="">All Status</option>
<option value="passed">Passed</option>
<option value="failed">Failed</option>
<option value="skipped">Skipped</option>
<option value="pending">Pending</option>
</select>
</div>
<div id="connectorTables"></div>
</section>
<!-- Failed Tests Details -->
<section class="failed-tests collapsed" id="failedTestsSection">
<h2 class="collapsible-header">
Failed Tests Details
<span class="failed-count" id="failedCount"></span>
</h2>
<div id="failedTestsList" class="collapsible-content"></div>
</section>
<!-- Test Runner Modal -->
<div id="testRunnerModal" class="modal">
<div class="modal-content">
<span class="close">&times;</span>
<h2>Run Individual Test</h2>
<div class="test-runner-form">
<label for="testConnector">Connector:</label>
<select id="testConnector"></select>
<label for="testFile">Test File:</label>
<select id="testFile"></select>
<label for="testCase">Test Case:</label>
<select id="testCase"></select>
<button id="runTestBtn" class="btn btn-primary">Run Test</button>
<div id="testOutput" class="test-output"></div>
</div>
</div>
</div>
</div>
<script src="dashboard.js"></script>
</body>
</html>

View File

@ -0,0 +1,604 @@
/* Reset and Base Styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
/* Light theme colors - Minimalistic */
--bg-primary: #ffffff;
--bg-secondary: #f8f9fa;
--text-primary: #212529;
--text-secondary: #6c757d;
--text-tertiary: #000000;
--border-color: #e9ecef;
--shadow-color: rgba(0, 0, 0, 0.05);
--hover-bg: #f1f3f5;
/* Status colors */
--success-color: #04c38d;
--success-bg: #e6fffa;
--success-text: #065f46;
--error-color: #dc2626;
--error-bg: #fee2e2;
--error-text: #991b1b;
--warning-color: #2563eb;
--warning-bg: #dbeafe;
--warning-text: #1e3a8a;
--info-color: #2563eb;
--info-bg: #dbeafe;
--info-text: #1e3a8a;
--cypress-green: #04c38d;
--cypress-dark: #171717;
}
/* Dark theme - Better readability */
body.dark-theme {
--bg-primary: #0f0f0f;
--bg-secondary: #1a1a1a;
--text-primary: #e0e0e0;
--text-secondary: #9ca3af;
--text-tertiary: #f3f4f6;
--border-color: #2d2d2d;
--shadow-color: rgba(0, 0, 0, 0.3);
--hover-bg: #262626;
/* Status colors for dark theme */
--success-bg: rgba(4, 195, 141, 0.15);
--success-text: #04c38d;
--error-bg: rgba(220, 38, 38, 0.15);
--error-text: #ef4444;
--warning-bg: rgba(37, 99, 235, 0.15);
--warning-text: #60a5fa;
--info-bg: rgba(37, 99, 235, 0.15);
--info-text: #60a5fa;
}
body {
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
Arial, sans-serif;
background-color: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
transition:
background-color 0.3s ease,
color 0.3s ease;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
/* Header Styles - Minimal */
header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid var(--border-color);
}
header h1 {
color: var(--text-primary);
font-size: 24px;
font-weight: 600;
}
.header-info {
display: flex;
align-items: center;
gap: 16px;
}
#lastUpdated {
color: var(--text-secondary);
font-size: 14px;
}
/* Button Styles - Minimal */
.btn {
padding: 8px 16px;
border: 1px solid var(--border-color);
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s ease;
background: transparent;
color: var(--text-primary);
}
.btn:hover {
background-color: var(--hover-bg);
transform: translateY(-1px);
}
.btn-primary {
background-color: var(--cypress-green);
color: white;
border-color: var(--cypress-green);
}
.btn-primary:hover {
background-color: #03a479;
}
.btn-secondary {
background-color: transparent;
color: var(--text-secondary);
}
/* Summary Cards - Minimal */
.summary-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 12px;
margin-bottom: 24px;
}
.card {
background: var(--bg-secondary);
padding: 16px;
border-radius: 4px;
text-align: center;
border: 1px solid var(--border-color);
transition: all 0.2s ease;
}
.card:hover {
border-color: var(--cypress-green);
}
.card h3 {
color: var(--text-secondary);
font-size: 12px;
font-weight: normal;
margin-bottom: 8px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.metric {
font-size: 28px;
font-weight: 500;
color: var(--text-primary);
}
.metric.success {
color: var(--success-color);
}
.metric.error {
color: var(--error-color);
}
.metric.warning {
color: var(--warning-color);
}
/* Charts Section - Minimal */
.charts-section {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 16px;
margin-bottom: 24px;
padding-bottom: 40px; /* Add padding to the bottom of the section */
}
.chart-container {
background: var(--bg-secondary);
padding: 20px;
padding-bottom: 60px; /* Increased bottom padding for rotated labels */
border-radius: 4px;
border: 1px solid var(--border-color);
height: 350px; /* Keep original height for the graph area */
}
.chart-container h3 {
color: var(--text-primary);
margin-bottom: 16px;
font-size: 16px;
font-weight: 500;
}
/* Connector Details - Minimal */
.connector-details {
margin-bottom: 24px;
}
.connector-details h2 {
color: var(--text-primary);
margin-bottom: 16px;
font-size: 20px;
font-weight: 500;
}
.filters {
display: flex;
gap: 12px;
margin-bottom: 16px;
}
.filter-select {
padding: 8px 12px;
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 14px;
background: var(--bg-secondary);
color: var(--text-primary);
}
/* Tables - Single column layout */
#connectorTables {
display: flex;
flex-direction: column;
gap: 12px;
max-width: 1200px;
margin: 0 auto;
}
.connector-table {
margin-bottom: 0;
border: 1px solid var(--border-color);
border-radius: 4px;
overflow: hidden;
background: var(--bg-secondary);
}
.connector-table h3 {
color: var(--text-primary);
padding: 12px 16px;
font-size: 16px;
display: flex;
align-items: center;
justify-content: space-between;
background: var(--bg-secondary);
cursor: pointer;
margin: 0;
user-select: none;
border-bottom: 1px solid var(--border-color);
}
.connector-table h3:hover {
background: var(--hover-bg);
}
.connector-table h3::before {
content: "▼";
margin-right: 8px;
transition: transform 0.3s ease;
color: var(--text-secondary);
font-size: 12px;
}
.connector-table.collapsed h3::before {
transform: rotate(-90deg);
}
.connector-table.collapsed table {
display: none;
}
.run-test-btn {
font-size: 13px;
padding: 6px 12px;
}
table {
width: 100%;
border-collapse: collapse;
}
th,
td {
padding: 12px;
text-align: left;
border-bottom: 1px solid var(--border-color);
}
th {
background-color: var(--bg-secondary);
font-weight: 500;
color: var(--text-primary);
font-size: 14px;
}
td {
font-size: 14px;
color: var(--text-primary);
}
tr:hover {
background-color: var(--hover-bg);
}
.status {
padding: 4px 8px;
border-radius: 3px;
font-size: 12px;
font-weight: 500;
display: inline-block;
}
.status.passed {
background-color: var(--success-bg);
color: var(--success-text);
}
.status.failed {
background-color: var(--error-bg);
color: var(--error-text);
}
.status.skipped {
background-color: var(--warning-bg);
color: var(--warning-text);
}
.status.pending {
background-color: var(--info-bg);
color: var(--info-text);
}
/* Failed Tests Section - Minimal */
.failed-tests {
background: var(--bg-secondary);
padding: 0;
border-radius: 4px;
border: 1px solid var(--border-color);
overflow: hidden;
}
.failed-tests h2 {
color: var(--error-color);
margin: 0;
font-size: 20px;
font-weight: 500;
padding: 16px 20px;
cursor: pointer;
user-select: none;
display: flex;
align-items: center;
justify-content: space-between;
}
.failed-tests h2:hover {
background: var(--hover-bg);
}
.failed-tests h2::before {
content: "▼";
margin-right: 8px;
transition: transform 0.3s ease;
color: var(--error-color);
font-size: 14px;
}
.failed-tests.collapsed h2::before {
transform: rotate(-90deg);
}
.failed-tests .collapsible-content {
padding: 0 20px 20px;
transition: all 0.3s ease;
}
.failed-tests.collapsed .collapsible-content {
display: none;
}
.failed-count {
font-size: 16px;
font-weight: normal;
color: var(--text-secondary);
}
.failed-test-item {
border: 1px solid var(--error-color);
border-radius: 4px;
padding: 16px;
margin-bottom: 16px;
background-color: var(--error-bg);
}
.failed-test-item h4 {
color: var(--error-text);
margin-bottom: 8px;
font-size: 16px;
font-weight: 500;
}
.failed-test-details {
display: grid;
gap: 8px;
font-size: 14px;
}
.failed-test-details span {
color: var(--text-secondary);
}
.error-message {
background-color: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 3px;
padding: 8px;
margin: 8px 0;
color: var(--error-text);
font-family: monospace;
font-size: 12px;
overflow-x: auto;
}
.media-links {
display: flex;
gap: 12px;
margin-top: 8px;
}
.media-links a {
color: var(--info-color);
text-decoration: none;
font-size: 14px;
}
.media-links a:hover {
text-decoration: underline;
}
/* Modal Styles - Minimal */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0, 0, 0, 0.4);
}
.modal-content {
background-color: var(--bg-secondary);
margin: 5% auto;
padding: 24px;
border: 1px solid var(--border-color);
width: 90%;
max-width: 500px;
border-radius: 4px;
position: relative;
}
.close {
color: var(--text-secondary);
float: right;
font-size: 24px;
font-weight: bold;
cursor: pointer;
position: absolute;
right: 16px;
top: 12px;
}
.close:hover,
.close:focus {
color: var(--text-primary);
}
/* Test Runner Form - Minimal */
.test-runner-form {
display: flex;
flex-direction: column;
gap: 12px;
}
.test-runner-form label {
font-weight: 500;
color: var(--text-primary);
margin-top: 8px;
font-size: 14px;
}
.test-runner-form select {
padding: 8px 12px;
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 14px;
background: var(--bg-primary);
color: var(--text-primary);
}
.test-output {
margin-top: 16px;
padding: 12px;
background-color: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 4px;
font-family: monospace;
font-size: 12px;
max-height: 300px;
overflow-y: auto;
white-space: pre-wrap;
}
/* Loading State */
.loading {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid var(--hover-bg);
border-top: 2px solid var(--cypress-green);
border-radius: 50%;
animation: spin 1s linear infinite;
margin-right: 8px;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* Responsive Design */
@media (max-width: 768px) {
.container {
padding: 16px;
}
header {
flex-direction: column;
text-align: center;
gap: 12px;
}
.summary-cards {
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
}
.charts-section {
grid-template-columns: 1fr;
}
.filters {
flex-direction: column;
}
table {
font-size: 13px;
}
th,
td {
padding: 8px;
}
}
/* Utility Classes */
.hidden {
display: none;
}
.text-center {
text-align: center;
}
.mb-20 {
margin-bottom: 20px;
}
.mt-20 {
margin-top: 20px;
}
/* Improved dark theme Chart.js support */
body.dark-theme canvas {
filter: brightness(0.95);
}

View File

@ -10,6 +10,9 @@ export default [
pluginCypress.configs.recommended,
eslintPluginPrettierRecommended,
eslintConfigPrettier,
{
ignores: ["dist/**", "build/**", "node_modules/**"],
},
{
languageOptions: {
globals: {

View File

@ -13,6 +13,12 @@
"cypress:payments": "cypress run --headless --spec 'cypress/e2e/spec/Payment/**/*'",
"cypress:payouts": "cypress run --headless --spec 'cypress/e2e/spec/Payout/**/*'",
"cypress:routing": "cypress run --headless --spec 'cypress/e2e/spec/Routing/**/*'",
"report:generate": "node scripts/report-generator.js",
"report:open": "open dashboard/index.html || start dashboard/index.html || xdg-open dashboard/index.html",
"dashboard:serve": "node scripts/serve-dashboard.js",
"dashboard": "npm run dashboard:serve",
"cypress:ci:report": "npm run cypress:ci && npm run report:generate",
"build": "node scripts/build.js",
"format": "prettier --config .prettierrc . --write",
"format:check": "prettier --config .prettierrc . --check",
"lint": "eslint ."

View File

@ -0,0 +1,104 @@
#!/usr/bin/env node
/* eslint-disable no-console */
import fs from "fs";
const DASHBOARD_DIR = "dashboard";
const DIST_DIR = "dist";
// Create dist directory
function createDirs() {
if (!fs.existsSync(DIST_DIR)) {
fs.mkdirSync(DIST_DIR, { recursive: true });
}
}
// Minify JavaScript
function minifyJS(inputPath, outputPath) {
let content = fs.readFileSync(inputPath, "utf8");
// Preserve eslint disable comments
const eslintComments = [];
content = content.replace(/\/\*\s*eslint-disable[\s\S]*?\*\//g, (match) => {
eslintComments.push(match);
return `/*ESLINT_PRESERVE_${eslintComments.length - 1}*/`;
});
content = content.replace(/\/\*\s*global[\s\S]*?\*\//g, (match) => {
eslintComments.push(match);
return `/*ESLINT_PRESERVE_${eslintComments.length - 1}*/`;
});
// Basic minification
content = content
.replace(/\/\*[\s\S]*?\*\//g, "") // Remove other block comments
.replace(/\/\/.*$/gm, "") // Remove line comments
.replace(/\s+/g, " ") // Replace multiple spaces with single space
.replace(/;\s*}/g, ";}") // Remove space before closing brace
.replace(/{\s*/g, "{") // Remove space after opening brace
.replace(/}\s*/g, "}") // Remove space after closing brace
.trim();
// Restore eslint comments
eslintComments.forEach((comment, index) => {
content = content.replace(`/*ESLINT_PRESERVE_${index}*/`, comment);
});
fs.writeFileSync(outputPath, content);
}
// Minify CSS
function minifyCSS(inputPath, outputPath) {
let content = fs.readFileSync(inputPath, "utf8");
// Basic CSS minification
content = content
.replace(/\/\*[\s\S]*?\*\//g, "") // Remove comments
.replace(/\s+/g, " ") // Replace multiple spaces
.replace(/;\s*}/g, ";}") // Remove space before closing brace
.replace(/{\s*/g, "{") // Remove space after opening brace
.replace(/}\s*/g, "}") // Remove space after closing brace
.replace(/:\s*/g, ":") // Remove space after colon
.replace(/;\s*/g, ";") // Remove space after semicolon
.trim();
fs.writeFileSync(outputPath, content);
}
// Process HTML with minified references
function processHTML() {
let content = fs.readFileSync(`${DASHBOARD_DIR}/index.html`, "utf8");
// Update references to minified files
content = content
.replace("dashboard.js", "dashboard.min.js")
.replace("styles-minimal.css", "styles-minimal.min.css");
fs.writeFileSync(`${DIST_DIR}/index.html`, content);
}
// Main build function
function build() {
console.log("Building dashboard...");
createDirs();
// Minify files
minifyJS(`${DASHBOARD_DIR}/dashboard.js`, `${DIST_DIR}/dashboard.min.js`);
minifyCSS(
`${DASHBOARD_DIR}/styles-minimal.css`,
`${DIST_DIR}/styles-minimal.min.css`
);
// Process HTML
processHTML();
// Copy vercel.json if it exists
if (fs.existsSync(`${DASHBOARD_DIR}/vercel.json`)) {
fs.copyFileSync(`${DASHBOARD_DIR}/vercel.json`, `${DIST_DIR}/vercel.json`);
}
console.log("✅ Build completed! Files ready in ./dist directory");
}
// Main script logic
build();

View File

@ -0,0 +1,41 @@
/* eslint-disable no-console */
import { exec } from "child_process";
import path from "path";
import { fileURLToPath } from "url";
import { promisify } from "util";
const execAsync = promisify(exec);
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// This script runs after Cypress tests complete
async function runPostTestTasks() {
console.log("🔄 Running post-test tasks...");
try {
// Generate the report
console.log("📊 Generating test report...");
const reportGeneratorPath = path.join(__dirname, "report-generator.js");
await execAsync(`node ${reportGeneratorPath}`);
// Open the dashboard in the default browser (optional)
const dashboardPath = path.join(__dirname, "../dashboard/index.html");
const openCommand =
process.platform === "darwin"
? `open ${dashboardPath}`
: process.platform === "win32"
? `start ${dashboardPath}`
: `xdg-open ${dashboardPath}`;
console.log("🌐 Opening dashboard...");
await execAsync(openCommand);
console.log("✅ Post-test tasks completed successfully!");
} catch (error) {
console.error("❌ Error in post-test tasks:", error);
process.exit(1);
}
}
// Run the tasks
runPostTestTasks();

View File

@ -0,0 +1,327 @@
/* eslint-disable no-console */
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
class CypressReportGenerator {
constructor(reportsDir = path.join(__dirname, "../cypress/reports")) {
this.reportsDir = reportsDir;
this.summaryData = {
connectors: {},
totalTests: 0,
totalPassed: 0,
totalFailed: 0,
totalSkipped: 0,
totalPending: 0,
executionTime: 0,
timestamp: new Date().toISOString(),
failedTests: [],
};
}
async generateReport() {
try {
await this.collectTestResults();
await this.calculateMetrics();
await this.generateSummaryReport();
await this.generateDashboardData();
console.log("✅ Report generation completed successfully!");
} catch (error) {
console.error("❌ Error generating report:", error);
process.exit(1);
}
}
async collectTestResults() {
const connectorDirs = fs
.readdirSync(this.reportsDir)
.filter((item) =>
fs.statSync(path.join(this.reportsDir, item)).isDirectory()
);
for (const connector of connectorDirs) {
const connectorPath = path.join(this.reportsDir, connector);
const jsonFiles = fs
.readdirSync(connectorPath)
.filter(
(file) => file.endsWith(".json") && file !== "mochawesome.json"
);
this.summaryData.connectors[connector] = {
tests: [],
totalTests: 0,
passed: 0,
failed: 0,
skipped: 0,
pending: 0,
executionTime: 0,
testsByFile: {},
};
for (const jsonFile of jsonFiles) {
const reportPath = path.join(connectorPath, jsonFile);
const reportData = JSON.parse(fs.readFileSync(reportPath, "utf8"));
this.processReportData(connector, reportData);
}
}
}
processReportData(connector, reportData) {
const connectorData = this.summaryData.connectors[connector];
// Process stats
if (reportData.stats) {
connectorData.totalTests += reportData.stats.tests || 0;
connectorData.passed += reportData.stats.passes || 0;
connectorData.failed += reportData.stats.failures || 0;
connectorData.skipped += reportData.stats.skipped || 0;
connectorData.pending += reportData.stats.pending || 0;
connectorData.executionTime += reportData.stats.duration || 0;
}
// Process individual test results
if (reportData.results && reportData.results.length > 0) {
reportData.results.forEach((result) => {
if (result.suites) {
this.processSuites(connector, result.suites, result.file);
}
});
}
}
processSuites(connector, suites, file) {
const connectorData = this.summaryData.connectors[connector];
suites.forEach((suite) => {
if (suite.tests) {
suite.tests.forEach((test) => {
const testInfo = {
title: test.title,
fullTitle: test.fullTitle || `${suite.title} - ${test.title}`,
state: test.state,
duration: test.duration || 0,
file: file,
error: test.err
? {
message: test.err.message,
stack: test.err.stack,
diff: test.err.diff,
}
: null,
};
connectorData.tests.push(testInfo);
// Track by file
if (!connectorData.testsByFile[file]) {
connectorData.testsByFile[file] = {
passed: 0,
failed: 0,
skipped: 0,
pending: 0,
tests: [],
};
}
connectorData.testsByFile[file].tests.push(testInfo);
connectorData.testsByFile[file][test.state || "pending"]++;
// Track failed tests globally
if (test.state === "failed") {
this.summaryData.failedTests.push({
connector,
...testInfo,
screenshot: this.findScreenshot(connector, test.title),
video: this.findVideo(connector, file),
});
}
});
}
// Process nested suites
if (suite.suites && suite.suites.length > 0) {
this.processSuites(connector, suite.suites, file);
}
});
}
findScreenshot(connector, testTitle) {
const screenshotDir = path.join(__dirname, "../screenshots", connector);
if (!fs.existsSync(screenshotDir)) return null;
const screenshots = fs.readdirSync(screenshotDir);
const sanitizedTitle = testTitle.replace(/[^a-zA-Z0-9]/g, "-");
const screenshot = screenshots.find((file) =>
file.toLowerCase().includes(sanitizedTitle.toLowerCase())
);
return screenshot ? `/screenshots/${connector}/${screenshot}` : null;
}
findVideo(connector, testFile) {
const videoDir = path.join(__dirname, "../cypress/videos", connector);
if (!fs.existsSync(videoDir)) return null;
const videos = fs.readdirSync(videoDir);
const testFileName = path.basename(testFile, ".cy.js");
const video = videos.find((file) => file.includes(testFileName));
return video ? `/videos/${connector}/${video}` : null;
}
calculateMetrics() {
// Calculate totals
Object.values(this.summaryData.connectors).forEach((connector) => {
this.summaryData.totalTests += connector.totalTests;
this.summaryData.totalPassed += connector.passed;
this.summaryData.totalFailed += connector.failed;
this.summaryData.totalSkipped += connector.skipped;
this.summaryData.totalPending += connector.pending;
this.summaryData.executionTime += connector.executionTime;
// Calculate rates
connector.successRate =
connector.totalTests > 0
? ((connector.passed / connector.totalTests) * 100).toFixed(2)
: 0;
connector.failureRate =
connector.totalTests > 0
? ((connector.failed / connector.totalTests) * 100).toFixed(2)
: 0;
});
// Calculate overall rates
this.summaryData.overallSuccessRate =
this.summaryData.totalTests > 0
? (
(this.summaryData.totalPassed / this.summaryData.totalTests) *
100
).toFixed(2)
: 0;
this.summaryData.overallFailureRate =
this.summaryData.totalTests > 0
? (
(this.summaryData.totalFailed / this.summaryData.totalTests) *
100
).toFixed(2)
: 0;
}
async generateSummaryReport() {
const reportContent = this.formatSummaryReport();
const outputPath = path.join(this.reportsDir, "summary-report.md");
fs.writeFileSync(outputPath, reportContent);
console.log(`📄 Summary report generated: ${outputPath}`);
}
formatSummaryReport() {
let report = `# Cypress Test Summary Report
**Generated:** ${new Date(this.summaryData.timestamp).toLocaleString()}
**Total Execution Time:** ${this.formatDuration(this.summaryData.executionTime)}
## Overall Summary
- **Total Connectors Tested:** ${Object.keys(this.summaryData.connectors).length}
- **Total Tests:** ${this.summaryData.totalTests}
- **Passed:** ${this.summaryData.totalPassed}
- **Failed:** ${this.summaryData.totalFailed}
- **Skipped:** ${this.summaryData.totalSkipped} ⏭️
- **Pending:** ${this.summaryData.totalPending} ⏸️
- **Overall Success Rate:** ${this.summaryData.overallSuccessRate}%
- **Overall Failure Rate:** ${this.summaryData.overallFailureRate}%
## Connector Details
`;
// Add connector details
Object.entries(this.summaryData.connectors).forEach(([connector, data]) => {
report += `### ${connector.toUpperCase()}
| Metric | Value |
|--------|-------|
| Total Tests | ${data.totalTests} |
| Passed | ${data.passed} ✅ |
| Failed | ${data.failed} ❌ |
| Skipped | ${data.skipped} ⏭️ |
| Pending | ${data.pending} ⏸️ |
| Success Rate | ${data.successRate}% |
| Failure Rate | ${data.failureRate}% |
| Execution Time | ${this.formatDuration(data.executionTime)} |
`;
// Add test details by file
if (Object.keys(data.testsByFile).length > 0) {
report += `#### Test Files\n\n`;
Object.entries(data.testsByFile).forEach(([file, fileData]) => {
const fileName = path.basename(file);
report += `- **${fileName}**: ${fileData.passed}${fileData.failed}${fileData.skipped}⏭️ ${fileData.pending}⏸️\n`;
});
report += "\n";
}
});
// Add failed tests section
if (this.summaryData.failedTests.length > 0) {
report += `## Failed Tests Details\n\n`;
this.summaryData.failedTests.forEach((test, index) => {
report += `### ${index + 1}. ${test.fullTitle}
- **Connector:** ${test.connector}
- **File:** ${path.basename(test.file)}
- **Duration:** ${test.duration}ms
- **Error:** ${test.error?.message || "No error message available"}
`;
if (test.screenshot) {
report += `- **Screenshot:** [View Screenshot](${test.screenshot})\n`;
}
if (test.video) {
report += `- **Video:** [View Recording](${test.video})\n`;
}
report += "\n";
});
}
return report;
}
formatDuration(ms) {
const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
if (hours > 0) {
return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
} else if (minutes > 0) {
return `${minutes}m ${seconds % 60}s`;
} else {
return `${seconds}s`;
}
}
async generateDashboardData() {
const dashboardDataPath = path.join(this.reportsDir, "dashboard-data.json");
fs.writeFileSync(
dashboardDataPath,
JSON.stringify(this.summaryData, null, 2)
);
console.log(`📊 Dashboard data generated: ${dashboardDataPath}`);
}
}
// Run the report generator
const generator = new CypressReportGenerator();
generator.generateReport();

View File

@ -0,0 +1,81 @@
#!/usr/bin/env node
/* eslint-disable no-console */
import fs from "fs";
import http from "http";
import path, { dirname } from "path";
import { fileURLToPath } from "url";
// Get the directory of the current module
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Define the port
const PORT = process.env.PORT || 3333;
// MIME types
const mimeTypes = {
".html": "text/html",
".js": "text/javascript",
".css": "text/css",
".json": "application/json",
".png": "image/png",
".jpg": "image/jpg",
".gif": "image/gif",
".svg": "image/svg+xml",
".ico": "image/x-icon",
};
// Create server
const server = http.createServer((req, res) => {
// Parse URL
let filePath = path.join(__dirname, "..", req.url);
// Default to index.html for root
if (req.url === "/") {
filePath = path.join(__dirname, "..", "dashboard", "index.html");
}
// Check if path is a directory and append index.html
if (fs.existsSync(filePath) && fs.statSync(filePath).isDirectory()) {
filePath = path.join(filePath, "index.html");
}
// Get file extension
const extname = String(path.extname(filePath)).toLowerCase();
const contentType = mimeTypes[extname] || "application/octet-stream";
// Read and serve the file
fs.readFile(filePath, (error, content) => {
if (error) {
if (error.code === "ENOENT") {
res.writeHead(404, { "Content-Type": "text/html" });
res.end("<h1>404 - File Not Found</h1>", "utf-8");
} else {
res.writeHead(500);
res.end(`Server Error: ${error.code}`, "utf-8");
}
} else {
res.writeHead(200, { "Content-Type": contentType });
res.end(content, "utf-8");
}
});
});
// Start server
server.listen(PORT, () => {
console.log(`\n🚀 Dashboard server running at http://localhost:${PORT}/`);
console.log(
`📊 Open http://localhost:${PORT}/dashboard/ to view the dashboard`
);
console.log("\nPress Ctrl+C to stop the server\n");
});
// Handle graceful shutdown
process.on("SIGINT", () => {
console.log("\n\nShutting down server...");
server.close(() => {
console.log("Server closed");
process.exit(0);
});
});