/* eslint-disable no-console */
/* global Chart */
// Dashboard JavaScript
let dashboardData = null;
let connectorChart = null;
let distributionChart = null;
let avgDurationChart = null;
// Convert connector name to PascalCase
function toPascalCase(str) {
return str
.toLowerCase()
.split("_")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join("");
}
// Initialize the dashboard
document.addEventListener("DOMContentLoaded", () => {
loadTheme();
setupUI();
loadDashboardData();
setupEventListeners();
// Refresh data every 30 seconds
setInterval(loadDashboardData, 30000);
});
// Setup UI based on environment
function setupUI() {
const isHosted = window.location.hostname === "integ.hyperswitch.io";
// Show/hide report loader for hosted environment
const reportLoader = document.getElementById("reportLoader");
if (isHosted) {
reportLoader.style.display = "flex";
}
}
// Setup event listeners
function setupEventListeners() {
document
.getElementById("refreshBtn")
.addEventListener("click", loadDashboardData);
document
.getElementById("loadLatestBtn")
.addEventListener("click", loadLatestReport);
document.getElementById("themeToggle").addEventListener("click", toggleTheme);
document
.getElementById("connectorFilter")
.addEventListener("change", filterData);
document
.getElementById("statusFilter")
.addEventListener("change", filterData);
// Add event listeners for hosted environment features
const loadReportBtn = document.getElementById("loadReportBtn");
const reportNameInput = document.getElementById("reportNameInput");
if (loadReportBtn) {
loadReportBtn.addEventListener("click", loadSpecificReport);
}
if (reportNameInput) {
// Allow Enter key to load report
reportNameInput.addEventListener("keypress", (e) => {
if (e.key === "Enter") {
loadSpecificReport();
}
});
}
// Modal controls
const modal = document.getElementById("testRunnerModal");
const closeBtn = document.getElementsByClassName("close")[0];
closeBtn.onclick = () => {
modal.style.display = "none";
};
window.onclick = (event) => {
if (event.target === modal) {
modal.style.display = "none";
}
};
document
.getElementById("runTestBtn")
.addEventListener("click", runIndividualTest);
document
.getElementById("testConnector")
.addEventListener("change", updateTestFiles);
document
.getElementById("testFile")
.addEventListener("change", updateTestCases);
}
// Load dashboard data
async function loadDashboardData() {
try {
let response;
// Simple logic: local uses dashboard-data.json, hosted uses report_latest.json
if (window.location.hostname === "integ.hyperswitch.io") {
// Hosted environment - use latest report
response = await fetch("./reports/report_latest.json");
} else {
// Local environment - use dashboard data
response = await fetch("../cypress/reports/dashboard-data.json");
}
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
dashboardData = await response.json();
updateDashboard();
} catch (error) {
console.error("Error loading dashboard data:", error);
const environment =
window.location.hostname === "integ.hyperswitch.io" ? "hosted" : "local";
showError(
`Failed to load dashboard data for ${environment} environment. ${environment === "local" ? "Make sure to run the report generator first." : "Check if the latest report is available."}`
);
}
}
// Load latest report (for hosted environment)
async function loadLatestReport() {
try {
let response;
if (window.location.hostname === "integ.hyperswitch.io") {
// Hosted environment - load latest report
response = await fetch("./reports/report_latest.json");
} else {
// Local environment - just reload the dashboard data
response = await fetch("../cypress/reports/dashboard-data.json");
}
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
dashboardData = await response.json();
updateDashboard();
// Show success message
showSuccess("Latest report loaded successfully!");
} catch (error) {
console.error("Error loading latest report:", error);
showError(
"Failed to load latest report. Please check if the report is available."
);
}
}
// Load specific report by name (for hosted environment)
async function loadSpecificReport() {
const reportNameInput = document.getElementById("reportNameInput");
const reportName = reportNameInput.value.trim();
if (!reportName) {
showError("Please enter a report name.");
return;
}
try {
// Add .json extension if not present
const fileName = reportName.endsWith(".json")
? reportName
: `${reportName}.json`;
const response = await fetch(`./reports/${fileName}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
dashboardData = await response.json();
updateDashboard();
// Show success message
showSuccess(`Report "${fileName}" loaded successfully!`);
// Clear the input
reportNameInput.value = "";
} catch (error) {
console.error("Error loading specific report:", error);
showError(
`Failed to load report "${reportName}". Please check if the report exists.`
);
}
}
// Update dashboard with loaded data
function updateDashboard() {
if (!dashboardData) return;
// Update last updated time (this is the test run timestamp)
document.getElementById("lastUpdated").textContent =
`Last Test Run: ${new Date(dashboardData.timestamp).toLocaleString()}`;
// Update summary cards
updateSummaryCards();
// Update charts
updateCharts();
// Update connector filters
updateConnectorFilters();
// Apply filters
filterData();
// Setup collapsible failed tests
setupFailedTestsCollapsible();
}
// Update summary cards
function updateSummaryCards() {
// Count active connectors
const activeConnectors = Object.entries(dashboardData.connectors).filter(
([, data]) => data.totalTests > 0
).length;
// Update total connectors
document.getElementById("totalConnectors").textContent = activeConnectors;
// Merge skipped and pending
const totalSkippedPending =
dashboardData.totalSkipped + dashboardData.totalPending;
document.getElementById("totalTests").textContent = dashboardData.totalTests;
document.getElementById("totalPassed").textContent =
dashboardData.totalPassed;
document.getElementById("totalFailed").textContent =
dashboardData.totalFailed;
document.getElementById("totalSkipped").textContent = totalSkippedPending;
document.getElementById("successRate").textContent =
`${dashboardData.overallSuccessRate}%`;
// Calculate and display failure rate
const failureRate =
dashboardData.totalTests > 0
? ((dashboardData.totalFailed / dashboardData.totalTests) * 100).toFixed(
2
)
: 0;
document.getElementById("failureRate").textContent = `${failureRate}%`;
document.getElementById("executionTime").textContent = formatDuration(
dashboardData.executionTime
);
}
// Format duration from milliseconds
function 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`;
} else if (minutes > 0) {
return `${minutes}m ${seconds % 60}s`;
} else {
return `${seconds}s`;
}
}
// Get chart colors based on theme
function getChartColors() {
const isDark = document.body.classList.contains("dark-theme");
return {
textColor: isDark ? "#e0e0e0" : "#212529",
gridColor: isDark ? "#2d2d2d" : "#e9ecef",
executionLineColor: "#f59e0b",
scales: {
x: {
ticks: {
color: isDark ? "#e0e0e0" : "#212529",
},
grid: {
color: isDark ? "#2d2d2d" : "#e9ecef",
},
},
y: {
ticks: {
color: isDark ? "#e0e0e0" : "#212529",
},
grid: {
color: isDark ? "#2d2d2d" : "#e9ecef",
},
},
},
};
}
// Update charts
function updateCharts() {
updateCombinedChart();
updateDistributionChart();
updateAvgDurationChart();
}
// Update combined chart (test results + execution time)
function updateCombinedChart() {
const ctx = document.getElementById("connectorChart").getContext("2d");
const colors = getChartColors();
// Filter out connectors with no tests
const activeConnectors = Object.entries(dashboardData.connectors).filter(
([, data]) => data.totalTests > 0
);
const connectorNames = activeConnectors.map(([name]) => name);
// Calculate execution times for secondary axis
const executionTimes = connectorNames.map((c) => {
const connector = dashboardData.connectors[c];
const totalTime = connector.executionTime || 0;
return (totalTime / 1000 / 60).toFixed(2); // Convert to minutes
});
const datasets = [
{
label: "Passed",
data: connectorNames.map((c) => dashboardData.connectors[c].passed),
backgroundColor: "#04c38d",
stack: "tests",
yAxisID: "y",
},
{
label: "Failed",
data: connectorNames.map((c) => dashboardData.connectors[c].failed),
backgroundColor: "#ef4444",
stack: "tests",
yAxisID: "y",
},
{
label: "Skipped/Pending",
data: connectorNames.map(
(c) =>
dashboardData.connectors[c].skipped +
dashboardData.connectors[c].pending
),
backgroundColor: "#3b82f6",
stack: "tests",
yAxisID: "y",
},
{
label: "Execution Time (min)",
data: executionTimes,
type: "line",
borderColor: colors.executionLineColor,
backgroundColor: "transparent",
borderWidth: 2,
pointBackgroundColor: colors.executionLineColor,
pointBorderColor: colors.executionLineColor,
pointRadius: 4,
yAxisID: "y1",
},
];
if (connectorChart) {
connectorChart.destroy();
}
connectorChart = new Chart(ctx, {
type: "bar",
data: {
labels: connectorNames.map((c) => toPascalCase(c)),
datasets: datasets,
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
stacked: true,
ticks: {
color: colors.textColor,
},
grid: {
color: colors.gridColor,
},
},
y: {
stacked: true,
beginAtZero: true,
position: "left",
title: {
display: true,
text: "Number of Tests",
color: colors.textColor,
},
ticks: {
color: colors.textColor,
},
grid: {
color: colors.gridColor,
},
},
y1: {
beginAtZero: true,
position: "right",
title: {
display: true,
text: "Time (minutes)",
color: colors.executionLineColor,
},
ticks: {
color: colors.executionLineColor,
},
grid: {
drawOnChartArea: false,
},
},
},
plugins: {
legend: {
position: "top",
labels: {
color: colors.textColor,
},
},
tooltip: {
mode: "index",
intersect: false,
},
},
},
});
}
// Update distribution chart
function updateDistributionChart() {
const ctx = document.getElementById("distributionChart").getContext("2d");
const colors = getChartColors();
const data = {
labels: ["Passed", "Failed", "Skipped/Pending"],
datasets: [
{
data: [
dashboardData.totalPassed,
dashboardData.totalFailed,
dashboardData.totalSkipped + dashboardData.totalPending,
],
backgroundColor: ["#04c38d", "#ef4444", "#3b82f6"],
},
],
};
if (distributionChart) {
distributionChart.destroy();
}
distributionChart = new Chart(ctx, {
type: "doughnut",
data: data,
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: "right",
labels: {
color: colors.textColor,
},
},
tooltip: {
callbacks: {
label: function (context) {
const label = context.label || "";
const value = context.parsed || 0;
const total = dashboardData.totalTests;
const percentage = ((value / total) * 100).toFixed(1);
return `${label}: ${value} (${percentage}%)`;
},
},
},
},
},
});
}
// Update average duration chart
function updateAvgDurationChart() {
const ctx = document.getElementById("avgDurationChart").getContext("2d");
const colors = getChartColors();
// Filter out connectors with no tests
const activeConnectors = Object.entries(dashboardData.connectors).filter(
([, data]) => data.totalTests > 0
);
const connectorNames = activeConnectors.map(([name]) => name);
const avgDurations = connectorNames.map((c) => {
const connector = dashboardData.connectors[c];
const totalTime = connector.executionTime || 0;
const totalTests = connector.totalTests || 1;
return (totalTime / totalTests / 1000).toFixed(2); // Average time in seconds
});
if (avgDurationChart) {
avgDurationChart.destroy();
}
avgDurationChart = new Chart(ctx, {
type: "bar",
data: {
labels: connectorNames.map((c) => toPascalCase(c)),
datasets: [
{
label: "Average Test Duration (seconds)",
data: avgDurations,
backgroundColor: "#04c38d",
borderColor: "#059669",
borderWidth: 1,
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
ticks: {
color: colors.textColor,
},
grid: {
color: colors.gridColor,
},
},
y: {
beginAtZero: true,
title: {
display: true,
text: "Time (seconds)",
color: colors.textColor,
},
ticks: {
color: colors.textColor,
},
grid: {
color: colors.gridColor,
},
},
},
plugins: {
legend: {
display: false,
},
tooltip: {
callbacks: {
label: function (context) {
return `${context.parsed.y} seconds`;
},
},
},
},
},
});
}
// Update connector filters
function updateConnectorFilters() {
const filterSelect = document.getElementById("connectorFilter");
filterSelect.innerHTML = '';
// Only show connectors with tests
Object.entries(dashboardData.connectors)
.filter(([, data]) => data.totalTests > 0)
.forEach(([connector]) => {
const option = document.createElement("option");
option.value = connector;
option.textContent = toPascalCase(connector);
filterSelect.appendChild(option);
});
}
// Filter data based on selections
function filterData() {
const connectorFilter = document.getElementById("connectorFilter").value;
const statusFilter = document.getElementById("statusFilter").value;
// Update tables
updateConnectorTables(connectorFilter);
// Update failed tests
updateFailedTests(connectorFilter, statusFilter);
}
// Update connector tables
function updateConnectorTables(connectorFilter) {
const container = document.getElementById("connectorTables");
container.innerHTML = "";
Object.entries(dashboardData.connectors).forEach(([connector, data]) => {
if (connectorFilter && connector !== connectorFilter) {
return;
}
// Hide connectors with no tests executed
if (data.totalTests === 0) {
return;
}
const tableDiv = createConnectorTable(connector, data);
container.appendChild(tableDiv);
});
}
// Create connector table
function createConnectorTable(connector, data) {
const div = document.createElement("div");
div.className = "connector-table collapsed";
div.dataset.connector = connector;
const header = document.createElement("h3");
const totalCount = data.totalTests;
const passedCount = data.passed;
const failedCount = data.failed;
const skippedPendingCount = data.skipped + data.pending;
// Create summary text
const summaryParts = [];
summaryParts.push(`${totalCount} tests`);
if (passedCount > 0) summaryParts.push(`${passedCount} passed`);
if (failedCount > 0)
summaryParts.push(
`${failedCount} failed`
);
if (skippedPendingCount > 0)
summaryParts.push(`${skippedPendingCount} skipped`);
// Add execution time if available
if (data.executionTime) {
const timeInMinutes = (data.executionTime / 1000 / 60).toFixed(1);
summaryParts.push(`${timeInMinutes} min`);
}
// Create properly aligned header with padding
header.innerHTML = `
${toPascalCase(connector)}
(${summaryParts.join(", ")})
`;
// Add click handler to toggle collapse
header.addEventListener("click", (e) => {
if (!e.target.classList.contains("run-test-btn")) {
div.classList.toggle("collapsed");
}
});
div.appendChild(header);
const table = document.createElement("table");
table.innerHTML = `
Test File
Passed
Failed
Skipped/Pending
Total
No failed tests! 🎉
'; document.getElementById("failedTestsSection").style.display = filteredTests.length === 0 && !connectorFilter ? "none" : "block"; return; } document.getElementById("failedTestsSection").style.display = "block"; // Update failed count in header document.getElementById("failedCount").textContent = `(${filteredTests.length})`; filteredTests.forEach((test, index) => { const testDiv = createFailedTestElement(test, index); container.appendChild(testDiv); }); } // Open test runner modal window.openTestRunner = function (connector) { const modal = document.getElementById("testRunnerModal"); modal.style.display = "block"; // Populate connector dropdown const connectorSelect = document.getElementById("testConnector"); connectorSelect.innerHTML = ""; if (connector) { const option = document.createElement("option"); option.value = connector; option.textContent = toPascalCase(connector); connectorSelect.appendChild(option); } else { Object.keys(dashboardData.connectors) .filter((conn) => dashboardData.connectors[conn].totalTests > 0) .forEach((conn) => { const option = document.createElement("option"); option.value = conn; option.textContent = toPascalCase(conn); connectorSelect.appendChild(option); }); } updateTestFiles(); }; // Update test files dropdown function updateTestFiles() { const connector = document.getElementById("testConnector").value; const fileSelect = document.getElementById("testFile"); fileSelect.innerHTML = ''; if (connector && dashboardData.connectors[connector]) { Object.keys(dashboardData.connectors[connector].testsByFile).forEach( (file) => { const option = document.createElement("option"); option.value = file; option.textContent = file.split("/").pop(); fileSelect.appendChild(option); } ); } updateTestCases(); } // Update test cases dropdown function updateTestCases() { const connector = document.getElementById("testConnector").value; const file = document.getElementById("testFile").value; const caseSelect = document.getElementById("testCase"); caseSelect.innerHTML = ''; if (connector && file && dashboardData.connectors[connector]) { const tests = dashboardData.connectors[connector].testsByFile[file].tests; tests.forEach((test) => { const option = document.createElement("option"); option.value = test.title; option.textContent = test.title; caseSelect.appendChild(option); }); } } // Run individual test async function runIndividualTest() { const connector = document.getElementById("testConnector").value; const file = document.getElementById("testFile").value; const testCase = document.getElementById("testCase").value; const output = document.getElementById("testOutput"); if (!connector || !file) { alert("Please select a connector and test file"); return; } output.innerHTML = ' Running test...'; try { // Construct the command const fileName = file.split("/").pop(); const spec = testCase ? `--spec "**/spec/**/${fileName}" --grep "${testCase}"` : `--spec "**/spec/**/${fileName}"`; const command = `CYPRESS_CONNECTOR="${connector}" npm run cypress:ci -- ${spec}`; // For demo purposes, we'll show the command output.innerHTML = ` Command to run:${command}
To run this test, execute the above command in your terminal.
Note: Real-time test execution requires a backend service to execute commands and stream results.
`; } catch (error) { output.innerHTML = `