/** * ds-test-results.js * Test results viewer with polling for Jest/test runner output */ import toolBridge from '../../services/tool-bridge.js'; import { ComponentHelpers } from '../../utils/component-helpers.js'; class DSTestResults extends HTMLElement { constructor() { super(); this.testResults = null; this.isRunning = false; this.pollInterval = null; this.autoRefresh = false; } async connectedCallback() { this.render(); this.setupEventListeners(); await this.loadTestResults(); } disconnectedCallback() { if (this.pollInterval) { clearInterval(this.pollInterval); } } setupEventListeners() { const runBtn = this.querySelector('#run-tests-btn'); if (runBtn) { runBtn.addEventListener('click', () => this.runTests()); } const refreshBtn = this.querySelector('#refresh-tests-btn'); if (refreshBtn) { refreshBtn.addEventListener('click', () => this.loadTestResults()); } const autoRefreshToggle = this.querySelector('#auto-refresh-tests'); if (autoRefreshToggle) { autoRefreshToggle.addEventListener('change', (e) => { this.autoRefresh = e.target.checked; if (this.autoRefresh) { this.pollInterval = setInterval(() => this.loadTestResults(), 3000); } else { if (this.pollInterval) { clearInterval(this.pollInterval); this.pollInterval = null; } } }); } } /** * Load test results from localStorage or file system * In a real implementation, this would call an MCP tool to read test output files */ async loadTestResults() { const content = this.querySelector('#test-results-content'); if (!content) return; try { // Try to load from localStorage (mock data for now) const stored = localStorage.getItem('ds-test-results'); if (stored) { this.testResults = JSON.parse(stored); this.renderResults(); } else { // No results yet content.innerHTML = ComponentHelpers.renderEmpty( 'No test results available. Run tests to see results.', '🧪' ); } } catch (error) { console.error('Failed to load test results:', error); content.innerHTML = ComponentHelpers.renderError('Failed to load test results', error); } } /** * Run tests (would call npm test or similar via MCP) */ async runTests() { if (this.isRunning) return; this.isRunning = true; const runBtn = this.querySelector('#run-tests-btn'); if (runBtn) { runBtn.disabled = true; runBtn.textContent = '🧪 Running Tests...'; } const content = this.querySelector('#test-results-content'); if (content) { content.innerHTML = ComponentHelpers.renderLoading('Running tests...'); } try { // MVP1: Execute real npm test command via MCP // Note: This requires project configuration with test scripts const context = toolBridge.getContext(); // Call backend API to run tests // The backend will execute `npm test` and return parsed results const response = await fetch('/api/test/run', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ projectId: context.projectId, testCommand: 'npm test' }) }); if (!response.ok) { throw new Error(`Test execution failed: ${response.statusText}`); } const testResults = await response.json(); // Validate results structure if (!testResults || !testResults.summary) { throw new Error('Invalid test results format'); } this.testResults = { ...testResults, timestamp: new Date().toISOString() }; // Save to localStorage for offline viewing localStorage.setItem('ds-test-results', JSON.stringify(this.testResults)); this.renderResults(); ComponentHelpers.showToast?.( `Tests completed: ${this.testResults.summary.passed}/${this.testResults.summary.total} passed`, this.testResults.summary.failed > 0 ? 'error' : 'success' ); } catch (error) { console.error('Failed to run tests:', error); ComponentHelpers.showToast?.(`Test execution failed: ${error.message}`, 'error'); if (content) { content.innerHTML = ComponentHelpers.renderError('Test execution failed', error); } } finally { this.isRunning = false; if (runBtn) { runBtn.disabled = false; runBtn.textContent = '🧪 Run Tests'; } } } getStatusIcon(status) { const icons = { passed: '✅', failed: '❌', skipped: '⏭️' }; return icons[status] || '⚪'; } getStatusBadge(status) { const types = { passed: 'success', failed: 'error', skipped: 'warning' }; return ComponentHelpers.createBadge(status, types[status] || 'info'); } renderResults() { const content = this.querySelector('#test-results-content'); if (!content || !this.testResults) return; const { summary, suites, coverage, timestamp } = this.testResults; // Calculate pass rate const passRate = ((summary.passed / summary.total) * 100).toFixed(1); content.innerHTML = `

Test Summary

${summary.failed === 0 ? ComponentHelpers.createBadge('All Tests Passed', 'success') : ComponentHelpers.createBadge(`${summary.failed} Failed`, 'error')}
${summary.total}
Total Tests
${summary.passed}
Passed
${summary.failed}
Failed
${summary.skipped}
Skipped
${passRate}%
Pass Rate
${summary.duration}s
Duration
Last run: ${ComponentHelpers.formatRelativeTime(new Date(timestamp))}
${coverage ? `

Code Coverage

${this.renderCoverageBar('Lines', coverage.lines)} ${this.renderCoverageBar('Functions', coverage.functions)} ${this.renderCoverageBar('Branches', coverage.branches)} ${this.renderCoverageBar('Statements', coverage.statements)}
` : ''}

Test Suites

${suites.map(suite => this.renderSuite(suite)).join('')}
`; } renderCoverageBar(label, percentage) { let color = '#f48771'; // Red if (percentage >= 80) color = '#89d185'; // Green else if (percentage >= 60) color = '#ffbf00'; // Yellow return `
${label} ${percentage}%
`; } renderSuite(suite) { const suiteId = `suite-${suite.name.replace(/\s+/g, '-').toLowerCase()}`; const passedCount = suite.tests.filter(t => t.status === 'passed').length; const failedCount = suite.tests.filter(t => t.status === 'failed').length; return `
${ComponentHelpers.escapeHtml(suite.name)}
${passedCount} passed, ${failedCount} failed of ${suite.tests.length} tests
`; } renderTest(test) { const icon = this.getStatusIcon(test.status); const badge = this.getStatusBadge(test.status); return `
${icon} ${ComponentHelpers.escapeHtml(test.name)} ${badge}
${test.error ? `
${ComponentHelpers.escapeHtml(test.error)}
` : ''}
${test.duration}s
`; } render() { this.innerHTML = `
${ComponentHelpers.renderLoading('Loading test results...')}
`; } } customElements.define('ds-test-results', DSTestResults); export default DSTestResults;