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>
163 lines
5.2 KiB
JavaScript
163 lines
5.2 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,
|
|
|
|
// 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`;
|
|
}
|
|
};
|
|
}
|