Initial commit: Clean DSS implementation
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
This commit is contained in:
352
admin-ui/js/components/tools/ds-test-results.js
Normal file
352
admin-ui/js/components/tools/ds-test-results.js
Normal file
@@ -0,0 +1,352 @@
|
||||
/**
|
||||
* 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;
|
||||
Reference in New Issue
Block a user