910 lines
32 KiB
HTML
910 lines
32 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Contract Tests - Ward</title>
|
|
<style>
|
|
* {
|
|
box-sizing: border-box;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
background: #111827;
|
|
color: #e5e7eb;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
.container {
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
}
|
|
|
|
header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 20px;
|
|
padding-bottom: 20px;
|
|
border-bottom: 1px solid #374151;
|
|
}
|
|
|
|
h1 {
|
|
font-size: 1.5rem;
|
|
font-weight: 600;
|
|
color: #f9fafb;
|
|
}
|
|
|
|
.config-info {
|
|
font-size: 0.875rem;
|
|
color: #9ca3af;
|
|
}
|
|
|
|
.config-info strong {
|
|
color: #60a5fa;
|
|
}
|
|
|
|
.toolbar {
|
|
display: flex;
|
|
gap: 10px;
|
|
margin-bottom: 20px;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
}
|
|
|
|
button {
|
|
padding: 8px 16px;
|
|
border: none;
|
|
border-radius: 6px;
|
|
font-size: 0.875rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.btn-primary {
|
|
background: #2563eb;
|
|
color: white;
|
|
}
|
|
|
|
.btn-primary:hover {
|
|
background: #1d4ed8;
|
|
}
|
|
|
|
.btn-primary:disabled {
|
|
background: #4b5563;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.btn-secondary {
|
|
background: #374151;
|
|
color: #e5e7eb;
|
|
}
|
|
|
|
.btn-secondary:hover {
|
|
background: #4b5563;
|
|
}
|
|
|
|
.main-content {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 20px;
|
|
}
|
|
|
|
@media (max-width: 900px) {
|
|
.main-content {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
|
|
.panel {
|
|
background: #1f2937;
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.panel-header {
|
|
padding: 12px 16px;
|
|
background: #374151;
|
|
font-weight: 600;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.panel-body {
|
|
padding: 16px;
|
|
max-height: 600px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
/* Test Tree */
|
|
.folder {
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.folder-header {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 6px 8px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
user-select: none;
|
|
}
|
|
|
|
.folder-header:hover {
|
|
background: #374151;
|
|
}
|
|
|
|
.folder-header input {
|
|
margin-right: 12px;
|
|
}
|
|
|
|
.folder-name {
|
|
font-weight: 500;
|
|
color: #f9fafb;
|
|
}
|
|
|
|
.test-count {
|
|
margin-left: auto;
|
|
font-size: 0.75rem;
|
|
color: #9ca3af;
|
|
background: #374151;
|
|
padding: 2px 8px;
|
|
border-radius: 10px;
|
|
}
|
|
|
|
.folder-content {
|
|
margin-left: 20px;
|
|
}
|
|
|
|
.module {
|
|
margin: 4px 0;
|
|
}
|
|
|
|
.module-header {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 4px 8px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.module-header:hover {
|
|
background: #374151;
|
|
}
|
|
|
|
.module-header input {
|
|
margin-right: 12px;
|
|
}
|
|
|
|
.module-name {
|
|
color: #93c5fd;
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.class-block {
|
|
margin-left: 20px;
|
|
}
|
|
|
|
.class-header {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 4px 8px;
|
|
font-size: 1rem;
|
|
color: #a78bfa;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.class-header:hover {
|
|
background: #374151;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.class-header input {
|
|
margin-right: 12px;
|
|
}
|
|
|
|
.test-list {
|
|
margin-left: 20px;
|
|
}
|
|
|
|
.test-item {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 6px 8px;
|
|
font-size: 0.95rem;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.test-item:hover {
|
|
background: #374151;
|
|
}
|
|
|
|
.test-item input {
|
|
margin-right: 12px;
|
|
}
|
|
|
|
.test-name {
|
|
color: #d1d5db;
|
|
}
|
|
|
|
/* Results */
|
|
.summary {
|
|
display: flex;
|
|
gap: 16px;
|
|
margin-bottom: 16px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.stat {
|
|
text-align: center;
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: 1.5rem;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 0.75rem;
|
|
color: #9ca3af;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.stat-passed .stat-value { color: #34d399; }
|
|
.stat-failed .stat-value { color: #f87171; }
|
|
.stat-skipped .stat-value { color: #fbbf24; }
|
|
.stat-running .stat-value { color: #60a5fa; }
|
|
|
|
.result-item {
|
|
padding: 8px 12px;
|
|
margin-bottom: 4px;
|
|
border-radius: 4px;
|
|
background: #374151;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.result-icon {
|
|
width: 20px;
|
|
height: 20px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 0.75rem;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.result-passed .result-icon {
|
|
background: #065f46;
|
|
color: #34d399;
|
|
}
|
|
|
|
.result-failed .result-icon,
|
|
.result-error .result-icon {
|
|
background: #7f1d1d;
|
|
color: #f87171;
|
|
}
|
|
|
|
.result-skipped .result-icon {
|
|
background: #78350f;
|
|
color: #fbbf24;
|
|
}
|
|
|
|
.result-running .result-icon {
|
|
background: #1e3a8a;
|
|
color: #60a5fa;
|
|
animation: pulse 1s infinite;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0.5; }
|
|
}
|
|
|
|
.result-info {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.result-name {
|
|
font-size: 0.875rem;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.result-test-id {
|
|
font-size: 0.75rem;
|
|
color: #6b7280;
|
|
}
|
|
|
|
.result-duration {
|
|
font-size: 0.75rem;
|
|
color: #9ca3af;
|
|
}
|
|
|
|
.result-error {
|
|
margin-top: 8px;
|
|
padding: 8px;
|
|
background: #1f2937;
|
|
border-radius: 4px;
|
|
font-size: 0.75rem;
|
|
font-family: monospace;
|
|
white-space: pre-wrap;
|
|
color: #f87171;
|
|
max-height: 200px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 40px;
|
|
color: #6b7280;
|
|
}
|
|
|
|
.progress-bar {
|
|
height: 4px;
|
|
background: #374151;
|
|
border-radius: 2px;
|
|
margin-bottom: 16px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.progress-fill {
|
|
height: 100%;
|
|
background: #2563eb;
|
|
transition: width 0.3s;
|
|
}
|
|
|
|
.current-test {
|
|
font-size: 0.75rem;
|
|
color: #60a5fa;
|
|
margin-bottom: 8px;
|
|
font-style: italic;
|
|
}
|
|
|
|
/* Collapsible */
|
|
.collapsed .folder-content,
|
|
.collapsed .module-content,
|
|
.collapsed .class-content {
|
|
display: none;
|
|
}
|
|
|
|
.toggle-icon {
|
|
margin-right: 4px;
|
|
transition: transform 0.2s;
|
|
}
|
|
|
|
.collapsed .toggle-icon {
|
|
transform: rotate(-90deg);
|
|
}
|
|
|
|
a {
|
|
color: #60a5fa;
|
|
text-decoration: none;
|
|
}
|
|
|
|
a:hover {
|
|
text-decoration: underline;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<header>
|
|
<div>
|
|
<h1>Contract HTTP Tests</h1>
|
|
<div style="display: flex; gap: 12px; margin-top: 8px; font-size: 0.875rem;">
|
|
<a href="/tools/tester/" style="color: #60a5fa; text-decoration: none; font-weight: 600;">Runner</a>
|
|
<a href="/tools/tester/filters" style="color: #60a5fa; text-decoration: none;">Filters</a>
|
|
</div>
|
|
</div>
|
|
<div class="config-info">
|
|
<div style="display: flex; align-items: center; gap: 12px;">
|
|
<span>Target:</span>
|
|
<select id="environmentSelector" style="background: #374151; color: #e5e7eb; border: 1px solid #4b5563; border-radius: 4px; padding: 4px 8px; font-size: 0.875rem; cursor: pointer;">
|
|
<option value="">Loading...</option>
|
|
</select>
|
|
<strong id="currentUrl">{{ config.CONTRACT_TEST_URL }}</strong>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="toolbar">
|
|
<button class="btn-primary" id="runAllBtn" onclick="runAll()">Run All</button>
|
|
<button class="btn-secondary" id="runSelectedBtn" onclick="runSelected()">Run Selected</button>
|
|
<button class="btn-secondary" onclick="clearResults()">Clear Results</button>
|
|
<span style="margin-left: auto; color: #6b7280;">{{ total_tests }} tests discovered</span>
|
|
</div>
|
|
|
|
<div class="main-content">
|
|
<div class="panel">
|
|
<div class="panel-header">
|
|
<span>Tests</span>
|
|
<button class="btn-secondary" onclick="toggleAll()" style="padding: 4px 8px; font-size: 0.75rem;">Toggle All</button>
|
|
</div>
|
|
<div class="panel-body" id="testsPanel">
|
|
{% for folder_name, folder in tests_tree.items() %}
|
|
<div class="folder" data-folder="{{ folder_name }}">
|
|
<div class="folder-header" onclick="toggleFolder(this)">
|
|
<span class="toggle-icon">▼</span>
|
|
<input type="checkbox" onclick="event.stopPropagation(); toggleFolderCheckbox(this)" checked>
|
|
<span class="folder-name">{{ folder_name }}/</span>
|
|
<span class="test-count">{{ folder.test_count }}</span>
|
|
</div>
|
|
<div class="folder-content">
|
|
{% for module_name, module in folder.modules.items() %}
|
|
<div class="module" data-module="{{ folder_name }}.{{ module_name }}">
|
|
<div class="module-header" onclick="toggleModule(this)">
|
|
<span class="toggle-icon">▼</span>
|
|
<input type="checkbox" onclick="event.stopPropagation(); toggleModuleCheckbox(this)" checked>
|
|
<span class="module-name">{{ module_name }}.py</span>
|
|
<span class="test-count">{{ module.test_count }}</span>
|
|
</div>
|
|
<div class="module-content">
|
|
{% for class_name, cls in module.classes.items() %}
|
|
<div class="class-block" data-class="{{ folder_name }}.{{ module_name }}.{{ class_name }}">
|
|
<div class="class-header" onclick="toggleClass(this)">
|
|
<span class="toggle-icon">▼</span>
|
|
<input type="checkbox" onclick="event.stopPropagation(); toggleClassCheckbox(this)" checked>
|
|
<span>{{ class_name }}</span>
|
|
<span class="test-count">{{ cls.test_count }}</span>
|
|
</div>
|
|
<div class="class-content test-list">
|
|
{% for test in cls.tests %}
|
|
<div class="test-item">
|
|
<input type="checkbox" data-test-id="{{ test.id }}" checked>
|
|
<span class="test-name" title="{{ test.doc or '' }}">{{ test.name }}</span>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="panel">
|
|
<div class="panel-header">
|
|
<span>Results</span>
|
|
<span id="runDuration" style="font-size: 0.75rem; color: #9ca3af;"></span>
|
|
</div>
|
|
<div class="panel-body" id="resultsPanel">
|
|
<div class="summary" id="summary" style="display: none;">
|
|
<div class="stat stat-passed">
|
|
<div class="stat-value" id="passedCount">0</div>
|
|
<div class="stat-label">Passed</div>
|
|
</div>
|
|
<div class="stat stat-failed">
|
|
<div class="stat-value" id="failedCount">0</div>
|
|
<div class="stat-label">Failed</div>
|
|
</div>
|
|
<div class="stat stat-skipped">
|
|
<div class="stat-value" id="skippedCount">0</div>
|
|
<div class="stat-label">Skipped</div>
|
|
</div>
|
|
<div class="stat stat-running">
|
|
<div class="stat-value" id="runningCount">0</div>
|
|
<div class="stat-label">Running</div>
|
|
</div>
|
|
</div>
|
|
<div class="progress-bar" id="progressBar" style="display: none;">
|
|
<div class="progress-fill" id="progressFill" style="width: 0%;"></div>
|
|
</div>
|
|
<div class="current-test" id="currentTest" style="display: none;"></div>
|
|
<div id="resultsList">
|
|
<div class="empty-state">
|
|
Run tests to see results
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let currentRunId = null;
|
|
let pollInterval = null;
|
|
|
|
// Parse URL parameters for filters
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const filterParams = {
|
|
search: urlParams.get('search') || '',
|
|
domains: urlParams.get('domains') ? new Set(urlParams.get('domains').split(',')) : new Set(),
|
|
modules: urlParams.get('modules') ? new Set(urlParams.get('modules').split(',')) : new Set(),
|
|
status: urlParams.get('status') ? new Set(urlParams.get('status').split(',')) : new Set(),
|
|
};
|
|
|
|
// Check if there's a run ID in URL
|
|
const autoRunId = urlParams.get('run');
|
|
|
|
// Format "TestCoverageCheck" -> "Coverage Check"
|
|
function formatClassName(name) {
|
|
// Remove "Test" prefix
|
|
let formatted = name.replace(/^Test/, '');
|
|
// Add space before each capital letter
|
|
formatted = formatted.replace(/([A-Z])/g, ' $1').trim();
|
|
return formatted;
|
|
}
|
|
|
|
// Format "test_returns_coverage_boolean" -> "returns coverage boolean"
|
|
function formatTestName(name) {
|
|
// Remove "test_" prefix
|
|
let formatted = name.replace(/^test_/, '');
|
|
// Replace underscores with spaces
|
|
formatted = formatted.replace(/_/g, ' ');
|
|
return formatted;
|
|
}
|
|
|
|
// Apply filters to test tree
|
|
function applyFilters() {
|
|
const folders = document.querySelectorAll('.folder');
|
|
|
|
folders.forEach(folder => {
|
|
const folderName = folder.dataset.folder;
|
|
let hasVisibleTests = false;
|
|
|
|
// Check domain filter
|
|
if (filterParams.domains.size > 0 && !filterParams.domains.has(folderName)) {
|
|
folder.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
// Check modules
|
|
const modules = folder.querySelectorAll('.module');
|
|
modules.forEach(module => {
|
|
const moduleName = module.dataset.module.split('.')[1];
|
|
let moduleVisible = true;
|
|
|
|
if (filterParams.modules.size > 0 && !filterParams.modules.has(moduleName)) {
|
|
moduleVisible = false;
|
|
}
|
|
|
|
// Check search filter on test names
|
|
if (filterParams.search && moduleVisible) {
|
|
const tests = module.querySelectorAll('.test-item');
|
|
let hasMatchingTest = false;
|
|
tests.forEach(test => {
|
|
const testName = test.querySelector('.test-name').textContent.toLowerCase();
|
|
if (testName.includes(filterParams.search.toLowerCase())) {
|
|
hasMatchingTest = true;
|
|
}
|
|
});
|
|
if (!hasMatchingTest) {
|
|
moduleVisible = false;
|
|
}
|
|
}
|
|
|
|
if (moduleVisible) {
|
|
module.style.display = '';
|
|
hasVisibleTests = true;
|
|
} else {
|
|
module.style.display = 'none';
|
|
}
|
|
});
|
|
|
|
folder.style.display = hasVisibleTests ? '' : 'none';
|
|
});
|
|
}
|
|
|
|
// Load environments
|
|
async function loadEnvironments() {
|
|
try {
|
|
const response = await fetch('/tools/tester/api/environments');
|
|
const data = await response.json();
|
|
const selector = document.getElementById('environmentSelector');
|
|
const currentUrl = document.getElementById('currentUrl');
|
|
|
|
// Get saved environment from localStorage
|
|
const savedEnvId = localStorage.getItem('selectedEnvironment');
|
|
let selectedEnv = null;
|
|
|
|
// Populate selector
|
|
selector.innerHTML = data.environments.map(env => {
|
|
const isDefault = env.default || env.id === savedEnvId;
|
|
if (isDefault) selectedEnv = env;
|
|
return `<option value="${env.id}" ${isDefault ? 'selected' : ''}>${env.name} ${env.has_api_key ? '🔑' : ''}</option>`;
|
|
}).join('');
|
|
|
|
// Update URL display
|
|
if (selectedEnv) {
|
|
currentUrl.textContent = selectedEnv.url;
|
|
}
|
|
|
|
// Handle environment changes
|
|
selector.addEventListener('change', async (e) => {
|
|
const envId = e.target.value;
|
|
try {
|
|
const response = await fetch(`/tools/tester/api/environment/select?env_id=${envId}`, {
|
|
method: 'POST'
|
|
});
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
currentUrl.textContent = data.environment.url;
|
|
localStorage.setItem('selectedEnvironment', envId);
|
|
|
|
// Show notification
|
|
const notification = document.createElement('div');
|
|
notification.textContent = `Switched to ${data.environment.name}`;
|
|
notification.style.cssText = 'position: fixed; top: 20px; right: 20px; background: #2563eb; color: white; padding: 12px 20px; border-radius: 6px; z-index: 1000; animation: fadeIn 0.3s;';
|
|
document.body.appendChild(notification);
|
|
setTimeout(() => notification.remove(), 3000);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to switch environment:', error);
|
|
alert('Failed to switch environment');
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('Failed to load environments:', error);
|
|
}
|
|
}
|
|
|
|
// Apply formatting and filters on page load
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Load environments
|
|
loadEnvironments();
|
|
// Format class names
|
|
document.querySelectorAll('.class-header > span:not(.toggle-icon):not(.test-count)').forEach(el => {
|
|
if (!el.querySelector('input')) {
|
|
el.textContent = formatClassName(el.textContent);
|
|
}
|
|
});
|
|
// Format test names
|
|
document.querySelectorAll('.test-name').forEach(el => {
|
|
el.textContent = formatTestName(el.textContent);
|
|
});
|
|
|
|
// Apply filters from URL
|
|
if (filterParams.domains.size > 0 || filterParams.modules.size > 0 || filterParams.search) {
|
|
applyFilters();
|
|
}
|
|
|
|
// Auto-start run if run ID in URL
|
|
if (autoRunId) {
|
|
currentRunId = autoRunId;
|
|
document.getElementById('summary').style.display = 'flex';
|
|
document.getElementById('progressBar').style.display = 'block';
|
|
pollInterval = setInterval(pollStatus, 1000);
|
|
pollStatus();
|
|
}
|
|
});
|
|
|
|
function getSelectedTestIds() {
|
|
const checkboxes = document.querySelectorAll('.test-item input[type="checkbox"]:checked');
|
|
return Array.from(checkboxes).map(cb => cb.dataset.testId);
|
|
}
|
|
|
|
async function runAll() {
|
|
await startRun(null);
|
|
}
|
|
|
|
async function runSelected() {
|
|
const testIds = getSelectedTestIds();
|
|
if (testIds.length === 0) {
|
|
alert('No tests selected');
|
|
return;
|
|
}
|
|
await startRun(testIds);
|
|
}
|
|
|
|
async function startRun(testIds) {
|
|
document.getElementById('runAllBtn').disabled = true;
|
|
document.getElementById('runSelectedBtn').disabled = true;
|
|
document.getElementById('summary').style.display = 'flex';
|
|
document.getElementById('progressBar').style.display = 'block';
|
|
document.getElementById('resultsList').innerHTML = '';
|
|
|
|
try {
|
|
const response = await fetch('/tools/tester/api/run', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ test_ids: testIds }),
|
|
});
|
|
const data = await response.json();
|
|
currentRunId = data.run_id;
|
|
|
|
// Start polling
|
|
pollInterval = setInterval(pollStatus, 1000);
|
|
pollStatus(); // Immediate first poll
|
|
} catch (error) {
|
|
console.error('Failed to start run:', error);
|
|
document.getElementById('runAllBtn').disabled = false;
|
|
document.getElementById('runSelectedBtn').disabled = false;
|
|
}
|
|
}
|
|
|
|
async function pollStatus() {
|
|
if (!currentRunId) return;
|
|
|
|
try {
|
|
const response = await fetch(`/tools/tester/api/run/${currentRunId}`);
|
|
const data = await response.json();
|
|
|
|
updateUI(data);
|
|
|
|
if (data.status === 'completed' || data.status === 'failed') {
|
|
clearInterval(pollInterval);
|
|
pollInterval = null;
|
|
document.getElementById('runAllBtn').disabled = false;
|
|
document.getElementById('runSelectedBtn').disabled = false;
|
|
}
|
|
} catch (error) {
|
|
console.error('Poll failed:', error);
|
|
}
|
|
}
|
|
|
|
function updateUI(data) {
|
|
// Update counts
|
|
document.getElementById('passedCount').textContent = data.passed;
|
|
document.getElementById('failedCount').textContent = data.failed + data.errors;
|
|
document.getElementById('skippedCount').textContent = data.skipped;
|
|
document.getElementById('runningCount').textContent = data.total - data.completed;
|
|
|
|
// Update progress
|
|
const progress = data.total > 0 ? (data.completed / data.total * 100) : 0;
|
|
document.getElementById('progressFill').style.width = progress + '%';
|
|
|
|
// Update duration
|
|
if (data.duration) {
|
|
document.getElementById('runDuration').textContent = data.duration.toFixed(1) + 's';
|
|
}
|
|
|
|
// Current test
|
|
const currentTestEl = document.getElementById('currentTest');
|
|
if (data.current_test) {
|
|
currentTestEl.textContent = 'Running: ' + data.current_test;
|
|
currentTestEl.style.display = 'block';
|
|
} else {
|
|
currentTestEl.style.display = 'none';
|
|
}
|
|
|
|
// Results list
|
|
const resultsList = document.getElementById('resultsList');
|
|
resultsList.innerHTML = data.results.map(r => renderResult(r)).join('');
|
|
}
|
|
|
|
function renderResult(result) {
|
|
const icons = {
|
|
passed: '✓',
|
|
failed: '✗',
|
|
error: '✗',
|
|
skipped: '−',
|
|
running: '●',
|
|
};
|
|
|
|
let errorHtml = '';
|
|
if (result.error_message) {
|
|
errorHtml = `<div class="result-error">${escapeHtml(result.error_message)}</div>`;
|
|
}
|
|
|
|
// Render artifacts (videos, screenshots)
|
|
let artifactsHtml = '';
|
|
if (result.artifacts && result.artifacts.length > 0) {
|
|
const artifactItems = result.artifacts.map(artifact => {
|
|
if (artifact.type === 'video') {
|
|
return `
|
|
<div style="margin-top: 8px;">
|
|
<div style="font-size: 0.75rem; color: #9ca3af; margin-bottom: 4px;">
|
|
📹 ${artifact.filename} (${formatBytes(artifact.size)})
|
|
</div>
|
|
<video controls style="max-width: 100%; border-radius: 4px; background: #000;">
|
|
<source src="${artifact.url}" type="video/webm">
|
|
Your browser does not support video playback.
|
|
</video>
|
|
</div>
|
|
`;
|
|
} else if (artifact.type === 'screenshot') {
|
|
return `
|
|
<div style="margin-top: 8px;">
|
|
<div style="font-size: 0.75rem; color: #9ca3af; margin-bottom: 4px;">
|
|
📸 ${artifact.filename} (${formatBytes(artifact.size)})
|
|
</div>
|
|
<img src="${artifact.url}" style="max-width: 100%; border-radius: 4px; border: 1px solid #374151;">
|
|
</div>
|
|
`;
|
|
} else {
|
|
return `
|
|
<div style="margin-top: 8px; font-size: 0.75rem; color: #9ca3af;">
|
|
📎 <a href="${artifact.url}" style="color: #60a5fa;">${artifact.filename}</a> (${formatBytes(artifact.size)})
|
|
</div>
|
|
`;
|
|
}
|
|
}).join('');
|
|
artifactsHtml = `<div class="result-artifacts">${artifactItems}</div>`;
|
|
}
|
|
|
|
return `
|
|
<div class="result-item result-${result.status}">
|
|
<div class="result-icon">${icons[result.status] || '?'}</div>
|
|
<div class="result-info">
|
|
<div class="result-name">${escapeHtml(result.name)}</div>
|
|
<div class="result-test-id">${escapeHtml(result.test_id)}</div>
|
|
${errorHtml}
|
|
${artifactsHtml}
|
|
</div>
|
|
<div class="result-duration">${result.duration.toFixed(3)}s</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function formatBytes(bytes) {
|
|
if (bytes < 1024) return bytes + ' B';
|
|
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
|
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
function clearResults() {
|
|
document.getElementById('summary').style.display = 'none';
|
|
document.getElementById('progressBar').style.display = 'none';
|
|
document.getElementById('currentTest').style.display = 'none';
|
|
document.getElementById('runDuration').textContent = '';
|
|
document.getElementById('resultsList').innerHTML = '<div class="empty-state">Run tests to see results</div>';
|
|
}
|
|
|
|
// Toggle functions
|
|
function toggleFolder(header) {
|
|
header.parentElement.classList.toggle('collapsed');
|
|
}
|
|
|
|
function toggleModule(header) {
|
|
header.parentElement.classList.toggle('collapsed');
|
|
}
|
|
|
|
function toggleClass(header) {
|
|
header.parentElement.classList.toggle('collapsed');
|
|
}
|
|
|
|
function toggleAll() {
|
|
const folders = document.querySelectorAll('.folder');
|
|
const allCollapsed = Array.from(folders).every(f => f.classList.contains('collapsed'));
|
|
|
|
folders.forEach(folder => {
|
|
if (allCollapsed) {
|
|
folder.classList.remove('collapsed');
|
|
} else {
|
|
folder.classList.add('collapsed');
|
|
}
|
|
});
|
|
}
|
|
|
|
function toggleFolderCheckbox(checkbox) {
|
|
const folder = checkbox.closest('.folder');
|
|
const childCheckboxes = folder.querySelectorAll('input[type="checkbox"]');
|
|
childCheckboxes.forEach(cb => cb.checked = checkbox.checked);
|
|
}
|
|
|
|
function toggleModuleCheckbox(checkbox) {
|
|
const module = checkbox.closest('.module');
|
|
const childCheckboxes = module.querySelectorAll('.test-item input[type="checkbox"]');
|
|
childCheckboxes.forEach(cb => cb.checked = checkbox.checked);
|
|
}
|
|
|
|
function toggleClassCheckbox(checkbox) {
|
|
const classBlock = checkbox.closest('.class-block');
|
|
const childCheckboxes = classBlock.querySelectorAll('.test-item input[type="checkbox"]');
|
|
childCheckboxes.forEach(cb => cb.checked = checkbox.checked);
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|