mirror of
				https://github.com/juspay/hyperswitch.git
				synced 2025-10-31 01:57:45 +08:00 
			
		
		
		
	 dc4eccb85c
			
		
	
	dc4eccb85c
	
	
	
		
			
			Co-authored-by: PiX <69745008+pixincreate@users.noreply.github.com> Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
		
			
				
	
	
		
			1007 lines
		
	
	
		
			28 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1007 lines
		
	
	
		
			28 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* 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 = '<option value="">All Connectors</option>';
 | |
| 
 | |
|   // 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(
 | |
|       `<span style="color: var(--error-color)">${failedCount} failed</span>`
 | |
|     );
 | |
|   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 = `
 | |
|         <span style="min-width: 150px; display: inline-block;">${toPascalCase(connector)}</span>
 | |
|         <span style="font-weight: normal; font-size: 16px; flex: 1;">(${summaryParts.join(", ")})</span>
 | |
|         <button class="btn btn-secondary run-test-btn" onclick="event.stopPropagation(); openTestRunner('${connector}')">
 | |
|             Run Tests
 | |
|         </button>
 | |
|     `;
 | |
| 
 | |
|   // 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 = `
 | |
|         <thead>
 | |
|             <tr>
 | |
|                 <th>Test File</th>
 | |
|                 <th>Passed</th>
 | |
|                 <th>Failed</th>
 | |
|                 <th>Skipped/Pending</th>
 | |
|                 <th>Total</th>
 | |
|             </tr>
 | |
|         </thead>
 | |
|         <tbody>
 | |
|             ${Object.entries(data.testsByFile)
 | |
|               .map(([file, fileData]) => {
 | |
|                 const fileName = file.split("/").pop();
 | |
|                 const skippedPending = fileData.skipped + fileData.pending;
 | |
|                 return `
 | |
|                         <tr>
 | |
|                             <td>${fileName}</td>
 | |
|                             <td><span class="status passed">${fileData.passed}</span></td>
 | |
|                             <td><span class="status failed">${fileData.failed}</span></td>
 | |
|                             <td><span class="status skipped">${skippedPending}</span></td>
 | |
|                             <td>${fileData.passed + fileData.failed + skippedPending}</td>
 | |
|                         </tr>
 | |
|                     `;
 | |
|               })
 | |
|               .join("")}
 | |
|         </tbody>
 | |
|         <tfoot>
 | |
|             <tr>
 | |
|                 <th>Total</th>
 | |
|                 <th><span class="status passed">${data.passed}</span></th>
 | |
|                 <th><span class="status failed">${data.failed}</span></th>
 | |
|                 <th><span class="status skipped">${data.skipped + data.pending}</span></th>
 | |
|                 <th>${data.totalTests}</th>
 | |
|             </tr>
 | |
|         </tfoot>
 | |
|     `;
 | |
| 
 | |
|   div.appendChild(table);
 | |
|   return div;
 | |
| }
 | |
| 
 | |
| // Create a failed test element
 | |
| function createFailedTestElement(test, index) {
 | |
|   const testDiv = document.createElement("div");
 | |
|   testDiv.className = "failed-test-item";
 | |
| 
 | |
|   // Create header
 | |
|   const header = document.createElement("h4");
 | |
|   header.textContent = `${index + 1}. ${test.fullTitle}`;
 | |
|   testDiv.appendChild(header);
 | |
| 
 | |
|   // Create test details
 | |
|   const details = createTestDetails(test);
 | |
|   testDiv.appendChild(details);
 | |
| 
 | |
|   // Add error message if exists
 | |
|   if (test.error) {
 | |
|     const errorDiv = createErrorMessage(test.error);
 | |
|     testDiv.appendChild(errorDiv);
 | |
|   }
 | |
| 
 | |
|   // Add media links
 | |
|   const mediaLinks = createMediaLinks(test);
 | |
|   testDiv.appendChild(mediaLinks);
 | |
| 
 | |
|   return testDiv;
 | |
| }
 | |
| 
 | |
| // Create test details section
 | |
| function createTestDetails(test) {
 | |
|   const detailsDiv = document.createElement("div");
 | |
|   detailsDiv.className = "failed-test-details";
 | |
| 
 | |
|   const details = [
 | |
|     { label: "Connector", value: toPascalCase(test.connector) },
 | |
|     { label: "File", value: test.file.split("/").pop() },
 | |
|     { label: "Duration", value: `${test.duration}ms` },
 | |
|   ];
 | |
| 
 | |
|   details.forEach(({ label, value }) => {
 | |
|     const detailDiv = document.createElement("div");
 | |
|     detailDiv.innerHTML = `<strong>${label}:</strong> <span>${value}</span>`;
 | |
|     detailsDiv.appendChild(detailDiv);
 | |
|   });
 | |
| 
 | |
|   return detailsDiv;
 | |
| }
 | |
| 
 | |
| // Create error message section
 | |
| function createErrorMessage(error) {
 | |
|   const errorDiv = document.createElement("div");
 | |
|   errorDiv.className = "error-message";
 | |
|   errorDiv.textContent = error.message || "No error message available";
 | |
|   return errorDiv;
 | |
| }
 | |
| 
 | |
| // Create media links section
 | |
| function createMediaLinks(test) {
 | |
|   const mediaDiv = document.createElement("div");
 | |
|   mediaDiv.className = "media-links";
 | |
| 
 | |
|   const links = [];
 | |
| 
 | |
|   if (test.screenshot) {
 | |
|     const screenshotLink = document.createElement("a");
 | |
|     screenshotLink.href = test.screenshot;
 | |
|     screenshotLink.target = "_blank";
 | |
|     screenshotLink.textContent = "📸 View Screenshot";
 | |
|     links.push(screenshotLink);
 | |
|   }
 | |
| 
 | |
|   if (test.video) {
 | |
|     const videoLink = document.createElement("a");
 | |
|     videoLink.href = test.video;
 | |
|     videoLink.target = "_blank";
 | |
|     videoLink.textContent = "🎥 View Video";
 | |
|     links.push(videoLink);
 | |
|   }
 | |
| 
 | |
|   if (links.length === 0) {
 | |
|     const noMediaSpan = document.createElement("span");
 | |
|     noMediaSpan.style.color = "var(--text-secondary)";
 | |
|     noMediaSpan.textContent = "No media available";
 | |
|     mediaDiv.appendChild(noMediaSpan);
 | |
|   } else {
 | |
|     links.forEach((link) => mediaDiv.appendChild(link));
 | |
|   }
 | |
| 
 | |
|   return mediaDiv;
 | |
| }
 | |
| 
 | |
| // Update failed tests section
 | |
| function updateFailedTests(connectorFilter, statusFilter) {
 | |
|   const container = document.getElementById("failedTestsList");
 | |
|   container.innerHTML = "";
 | |
| 
 | |
|   // Filter failed tests based on connector
 | |
|   let filteredTests = dashboardData.failedTests;
 | |
|   if (connectorFilter) {
 | |
|     filteredTests = filteredTests.filter(
 | |
|       (test) => test.connector === connectorFilter
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   // If status filter is set and not "failed", hide the failed tests section
 | |
|   if (statusFilter && statusFilter !== "failed") {
 | |
|     document.getElementById("failedTestsSection").style.display = "none";
 | |
|     return;
 | |
|   }
 | |
| 
 | |
|   if (filteredTests.length === 0) {
 | |
|     container.innerHTML = '<p class="text-center">No failed tests! 🎉</p>';
 | |
|     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 = '<option value="">Select a test file</option>';
 | |
| 
 | |
|   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 = '<option value="">All tests in file</option>';
 | |
| 
 | |
|   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 = '<div class="loading"></div> 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 = `
 | |
|             <strong>Command to run:</strong>
 | |
|             <pre>${command}</pre>
 | |
| 
 | |
|             <p>To run this test, execute the above command in your terminal.</p>
 | |
| 
 | |
|             <p><em>Note: Real-time test execution requires a backend service to execute commands and stream results.</em></p>
 | |
|         `;
 | |
|   } catch (error) {
 | |
|     output.innerHTML = `<div style="color: red;">Error: ${error.message}</div>`;
 | |
|   }
 | |
| }
 | |
| 
 | |
| // Show error message
 | |
| function showError(message) {
 | |
|   const container = document.querySelector(".container");
 | |
|   const errorDiv = document.createElement("div");
 | |
|   errorDiv.className = "error-message";
 | |
|   errorDiv.style.position = "fixed";
 | |
|   errorDiv.style.top = "20px";
 | |
|   errorDiv.style.right = "20px";
 | |
|   errorDiv.style.zIndex = "1001";
 | |
|   errorDiv.textContent = message;
 | |
| 
 | |
|   container.appendChild(errorDiv);
 | |
| 
 | |
|   setTimeout(() => {
 | |
|     errorDiv.remove();
 | |
|   }, 5000);
 | |
| }
 | |
| 
 | |
| // Show success message
 | |
| function showSuccess(message) {
 | |
|   const container = document.querySelector(".container");
 | |
|   const successDiv = document.createElement("div");
 | |
|   successDiv.className = "success-message";
 | |
|   successDiv.style.position = "fixed";
 | |
|   successDiv.style.top = "20px";
 | |
|   successDiv.style.right = "20px";
 | |
|   successDiv.style.zIndex = "1001";
 | |
|   successDiv.style.backgroundColor = "#04c38d";
 | |
|   successDiv.style.color = "white";
 | |
|   successDiv.style.padding = "12px 20px";
 | |
|   successDiv.style.borderRadius = "4px";
 | |
|   successDiv.style.boxShadow = "0 2px 10px rgba(0,0,0,0.1)";
 | |
|   successDiv.textContent = message;
 | |
| 
 | |
|   container.appendChild(successDiv);
 | |
| 
 | |
|   setTimeout(() => {
 | |
|     successDiv.remove();
 | |
|   }, 5000);
 | |
| }
 | |
| 
 | |
| // Load theme from localStorage
 | |
| function loadTheme() {
 | |
|   const savedTheme = localStorage.getItem("dashboardTheme");
 | |
|   if (savedTheme === "dark") {
 | |
|     document.body.classList.add("dark-theme");
 | |
|   }
 | |
| }
 | |
| 
 | |
| // Toggle theme
 | |
| function toggleTheme() {
 | |
|   document.body.classList.toggle("dark-theme");
 | |
|   const isDark = document.body.classList.contains("dark-theme");
 | |
|   localStorage.setItem("dashboardTheme", isDark ? "dark" : "light");
 | |
| 
 | |
|   // Update charts if they exist
 | |
|   if (connectorChart || distributionChart || avgDurationChart) {
 | |
|     updateCharts();
 | |
|   }
 | |
| }
 | |
| 
 | |
| // Setup collapsible failed tests section
 | |
| function setupFailedTestsCollapsible() {
 | |
|   const failedSection = document.getElementById("failedTestsSection");
 | |
|   const header = failedSection.querySelector("h2");
 | |
| 
 | |
|   header.addEventListener("click", () => {
 | |
|     failedSection.classList.toggle("collapsed");
 | |
|   });
 | |
| }
 |