feat: add pytest testing dashboard with run history and statistics

Add a new Testing Dashboard page that replaces the old Testing Hub with
pytest integration:

- Database models for test runs, results, and collections (TestRun,
  TestResult, TestCollection)
- Test runner service that executes pytest with JSON reporting and
  stores results in the database
- REST API endpoints for running tests, viewing history, and statistics
- Dashboard UI showing pass rates, trends, tests by category, and top
  failing tests
- Alembic migration for the new test_* tables

The dashboard allows admins to:
- Run pytest directly from the UI
- View test run history with pass/fail statistics
- See trend data across recent runs
- Identify frequently failing tests
- Collect test information without running

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-12 23:04:41 +01:00
parent e6ed4a14dd
commit e3a10b4a53
9 changed files with 1539 additions and 2 deletions

View File

@@ -0,0 +1,162 @@
/**
* 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,
// 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();
},
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;
testingDashboardLog.info('Running tests:', testPath);
try {
const result = await apiClient.post('/admin/tests/run', {
test_path: testPath
});
testingDashboardLog.info('Test run completed:', result);
// Format success message
const status = result.status === 'passed' ? 'All tests passed!' : 'Tests completed with failures.';
this.successMessage = `${status} ${result.passed}/${result.total_tests} passed (${result.pass_rate.toFixed(1)}%) in ${this.formatDuration(result.duration_seconds)}`;
// Reload stats and runs
await this.loadStats();
await this.loadRuns();
// Show toast notification
Utils.showToast(this.successMessage, result.status === 'passed' ? 'success' : 'warning');
// Clear success message after 10 seconds
setTimeout(() => {
this.successMessage = null;
}, 10000);
} catch (err) {
testingDashboardLog.error('Failed to run tests:', err);
this.error = err.message;
Utils.showToast('Failed to run tests: ' + err.message, 'error');
// Redirect to login if unauthorized
if (err.message.includes('Unauthorized')) {
window.location.href = '/admin/login';
}
} finally {
this.running = false;
}
},
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`;
}
};
}