test-reporter.ts•9.12 kB
/**
 * Test Report Generator
 * 
 * Generates a comprehensive test report from Vitest results
 */
import { readFileSync, existsSync, writeFileSync } from 'fs';
import { join } from 'path';
interface TestResult {
    name: string;
    status: 'passed' | 'failed' | 'skipped';
    duration: number;
    error?: string;
}
interface TestSuite {
    name: string;
    tests: TestResult[];
    totalTests: number;
    passed: number;
    failed: number;
    skipped: number;
    duration: number;
}
interface TestReport {
    timestamp: string;
    totalSuites: number;
    totalTests: number;
    passed: number;
    failed: number;
    skipped: number;
    duration: number;
    coverage?: {
        lines: number;
        functions: number;
        branches: number;
        statements: number;
    };
    suites: TestSuite[];
}
function generateMarkdownReport(report: TestReport): string {
    const passRate = report.totalTests > 0
        ? ((report.passed / report.totalTests) * 100).toFixed(2)
        : '0';
    let markdown = `# Test Report
**Generated**: ${report.timestamp}
## Summary
| Metric | Value |
|--------|-------|
| Total Test Suites | ${report.totalSuites} |
| Total Tests | ${report.totalTests} |
| ✅ Passed | ${report.passed} |
| ❌ Failed | ${report.failed} |
| ⏭️ Skipped | ${report.skipped} |
| Pass Rate | ${passRate}% |
| Duration | ${(report.duration / 1000).toFixed(2)}s |
`;
    if (report.coverage) {
        markdown += `## Coverage
| Type | Percentage |
|------|------------|
| Lines | ${report.coverage.lines}% |
| Functions | ${report.coverage.functions}% |
| Branches | ${report.coverage.branches}% |
| Statements | ${report.coverage.statements}% |
`;
    }
    if (report.failed > 0) {
        markdown += `## ❌ Failed Tests
`;
        for (const suite of report.suites) {
            const failedTests = suite.tests.filter(t => t.status === 'failed');
            if (failedTests.length > 0) {
                markdown += `### ${suite.name}\n\n`;
                for (const test of failedTests) {
                    markdown += `- **${test.name}** (${test.duration}ms)\n`;
                    if (test.error) {
                        markdown += `  \`\`\`\n  ${test.error}\n  \`\`\`\n`;
                    }
                }
                markdown += '\n';
            }
        }
    }
    markdown += `## Test Suites
`;
    for (const suite of report.suites) {
        const icon = suite.failed > 0 ? '❌' : '✅';
        markdown += `### ${icon} ${suite.name}
- **Tests**: ${suite.totalTests}
- **Passed**: ${suite.passed}
- **Failed**: ${suite.failed}
- **Skipped**: ${suite.skipped}
- **Duration**: ${(suite.duration / 1000).toFixed(2)}s
`;
    }
    markdown += `---
*Generated by In-Memoria Test Reporter*
`;
    return markdown;
}
function generateHtmlReport(report: TestReport): string {
    const passRate = report.totalTests > 0
        ? ((report.passed / report.totalTests) * 100).toFixed(2)
        : '0';
    const statusColor = report.failed === 0 ? '#28a745' : '#dc3545';
    return `<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Test Report - In-Memoria</title>
  <style>
    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
      max-width: 1200px;
      margin: 0 auto;
      padding: 20px;
      background: #f5f5f5;
    }
    .header {
      background: white;
      padding: 30px;
      border-radius: 8px;
      box-shadow: 0 2px 4px rgba(0,0,0,0.1);
      margin-bottom: 20px;
    }
    .header h1 {
      margin: 0 0 10px 0;
      color: #333;
    }
    .timestamp {
      color: #666;
      font-size: 14px;
    }
    .summary {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
      gap: 15px;
      margin-bottom: 20px;
    }
    .metric {
      background: white;
      padding: 20px;
      border-radius: 8px;
      box-shadow: 0 2px 4px rgba(0,0,0,0.1);
    }
    .metric-label {
      font-size: 14px;
      color: #666;
      margin-bottom: 5px;
    }
    .metric-value {
      font-size: 32px;
      font-weight: bold;
      color: ${statusColor};
    }
    .coverage {
      background: white;
      padding: 20px;
      border-radius: 8px;
      box-shadow: 0 2px 4px rgba(0,0,0,0.1);
      margin-bottom: 20px;
    }
    .coverage h2 {
      margin-top: 0;
    }
    .coverage-bar {
      height: 20px;
      background: #e0e0e0;
      border-radius: 10px;
      overflow: hidden;
      margin: 10px 0;
    }
    .coverage-fill {
      height: 100%;
      background: linear-gradient(to right, #28a745, #20c997);
      transition: width 0.3s;
    }
    .suites {
      background: white;
      padding: 20px;
      border-radius: 8px;
      box-shadow: 0 2px 4px rgba(0,0,0,0.1);
    }
    .suite {
      border-left: 4px solid #28a745;
      padding-left: 15px;
      margin-bottom: 20px;
    }
    .suite.failed {
      border-left-color: #dc3545;
    }
    .suite-name {
      font-size: 18px;
      font-weight: bold;
      margin-bottom: 10px;
    }
    .test {
      padding: 8px 0;
      border-bottom: 1px solid #eee;
    }
    .test:last-child {
      border-bottom: none;
    }
    .test.passed { color: #28a745; }
    .test.failed { color: #dc3545; }
    .test.skipped { color: #ffc107; }
    .error {
      background: #f8f9fa;
      padding: 10px;
      border-left: 3px solid #dc3545;
      margin-top: 5px;
      font-family: monospace;
      font-size: 12px;
      white-space: pre-wrap;
    }
  </style>
</head>
<body>
  <div class="header">
    <h1>Test Report</h1>
    <div class="timestamp">${report.timestamp}</div>
  </div>
  <div class="summary">
    <div class="metric">
      <div class="metric-label">Pass Rate</div>
      <div class="metric-value">${passRate}%</div>
    </div>
    <div class="metric">
      <div class="metric-label">Total Tests</div>
      <div class="metric-value">${report.totalTests}</div>
    </div>
    <div class="metric">
      <div class="metric-label">Passed</div>
      <div class="metric-value" style="color: #28a745;">${report.passed}</div>
    </div>
    <div class="metric">
      <div class="metric-label">Failed</div>
      <div class="metric-value" style="color: #dc3545;">${report.failed}</div>
    </div>
  </div>
  ${report.coverage ? `
  <div class="coverage">
    <h2>Coverage</h2>
    <div>
      <div class="metric-label">Lines</div>
      <div class="coverage-bar">
        <div class="coverage-fill" style="width: ${report.coverage.lines}%"></div>
      </div>
      <div>${report.coverage.lines}%</div>
    </div>
    <div>
      <div class="metric-label">Functions</div>
      <div class="coverage-bar">
        <div class="coverage-fill" style="width: ${report.coverage.functions}%"></div>
      </div>
      <div>${report.coverage.functions}%</div>
    </div>
    <div>
      <div class="metric-label">Branches</div>
      <div class="coverage-bar">
        <div class="coverage-fill" style="width: ${report.coverage.branches}%"></div>
      </div>
      <div>${report.coverage.branches}%</div>
    </div>
  </div>
  ` : ''}
  <div class="suites">
    <h2>Test Suites</h2>
    ${report.suites.map(suite => `
      <div class="suite ${suite.failed > 0 ? 'failed' : ''}">
        <div class="suite-name">${suite.failed > 0 ? '❌' : '✅'} ${suite.name}</div>
        <div>Tests: ${suite.totalTests} | Passed: ${suite.passed} | Failed: ${suite.failed} | Duration: ${(suite.duration / 1000).toFixed(2)}s</div>
        ${suite.tests.filter(t => t.status === 'failed').map(test => `
          <div class="test failed">
            ❌ ${test.name} (${test.duration}ms)
            ${test.error ? `<div class="error">${test.error}</div>` : ''}
          </div>
        `).join('')}
      </div>
    `).join('')}
  </div>
</body>
</html>`;
}
// Export for use as a module
export { TestReport, generateMarkdownReport, generateHtmlReport };
// CLI usage
if (import.meta.url === `file://${process.argv[1]}`) {
    const reportPath = process.argv[2] || './test-results.json';
    const outputFormat = process.argv[3] || 'markdown';
    if (!existsSync(reportPath)) {
        console.error(`Error: Report file not found: ${reportPath}`);
        process.exit(1);
    }
    const reportData: TestReport = JSON.parse(readFileSync(reportPath, 'utf-8'));
    if (outputFormat === 'markdown') {
        const markdown = generateMarkdownReport(reportData);
        const outputPath = reportPath.replace('.json', '.md');
        writeFileSync(outputPath, markdown);
        console.log(`✅ Markdown report generated: ${outputPath}`);
    } else if (outputFormat === 'html') {
        const html = generateHtmlReport(reportData);
        const outputPath = reportPath.replace('.json', '.html');
        writeFileSync(outputPath, html);
        console.log(`✅ HTML report generated: ${outputPath}`);
    } else {
        console.error(`Unknown format: ${outputFormat}. Use 'markdown' or 'html'.`);
        process.exit(1);
    }
}