soleprint init commit
This commit is contained in:
862
station/tools/tester/templates/filters.html
Normal file
862
station/tools/tester/templates/filters.html
Normal file
@@ -0,0 +1,862 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Test Filters - 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;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.nav-links a {
|
||||
color: #60a5fa;
|
||||
text-decoration: none;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.nav-links a:hover {
|
||||
background: #374151;
|
||||
}
|
||||
|
||||
.nav-links a.active {
|
||||
background: #2563eb;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Filter Panel */
|
||||
.filter-panel {
|
||||
background: #1f2937;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.filter-section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
color: #9ca3af;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 10px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-chip {
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
background: #374151;
|
||||
color: #e5e7eb;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.filter-chip:hover {
|
||||
background: #4b5563;
|
||||
}
|
||||
|
||||
.filter-chip.active {
|
||||
background: #2563eb;
|
||||
color: white;
|
||||
border-color: #1d4ed8;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
background: #374151;
|
||||
border: 2px solid #4b5563;
|
||||
border-radius: 6px;
|
||||
color: #e5e7eb;
|
||||
font-size: 0.875rem;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.search-box:focus {
|
||||
outline: none;
|
||||
border-color: #2563eb;
|
||||
}
|
||||
|
||||
.search-box::placeholder {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
/* Test List */
|
||||
.test-list {
|
||||
background: #1f2937;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.list-header {
|
||||
padding: 12px 16px;
|
||||
background: #374151;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.test-count {
|
||||
font-size: 0.75rem;
|
||||
color: #9ca3af;
|
||||
background: #1f2937;
|
||||
padding: 4px 10px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.list-body {
|
||||
padding: 16px;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.test-card {
|
||||
background: #374151;
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.test-card:hover {
|
||||
background: #4b5563;
|
||||
border-color: #2563eb;
|
||||
}
|
||||
|
||||
.test-card.selected {
|
||||
border-color: #2563eb;
|
||||
background: #1e3a8a;
|
||||
}
|
||||
|
||||
.test-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: start;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.test-title {
|
||||
font-weight: 600;
|
||||
color: #f9fafb;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.test-status-badge {
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.status-passed {
|
||||
background: #065f46;
|
||||
color: #34d399;
|
||||
}
|
||||
|
||||
.status-failed {
|
||||
background: #7f1d1d;
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
.status-skipped {
|
||||
background: #78350f;
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.status-unknown {
|
||||
background: #374151;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.test-path {
|
||||
font-size: 0.75rem;
|
||||
color: #9ca3af;
|
||||
font-family: monospace;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.test-doc {
|
||||
font-size: 0.875rem;
|
||||
color: #d1d5db;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.test-meta {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 8px;
|
||||
font-size: 0.75rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.test-meta span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.empty-state-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Action Bar */
|
||||
.action-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
background: #1f2937;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
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;
|
||||
}
|
||||
|
||||
.selection-info {
|
||||
font-size: 0.875rem;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.selection-info strong {
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.filter-section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.selection-info {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<div>
|
||||
<h1>Contract HTTP Tests - Filters</h1>
|
||||
<div class="nav-links">
|
||||
<a href="/tools/tester/">Runner</a>
|
||||
<a href="/tools/tester/filters" class="active">Filters</a>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 12px; font-size: 0.875rem; color: #9ca3af;">
|
||||
<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" style="color: #60a5fa;">Loading...</strong>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="filter-panel">
|
||||
<div class="filter-section">
|
||||
<label class="filter-label">Search</label>
|
||||
<input
|
||||
type="text"
|
||||
class="search-box"
|
||||
id="searchInput"
|
||||
placeholder="Search by test name, class, or description..."
|
||||
autocomplete="off"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="filter-section">
|
||||
<label class="filter-label">Domain</label>
|
||||
<div class="filter-group" id="domainFilters">
|
||||
<div class="filter-chip active" data-filter="all" onclick="toggleDomainFilter(this)">
|
||||
All Domains
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="filter-section">
|
||||
<label class="filter-label">Module</label>
|
||||
<div class="filter-group" id="moduleFilters">
|
||||
<div class="filter-chip active" data-filter="all" onclick="toggleModuleFilter(this)">
|
||||
All Modules
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="filter-section">
|
||||
<label class="filter-label">Status (from last run)</label>
|
||||
<div class="filter-group">
|
||||
<div class="filter-chip active" data-status="all" onclick="toggleStatusFilter(this)">
|
||||
All
|
||||
</div>
|
||||
<div class="filter-chip" data-status="passed" onclick="toggleStatusFilter(this)">
|
||||
Passed
|
||||
</div>
|
||||
<div class="filter-chip" data-status="failed" onclick="toggleStatusFilter(this)">
|
||||
Failed
|
||||
</div>
|
||||
<div class="filter-chip" data-status="skipped" onclick="toggleStatusFilter(this)">
|
||||
Skipped
|
||||
</div>
|
||||
<div class="filter-chip" data-status="unknown" onclick="toggleStatusFilter(this)">
|
||||
Not Run
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="filter-section">
|
||||
<button class="btn btn-secondary" onclick="clearFilters()">Clear All Filters</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="action-bar">
|
||||
<div class="selection-info">
|
||||
<span id="selectedCount">0</span> tests selected
|
||||
</div>
|
||||
<div style="display: flex; gap: 10px;">
|
||||
<button class="btn btn-secondary" onclick="selectAll()">Select All Visible</button>
|
||||
<button class="btn btn-secondary" onclick="deselectAll()">Deselect All</button>
|
||||
<button class="btn btn-primary" id="runSelectedBtn" onclick="runSelected()">Run Selected</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="test-list">
|
||||
<div class="list-header">
|
||||
<span>Tests</span>
|
||||
<span class="test-count" id="testCount">Loading...</span>
|
||||
</div>
|
||||
<div class="list-body" id="testListBody">
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">🔍</div>
|
||||
<div>Loading tests...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let allTests = [];
|
||||
let selectedTests = new Set();
|
||||
let lastRunResults = {};
|
||||
|
||||
// Filter state
|
||||
let filters = {
|
||||
search: '',
|
||||
domains: new Set(['all']),
|
||||
modules: new Set(['all']),
|
||||
status: new Set(['all'])
|
||||
};
|
||||
|
||||
// Load tests on page load
|
||||
async function loadTests() {
|
||||
try {
|
||||
const response = await fetch('/tools/tester/api/tests');
|
||||
const data = await response.json();
|
||||
allTests = data.tests;
|
||||
|
||||
// Extract unique domains and modules
|
||||
const domains = new Set();
|
||||
const modules = new Set();
|
||||
|
||||
allTests.forEach(test => {
|
||||
const parts = test.id.split('.');
|
||||
if (parts.length >= 2) {
|
||||
domains.add(parts[0]);
|
||||
modules.add(parts[1]);
|
||||
}
|
||||
});
|
||||
|
||||
// Populate domain filters
|
||||
const domainFilters = document.getElementById('domainFilters');
|
||||
domains.forEach(domain => {
|
||||
const chip = document.createElement('div');
|
||||
chip.className = 'filter-chip';
|
||||
chip.dataset.filter = domain;
|
||||
chip.textContent = domain;
|
||||
chip.onclick = function() { toggleDomainFilter(this); };
|
||||
domainFilters.appendChild(chip);
|
||||
});
|
||||
|
||||
// Populate module filters
|
||||
const moduleFilters = document.getElementById('moduleFilters');
|
||||
modules.forEach(module => {
|
||||
const chip = document.createElement('div');
|
||||
chip.className = 'filter-chip';
|
||||
chip.dataset.filter = module;
|
||||
chip.textContent = module.replace('test_', '');
|
||||
chip.onclick = function() { toggleModuleFilter(this); };
|
||||
moduleFilters.appendChild(chip);
|
||||
});
|
||||
|
||||
// Try to load last run results
|
||||
await loadLastRunResults();
|
||||
|
||||
renderTests();
|
||||
} catch (error) {
|
||||
console.error('Failed to load tests:', error);
|
||||
document.getElementById('testListBody').innerHTML = `
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">⚠️</div>
|
||||
<div>Failed to load tests</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadLastRunResults() {
|
||||
try {
|
||||
const response = await fetch('/tools/tester/api/runs');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.runs && data.runs.length > 0) {
|
||||
const lastRunId = data.runs[0];
|
||||
const runResponse = await fetch(`/tools/tester/api/run/${lastRunId}`);
|
||||
const runData = await runResponse.json();
|
||||
|
||||
runData.results.forEach(result => {
|
||||
lastRunResults[result.test_id] = result.status;
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load last run results:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function getTestStatus(testId) {
|
||||
return lastRunResults[testId] || 'unknown';
|
||||
}
|
||||
|
||||
function toggleDomainFilter(chip) {
|
||||
const filter = chip.dataset.filter;
|
||||
|
||||
if (filter === 'all') {
|
||||
// Deselect all others
|
||||
document.querySelectorAll('#domainFilters .filter-chip').forEach(c => {
|
||||
c.classList.remove('active');
|
||||
});
|
||||
chip.classList.add('active');
|
||||
filters.domains = new Set(['all']);
|
||||
} else {
|
||||
// Remove 'all'
|
||||
document.querySelector('#domainFilters [data-filter="all"]').classList.remove('active');
|
||||
|
||||
if (filters.domains.has(filter)) {
|
||||
filters.domains.delete(filter);
|
||||
chip.classList.remove('active');
|
||||
} else {
|
||||
filters.domains.add(filter);
|
||||
chip.classList.add('active');
|
||||
}
|
||||
|
||||
// If nothing selected, select all
|
||||
if (filters.domains.size === 0 || filters.domains.has('all')) {
|
||||
document.querySelector('#domainFilters [data-filter="all"]').classList.add('active');
|
||||
document.querySelectorAll('#domainFilters .filter-chip:not([data-filter="all"])').forEach(c => {
|
||||
c.classList.remove('active');
|
||||
});
|
||||
filters.domains = new Set(['all']);
|
||||
}
|
||||
}
|
||||
|
||||
renderTests();
|
||||
}
|
||||
|
||||
function toggleModuleFilter(chip) {
|
||||
const filter = chip.dataset.filter;
|
||||
|
||||
if (filter === 'all') {
|
||||
document.querySelectorAll('#moduleFilters .filter-chip').forEach(c => {
|
||||
c.classList.remove('active');
|
||||
});
|
||||
chip.classList.add('active');
|
||||
filters.modules = new Set(['all']);
|
||||
} else {
|
||||
document.querySelector('#moduleFilters [data-filter="all"]').classList.remove('active');
|
||||
|
||||
if (filters.modules.has(filter)) {
|
||||
filters.modules.delete(filter);
|
||||
chip.classList.remove('active');
|
||||
} else {
|
||||
filters.modules.add(filter);
|
||||
chip.classList.add('active');
|
||||
}
|
||||
|
||||
if (filters.modules.size === 0 || filters.modules.has('all')) {
|
||||
document.querySelector('#moduleFilters [data-filter="all"]').classList.add('active');
|
||||
document.querySelectorAll('#moduleFilters .filter-chip:not([data-filter="all"])').forEach(c => {
|
||||
c.classList.remove('active');
|
||||
});
|
||||
filters.modules = new Set(['all']);
|
||||
}
|
||||
}
|
||||
|
||||
renderTests();
|
||||
}
|
||||
|
||||
function toggleStatusFilter(chip) {
|
||||
const status = chip.dataset.status;
|
||||
|
||||
if (status === 'all') {
|
||||
document.querySelectorAll('[data-status]').forEach(c => {
|
||||
c.classList.remove('active');
|
||||
});
|
||||
chip.classList.add('active');
|
||||
filters.status = new Set(['all']);
|
||||
} else {
|
||||
document.querySelector('[data-status="all"]').classList.remove('active');
|
||||
|
||||
if (filters.status.has(status)) {
|
||||
filters.status.delete(status);
|
||||
chip.classList.remove('active');
|
||||
} else {
|
||||
filters.status.add(status);
|
||||
chip.classList.add('active');
|
||||
}
|
||||
|
||||
if (filters.status.size === 0 || filters.status.has('all')) {
|
||||
document.querySelector('[data-status="all"]').classList.add('active');
|
||||
document.querySelectorAll('[data-status]:not([data-status="all"])').forEach(c => {
|
||||
c.classList.remove('active');
|
||||
});
|
||||
filters.status = new Set(['all']);
|
||||
}
|
||||
}
|
||||
|
||||
renderTests();
|
||||
}
|
||||
|
||||
function clearFilters() {
|
||||
// Reset search
|
||||
document.getElementById('searchInput').value = '';
|
||||
filters.search = '';
|
||||
|
||||
// Reset domains
|
||||
document.querySelectorAll('#domainFilters .filter-chip').forEach(c => c.classList.remove('active'));
|
||||
document.querySelector('#domainFilters [data-filter="all"]').classList.add('active');
|
||||
filters.domains = new Set(['all']);
|
||||
|
||||
// Reset modules
|
||||
document.querySelectorAll('#moduleFilters .filter-chip').forEach(c => c.classList.remove('active'));
|
||||
document.querySelector('#moduleFilters [data-filter="all"]').classList.add('active');
|
||||
filters.modules = new Set(['all']);
|
||||
|
||||
// Reset status
|
||||
document.querySelectorAll('[data-status]').forEach(c => c.classList.remove('active'));
|
||||
document.querySelector('[data-status="all"]').classList.add('active');
|
||||
filters.status = new Set(['all']);
|
||||
|
||||
renderTests();
|
||||
}
|
||||
|
||||
function filterTests() {
|
||||
return allTests.filter(test => {
|
||||
const parts = test.id.split('.');
|
||||
const domain = parts[0];
|
||||
const module = parts[1];
|
||||
const status = getTestStatus(test.id);
|
||||
|
||||
// Search filter
|
||||
if (filters.search) {
|
||||
const searchLower = filters.search.toLowerCase();
|
||||
const matchesSearch =
|
||||
test.name.toLowerCase().includes(searchLower) ||
|
||||
test.class_name.toLowerCase().includes(searchLower) ||
|
||||
(test.doc && test.doc.toLowerCase().includes(searchLower)) ||
|
||||
test.id.toLowerCase().includes(searchLower);
|
||||
|
||||
if (!matchesSearch) return false;
|
||||
}
|
||||
|
||||
// Domain filter
|
||||
if (!filters.domains.has('all') && !filters.domains.has(domain)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Module filter
|
||||
if (!filters.modules.has('all') && !filters.modules.has(module)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Status filter
|
||||
if (!filters.status.has('all') && !filters.status.has(status)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function renderTests() {
|
||||
const filteredTests = filterTests();
|
||||
const container = document.getElementById('testListBody');
|
||||
|
||||
document.getElementById('testCount').textContent = `${filteredTests.length} of ${allTests.length}`;
|
||||
|
||||
if (filteredTests.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">🔍</div>
|
||||
<div>No tests match your filters</div>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = filteredTests.map(test => {
|
||||
const status = getTestStatus(test.id);
|
||||
const isSelected = selectedTests.has(test.id);
|
||||
const parts = test.id.split('.');
|
||||
const domain = parts[0];
|
||||
const module = parts[1];
|
||||
|
||||
return `
|
||||
<div class="test-card ${isSelected ? 'selected' : ''}" onclick="toggleTestSelection('${test.id}')" data-test-id="${test.id}">
|
||||
<div class="test-header">
|
||||
<div class="test-title">${formatTestName(test.method_name)}</div>
|
||||
<div class="test-status-badge status-${status}">${status}</div>
|
||||
</div>
|
||||
<div class="test-path">${test.id}</div>
|
||||
<div class="test-doc">${test.doc || 'No description'}</div>
|
||||
<div class="test-meta">
|
||||
<span>📁 ${domain}</span>
|
||||
<span>📄 ${module}</span>
|
||||
<span>🏷️ ${test.class_name}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
updateSelectionInfo();
|
||||
}
|
||||
|
||||
function formatTestName(name) {
|
||||
return name.replace(/^test_/, '').replace(/_/g, ' ');
|
||||
}
|
||||
|
||||
function toggleTestSelection(testId) {
|
||||
if (selectedTests.has(testId)) {
|
||||
selectedTests.delete(testId);
|
||||
} else {
|
||||
selectedTests.add(testId);
|
||||
}
|
||||
|
||||
// Update UI
|
||||
const card = document.querySelector(`[data-test-id="${testId}"]`);
|
||||
if (card) {
|
||||
card.classList.toggle('selected');
|
||||
}
|
||||
|
||||
updateSelectionInfo();
|
||||
}
|
||||
|
||||
function selectAll() {
|
||||
const filteredTests = filterTests();
|
||||
filteredTests.forEach(test => selectedTests.add(test.id));
|
||||
renderTests();
|
||||
}
|
||||
|
||||
function deselectAll() {
|
||||
selectedTests.clear();
|
||||
renderTests();
|
||||
}
|
||||
|
||||
function updateSelectionInfo() {
|
||||
document.getElementById('selectedCount').textContent = selectedTests.size;
|
||||
document.getElementById('runSelectedBtn').disabled = selectedTests.size === 0;
|
||||
}
|
||||
|
||||
async function runSelected() {
|
||||
if (selectedTests.size === 0) {
|
||||
alert('No tests selected');
|
||||
return;
|
||||
}
|
||||
|
||||
const testIds = Array.from(selectedTests);
|
||||
|
||||
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();
|
||||
|
||||
// Build URL params to preserve filter state in runner
|
||||
const params = new URLSearchParams();
|
||||
params.set('run', data.run_id);
|
||||
|
||||
// Pass filter state
|
||||
if (filters.search) params.set('search', filters.search);
|
||||
if (!filters.domains.has('all')) {
|
||||
params.set('domains', Array.from(filters.domains).join(','));
|
||||
}
|
||||
if (!filters.modules.has('all')) {
|
||||
params.set('modules', Array.from(filters.modules).join(','));
|
||||
}
|
||||
if (!filters.status.has('all')) {
|
||||
params.set('status', Array.from(filters.status).join(','));
|
||||
}
|
||||
|
||||
// Redirect to main runner with filters applied
|
||||
window.location.href = `/tools/tester/?${params.toString()}`;
|
||||
} catch (error) {
|
||||
console.error('Failed to start run:', error);
|
||||
alert('Failed to start test run');
|
||||
}
|
||||
}
|
||||
|
||||
// Search input handler
|
||||
document.getElementById('searchInput').addEventListener('input', (e) => {
|
||||
filters.search = e.target.value;
|
||||
renderTests();
|
||||
});
|
||||
|
||||
// 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');
|
||||
|
||||
const savedEnvId = localStorage.getItem('selectedEnvironment');
|
||||
let selectedEnv = null;
|
||||
|
||||
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('');
|
||||
|
||||
if (selectedEnv) {
|
||||
currentUrl.textContent = selectedEnv.url;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to switch environment:', error);
|
||||
alert('Failed to switch environment');
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to load environments:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Load tests on page load
|
||||
loadEnvironments();
|
||||
loadTests();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
1187
station/tools/tester/templates/filters_v2.html
Normal file
1187
station/tools/tester/templates/filters_v2.html
Normal file
File diff suppressed because it is too large
Load Diff
909
station/tools/tester/templates/index.html
Normal file
909
station/tools/tester/templates/index.html
Normal file
@@ -0,0 +1,909 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user