Migrated from design-system-swarm with fresh git history.
Old project history preserved in /home/overbits/apps/design-system-swarm
Core components:
- MCP Server (Python FastAPI with mcp 1.23.1)
- Claude Plugin (agents, commands, skills, strategies, hooks, core)
- DSS Backend (dss-mvp1 - token translation, Figma sync)
- Admin UI (Node.js/React)
- Server (Node.js/Express)
- Storybook integration (dss-mvp1/.storybook)
Self-contained configuration:
- All paths relative or use DSS_BASE_PATH=/home/overbits/dss
- PYTHONPATH configured for dss-mvp1 and dss-claude-plugin
- .env file with all configuration
- Claude plugin uses ${CLAUDE_PLUGIN_ROOT} for portability
Migration completed: $(date)
🤖 Clean migration with full functionality preserved
353 lines
13 KiB
JavaScript
353 lines
13 KiB
JavaScript
/**
|
|
* 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 = `
|
|
<!-- Summary Stats -->
|
|
<div style="background-color: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px; padding: 16px; margin-bottom: 16px;">
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
|
|
<h4 style="font-size: 12px; font-weight: 600;">Test Summary</h4>
|
|
${summary.failed === 0 ? ComponentHelpers.createBadge('All Tests Passed', 'success') : ComponentHelpers.createBadge(`${summary.failed} Failed`, 'error')}
|
|
</div>
|
|
|
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 16px; font-size: 11px; margin-bottom: 12px;">
|
|
<div style="text-align: center;">
|
|
<div style="font-size: 24px; font-weight: 600; color: var(--vscode-text);">${summary.total}</div>
|
|
<div style="color: var(--vscode-text-dim); margin-top: 4px;">Total Tests</div>
|
|
</div>
|
|
<div style="text-align: center;">
|
|
<div style="font-size: 24px; font-weight: 600; color: #89d185;">${summary.passed}</div>
|
|
<div style="color: var(--vscode-text-dim); margin-top: 4px;">Passed</div>
|
|
</div>
|
|
<div style="text-align: center;">
|
|
<div style="font-size: 24px; font-weight: 600; color: #f48771;">${summary.failed}</div>
|
|
<div style="color: var(--vscode-text-dim); margin-top: 4px;">Failed</div>
|
|
</div>
|
|
<div style="text-align: center;">
|
|
<div style="font-size: 24px; font-weight: 600; color: #ffbf00;">${summary.skipped}</div>
|
|
<div style="color: var(--vscode-text-dim); margin-top: 4px;">Skipped</div>
|
|
</div>
|
|
<div style="text-align: center;">
|
|
<div style="font-size: 24px; font-weight: 600; color: var(--vscode-text);">${passRate}%</div>
|
|
<div style="color: var(--vscode-text-dim); margin-top: 4px;">Pass Rate</div>
|
|
</div>
|
|
<div style="text-align: center;">
|
|
<div style="font-size: 24px; font-weight: 600; color: var(--vscode-text);">${summary.duration}s</div>
|
|
<div style="color: var(--vscode-text-dim); margin-top: 4px;">Duration</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="font-size: 10px; color: var(--vscode-text-dim);">
|
|
Last run: ${ComponentHelpers.formatRelativeTime(new Date(timestamp))}
|
|
</div>
|
|
</div>
|
|
|
|
${coverage ? `
|
|
<!-- Coverage Stats -->
|
|
<div style="background-color: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px; padding: 16px; margin-bottom: 16px;">
|
|
<h4 style="font-size: 12px; font-weight: 600; margin-bottom: 12px;">Code Coverage</h4>
|
|
|
|
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px;">
|
|
${this.renderCoverageBar('Lines', coverage.lines)}
|
|
${this.renderCoverageBar('Functions', coverage.functions)}
|
|
${this.renderCoverageBar('Branches', coverage.branches)}
|
|
${this.renderCoverageBar('Statements', coverage.statements)}
|
|
</div>
|
|
</div>
|
|
` : ''}
|
|
|
|
<!-- Test Suites -->
|
|
<div style="background-color: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px; padding: 16px;">
|
|
<h4 style="font-size: 12px; font-weight: 600; margin-bottom: 12px;">Test Suites</h4>
|
|
|
|
${suites.map(suite => this.renderSuite(suite)).join('')}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
renderCoverageBar(label, percentage) {
|
|
let color = '#f48771'; // Red
|
|
if (percentage >= 80) color = '#89d185'; // Green
|
|
else if (percentage >= 60) color = '#ffbf00'; // Yellow
|
|
|
|
return `
|
|
<div>
|
|
<div style="display: flex; justify-content: space-between; margin-bottom: 4px; font-size: 11px;">
|
|
<span>${label}</span>
|
|
<span style="font-weight: 600;">${percentage}%</span>
|
|
</div>
|
|
<div style="height: 8px; background-color: var(--vscode-bg); border-radius: 4px; overflow: hidden;">
|
|
<div style="height: 100%; width: ${percentage}%; background-color: ${color}; transition: width 0.3s;"></div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
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 `
|
|
<div style="margin-bottom: 16px; border: 1px solid var(--vscode-border); border-radius: 4px; overflow: hidden;">
|
|
<div
|
|
style="padding: 12px; background-color: var(--vscode-bg); cursor: pointer; display: flex; justify-content: space-between; align-items: center;"
|
|
onclick="document.getElementById('${suiteId}').style.display = document.getElementById('${suiteId}').style.display === 'none' ? 'block' : 'none'"
|
|
>
|
|
<div>
|
|
<div style="font-size: 12px; font-weight: 600; margin-bottom: 4px;">${ComponentHelpers.escapeHtml(suite.name)}</div>
|
|
<div style="font-size: 10px; color: var(--vscode-text-dim);">
|
|
${passedCount} passed, ${failedCount} failed of ${suite.tests.length} tests
|
|
</div>
|
|
</div>
|
|
<div style="font-size: 18px;">▼</div>
|
|
</div>
|
|
|
|
<div id="${suiteId}" style="display: none;">
|
|
${suite.tests.map(test => this.renderTest(test)).join('')}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
renderTest(test) {
|
|
const icon = this.getStatusIcon(test.status);
|
|
const badge = this.getStatusBadge(test.status);
|
|
|
|
return `
|
|
<div style="padding: 12px; border-top: 1px solid var(--vscode-border); display: flex; justify-content: space-between; align-items: start;">
|
|
<div style="flex: 1;">
|
|
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 4px;">
|
|
<span style="font-size: 14px;">${icon}</span>
|
|
<span style="font-size: 11px; font-family: 'Courier New', monospace;">${ComponentHelpers.escapeHtml(test.name)}</span>
|
|
${badge}
|
|
</div>
|
|
|
|
${test.error ? `
|
|
<div style="margin-top: 8px; padding: 8px; background-color: rgba(244, 135, 113, 0.1); border-left: 3px solid #f48771; border-radius: 2px;">
|
|
<div style="font-size: 10px; font-family: 'Courier New', monospace; color: #f48771;">
|
|
${ComponentHelpers.escapeHtml(test.error)}
|
|
</div>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
|
|
<div style="font-size: 10px; color: var(--vscode-text-dim); white-space: nowrap; margin-left: 12px;">
|
|
${test.duration}s
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
render() {
|
|
this.innerHTML = `
|
|
<div style="padding: 16px; height: 100%; display: flex; flex-direction: column;">
|
|
<div style="margin-bottom: 16px; display: flex; gap: 12px; align-items: center; justify-content: flex-end;">
|
|
<label style="display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--vscode-text);">
|
|
<input type="checkbox" id="auto-refresh-tests" />
|
|
Auto-refresh
|
|
</label>
|
|
<button id="refresh-tests-btn" class="button" style="padding: 4px 12px; font-size: 11px;">
|
|
🔄 Refresh
|
|
</button>
|
|
<button id="run-tests-btn" class="button" style="padding: 4px 12px; font-size: 11px;">
|
|
🧪 Run Tests
|
|
</button>
|
|
</div>
|
|
|
|
<div id="test-results-content" style="flex: 1; overflow-y: auto;">
|
|
${ComponentHelpers.renderLoading('Loading test results...')}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
customElements.define('ds-test-results', DSTestResults);
|
|
|
|
export default DSTestResults;
|