chrome-extension-test-framework
Version:
Universal testing framework for Chrome extensions - Fast static analysis without browser dependencies
543 lines (495 loc) • 20.6 kB
JavaScript
/**
* Reporter - テスト結果のレポート生成
*/
const fs = require('fs');
const path = require('path');
const SummaryReporter = require('./SummaryReporter');
const SeverityManager = require('./SeverityManager');
const ImprovedErrorMessages = require('./ImprovedErrorMessages');
class Reporter {
constructor(config) {
this.config = config;
this.reporters = new Map();
this.severityManager = new SeverityManager(config);
this.errorMessages = new ImprovedErrorMessages();
this.registerBuiltinReporters();
}
/**
* ビルトインレポーターを登録
*/
registerBuiltinReporters() {
// コンソールレポーター
this.register('console', {
generate: (results) => {
const isQuiet = this.config.quiet;
const summary = results.summary;
// Quiet mode: errors and warnings only
if (isQuiet) {
if (summary.failed > 0 || (results.warnings && results.warnings.length > 0)) {
// Summary line for CI
console.log(`cext-test: ${summary.failed} errors, ${summary.warningCount || 0} warnings`);
// Errors
if (summary.errors && summary.errors.length > 0) {
summary.errors.forEach(error => {
if (error.error && error.error.enhanced) {
const enhanced = error.error.enhanced;
console.error(`ERROR: [${error.suite}] ${error.test} - ${enhanced.message}`);
} else {
const message = error.error ? error.error.message : error.message;
console.error(`ERROR: [${error.suite}] ${error.test} - ${message}`);
}
});
}
// Warnings
if (results.warnings && results.warnings.length > 0) {
const uniqueWarnings = [...new Set(results.warnings)];
uniqueWarnings.forEach(warning => {
console.warn(`WARNING: ${warning}`);
});
}
}
return;
}
// Normal mode
console.log('\n' + '═'.repeat(60));
console.log('📊 テスト結果サマリー');
console.log('═'.repeat(60));
console.log(`✅ 成功: ${summary.passed}`);
console.log(`❌ 失敗: ${summary.failed}`);
console.log(`⏭️ スキップ: ${summary.skipped}`);
console.log(`📈 成功率: ${summary.successRate}%`);
console.log(`⏱️ 実行時間: ${results.duration}ms`);
if (summary.errors && summary.errors.length > 0) {
console.log('\n❌ エラー詳細:');
summary.errors.forEach(error => {
console.log(`\n[${error.suite}] ${error.test}`);
if (error.error.enhanced) {
// エラーハンドラーで拡張されたエラー情報を表示
const enhanced = error.error.enhanced;
console.log(` ${enhanced.prefix} ${enhanced.message}`);
console.log(` Category: ${enhanced.category}`);
if (enhanced.context.file) {
console.log(` File: ${enhanced.context.file}:${enhanced.context.line || '?'}`);
}
if (enhanced.suggestions.length > 0) {
console.log(' Suggestions:');
enhanced.suggestions.forEach(s => console.log(` • ${s}`));
}
} else {
console.log(` ${error.error.message}`);
}
});
}
console.log('\n' + '═'.repeat(60));
// サマリーを表示(verboseモードでない場合)
if (!this.config.verbose && !this.config.quiet) {
const summaryReporter = new SummaryReporter({
detailed: false,
quiet: false
});
const summaryData = summaryReporter.generateSummary(results);
console.log(summaryReporter.formatForConsole(summaryData));
}
}
});
// JSONレポーター
this.register('json', {
generate: async (results) => {
const outputPath = path.join(
this.config.output.directory,
`${this.config.output.filename || 'test-report'}.json`
);
await this.ensureDirectory(this.config.output.directory);
const jsonContent = JSON.stringify(results, null, 2);
fs.writeFileSync(outputPath, jsonContent);
console.log(`📄 JSONレポート生成: ${outputPath}`);
}
});
// HTMLレポーター
this.register('html', {
generate: async (results) => {
const outputPath = path.join(
this.config.output.directory,
`${this.config.output.filename || 'test-report'}.html`
);
await this.ensureDirectory(this.config.output.directory);
const htmlContent = this.generateHTML(results);
fs.writeFileSync(outputPath, htmlContent);
console.log(`🌐 HTMLレポート生成: ${outputPath}`);
}
});
// Markdownレポーター
this.register('markdown', {
generate: async (results) => {
const outputPath = path.join(
this.config.output.directory,
`${this.config.output.filename || 'test-report'}.md`
);
await this.ensureDirectory(this.config.output.directory);
const mdContent = this.generateMarkdown(results);
fs.writeFileSync(outputPath, mdContent);
console.log(`📝 Markdownレポート生成: ${outputPath}`);
}
});
}
/**
* レポーターを登録
*/
register(name, reporter) {
this.reporters.set(name, reporter);
}
/**
* レポートを生成
*/
async generate(results) {
const formats = Array.isArray(this.config.output.format)
? this.config.output.format
: [this.config.output.format];
for (const format of formats) {
const reporter = this.reporters.get(format);
if (reporter) {
await reporter.generate(results);
} else {
console.warn(`⚠️ Unknown report format: ${format}`);
}
}
}
/**
* ディレクトリを確保
*/
async ensureDirectory(dirPath) {
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
}
/**
* HTML形式のレポートを生成
*/
generateHTML(results) {
const summary = results.summary;
const statusColors = {
passed: '#28a745',
failed: '#dc3545',
skipped: '#6c757d'
};
return `<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chrome Extension Test Report</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
color: #333;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background: #f5f5f5;
}
.header {
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
margin-bottom: 30px;
}
h1 {
margin: 0 0 20px 0;
color: #2c3e50;
}
.summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.summary-card {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
text-align: center;
}
.summary-card h3 {
margin: 0 0 10px 0;
color: #666;
font-size: 14px;
text-transform: uppercase;
}
.summary-card .value {
font-size: 36px;
font-weight: bold;
margin: 0;
}
.success-rate {
color: ${summary.successRate >= 80 ? '#28a745' : summary.successRate >= 50 ? '#ffc107' : '#dc3545'};
}
.suite {
background: white;
margin-bottom: 20px;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
overflow: hidden;
}
.suite-header {
background: #f8f9fa;
padding: 15px 20px;
border-bottom: 1px solid #dee2e6;
cursor: pointer;
}
.suite-header h2 {
margin: 0;
font-size: 18px;
color: #495057;
}
.test {
padding: 10px 20px;
border-bottom: 1px solid #f0f0f0;
display: flex;
align-items: center;
gap: 10px;
}
.test:last-child {
border-bottom: none;
}
.test-status {
width: 20px;
height: 20px;
border-radius: 50%;
flex-shrink: 0;
}
.test-status.passed {
background: #28a745;
}
.test-status.failed {
background: #dc3545;
}
.test-status.skipped {
background: #6c757d;
}
.test-name {
flex: 1;
font-weight: 500;
}
.test-duration {
color: #666;
font-size: 14px;
}
.error {
background: #f8d7da;
color: #721c24;
padding: 15px;
margin: 10px 20px;
border-radius: 6px;
font-size: 13px;
border-left: 4px solid #dc3545;
}
.error-header {
font-weight: bold;
margin-bottom: 10px;
}
.error-prefix {
font-size: 16px;
margin-right: 8px;
}
.error-category {
color: #666;
font-size: 12px;
margin-left: 10px;
}
.error-location {
color: #555;
margin: 5px 0;
font-family: monospace;
}
.error-suggestions {
margin-top: 10px;
background: #fff3cd;
padding: 10px;
border-radius: 4px;
}
.error-suggestions ul {
margin: 5px 0 0 20px;
padding: 0;
}
.error-code {
margin-top: 10px;
background: #f0f0f0;
padding: 10px;
border-radius: 4px;
overflow-x: auto;
}
.error-code pre {
margin: 0;
font-family: monospace;
font-size: 12px;
line-height: 1.4;
}
.footer {
text-align: center;
margin-top: 40px;
color: #666;
font-size: 14px;
}
</style>
</head>
<body>
<div class="header">
<h1>🧪 Chrome Extension Test Report</h1>
<p>Framework Version: ${results.framework} | Generated: ${new Date(results.timestamp).toLocaleString()}</p>
</div>
<div class="summary">
<div class="summary-card">
<h3>Total Tests</h3>
<p class="value">${summary.total}</p>
</div>
<div class="summary-card">
<h3>Passed</h3>
<p class="value" style="color: #28a745">${summary.passed}</p>
</div>
<div class="summary-card">
<h3>Failed</h3>
<p class="value" style="color: #dc3545">${summary.failed}</p>
</div>
<div class="summary-card">
<h3>Success Rate</h3>
<p class="value success-rate">${summary.successRate}%</p>
</div>
</div>
${results.suites.map(suite => `
<div class="suite">
<div class="suite-header">
<h2>${suite.name}</h2>
${suite.description ? `<p style="margin: 5px 0 0 0; color: #666">${suite.description}</p>` : ''}
</div>
<div class="suite-body">
${suite.tests.map(test => `
<div class="test">
<div class="test-status ${test.status}"></div>
<div class="test-name">${test.name}</div>
<div class="test-duration">${test.duration || 0}ms</div>
</div>
${test.error ? `
<div class="error">
${test.error.enhanced ? `
<div class="error-header">
<span class="error-prefix">${test.error.enhanced.prefix}</span>
<span class="error-message">${test.error.enhanced.message}</span>
<span class="error-category">[${test.error.enhanced.category}]</span>
</div>
${test.error.enhanced.context.file ? `
<div class="error-location">📍 ${test.error.enhanced.context.file}:${test.error.enhanced.context.line || '?'}</div>
` : ''}
${test.error.enhanced.suggestions.length > 0 ? `
<div class="error-suggestions">
<strong>💡 Suggestions:</strong>
<ul>
${test.error.enhanced.suggestions.map(s => `<li>${s}</li>`).join('')}
</ul>
</div>
` : ''}
${test.error.enhanced.codeSnippet ? `
<div class="error-code">
<pre>${test.error.enhanced.codeSnippet.code}</pre>
</div>
` : ''}
` : test.error.message}
</div>
` : ''}
`).join('')}
</div>
</div>
`).join('')}
<div class="footer">
<p>Chrome Extension Test Framework v${results.framework} | Execution time: ${results.duration}ms</p>
</div>
</body>
</html>`;
}
/**
* Markdown形式のレポートを生成
*/
generateMarkdown(results) {
const summary = results.summary;
let md = `# Chrome Extension Test Report\n\n`;
md += `**Generated:** ${new Date(results.timestamp).toLocaleString()}\n`;
md += `**Framework Version:** ${results.framework}\n`;
md += `**Extension Path:** ${results.config.extensionPath}\n\n`;
md += `## Summary\n\n`;
md += `| Metric | Value |\n`;
md += `|--------|-------|\n`;
md += `| Total Tests | ${summary.total} |\n`;
md += `| Passed | ${summary.passed} |\n`;
md += `| Failed | ${summary.failed} |\n`;
md += `| Skipped | ${summary.skipped} |\n`;
md += `| Success Rate | ${summary.successRate}% |\n`;
md += `| Execution Time | ${results.duration}ms |\n\n`;
md += `## Test Results\n\n`;
results.suites.forEach(suite => {
md += `### ${suite.name}\n`;
if (suite.description) {
md += `> ${suite.description}\n`;
}
md += `\n`;
suite.tests.forEach(test => {
const icon = test.status === 'passed' ? '✅' :
test.status === 'failed' ? '❌' : '⏭️';
md += `- ${icon} ${test.name} (${test.duration || 0}ms)\n`;
if (test.error) {
if (test.error.enhanced) {
const enhanced = test.error.enhanced;
md += ` - ${enhanced.prefix} Error: ${enhanced.message}\n`;
md += ` - Category: ${enhanced.category}\n`;
md += ` - Level: ${enhanced.level}\n`;
if (enhanced.context.file) {
md += ` - Location: ${enhanced.context.file}:${enhanced.context.line || '?'}\n`;
}
if (enhanced.suggestions.length > 0) {
md += ` - Suggestions:\n`;
enhanced.suggestions.forEach(s => {
md += ` - ${s}\n`;
});
}
} else {
md += ` - Error: ${test.error.message}\n`;
}
}
});
md += `\n`;
});
if (summary.errors.length > 0) {
md += `## Error Details\n\n`;
summary.errors.forEach(error => {
md += `### [${error.suite}] ${error.test}\n`;
if (error.error.enhanced) {
const enhanced = error.error.enhanced;
md += `**${enhanced.prefix} ${enhanced.category}** (${enhanced.level})\n\n`;
md += `**Error:** ${enhanced.message}\n\n`;
if (enhanced.context.file) {
md += `**Location:** \`${enhanced.context.file}:${enhanced.context.line || '?'}\`\n\n`;
}
if (enhanced.suggestions.length > 0) {
md += `**Suggestions:**\n`;
enhanced.suggestions.forEach(s => {
md += `- ${s}\n`;
});
md += `\n`;
}
if (enhanced.codeSnippet) {
md += `**Code:**\n`;
md += `\`\`\`javascript\n${enhanced.codeSnippet.code}\n\`\`\`\n\n`;
}
if (enhanced.stack) {
md += `<details>\n<summary>Stack Trace</summary>\n\n\`\`\`\n${enhanced.stack}\n\`\`\`\n</details>\n\n`;
}
} else {
md += `\`\`\`\n${error.error.message}\n\`\`\`\n\n`;
}
});
}
return md;
}
}
module.exports = Reporter;