Skip to main content
Glama
ooples

MCP Console Automation Server

HTMLReporter.ts12.7 kB
/** * HTML Reporter * * Generates beautiful HTML test reports with interactive features. */ import { promises as fs } from 'fs'; import * as path from 'path'; import { TestReport } from '../../types/test-framework.js'; import { TestReporter } from '../TestReporter.js'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); export class HTMLReporter extends TestReporter { constructor() { super('html'); } protected getFileExtension(): string { return '.html'; } protected async formatReport(report: TestReport): Promise<string> { const { summary, suiteResults, generatedAt } = report; // Try to load template, fall back to inline if not available let template: string; try { const templatePath = path.join( __dirname, '../templates/report-template.html' ); template = await fs.readFile(templatePath, 'utf-8'); } catch { template = this.getInlineTemplate(); } // Generate suite HTML const suitesHtml = suiteResults .map((suiteResult) => this.generateSuiteHtml(suiteResult)) .join('\n'); // Replace template variables const html = template .replace('{{TITLE}}', 'Test Report') .replace('{{GENERATED_AT}}', new Date(generatedAt).toLocaleString()) .replace('{{TOTAL_SUITES}}', summary.totalSuites.toString()) .replace('{{TOTAL_TESTS}}', summary.totalTests.toString()) .replace('{{PASSED}}', summary.passed.toString()) .replace('{{FAILED}}', summary.failed.toString()) .replace('{{SKIPPED}}', summary.skipped.toString()) .replace('{{DURATION}}', this.formatDuration(summary.duration)) .replace('{{PASS_RATE}}', (summary.passRate * 100).toFixed(1)) .replace( '{{PASS_RATE_CLASS}}', summary.passRate >= 0.8 ? 'success' : summary.passRate >= 0.5 ? 'warning' : 'error' ) .replace('{{SUITES}}', suitesHtml); return html; } private generateSuiteHtml(suiteResult: any): string { const statusClass = suiteResult.failed > 0 ? 'error' : suiteResult.skipped > 0 ? 'warning' : 'success'; const testsHtml = suiteResult.tests .map((testResult: any) => this.generateTestHtml(testResult)) .join('\n'); return ` <div class="suite ${statusClass}"> <div class="suite-header" onclick="toggleSuite(this)"> <div class="suite-title"> <span class="status-icon">${this.getStatusIcon(statusClass)}</span> <strong>${this.escapeHtml(suiteResult.suite.name)}</strong> <span class="suite-stats"> ${suiteResult.passed}/${suiteResult.totalTests} passed </span> </div> <div class="suite-meta"> ${this.formatDuration(suiteResult.duration)} <span class="toggle-icon">▼</span> </div> </div> <div class="suite-description"> ${this.escapeHtml(suiteResult.suite.description || '')} </div> <div class="suite-body"> ${testsHtml} </div> </div> `; } private generateTestHtml(testResult: any): string { const statusClass = this.getTestStatusClass(testResult.status); const statusIcon = this.getStatusIcon(statusClass); const assertionsHtml = testResult.assertions .map((assertion: any) => this.generateAssertionHtml(assertion)) .join('\n'); const errorHtml = testResult.error ? ` <div class="error-box"> <strong>Error:</strong> ${this.escapeHtml(testResult.error.message)} ${testResult.error.stack ? `<pre>${this.escapeHtml(testResult.error.stack)}</pre>` : ''} </div> ` : ''; return ` <div class="test ${statusClass}"> <div class="test-header" onclick="toggleTest(this)"> <div class="test-title"> <span class="status-icon">${statusIcon}</span> ${this.escapeHtml(testResult.test.name)} ${testResult.retries ? `<span class="retry-badge">↻${testResult.retries}</span>` : ''} </div> <div class="test-meta"> ${this.formatDuration(testResult.duration)} ${assertionsHtml ? '<span class="toggle-icon">▼</span>' : ''} </div> </div> ${testResult.test.description ? `<div class="test-description">${this.escapeHtml(testResult.test.description)}</div>` : ''} ${ assertionsHtml || errorHtml ? ` <div class="test-body"> ${assertionsHtml} ${errorHtml} </div> ` : '' } </div> `; } private generateAssertionHtml(assertion: any): string { const statusClass = assertion.passed ? 'success' : 'error'; const statusIcon = assertion.passed ? '✓' : '✗'; return ` <div class="assertion ${statusClass}"> <span class="status-icon">${statusIcon}</span> <span class="assertion-message">${this.escapeHtml(assertion.message)}</span> </div> `; } private getTestStatusClass(status: string): string { switch (status) { case 'pass': return 'success'; case 'fail': return 'error'; case 'skip': return 'skipped'; case 'timeout': return 'timeout'; default: return 'unknown'; } } private getStatusIcon(statusClass: string): string { switch (statusClass) { case 'success': return '✓'; case 'error': return '✗'; case 'warning': return '⚠'; case 'skipped': return '○'; case 'timeout': return '⏱'; default: return '?'; } } private getInlineTemplate(): string { return `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{{TITLE}}</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background: #f5f5f5; padding: 20px; } .container { max-width: 1200px; margin: 0 auto; } .header { background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-bottom: 20px; } h1 { color: #333; margin-bottom: 10px; } .meta { color: #666; font-size: 14px; } .summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 15px; margin-top: 20px; } .summary-card { background: #f8f9fa; padding: 15px; border-radius: 4px; border-left: 4px solid #ddd; } .summary-card.success { border-left-color: #28a745; } .summary-card.error { border-left-color: #dc3545; } .summary-card.warning { border-left-color: #ffc107; } .summary-card .value { font-size: 32px; font-weight: bold; color: #333; } .summary-card .label { font-size: 12px; color: #666; text-transform: uppercase; margin-top: 5px; } .suite { background: white; margin-bottom: 15px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); overflow: hidden; } .suite.success { border-left: 4px solid #28a745; } .suite.error { border-left: 4px solid #dc3545; } .suite.warning { border-left: 4px solid #ffc107; } .suite-header { padding: 20px; cursor: pointer; background: #fafafa; display: flex; justify-content: space-between; align-items: center; } .suite-header:hover { background: #f0f0f0; } .suite-title { display: flex; align-items: center; gap: 10px; flex: 1; } .suite-stats { color: #666; font-size: 14px; margin-left: auto; margin-right: 20px; } .suite-meta { display: flex; align-items: center; gap: 10px; color: #666; font-size: 14px; } .suite-description { padding: 0 20px 10px 20px; color: #666; font-size: 14px; background: #fafafa; } .suite-body { padding: 15px; display: none; } .suite.open .suite-body { display: block; } .suite.open .toggle-icon { transform: rotate(180deg); } .toggle-icon { transition: transform 0.3s; display: inline-block; } .test { border: 1px solid #e0e0e0; margin-bottom: 10px; border-radius: 4px; overflow: hidden; } .test.success { border-left: 3px solid #28a745; } .test.error { border-left: 3px solid #dc3545; } .test.skipped { border-left: 3px solid #6c757d; opacity: 0.7; } .test.timeout { border-left: 3px solid #ff9800; } .test-header { padding: 12px 15px; cursor: pointer; background: #fafafa; display: flex; justify-content: space-between; align-items: center; } .test-header:hover { background: #f0f0f0; } .test-title { display: flex; align-items: center; gap: 8px; } .test-meta { display: flex; align-items: center; gap: 10px; color: #666; font-size: 13px; } .test-description { padding: 8px 15px; background: #f9f9f9; color: #666; font-size: 13px; border-top: 1px solid #e0e0e0; } .test-body { padding: 15px; background: #f9f9f9; display: none; border-top: 1px solid #e0e0e0; } .test.open .test-body { display: block; } .test.open .toggle-icon { transform: rotate(180deg); } .status-icon { font-size: 16px; font-weight: bold; } .success .status-icon { color: #28a745; } .error .status-icon { color: #dc3545; } .warning .status-icon { color: #ffc107; } .skipped .status-icon { color: #6c757d; } .timeout .status-icon { color: #ff9800; } .retry-badge { background: #ff9800; color: white; padding: 2px 6px; border-radius: 3px; font-size: 11px; font-weight: bold; } .assertion { padding: 8px; margin-bottom: 5px; border-radius: 4px; display: flex; align-items: center; gap: 8px; } .assertion.success { background: #d4edda; } .assertion.error { background: #f8d7da; } .assertion-message { font-size: 13px; } .error-box { background: #f8d7da; border: 1px solid #f5c6cb; padding: 12px; border-radius: 4px; margin-top: 10px; } .error-box strong { color: #721c24; display: block; margin-bottom: 8px; } .error-box pre { background: white; padding: 10px; border-radius: 4px; overflow-x: auto; font-size: 12px; margin-top: 8px; } </style> </head> <body> <div class="container"> <div class="header"> <h1>{{TITLE}}</h1> <div class="meta">Generated: {{GENERATED_AT}}</div> <div class="summary"> <div class="summary-card"> <div class="value">{{TOTAL_SUITES}}</div> <div class="label">Test Suites</div> </div> <div class="summary-card"> <div class="value">{{TOTAL_TESTS}}</div> <div class="label">Total Tests</div> </div> <div class="summary-card success"> <div class="value">{{PASSED}}</div> <div class="label">Passed</div> </div> <div class="summary-card error"> <div class="value">{{FAILED}}</div> <div class="label">Failed</div> </div> <div class="summary-card warning"> <div class="value">{{SKIPPED}}</div> <div class="label">Skipped</div> </div> <div class="summary-card"> <div class="value">{{DURATION}}</div> <div class="label">Duration</div> </div> <div class="summary-card {{PASS_RATE_CLASS}}"> <div class="value">{{PASS_RATE}}%</div> <div class="label">Pass Rate</div> </div> </div> </div> <div class="suites"> {{SUITES}} </div> </div> <script> function toggleSuite(header) { const suite = header.closest('.suite'); suite.classList.toggle('open'); } function toggleTest(header) { const test = header.closest('.test'); test.classList.toggle('open'); } // Auto-open failed suites document.addEventListener('DOMContentLoaded', function() { document.querySelectorAll('.suite.error').forEach(suite => { suite.classList.add('open'); }); }); </script> </body> </html>`; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ooples/mcp-console-automation'

If you have feedback or need assistance with the MCP directory API, please join our Discord server