super-pancake-automation
Version:
A lightweight DOM-based UI automation framework using Chrome DevTools Protocol
2,435 lines • 89.2 kB
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🥞 Super Pancake Test Runner</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
/* Light Theme */
--primary-color: #7c3aed;
--primary-light: #8b5cf6;
--primary-dark: #6d28d9;
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--success-color: #10b981;
--success-light: #34d399;
--warning-color: #f59e0b;
--warning-light: #fbbf24;
--danger-color: #ef4444;
--danger-light: #f87171;
--info-color: #3b82f6;
--info-light: #60a5fa;
/* Text Colors */
--text-primary: #1f2937;
--text-secondary: #6b7280;
--text-muted: #9ca3af;
--text-inverse: #ffffff;
/* Background Colors */
--bg-primary: #f8fafc;
--bg-secondary: #ffffff;
--bg-tertiary: #f1f5f9;
--bg-dark: #0f172a;
--bg-sidebar: #ffffff;
--bg-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--bg-glass: rgba(255, 255, 255, 0.95);
/* Border & Effects */
--border-color: #e2e8f0;
--border-light: #f1f5f9;
--border-radius: 12px;
--border-radius-lg: 20px;
/* Shadows */
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--shadow-lg: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
--shadow-xl: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
--glow: 0 0 20px rgba(139, 92, 246, 0.3);
/* Animations */
--transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
--transition-slow: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
[data-theme="dark"] {
--text-primary: #f8fafc;
--text-secondary: #cbd5e1;
--text-muted: #64748b;
--bg-primary: #0f172a;
--bg-secondary: #1e293b;
--bg-tertiary: #334155;
--bg-sidebar: #1e293b;
--bg-glass: rgba(30, 41, 59, 0.95);
--border-color: #334155;
--border-light: #475569;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg-gradient);
color: var(--text-primary);
line-height: 1.6;
min-height: 100vh;
transition: var(--transition);
overflow-x: hidden;
}
.app-container {
display: flex;
height: 100vh;
gap: 24px;
padding: 24px;
max-width: 1800px;
margin: 0 auto;
position: relative;
}
.app-container::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: radial-gradient(circle at 25% 25%, rgba(139, 92, 246, 0.1) 0%, transparent 50%),
radial-gradient(circle at 75% 75%, rgba(59, 130, 246, 0.1) 0%, transparent 50%);
pointer-events: none;
z-index: -1;
}
.card {
background: var(--bg-glass);
backdrop-filter: blur(20px);
border: 1px solid var(--border-color);
border-radius: var(--border-radius-lg);
box-shadow: var(--shadow-xl);
overflow: hidden;
transition: var(--transition-slow);
position: relative;
}
.card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.8), transparent);
z-index: 1;
}
.card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-xl), var(--glow);
}
/* Sidebar Styles */
.sidebar {
width: 20%;
min-width: 320px;
max-width: 50%;
display: flex;
flex-direction: column;
height: 100%;
position: relative;
transition: var(--transition-slow);
overflow: hidden;
}
.sidebar-resizer {
position: absolute;
top: 0;
right: -12px;
width: 24px;
height: 100%;
cursor: col-resize;
z-index: 5;
background: transparent;
transition: var(--transition);
pointer-events: auto;
}
.sidebar-resizer:hover {
background: linear-gradient(90deg, transparent, rgba(124, 58, 237, 0.3), transparent);
}
.sidebar-resizer::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 3px;
height: 40px;
background: var(--border-color);
border-radius: 2px;
opacity: 0;
transition: var(--transition);
}
.sidebar-resizer:hover::after {
opacity: 1;
}
.app-container.resizing {
user-select: none;
}
.app-container.resizing .sidebar-resizer {
background: linear-gradient(90deg, transparent, rgba(124, 58, 237, 0.5), transparent);
}
.app-container.resizing .sidebar-resizer::after {
opacity: 1;
}
.sidebar-header {
background: var(--primary-gradient);
color: var(--text-inverse);
padding: 20px 16px;
position: relative;
overflow: hidden;
z-index: 2;
}
.sidebar-header::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(255,255,255,0.15) 0%, transparent 70%);
animation: float 8s ease-in-out infinite;
}
.sidebar-header::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
}
@keyframes float {
0%, 100% { transform: translateY(0px) rotate(0deg); }
50% { transform: translateY(-12px) rotate(2deg); }
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
@keyframes slideInDown {
0% { opacity: 0; transform: translateY(-20px); }
100% { opacity: 1; transform: translateY(0); }
}
@keyframes slideInUp {
0% { opacity: 0; transform: translateY(20px); }
100% { opacity: 1; transform: translateY(0); }
}
@keyframes fadeIn {
0% { opacity: 0; }
100% { opacity: 1; }
}
.header-content {
position: relative;
z-index: 1;
animation: slideInDown 0.6s ease-out;
}
.header-top {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 16px;
flex-wrap: nowrap;
}
.icon {
font-size: 2.2rem;
text-shadow: 0 4px 8px rgba(0,0,0,0.3);
animation: pulse 4s ease-in-out infinite;
filter: drop-shadow(0 0 10px rgba(255,255,255,0.5));
flex-shrink: 0;
}
/* Pancake running animation */
.icon.running {
animation: pancakeFlip 1.5s ease-in-out infinite;
}
@keyframes pancakeFlip {
0%, 100% { transform: rotateY(0deg) scale(1); }
25% { transform: rotateY(90deg) scale(1.1); }
50% { transform: rotateY(180deg) scale(1.2); }
75% { transform: rotateY(270deg) scale(1.1); }
}
.sidebar-header h1 {
font-size: 1.1rem;
font-weight: 800;
text-shadow: 0 2px 4px rgba(0,0,0,0.2);
letter-spacing: -0.025em;
background: linear-gradient(135deg, #ffffff 0%, #f1f5f9 100%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
flex: 1;
}
/* Responsive title sizing */
@media (max-width: 400px) {
.sidebar-header h1 {
font-size: 0.95rem;
}
.icon {
font-size: 1.8rem;
}
}
/* Sidebar at minimum width */
.sidebar[style*="width: 15%"] h1,
.sidebar[style*="width: 16%"] h1,
.sidebar[style*="width: 17%"] h1,
.sidebar[style*="width: 18%"] h1,
.sidebar[style*="width: 19%"] h1,
.sidebar[style*="width: 20%"] h1 {
font-size: 0.95rem;
}
.sidebar[style*="width: 15%"] .icon,
.sidebar[style*="width: 16%"] .icon,
.sidebar[style*="width: 17%"] .icon,
.sidebar[style*="width: 18%"] .icon,
.sidebar[style*="width: 19%"] .icon,
.sidebar[style*="width: 20%"] .icon {
font-size: 1.8rem;
}
.header-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 4px;
animation: slideInUp 0.6s ease-out 0.2s both;
}
.header-stat-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 6px 4px;
border-radius: var(--border-radius);
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.3);
transition: var(--transition);
position: relative;
overflow: hidden;
min-width: 0;
}
.header-stat-item::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
transition: left 0.5s ease;
}
.header-stat-item:hover::before {
left: 100%;
}
.header-stat-item:hover {
transform: translateY(-2px);
background: rgba(255, 255, 255, 0.25);
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
}
.header-stat-number {
font-size: 0.9rem;
font-weight: 700;
line-height: 1;
margin-bottom: 4px;
transition: var(--transition);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.header-stat-label {
font-size: 0.6rem;
opacity: 0.95;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.header-stat-dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--danger-color);
margin-bottom: 8px;
box-shadow: 0 0 0 3px rgba(255,255,255,0.4), 0 0 20px rgba(239, 68, 68, 0.5);
animation: pulse 2s ease-in-out infinite;
}
.status.connected .header-stat-dot {
background: var(--success-color);
box-shadow: 0 0 0 3px rgba(255,255,255,0.4), 0 0 20px rgba(16, 185, 129, 0.6);
}
.controls {
padding: 12px 20px;
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(10px);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
gap: 12px;
animation: slideInUp 0.6s ease-out 0.4s both;
justify-content: center;
align-items: center;
flex-shrink: 0;
}
.btn {
padding: 10px 14px;
border: none;
border-radius: var(--border-radius);
font-weight: 600;
cursor: pointer;
transition: var(--transition);
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
font-size: 12px;
text-decoration: none;
font-family: 'Inter', sans-serif;
position: relative;
overflow: hidden;
text-transform: uppercase;
letter-spacing: 0.5px;
white-space: nowrap;
min-width: 0;
}
.btn i {
flex-shrink: 0;
}
.btn::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
transition: left 0.4s ease;
}
.btn:hover::before {
left: 100%;
}
.btn-primary {
background: var(--primary-gradient);
color: var(--text-inverse);
min-width: 100px;
box-shadow: var(--shadow-lg), 0 0 20px rgba(124, 58, 237, 0.3);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.btn-primary:hover:not(:disabled) {
transform: translateY(-2px) scale(1.02);
box-shadow: var(--shadow-xl), 0 0 30px rgba(124, 58, 237, 0.4);
}
.btn-primary:active:not(:disabled) {
transform: translateY(0) scale(1);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
box-shadow: var(--shadow);
}
.btn-primary.loading {
background: linear-gradient(45deg, var(--primary-color), var(--primary-light), var(--primary-color));
background-size: 200% 200%;
animation: shimmer 1.5s ease-in-out infinite;
}
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
.btn-secondary {
background: rgba(255, 255, 255, 0.1);
color: var(--text-primary);
border: 1px solid var(--border-color);
backdrop-filter: blur(10px);
}
.btn-secondary:hover {
background: var(--primary-gradient);
color: var(--text-inverse);
border-color: transparent;
transform: translateY(-1px);
box-shadow: var(--shadow-lg);
}
.btn i {
font-size: 16px;
transition: var(--transition);
}
.btn:hover i {
transform: scale(1.1);
}
.btn-icon {
min-width: 60px;
width: 60px;
height: 50px;
padding: 6px 4px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: var(--border-radius);
gap: 4px;
}
.btn-icon i {
font-size: 16px;
margin: 0;
flex-shrink: 0;
}
.btn-text-small {
font-size: 9px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
line-height: 1;
white-space: nowrap;
opacity: 0.9;
}
.btn-icon:hover {
transform: translateY(-2px) scale(1.05);
}
.btn-icon:hover .btn-text-small {
opacity: 1;
}
/* Search and Filter Section */
.search-section {
padding: 16px 20px;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
animation: slideInUp 0.6s ease-out 0.6s both;
flex-shrink: 0;
}
.test-files-wrapper {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
min-height: 0;
}
.test-files {
overflow-y: auto;
flex: 1;
min-height: 0;
max-height: 100%;
padding: 0; /* Remove any default padding */
}
.search-container {
position: relative;
margin-bottom: 16px;
}
.search-input {
width: 100%;
padding: 12px 16px 12px 44px;
border: 2px solid var(--border-color);
border-radius: var(--border-radius);
font-size: 14px;
font-family: 'Inter', sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
transition: var(--transition);
outline: none;
}
.search-input:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.1);
background: var(--bg-secondary);
}
.search-icon {
position: absolute;
left: 16px;
top: 50%;
transform: translateY(-50%);
color: var(--text-muted);
font-size: 14px;
transition: var(--transition);
}
.search-input:focus + .search-icon {
color: var(--primary-color);
}
.search-clear {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: var(--text-muted);
cursor: pointer;
padding: 4px;
border-radius: 50%;
transition: var(--transition);
opacity: 0;
visibility: hidden;
}
.search-input:not(:placeholder-shown) + .search-icon + .search-clear {
opacity: 1;
visibility: visible;
}
.search-clear:hover {
background: var(--danger-color);
color: white;
}
.filter-tabs {
display: flex;
gap: 8px;
}
.filter-tab {
padding: 8px 16px;
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 20px;
font-size: 12px;
font-weight: 600;
color: var(--text-secondary);
cursor: pointer;
transition: var(--transition);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.filter-tab:hover {
background: var(--primary-color);
color: white;
border-color: var(--primary-color);
transform: translateY(-1px);
}
.filter-tab.active {
background: var(--primary-gradient);
color: white;
border-color: transparent;
box-shadow: var(--shadow);
}
.select-all-button {
padding: 8px 16px;
background: var(--success-color);
border: 1px solid var(--success-color);
border-radius: 20px;
font-size: 12px;
font-weight: 600;
color: white;
cursor: pointer;
transition: var(--transition);
text-transform: uppercase;
letter-spacing: 0.5px;
display: flex;
align-items: center;
gap: 6px;
margin-right: 8px;
}
.select-all-button:hover {
background: var(--success-light);
border-color: var(--success-light);
transform: translateY(-1px);
box-shadow: var(--shadow);
}
.select-all-button.deselect-mode {
background: var(--warning-color);
border-color: var(--warning-color);
}
.select-all-button.deselect-mode:hover {
background: var(--warning-light);
border-color: var(--warning-light);
}
.select-all-button i {
font-size: 10px;
}
.test-files {
flex: 1;
overflow-y: auto;
background: var(--bg-secondary);
position: relative;
max-height: 100%;
}
#test-form {
height: 100%;
overflow-y: auto;
display: flex;
flex-direction: column;
margin: 0;
padding: 0;
}
#test-files-container {
flex: 1;
overflow-y: auto;
min-height: 0;
max-height: 100%;
padding: 0;
margin: 0;
}
.test-files::-webkit-scrollbar {
width: 8px;
}
.test-files::-webkit-scrollbar-track {
background: var(--bg-tertiary);
border-radius: 4px;
}
.test-files::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 4px;
transition: var(--transition);
}
.test-files::-webkit-scrollbar-thumb:hover {
background: var(--primary-color);
}
.test-file {
border-bottom: 1px solid var(--border-light);
}
.test-file:last-child {
border-bottom: none;
}
.test-file-header {
padding: 12px 16px;
cursor: pointer;
display: flex;
align-items: center;
gap: 12px;
transition: all 0.2s ease;
background: var(--bg-secondary);
}
.test-file-header:hover {
background: var(--bg-primary);
}
.test-file.expanded .test-file-header {
background: var(--bg-primary);
border-bottom: 1px solid var(--border-color);
}
.test-file-header-left {
flex: 1;
min-width: 0;
}
.test-file-title {
display: flex;
align-items: center;
gap: 10px;
font-weight: 600;
color: var(--text-primary);
}
.file-select-all {
display: flex;
align-items: center;
gap: 10px;
margin-right: auto;
padding-right: 16px;
pointer-events: auto;
z-index: 1000;
}
.file-select-all-checkbox {
width: 18px;
height: 18px;
border-radius: 4px;
border: 2px solid var(--border-color);
background: var(--bg-secondary);
cursor: pointer;
transition: var(--transition);
appearance: none;
position: relative;
flex-shrink: 0;
z-index: 999;
pointer-events: auto;
outline: none;
}
.file-select-all-checkbox:checked {
background: var(--primary-color);
border-color: var(--primary-color);
box-shadow: 0 0 10px rgba(124, 58, 237, 0.5);
}
.file-select-all-checkbox:hover {
background: var(--primary-light);
border-color: var(--primary-light);
transform: scale(1.1);
}
.file-select-all-checkbox:checked::after {
content: '✓';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 11px;
font-weight: bold;
}
.file-select-all-checkbox:indeterminate {
background: var(--warning-color);
border-color: var(--warning-color);
}
.file-select-all-checkbox:indeterminate::after {
content: '−';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 13px;
font-weight: bold;
}
.file-select-all-label {
font-size: 12px;
color: var(--text-secondary);
cursor: pointer;
font-weight: 500;
}
.toggle-icon {
color: var(--text-muted);
transition: transform 0.2s ease;
cursor: pointer;
font-size: 14px;
}
.test-file.expanded .toggle-icon {
transform: rotate(180deg);
}
.test-cases {
display: none;
max-height: 300px;
overflow-y: auto;
background: var(--bg-secondary);
border-top: 1px solid var(--border-color);
}
.test-file.expanded .test-cases {
display: block;
}
.test-case-item {
padding: 10px 16px;
display: flex;
align-items: center;
gap: 12px;
transition: all 0.2s ease;
border-bottom: 1px solid var(--border-light);
}
.test-case-checkbox {
width: 20px;
height: 20px;
margin-right: 12px;
border-radius: 4px;
border: 2px solid var(--border-color);
background: var(--bg-secondary);
cursor: pointer;
transition: var(--transition);
appearance: none;
position: relative;
flex-shrink: 0;
z-index: 999;
pointer-events: auto;
outline: none;
}
.test-case-checkbox:checked {
background: var(--primary-color);
border-color: var(--primary-color);
}
.test-case-checkbox:checked::after {
content: '✓';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 12px;
font-weight: bold;
}
.test-case-item:last-child {
border-bottom: none;
}
.test-case-item:hover {
background: rgba(22, 163, 74, 0.05);
}
.test-case-item.selected {
background: rgba(22, 163, 74, 0.1);
border-left: 3px solid var(--primary-color);
}
.test-case-checkbox {
margin: 0;
width: 16px;
height: 16px;
accent-color: var(--primary-color);
}
.test-case-label {
flex: 1;
cursor: pointer;
font-size: 14px;
color: var(--text-primary);
line-height: 1.4;
}
/* Main Content Styles */
.main-content {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
animation: slideInUp 0.6s ease-out 0.8s both;
}
.console-panel {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
}
.console-header {
background: var(--primary-gradient);
color: var(--text-inverse);
padding: 28px 32px;
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
overflow: hidden;
}
.console-header::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
animation: float 10s ease-in-out infinite;
}
.console-header h3 {
display: flex;
align-items: center;
gap: 12px;
font-size: 1.5rem;
font-weight: 700;
position: relative;
z-index: 1;
text-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
.console-header h3 i {
font-size: 1.75rem;
filter: drop-shadow(0 0 8px rgba(255,255,255,0.3));
}
.console-controls {
display: flex;
align-items: center;
gap: 12px;
position: relative;
z-index: 1;
}
.console-btn {
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(10px);
color: white;
border: 1px solid rgba(255, 255, 255, 0.2);
padding: 10px 16px;
border-radius: var(--border-radius);
cursor: pointer;
transition: var(--transition);
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
font-weight: 600;
font-family: 'Inter', sans-serif;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.console-btn:hover {
background: rgba(255, 255, 255, 0.25);
transform: translateY(-2px);
box-shadow: 0 8px 32px rgba(0,0,0,0.2);
}
.console-btn.btn-primary {
background: var(--primary-gradient);
color: var(--text-inverse);
border-color: var(--primary-color);
}
.console-btn.btn-primary:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: var(--glow), 0 8px 32px rgba(0,0,0,0.2);
}
.console-btn.btn-danger {
background: linear-gradient(135deg, var(--danger-color), var(--danger-light));
color: var(--text-inverse);
border-color: var(--danger-color);
}
.console-btn.btn-danger:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(239, 68, 68, 0.3), 0 8px 32px rgba(0,0,0,0.2);
}
.console-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none !important;
}
.console-btn i {
font-size: 14px;
}
.status-bar {
padding: 16px 32px;
border-bottom: 1px solid var(--border-color);
display: flex;
align-items: center;
justify-content: space-between;
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(10px);
position: relative;
}
.status-bar::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(124, 58, 237, 0.3), transparent);
}
.status-indicator {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 500;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--danger-color);
}
.status-indicator.status-connected .status-dot {
background: var(--success-color);
box-shadow: 0 0 6px rgba(16, 185, 129, 0.4);
}
.log-container {
flex: 1;
padding: 24px 32px;
overflow-y: auto;
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
color: #e2e8f0;
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
font-size: 13px;
line-height: 1.8;
position: relative;
}
.log-container::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: radial-gradient(circle at 80% 20%, rgba(124, 58, 237, 0.1) 0%, transparent 50%);
pointer-events: none;
}
.log-container::-webkit-scrollbar {
width: 12px;
}
.log-container::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.2);
border-radius: 6px;
}
.log-container::-webkit-scrollbar-thumb {
background: linear-gradient(135deg, #7c3aed, #8b5cf6);
border-radius: 6px;
border: 2px solid transparent;
background-clip: content-box;
}
.log-container::-webkit-scrollbar-thumb:hover {
background: linear-gradient(135deg, #8b5cf6, #a78bfa);
background-clip: content-box;
}
.log-info {
color: #38bdf8;
margin-bottom: 6px;
}
.log-success {
color: #4ade80;
margin-bottom: 6px;
}
.log-warning {
color: #facc15;
margin-bottom: 6px;
}
.log-fail {
color: #f87171;
margin-bottom: 6px;
}
.log-error {
color: #ef4444;
margin-bottom: 6px;
}
/* Loading animation */
.loading::after {
content: '';
display: inline-block;
width: 0;
animation: loading 1.5s infinite;
}
@keyframes loading {
0% { content: ''; }
25% { content: '.'; }
50% { content: '..'; }
75% { content: '...'; }
100% { content: ''; }
}
/* Scrollbar styling */
.test-files::-webkit-scrollbar,
.test-cases::-webkit-scrollbar,
.log-container::-webkit-scrollbar {
width: 6px;
}
.test-files::-webkit-scrollbar-track,
.test-cases::-webkit-scrollbar-track {
background: var(--bg-primary);
}
.test-files::-webkit-scrollbar-thumb,
.test-cases::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 3px;
}
.log-container::-webkit-scrollbar-track {
background: #1e293b;
}
.log-container::-webkit-scrollbar-thumb {
background: #475569;
border-radius: 3px;
}
/* Responsive design */
@media (max-width: 1200px) {
.sidebar {
width: 25%;
min-width: 300px;
}
}
@media (max-width: 768px) {
.app-container {
flex-direction: column;
height: auto;
padding: 10px;
gap: 10px;
}
.sidebar {
width: 100%;
max-height: 50vh;
}
.main-content {
height: 50vh;
}
}
</style>
</head>
<body>
<div class="app-container">
<!-- Left Sidebar - Test Selection -->
<div class="sidebar card">
<div class="sidebar-resizer"></div>
<div class="sidebar-header">
<div class="header-content">
<div class="header-top">
<div class="icon">🥞</div>
<h1>Super Pancake Test Runner</h1>
</div>
<div class="header-stats">
<div class="header-stat-item selected" id="selected-stat">
<span class="header-stat-number" id="selected-count-number">0</span>
<span class="header-stat-label">Selected</span>
</div>
<div class="header-stat-item files" id="files-stat">
<span class="header-stat-number" id="files-count">0</span>
<span class="header-stat-label">Files</span>
</div>
<div class="header-stat-item status disconnected" id="status-stat">
<span class="header-stat-dot"></span>
<span class="header-stat-number" id="status-text-main">Disconnected</span>
</div>
</div>
</div>
</div>
<div class="controls">
<button class="btn btn-icon" onclick="location.reload()" title="Refresh test files">
<i class="fas fa-sync-alt"></i>
<span class="btn-text-small">Refresh</span>
</button>
<button class="btn btn-icon" onclick="openTestReport()" title="Open test report">
<i class="fas fa-file-alt"></i>
<span class="btn-text-small">Report</span>
</button>
<button class="btn btn-icon" onclick="toggleHeadlessMode()" title="Toggle headless/headed mode" id="headless-btn">
<i class="fas fa-eye" id="headless-icon"></i>
<span class="btn-text-small" id="headless-text">Headed</span>
</button>
<button class="btn btn-icon" onclick="toggleBrowserType()" title="Toggle browser (Chrome/Firefox)" id="browser-btn">
<i class="fab fa-chrome" id="browser-icon"></i>
<span class="btn-text-small" id="browser-text">Chrome</span>
</button>
<button class="btn btn-icon" onclick="toggleTheme()" title="Toggle dark/light mode">
<i class="fas fa-moon" id="theme-icon"></i>
<span class="btn-text-small">Theme</span>
</button>
</div>
<!-- Search and Filter Section -->
<div class="search-section">
<div class="search-container">
<i class="fas fa-search search-icon"></i>
<input type="text" class="search-input" placeholder="Search test files..." id="search-input">
<button class="search-clear" onclick="clearSearch()" title="Clear search">
<i class="fas fa-times"></i>
</button>
</div>
<div class="filter-tabs">
<button class="select-all-button" id="global-select-all" onclick="toggleAllTests()" title="Select/Deselect all tests">
<i class="fas fa-check-double"></i>
<span>Select All</span>
</button>
<button class="filter-tab active" data-filter="all">All</button>
<button class="filter-tab" data-filter="unit">Unit</button>
<button class="filter-tab" data-filter="integration">Integration</button>
<button class="filter-tab" data-filter="e2e">E2E</button>
</div>
</div>
<div class="test-files-wrapper">
<form id="test-form">
<div class="test-files" id="test-files-container">
<div style="padding: 60px 20px; text-align: center; color: var(--text-muted);">
<i class="fas fa-spinner fa-spin" style="font-size: 2rem; margin-bottom: 16px; color: var(--primary-color);"></i>
<div style="font-weight: 600;">Loading test files...</div>
<div style="font-size: 12px; margin-top: 8px;">Please wait while we discover your tests</div>
</div>
</div>
</form>
</div>
</div>
<!-- Right Side - Console -->
<div class="main-content">
<div class="console-panel card">
<div class="console-header">
<h3>
<i class="fas fa-terminal"></i>
Test Console & Live Reports
</h3>
<div class="console-controls">
<button class="console-btn" onclick="clearLogs()">
<i class="fas fa-trash-alt"></i>
Clear
</button>
<button class="console-btn" onclick="copyLogs()">
<i class="fas fa-copy"></i>
Copy
</button>
<button type="submit" class="console-btn btn-primary" id="run-btn" disabled>
<i class="fas fa-play"></i>
Run Tests
</button>
<button class="console-btn btn-success" id="live-btn" onclick="toggleLiveMode()" title="Toggle live monitoring mode">
<i class="fas fa-video"></i>
<span id="live-btn-text">Live Mode</span>
</button>
<button class="console-btn btn-danger" id="stop-btn" onclick="stopTests()" disabled>
<i class="fas fa-stop"></i>
Stop Tests
</button>
</div>
</div>
<div class="status-bar">
<div class="status-indicator status-error" id="status-indicator">
<div class="status-dot"></div>
<span id="status-text">Disconnected</span>
</div>
<div class="status-indicator">
<i class="fas fa-clock"></i>
<span id="execution-time">00:00</span>
</div>
</div>
<div class="log-container" id="log">
<div class="log-info">🎯 Welcome to the Super Pancake Test Runner!</div>
<div class="log-info">✨ Select tests from the left panel and click "Run Tests" to begin</div>
<div class="log-info">📊 Real-time results and reports will appear here</div>
<div class="log-info">🔌 Connecting to test server...</div>
</div>
</div>
</div>
</div>
<script>
// WebSocket connection
let ws;
let liveWs; // Separate WebSocket for live monitoring
let isConnected = false;
let isLiveModeEnabled = false;
let selectedTests = new Set();
let executionStartTime = null;
let executionTimer = null;
let reconnectAttempts = 0;
const maxReconnectAttempts = 5;
// Initialize connection with better retry logic
function connectWebSocket() {
if (reconnectAttempts >= maxReconnectAttempts) {
logMessage('❌ Max reconnection attempts reached. Please refresh the page.', 'error');
return;
}
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}`;
try {
// Close existing connection if any
if (ws && ws.readyState !== WebSocket.CLOSED) {
ws.close();
}
ws = new WebSocket(wsUrl);
// Set a connection timeout
const connectionTimeout = setTimeout(() => {
if (ws.readyState === WebSocket.CONNECTING) {
ws.close();
logMessage('⚠️ Connection timeout, retrying...', 'warning');
reconnectAttempts++;
setTimeout(connectWebSocket, 2000);
}
}, 10000); // 10 second timeout
ws.onopen = function() {
clearTimeout(connectionTimeout);
isConnected = true;
reconnectAttempts = 0;
updateConnectionStatus();
logMessage('🔌 Connected to test runner successfully', 'success');
};
ws.onmessage = function(event) {
try {
const message = event.data.toString();
logMessage(message, 'info');
// Check for test completion
if (message.includes('All tests finished') ||
message.includes('HTML report generated') ||
message.includes('🎉 All tests completed successfully')) {
stopTestExecution();
logMessage('✅ Test execution completed', 'success');
}
} catch (error) {
console.error('Error processing WebSocket message:', error);
}
};
ws.onclose = function(event) {
clearTimeout(connectionTimeout);
isConnected = false;
updateConnectionStatus();
// Only reconnect if it wasn't a clean close and we haven't exceeded attempts
if (event.code !== 1000 && reconnectAttempts < maxReconnectAttempts) {
reconnectAttempts++;
const delay = Math.min(2000 * reconnectAttempts, 10000); // Max 10s delay
logMessage(`❌ Connection lost. Attempting to reconnect... (${reconnectAttempts}/${maxReconnectAttempts})`, 'warning');
setTimeout(connectWebSocket, delay);
} else if (reconnectAttempts >= maxReconnectAttempts) {
logMessage('❌ Max reconnection attempts reached. Please refresh the page.', 'error');
}
};
ws.onerror = function(error) {
clearTimeout(connectionTimeout);
logMessage('⚠️ WebSocket connection error', 'warning');
console.error('WebSocket error:', error);
};
} catch (error) {
logMessage('❌ Failed to create WebSocket connection', 'error');
console.error('WebSocket creation error:', error);
reconnectAttempts++;
if (reconnectAttempts < maxReconnectAttempts) {
setTimeout(connectWebSocket, 3000);
}
}
}
function updateConnectionStatus() {
const statusEl = document.getElementById('status-indicator');
const statusTextEl = document.getElementById('status-text');
const statusStatEl = document.getElementById('status-stat');
const statusTextMainEl = document.getElementById('status-text-main');
if (isConnected) {
statusEl.className = 'status-indicator status-connected';
statusTextEl.textContent = 'Connected';
statusStatEl.className = 'header-stat-item status connected';
statusTextMainEl.textContent = 'Connected';
} else {
statusEl.className = 'status-indicator status-error';
statusTextEl.textContent = 'Disconnected';
statusStatEl.className = 'header-stat-item status disconnected';
statusTextMainEl.textContent = 'Disconnected';
}
}
function logMessage(message, type = 'info') {
const logContainer = document.getElementById('log');
const logDiv = document.createElement('div');
logDiv.className = `log-${type}`;
logDiv.textContent = message;
logContainer.appendChild(logDiv);
logContainer.scrollTop = logContainer.scrollHeight;
}
function clearLogs() {
const logContainer = document.getElementById('log');
logContainer.innerHTML = `
<div class="log-info">🎯 Welcome to the Super Pancake Test Runner!</div>
<div class="log-info">✨ Select tests from the left panel and click "Run Tests" to begin</div>
<div class="log-info">📊 Real-time results and reports will appear here</div>
`;
}
function copyLogs() {
const logContainer = document.getElementById('log');
const logs = Array.from(logContainer.children).map(div => div.textContent).join('\n');
navigator.clipboard.writeText(logs).then(() => {
logMessage('📋 Logs copied to clipboard!', 'success');
});
}
// Function to open the test report
function openTestReport() {
try {
// Try to open the report in a new tab
const reportUrl = '/automationTestReport.html';
const newWindow = window.open(reportUrl, '_blank');
if (newWindow) {
logMessage('📊 Opening test report in new tab...', 'success');
} else {
// If popup blocked, try to navigate
logMessage('⚠️ Popup blocked. Trying to navigate to report...', 'warning');
setTimeout(() => {
window.location.href = reportUrl;
}, 1000);
}
} catch (error) {
logMessage('❌ Failed to open test report: ' + error.message, 'error');
console.error('Error opening test report:', error);
}
}
// Test file management
async function loadTestFiles() {
try {
const response = await fetch('/api/test-files');
const testFiles = await response.json();
const container = document.getElementById('test-files-container');
container.innerHTML = '';
document.getElementById('files-count').textContent = testFiles.length;
for (const file of testFiles) {
await createTestFileElement(file, container);
}
// Update the select all button after files are loaded
setTimeout(() => {
updateSelectAllButton();
}, 100);
logMessage(`📁 Loaded ${testFiles.length} test files successfully`, 'success');
} catch (error) {
logMessage(`❌ Failed to load test files: ${error.message}`, 'error');
const container = document.getElementById('test-files-container');
container.innerHTML = `
<div style="padding: 40px 20px; text-align: center; color: var(--danger-color);">
<i class="fas fa-exclamation-triangle" style="font-size: 2rem; margin-bottom: 16px;"></i>
<div style="font-weight: 600;">Failed to load test files</div>
<div style="font-size: 12px; margin-top: 8px;">${error.message}</div>
</div>
`;
}
}
async function createTestFileElement(file, container) {
const groupId = file.replace(/[\/\.]/g, '_');
const fileDiv = document.createElement('div');
fileDiv.className = 'test-file';
fileDiv.id = `group-${groupId}`;
// Load test cases
let testCases = [];
try {
// Normalize file path for Windows compatibility
const normalizedFile = file.replace(/\\/g, '/');
const casesResponse = await fetch('/api/test-cases', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filePath: normalizedFile })
});
testCases = await casesResponse.json();
} catch (error) {
console.error(`Failed to load test cases for ${file}:`, error);
}
fileDiv.innerHTML = `
<div class="test-file-header">
<div class="file-select-all">
<input type="checkbox" class="file-select-all-checkbox" data-group="${groupId}" id="select-all-${groupId}">
<label for="select-all-${groupId}" class="file-select-all-label">All</label>
</div>
<div class="test-file-header-left">
<div class="test-file-title">
${file}
</div>
</div>
<i class="fas fa-chevron-down toggle-icon" data-group="${groupId}" id="toggle-${groupId}"></i>
</div>
<div class="test-cases" id="cases-${groupId}">
${testCases.map(testCase => `
<div class="test-case-item" data-test-id="${file}::${testCase}">
<input type="checkbox" name="tests" value="${file}::${testCase}" class="test-case-checkbox" id="${file}::${testCase}">
<label for="${file}::${testCase}" class="test-case-label">${testCase}</label>
</div>
`).join('')}
</div>
`;
container.appendChild(fileDiv);
}
function toggleFile(groupId) {
console.log('toggleFile called with groupId:', groupId);
// Escape backslashes in the group ID for CSS selector
const escapedGroupId = groupId.replace(/\\/g, '\\\\');
console.log('Escaped groupId for toggleFile:', escapedGroupId);
const fileElement = document.getElementById(`group-${escapedGroupId}`);
console.log('Found file element:', fileElement);
if (fileElement) {
fileElement.classList.toggle('expanded');
} else {
console.error('File element not found for groupId:', groupId);
}
}
function toggleGroup(groupId, checked) {
console.log('toggleGroup called with groupId:', groupId, 'checked:', checked);
// Escape backslashes in the group ID for CSS selector
const escapedGroupId = groupId.replace(/\\/g, '\\\\');
console.log('Escaped groupId:', escapedGroupId);
const selector = `#cases-${escapedGroupId} input[type="checkbox"]`;
console.log('Using selector:', selector);
const checkboxes = document.querySelectorAll(selector);
console.log('Found checkboxes:', checkboxes.length);
checkboxes.forEach(cb => {
cb.checked = checked;
const item = cb.closest('.test-case-item');
if (checked) {
item.classList.add('selected');
} else {
item.classList.remove('selected');
}
});
updateSelectedCount();
}
function updateSelectedCount() {
const checkboxes = document.querySelectorAll('.test-case-checkbox:checked');
const count = checkboxes.length;
document.getElementById('selected-count-number').textContent = count;
document.getElementById('run-btn').disabled = count === 0;
// Update visual selection
document.querySelectorAll('.test-case-checkbox').forEach(cb => {
const item = cb.closest('.test-case-item');
if (cb.checked) {
item.classList.add('selected');
} else {
item.classList.remove('selected');
}
});
// Update group checkboxes
document.querySelectorAll('.test-file').forEach(file => {
const groupCheckbox = file.querySelector('.file-select-all-checkbox');
const testCheckboxes = file.querySelectorAll('.test-case-checkbox');
const checkedTestBoxes = file.querySelectorAll('.test-case-checkbox:checked');
console.log('Updating group checkbox for file:', file.id, 'checked:', checkedTestBoxes.length, 'total:', testCheckboxes.length);
if (checkedTestBoxes.length === 0) {
groupCheckbox.checked = false;
groupCheckbox.indeterminate = false;
} else if (checkedTestBoxes.length === testCheckboxes.length) {
groupCheckbox.checked = true;
groupCheckbox.indeterminate = false;
} else {
groupCheckbox.checked = false;
groupCheckbox.indeterminate = true;
}
});
}
async function runTests() {
if (!isConnected) {
logMessage('⚠️ Not connected to test runner. Please wait for connection.', 'warning');
return;
}
const checkboxes = document.querySelectorAll('.test-case-checkbox:checked');
const tests = Array.from(checkboxes).map(cb => cb.value);
if (tests.length === 0) {
logMessage('⚠️ Please select at least one test to run.', 'warning');
return;
}
// Update button states
document.getElementById('run-btn').disabled = true;
document.getElementById('run-btn').classList.add('loading');
document.getElementById('stop-btn').disabled = false;
// Start pancake animation
const icon = document.querySelector('.icon');
icon.classList.add('running');
// Start execution timer
executionStartTime = Date.now();
executionTimer = setInterval(updateExecutionTime, 1000);
clearLogs();
logMessage(`🚀 Starting execution of ${tests.length} selected tests...`, 'info');
try {
const response = await fetch('/run', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
tests,
headless: isHeadlessMode,
browser: selectedBrowser
})
});
if (!response.ok) {
throw new Error(`Server responded with ${response.status}: ${response.statusText}`);
}
const modeText = isHeadlessMode ? 'headless' : 'headed';
const browserName = selectedBrowser === 'chrome' ? 'Chrome' : 'Firefox';
logMessage(`📤 Test execution request sent successfully (${browserName} ${modeText} mode)`, 'success');
} catch (error) {
logMessage(`❌ Failed to start test execution: ${error.message}`, 'error');
stopTestExecution();
}
}
async function stopTests() {
try {
// Stop via live WebSocket if in live mode
if (isLiveModeEnabled && liveWs && liveWs.readyState === WebSocket.OPEN) {
liveWs.send(JSON.stringify({
type: 'stop_tests'
}));
logMessage('🔴 [LIVE] Test stop requested', 'warning');
} else {
// Regular stop via HTTP
const response = await fetch('/stop', {
method: 'POST'
});
if (response.ok) {
logMessage('🛑 Test execution stop requested', 'warning');
} else {
logMessage('⚠️ Failed to stop tests - they may complete naturally', 'warning');
}
}
} catch (error) {
logMessage(`❌ Error stopping tests: ${error.message}`, 'error');
}
stopTestExecution();
}
function stopTestExecution() {
// Update button states
document.getElementById('run-btn').disabled = false;
document.getElementById('run-btn').classList.remove('loading');
document.getElementById('stop-btn').disabled = true;
// Stop pancake animation
const icon = document.querySelector('.icon');
icon.classList.remove('running');
// Stop execution timer
if (executionTimer) {
clearInterval(executionTimer);
executionTimer = null;
}
}
function updateExecutionTime() {
if (!executionStartTime) return;
const elapsed = Math.floor((Date.now() - executionStartTime) / 1000);
const minutes = Math.floor(elapsed / 60);
const seconds = elapsed % 60;
document.getElementById('execution-time').textContent =
`${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
}
// Live Mode Functionality
function toggleLiveMode() {
isLiveModeEnabled = !isLiveModeEnabled;
const liveBtn = document.getElementById('live-btn');
const liveBtnText = document.getElementById('live-btn-text');
const liveIcon = liveBtn.querySelector('i');
if (isLiveModeEnabled) {
// Enable live mode
liveBtn.style.background = 'linear-gradient(135deg, #10b981, #34d399)';
liveBtn.style.borderColor = '#10b981';
liveIcon.className = 'fas fa-video';
liveBtnText.textContent = 'Live ON';
liveBtn.title = 'Live monitoring enabled - Click to disable';
// Connect to live WebSocket server
connectLiveWebSocket();
logMessage('🔴 Live monitoring mode enabled - Real-time test streaming active', 'success');
localStorage.setItem('liveModeEnabled', 'true');
} else {
// Disable live mode
liveBtn.style.background = '';
liveBtn.style.borderColor = '';
liveIcon.className = 'fas fa-video';
liveBtnText.textContent = 'Live Mode';
liveBtn.title = 'Toggle live monitoring mode';
// Disconnect live WebSocket
if (liveWs) {
liveWs.close();
liveWs = null;
}
logMessage('⚪ Live monitoring mode disabled', 'info');
localStorage.setItem('liveModeEnabled', 'false');
}
}
// Connect to Live WebSocket Server for real-time monitoring
async function connectLiveWebSocket() {
try {
// Try to discover live server port
let livePort = 3002; // fallback
try {
const response = await fetch('/api/config');
const config = await response.json();
livePort = config.wsPort || 3002;
} catch (error) {
console.warn('Could not fetch live server config, using default port');
}
const liveWsUrl = `ws://${window.location.hostname}:${livePort}`;
liveWs = new WebSocket(liveWsUrl);
liveWs.onopen = () => {
logMessage('🔴 Connected to live monitoring server', 'success');
};
liveWs.onmessage = (event) => {
try {
const message = JSON.parse(event.data);
handleLiveMessage(message);
} catch (error) {
// Handle plain text messages from live server
const plainMessage = event.data.toString();
logMessage(`[LIVE] ${plainMessage}`, 'info');
}
};
liveWs.onclose = () => {
logMessage('🔴 Live monitoring disconnected', 'warning');
if (isLiveModeEnabled) {
// Try to reconnect after 3 seconds if live mode is still enabled
setTimeout(() => {
if (isLiveModeEnabled) {
connectLiveWebSocket();
}
}, 3000);
}
};
liveWs.onerror = (error) => {
console.error('Live WebSocket error:', error);
logMessage('⚠️ Live monitoring connection error', 'warning');
};
} catch (error) {
console.error('Failed to create live WebSocket:', error);
logMessage('❌ Failed to connect to live monitoring server', 'error');
}
}
// Handle live monitoring messages
function handleLiveMessage(message) {
switch (message.type) {
case 'test_started':
logMessage(`🔴 [LIVE] Test execution started - ${message.data.totalTests || 'unknown'} tests`, 'success');
break;
case 'console_output':
const outputData = message.data;
const outputClass = outputData.type === 'stderr' ? 'error' : 'info';
logMessage(`🔴 [LIVE] ${outputData.output}`, outputClass);
break;
case 'test_completed':
const status = message.data.exitCode === 0 ? 'passed' : 'failed';
const statusClass = status === 'passed' ? 'success' : 'error';
logMessage(`🔴 [LIVE] Tests completed - Exit code: ${message.data.exitCode}`, statusClass);
break;
case 'test_stopped':
logMessage('🔴 [LIVE] Test execution stopped', 'warning');
break;
case 'state_update':
// Update progress indicators based on live state
updateLiveProgress(message.data);
break;
default:
logMessage(`🔴 [LIVE] ${message.data?.output || JSON.stringify(message)}`, 'info');
break;
}
}
// Update progress indicators in live mode
function updateLiveProgress(liveState) {
if (!isLiveModeEnabled) return;
// Update execution time for live mode
if (liveState.startTime && liveState.status === 'running') {
const startTime = new Date(liveState.startTime).getTime();
const elapsed = Math.floor((Date.now() - startTime) / 1000);
const minutes = Math.floor(elapsed / 60);
const seconds = elapsed % 60;
document.getElementById('execution-time').textContent =
`${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
}
// Show current test in status
if (liveState.currentTest) {
const statusText = document.getElementById('status-text');
statusText.textContent = `Running: ${liveState.currentTest}`;
}
}
// Enhanced run tests function with live mode support
async function runTestsWithLiveMode() {
if (isLiveModeEnabled && liveWs && liveWs.readyState === WebSocket.OPEN) {
// Use live WebSocket to run tests
const checkboxes = document.querySelectorAll('.test-case-checkbox:checked');
const tests = Array.from(checkboxes).map(cb => cb.value);
if (tests.length === 0) {
logMessage('⚠️ Please select at least one test to run.', 'warning');
return;
}
liveWs.send(JSON.stringify({
type: 'run_tests',
testFiles: tests
}));
logMessage(`🔴 [LIVE] Starting ${tests.length} tests in live monitoring mode...`, 'success');
// Update UI state
document.getElementById('run-btn').disabled = true;
document.getElementById('stop-btn').disabled = false;
// Start pancake animation
const icon = document.querySelector('.icon');
icon.classList.add('running');
} else {
// Fall back to regular execution
await runTests();
}
}
// Event listeners
document.getElementById('run-btn').addEventListener('click', () => {
if (isLiveModeEnabled) {
runTestsWithLiveMode();
} else {
runTests();
}
});
// Event delegation for dynamically created checkboxes
document.addEventListener('change', function(e) {
console.log('Change event triggered on:', e.target);
if (e.target.classList.contains('file-select-all-checkbox')) {
console.log('File select all checkbox changed to:', e.target.checked);
const groupId = e.target.dataset.group;
if (groupId) {
toggleGroup(groupId, e.target.checked);
} else {
console.error('No group ID found for checkbox:', e.target);
}
} else if (e.target.classList.contains('test-case-checkbox')) {
console.log('Test case checkbox changed to:', e.target.checked);
updateSelectedCount();
}
});
// Event delegation for toggle icons and non-checkbox elements
document.addEventListener('click', function(e) {
console.log('Click event triggered on:', e.target);
// Handle toggle icons
if (e.target.classList.contains('toggle-icon')) {
const groupId = e.target.dataset.group;
toggleFile(groupId);
return;
}
// Handle checkbox labels
if (e.target.classList.contains('file-select-all-label')) {
console.log('Checkbox label clicked');
// Let the browser handle the label click naturally
return;
}
// Handle test case labels
if (e.target.classList.contains('test-case-label')) {
console.log('Test case label clicked');
// Let the browser handle the label click naturally
return;
}
// Handle header clicks (but not if clicking on checkbox area)
if (e.target.classList.contains('test-file-header')) {
if (!e.target.closest('.file-select-all')) {
const toggleIcon = e.target.querySelector('.toggle-icon');
if (toggleIcon) {
const groupId = toggleIcon.dataset.group;
toggleFile(groupId);
}
}
return;
}
// Stop propagation for checkbox area to prevent header clicks
if (e.target.closest('.file-select-all')) {
e.stopPropagation();
console.log('Stopped propagation for checkbox area');
return;
}
});
// Initialize everything
document.addEventListener('DOMContentLoaded', () => {
// Load saved theme first
loadSavedTheme();
// Load saved headless mode
loadSavedHeadlessMode();
// Load saved browser type
loadSavedBrowserType();
// Load saved live mode state
loadSavedLiveModeState();
// Setup search and filter functionality
setupSearchAndFilter();
// Setup sidebar resizer
setupSidebarResizer();
// Start connecting immediately
connectWebSocket();
// Load test files after a short delay
setTimeout(() => {
loadTestFiles();
}, 500);
});
// Search and filter functionality
function setupSearchAndFilter() {
const searchInput = document.getElementById('search-input');
const filterTabs = document.querySelectorAll('.filter-tab');
searchInput.addEventListener('input', filterTestFiles);
filterTabs.forEach(tab => {
tab.addEventListener('click', () => {
// Update active tab
filterTabs.forEach(t => t.classList.remove('active'));
tab.classList.add('active');
// Apply filter
filterTestFiles();
});
});
}
function filterTestFiles() {
const searchTerm = document.getElementById('search-input').value.toLowerCase();
const activeFilter = document.querySelector('.filter-tab.active').dataset.filter;
const testFiles = document.querySelectorAll('.test-file');
testFiles.forEach(file => {
const fileName = file.querySelector('.test-file-title').textContent.toLowerCase();
// Check search match
const searchMatch = searchTerm === '' || fileName.includes(searchTerm);
// Check filter match
let filterMatch = true;
if (activeFilter !== 'all') {
filterMatch = fileName.includes(activeFilter);
}
// Show/hide file
file.style.display = (searchMatch && filterMatch) ? 'block' : 'none';
});
}
function clearSearch() {
document.getElementById('search-input').value = '';
filterTestFiles();
}
// Global select all functionality
function toggleAllTests() {
const button = document.getElementById('global-select-all');
const allTestCheckboxes = document.querySelectorAll('.test-case-checkbox');
const allFileCheckboxes = document.querySelectorAll('.file-select-all-checkbox');
// Determine if we should select all or deselect all
const checkedTestBoxes = document.querySelectorAll('.test-case-checkbox:checked');
const shouldSelectAll = checkedTestBoxes.length === 0 || checkedTestBoxes.length < allTestCheckboxes.length;
// Update all test checkboxes
allTestCheckboxes.forEach(checkbox => {
const wasChecked = checkbox.checked;
checkbox.checked = shouldSelectAll;
// Trigger change event to update selectedTests set and visual state
if (wasChecked !== shouldSelectAll) {
const event = new Event('change', { bubbles: true });
checkbox.dispatchEvent(event);
}
});
// Update all file checkboxes to match
allFileCheckboxes.forEach(fileCheckbox => {
fileCheckbox.checked = shouldSelectAll;
fileCheckbox.indeterminate = false;
});
// Update button appearance and text
updateSelectAllButton();
// Log the action
const action = shouldSelectAll ? 'selected' : 'deselected';
const count = allTestCheckboxes.length;
logMessage(`🔘 ${action} all ${count} tests`, shouldSelectAll ? 'success' : 'info');
}
// Update the select all button appearance based on current selection state
function updateSelectAllButton() {
const button = document.getElementById('global-select-all');
const allTestCheckboxes = document.querySelectorAll('.test-case-checkbox');
const checkedTestBoxes = document.querySelectorAll('.test-case-checkbox:checked');
if (!button || allTestCheckboxes.length === 0) return;
const isAllSelected = checkedTestBoxes.length === allTestCheckboxes.length;
const isNoneSelected = checkedTestBoxes.length === 0;
const icon = button.querySelector('i');
const text = button.querySelector('span');
if (isAllSelected) {
// All selected - show deselect mode
button.classList.add('deselect-mode');
icon.className = 'fas fa-minus-circle';
text.textContent = 'Deselect All';
button.title = 'Deselect all tests';
} else {
// Some or none selected - show select mode
button.classList.remove('deselect-mode');
icon.className = 'fas fa-check-double';
text.textContent = 'Select All';
button.title = 'Select all tests';
}
}
// Headless mode toggle functionality
let isHeadlessMode = false;
function toggleHeadlessMode() {
const headlessIcon = document.getElementById('headless-icon');
const headlessText = document.getElementById('headless-text');
const headlessBtn = document.getElementById('headless-btn');
isHeadlessMode = !isHeadlessMode;
if (isHeadlessMode) {
// Switch to headless mode
headlessIcon.className = 'fas fa-eye-slash';
headlessText.textContent = 'Headless';
headlessBtn.style.background = 'linear-gradient(135deg, #f59e0b, #fbbf24)';
headlessBtn.style.color = 'white';
headlessBtn.style.borderColor = '#f59e0b';
headlessBtn.title = 'Toggle headless/headed mode (Currently: Headless)';
localStorage.setItem('headlessMode', 'true');
logMessage('⚡ Switched to headless mode - tests will run faster without GUI', 'info');
} else {
// Switch to headed mode
headlessIcon.className = 'fas fa-eye';
headlessText.textContent = 'Headed';
headlessBtn.style.background = '';
headlessBtn.style.color = '';
headlessBtn.style.borderColor = '';
headlessBtn.title = 'Toggle headless/headed mode (Currently: Headed)';
localStorage.setItem('headlessMode', 'false');
logMessage('👀 Switched to headed mode - browser windows will be visible', 'info');
}
}
// Browser type toggle functionality
let selectedBrowser = 'chrome';
function toggleBrowserType() {
const browserIcon = document.getElementById('browser-icon');
const browserText = document.getElementById('browser-text');
const browserBtn = document.getElementById('browser-btn');
selectedBrowser = selectedBrowser === 'chrome' ? 'firefox' : 'chrome';
if (selectedBrowser === 'firefox') {
// Switch to Firefox
browserIcon.className = 'fab fa-firefox-browser';
browserText.textContent = 'Firefox';
browserBtn.style.background = 'linear-gradient(135deg, #ff6600, #ff9500)';
browserBtn.style.color = 'white';
browserBtn.style.borderColor = '#ff6600';
browserBtn.title = 'Toggle browser (Chrome/Firefox) - Currently: Firefox';
localStorage.setItem('selectedBrowser', 'firefox');
logMessage('🦊 Switched to Firefox browser - tests will run in Firefox', 'info');
} else {
// Switch to Chrome
browserIcon.className = 'fab fa-chrome';
browserText.textContent = 'Chrome';
browserBtn.style.background = '';
browserBtn.style.color = '';
browserBtn.style.borderColor = '';
browserBtn.title = 'Toggle browser (Chrome/Firefox) - Currently: Chrome';
localStorage.setItem('selectedBrowser', 'chrome');
logMessage('🔶 Switched to Chrome browser - tests will run in Chrome', 'info');
}
}
// Load saved browser type
function loadSavedBrowserType() {
const savedBrowser = localStorage.getItem('selectedBrowser');
const browserIcon = document.getElementById('browser-icon');
const browserText = document.getElementById('browser-text');
const browserBtn = document.getElementById('browser-btn');
if (savedBrowser === 'firefox') {
selectedBrowser = 'firefox';
browserIcon.className = 'fab fa-firefox-browser';
browserText.textContent = 'Firefox';
browserBtn.style.background = 'linear-gradient(135deg, #ff6600, #ff9500)';
browserBtn.style.color = 'white';
browserBtn.style.borderColor = '#ff6600';
browserBtn.title = 'Toggle browser (Chrome/Firefox) - Currently: Firefox';
} else {
selectedBrowser = 'chrome';
browserIcon.className = 'fab fa-chrome';
browserText.textContent = 'Chrome';
browserBtn.title = 'Toggle browser (Chrome/Firefox) - Currently: Chrome';
}
}
// Load saved live mode state
function loadSavedLiveModeState() {
const savedLiveMode = localStorage.getItem('liveModeEnabled');
if (savedLiveMode === 'true') {
// Auto-enable live mode on page load
toggleLiveMode();
}
}
// Load saved headless mode
function loadSavedHeadlessMode() {
const savedHeadlessMode = localStorage.getItem('headlessMode');
const headlessIcon = document.getElementById('headless-icon');
const headlessText = document.getElementById('headless-text');
const headlessBtn = document.getElementById('headless-btn');
if (savedHeadlessMode === 'true') {
isHeadlessMode = true;
headlessIcon.className = 'fas fa-eye-slash';
headlessText.textContent = 'Headless';
headlessBtn.style.background = 'linear-gradient(135deg, #f59e0b, #fbbf24)';
headlessBtn.style.color = 'white';
headlessBtn.style.borderColor = '#f59e0b';
headlessBtn.title = 'Toggle headless/headed mode (Currently: Headless)';
} else {
isHeadlessMode = false;
headlessIcon.className = 'fas fa-eye';
headlessText.textContent = 'Headed';
headlessBtn.title = 'Toggle headless/headed mode (Currently: Headed)';
}
}
// Theme toggle functionality
function toggleTheme() {
const body = document.body;
const themeIcon = document.getElementById('theme-icon');
if (body.hasAttribute('data-theme')) {
// Switch to light mode
body.removeAttribute('data-theme');
themeIcon.className = 'fas fa-moon';
localStorage.setItem('theme', 'light');
} else {
// Switch to dark mode
body.setAttribute('data-theme', 'dark');
themeIcon.className = 'fas fa-sun';
localStorage.setItem('theme', 'dark');
}
}
// Load saved theme
function loadSavedTheme() {
const savedTheme = localStorage.getItem('theme');
const themeIcon = document.getElementById('theme-icon');
if (savedTheme === 'dark') {
document.body.setAttribute('data-theme', 'dark');
themeIcon.className = 'fas fa-sun';
} else {
themeIcon.className = 'fas fa-moon';
}
}
// Sidebar resizer functionality
function setupSidebarResizer() {
const resizer = document.querySelector('.sidebar-resizer');
const sidebar = document.querySelector('.sidebar');
const container = document.querySelector('.app-container');
let isResizing = false;
resizer.addEventListener('mousedown', (e) => {
isResizing = true;
container.classList.add('resizing');
document.addEventListener('mousemove', handleResize);
document.addEventListener('mouseup', stopResize);
e.preventDefault();
});
function handleResize(e) {
if (!isResizing) return;
const containerRect = container.getBoundingClientRect();
const newWidth = ((e.clientX - containerRect.left) / containerRect.width) * 100;
// Constrain between 15% and 60%
const constrainedWidth = Math.max(15, Math.min(60, newWidth));
sidebar.style.width = constrainedWidth + '%';
}
function stopResize() {
isResizing = false;
container.classList.remove('resizing');
document.removeEventListener('mousemove', handleResize);
document.removeEventListener('mouseup', stopResize);
}
}
// Cleanup on page unload
window.addEventListener('beforeunload', () => {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.close(1000, 'Page unloading');
}
});
</script>
</body>
</html>