soleprint init commit

This commit is contained in:
buenosairesam
2025-12-24 05:38:37 -03:00
commit 329c401ff5
96 changed files with 11564 additions and 0 deletions

View 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">&#9660;</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">&#9660;</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">&#9660;</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: '&#10003;',
failed: '&#10007;',
error: '&#10007;',
skipped: '&#8722;',
running: '&#9679;',
};
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>