Files
orion/static/admin/js/testing-dashboard.js
Samir Boulahtit 0e6c9e3eea feat: run tests in background with progress polling
Improve the testing dashboard to run pytest in the background:

- Add background task execution using FastAPI's BackgroundTasks
- Create test_runner_tasks.py following existing background task pattern
- API now returns immediately after starting the test run
- Frontend polls for status every 2 seconds until completion
- Show running indicator with elapsed time counter
- Resume polling if user navigates away and returns while tests running
- Tests continue running even if user closes the page

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-12 23:20:26 +01:00

235 lines
7.8 KiB
JavaScript

/**
* Testing Dashboard Component
* Manages the pytest testing dashboard page
*/
// Use centralized logger
const testingDashboardLog = window.LogConfig.createLogger('TESTING-DASHBOARD');
function testingDashboard() {
return {
// Extend base data
...data(),
// Set current page for navigation
currentPage: 'testing',
// Dashboard-specific data
loading: false,
running: false,
collecting: false,
error: null,
successMessage: null,
activeRunId: null,
pollInterval: null,
elapsedTime: 0,
elapsedTimer: null,
// Statistics
stats: {
total_tests: 0,
passed: 0,
failed: 0,
errors: 0,
skipped: 0,
pass_rate: 0,
duration_seconds: 0,
coverage_percent: null,
last_run: null,
last_run_status: null,
total_test_files: 0,
trend: [],
by_category: {},
top_failing: []
},
// Recent runs
runs: [],
async init() {
testingDashboardLog.info('Initializing testing dashboard');
await this.loadStats();
await this.loadRuns();
// Check if there's a running test and resume polling
await this.checkForRunningTests();
},
async checkForRunningTests() {
// Check if there's already a test running
const runningRun = this.runs.find(r => r.status === 'running');
if (runningRun) {
testingDashboardLog.info('Found running test:', runningRun.id);
this.running = true;
this.activeRunId = runningRun.id;
// Calculate elapsed time from when the run started
const startTime = new Date(runningRun.timestamp);
this.elapsedTime = Math.floor((Date.now() - startTime.getTime()) / 1000);
// Start elapsed time counter
this.elapsedTimer = setInterval(() => {
this.elapsedTime++;
}, 1000);
// Start polling for status
this.pollInterval = setInterval(() => this.pollRunStatus(), 2000);
}
},
async loadStats() {
this.loading = true;
this.error = null;
try {
const stats = await apiClient.get('/admin/tests/stats');
this.stats = stats;
testingDashboardLog.info('Stats loaded:', stats);
} catch (err) {
testingDashboardLog.error('Failed to load stats:', err);
this.error = err.message;
// Redirect to login if unauthorized
if (err.message.includes('Unauthorized')) {
window.location.href = '/admin/login';
}
} finally {
this.loading = false;
}
},
async loadRuns() {
try {
const runs = await apiClient.get('/admin/tests/runs?limit=10');
this.runs = runs;
testingDashboardLog.info('Runs loaded:', runs.length);
} catch (err) {
testingDashboardLog.error('Failed to load runs:', err);
// Don't set error - stats are more important
}
},
async runTests(testPath = 'tests') {
this.running = true;
this.error = null;
this.successMessage = null;
this.elapsedTime = 0;
testingDashboardLog.info('Starting tests:', testPath);
try {
// Start the test run (returns immediately)
const result = await apiClient.post('/admin/tests/run', {
test_path: testPath
});
testingDashboardLog.info('Test run started:', result);
this.activeRunId = result.id;
// Start elapsed time counter
this.elapsedTimer = setInterval(() => {
this.elapsedTime++;
}, 1000);
// Start polling for status
this.pollInterval = setInterval(() => this.pollRunStatus(), 2000);
Utils.showToast('Test run started...', 'info');
} catch (err) {
testingDashboardLog.error('Failed to start tests:', err);
this.error = err.message;
this.running = false;
Utils.showToast('Failed to start tests: ' + err.message, 'error');
// Redirect to login if unauthorized
if (err.message.includes('Unauthorized')) {
window.location.href = '/admin/login';
}
}
},
async pollRunStatus() {
if (!this.activeRunId) return;
try {
const run = await apiClient.get(`/admin/tests/runs/${this.activeRunId}`);
if (run.status !== 'running') {
// Test run completed
this.stopPolling();
testingDashboardLog.info('Test run completed:', run);
// Format success message
const status = run.status === 'passed' ? 'All tests passed!' : 'Tests completed with failures.';
this.successMessage = `${status} ${run.passed}/${run.total_tests} passed (${run.pass_rate.toFixed(1)}%) in ${this.formatDuration(run.duration_seconds)}`;
// Reload stats and runs
await this.loadStats();
await this.loadRuns();
// Show toast notification
Utils.showToast(this.successMessage, run.status === 'passed' ? 'success' : 'warning');
// Clear success message after 10 seconds
setTimeout(() => {
this.successMessage = null;
}, 10000);
}
} catch (err) {
testingDashboardLog.error('Failed to poll run status:', err);
// Don't stop polling on error, might be transient
}
},
stopPolling() {
if (this.pollInterval) {
clearInterval(this.pollInterval);
this.pollInterval = null;
}
if (this.elapsedTimer) {
clearInterval(this.elapsedTimer);
this.elapsedTimer = null;
}
this.running = false;
this.activeRunId = null;
},
async collectTests() {
this.collecting = true;
this.error = null;
testingDashboardLog.info('Collecting tests');
try {
const result = await apiClient.post('/admin/tests/collect');
testingDashboardLog.info('Collection completed:', result);
Utils.showToast(`Collected ${result.total_tests} tests from ${result.total_files} files`, 'success');
// Reload stats
await this.loadStats();
} catch (err) {
testingDashboardLog.error('Failed to collect tests:', err);
Utils.showToast('Failed to collect tests: ' + err.message, 'error');
} finally {
this.collecting = false;
}
},
async refresh() {
await this.loadStats();
await this.loadRuns();
},
formatDuration(seconds) {
if (seconds === null || seconds === undefined) return 'N/A';
if (seconds < 1) return `${Math.round(seconds * 1000)}ms`;
if (seconds < 60) return `${seconds.toFixed(1)}s`;
const minutes = Math.floor(seconds / 60);
const secs = Math.round(seconds % 60);
return `${minutes}m ${secs}s`;
}
};
}