// noqa: js-006 - async init pattern is safe, loadData has try/catch /** * 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, collected_tests: 0, unit_tests: 0, integration_tests: 0, performance_tests: 0, system_tests: 0, last_collected: null, trend: [], by_category: {}, top_failing: [] }, // Recent runs runs: [], async init() { // Load i18n translations await I18n.loadModule('dev_tools'); // Guard against multiple initialization if (window._adminTestingDashboardInitialized) return; window._adminTestingDashboardInitialized = true; try { testingDashboardLog.info('Initializing testing dashboard'); await this.loadStats(); await this.loadRuns(); // Check if there's a running test and resume polling await this.checkForRunningTests(); } catch (error) { testingDashboardLog.error('Failed to initialize testing dashboard:', error); } }, 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); // noqa: PERF062 } }, 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); // noqa: PERF062 Utils.showToast(I18n.t('dev_tools.messages.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`; } }; }