playwright-ai-auto-debug
Version:
Automatic Playwright test debugging with AI assistance + UI Test Coverage Analysis
792 lines (704 loc) • 37.4 kB
JavaScript
// DemoProject/lib/globalCoverageManager.js
import fs from 'fs';
import path from 'path';
import { DetailedCoverageTracker } from './detailedCoverageTracker.js';
/**
* Глобальный менеджер покрытия UI элементов
* Агрегирует данные всех тестов в единый отчет
*/
export class GlobalCoverageManager {
constructor(config = {}) {
this.config = {
outputDir: config.outputDir || 'unified-coverage',
sessionName: config.sessionName || `unified-session-${Date.now()}`,
...config
};
// Единый трекер для всех тестов
this.coverageTracker = new DetailedCoverageTracker({
outputDir: this.config.outputDir,
trackingEnabled: true,
includeSelectors: true,
includeScreenshots: false
});
this.isInitialized = false;
this.testCounter = 0;
this.testResults = new Map();
}
/**
* 🎬 Инициализация глобальной сессии
*/
initializeGlobalSession() {
if (!this.isInitialized) {
this.coverageTracker.startSession(this.config.sessionName);
this.isInitialized = true;
console.log(`🌐 Инициализирован глобальный трекер покрытия: ${this.config.sessionName}`);
}
return this.config.sessionName;
}
/**
* 📊 Регистрация элементов страницы от теста
*/
registerTestPageElements(testName, pageName, mcpSnapshot) {
this.initializeGlobalSession();
const elements = this.coverageTracker.registerPageElements(
pageName,
mcpSnapshot,
testName
);
// Сохранение информации о тесте
if (!this.testResults.has(testName)) {
this.testResults.set(testName, {
name: testName,
pages: new Set(),
elementsFound: 0,
elementsCovered: 0,
interactions: []
});
}
const testInfo = this.testResults.get(testName);
testInfo.pages.add(pageName);
testInfo.elementsFound += elements.length;
console.log(`📋 [${testName}] Зарегистрировано ${elements.length} элементов на странице ${pageName}`);
return elements;
}
/**
* ✅ Отметка покрытия элемента от теста
*/
markTestElementCovered(testName, element, interactionType = 'unknown') {
this.initializeGlobalSession();
this.coverageTracker.markElementCovered(element, testName, interactionType);
// Обновление статистики теста
const testInfo = this.testResults.get(testName);
if (testInfo) {
testInfo.elementsCovered++;
testInfo.interactions.push({
element,
interactionType,
timestamp: new Date().toISOString()
});
}
console.log(`✅ [${testName}] Элемент покрыт: ${element.text || element.type} (${interactionType})`);
}
/**
* 📝 Регистрация завершения теста
*/
completeTest(testName, status = 'passed') {
const testInfo = this.testResults.get(testName);
if (testInfo) {
testInfo.status = status;
testInfo.completedAt = new Date().toISOString();
console.log(`🏁 [${testName}] Тест завершен: ${status}`);
console.log(` Страниц: ${testInfo.pages.size}, Элементов: ${testInfo.elementsFound}, Покрыто: ${testInfo.elementsCovered}`);
}
this.testCounter++;
}
/**
* 📊 Генерация единого отчета по всем тестам
*/
generateUnifiedReport() {
if (!this.isInitialized) {
console.warn('⚠️ Глобальный трекер не инициализирован');
return null;
}
const baseReport = this.coverageTracker.generateDetailedCoverageReport();
// Добавление информации о тестах
const testsSummary = Array.from(this.testResults.values()).map(test => ({
name: test.name,
status: test.status || 'unknown',
pages: Array.from(test.pages),
elementsFound: test.elementsFound,
elementsCovered: test.elementsCovered,
interactions: test.interactions.length,
coveragePercentage: test.elementsFound > 0 ?
Math.round((test.elementsCovered / test.elementsFound) * 100) : 0,
completedAt: test.completedAt
}));
// Собираем все элементы из всех тестов
const allElementsRegistry = [];
const pageElementsMap = new Map();
// Получаем все элементы из базового отчета
const allElements = [...baseReport.detailedElements.covered, ...baseReport.detailedElements.uncovered];
// Обрабатываем каждый элемент и добавляем метаданные
allElements.forEach(element => {
// Элемент уже содержит информацию о покрытии, тестах и взаимодействиях
allElementsRegistry.push(element);
// Группируем элементы по страницам
if (element.pages && element.pages.length > 0) {
element.pages.forEach(pageName => {
if (!pageElementsMap.has(pageName)) {
pageElementsMap.set(pageName, []);
}
pageElementsMap.get(pageName).push(element);
});
}
});
// Расширенный отчет
const unifiedReport = {
...baseReport,
// Информация о глобальной сессии
globalSession: {
sessionName: this.config.sessionName,
totalTests: this.testCounter,
testsCompleted: testsSummary.filter(t => t.status !== 'unknown').length,
testsPassed: testsSummary.filter(t => t.status === 'passed').length,
testsFailed: testsSummary.filter(t => t.status === 'failed').length,
allElementsRegistry: allElementsRegistry,
pageElementsMap: Object.fromEntries(pageElementsMap)
},
// Детальная информация о тестах
testsDetails: testsSummary,
// Агрегированная статистика
aggregatedStats: this.calculateAggregatedStats(testsSummary),
// Покрытие по тестам
coverageByTest: this.calculateCoverageByTest(testsSummary),
// Топ непокрытых элементов
topUncoveredElements: this.getTopUncoveredElements(baseReport.detailedElements.uncovered),
// Рекомендации на основе всех тестов
unifiedRecommendations: this.generateUnifiedRecommendations(baseReport, testsSummary)
};
return unifiedReport;
}
/**
* Расчет агрегированной статистики
*/
calculateAggregatedStats(testsSummary) {
const totalElementsFound = testsSummary.reduce((sum, test) => sum + test.elementsFound, 0);
const totalElementsCovered = testsSummary.reduce((sum, test) => sum + test.elementsCovered, 0);
const totalInteractions = testsSummary.reduce((sum, test) => sum + test.interactions, 0);
const avgCoveragePerTest = testsSummary.length > 0 ?
Math.round(testsSummary.reduce((sum, test) => sum + test.coveragePercentage, 0) / testsSummary.length) : 0;
return {
totalElementsFound,
totalElementsCovered,
totalInteractions,
avgCoveragePerTest,
testsWithFullCoverage: testsSummary.filter(t => t.coveragePercentage === 100).length,
testsWithNoCoverage: testsSummary.filter(t => t.coveragePercentage === 0).length,
mostProductiveTest: testsSummary.reduce((max, test) =>
test.elementsCovered > (max?.elementsCovered || 0) ? test : max, null
),
leastProductiveTest: testsSummary.reduce((min, test) =>
test.elementsCovered < (min?.elementsCovered || Infinity) ? test : min, null
)
};
}
/**
* Расчет покрытия по тестам
*/
calculateCoverageByTest(testsSummary) {
return testsSummary.map(test => ({
testName: test.name,
coverage: test.coveragePercentage,
elementsFound: test.elementsFound,
elementsCovered: test.elementsCovered,
efficiency: test.elementsFound > 0 ?
Math.round((test.elementsCovered / test.elementsFound) * 100) : 0,
pages: test.pages
})).sort((a, b) => b.coverage - a.coverage);
}
/**
* Получение топ непокрытых элементов
*/
getTopUncoveredElements(uncoveredElements) {
// Группировка по типам
const byType = uncoveredElements.reduce((groups, element) => {
if (!groups[element.type]) {
groups[element.type] = [];
}
groups[element.type].push(element);
return groups;
}, {});
// Топ по типам
const topByType = Object.entries(byType)
.map(([type, elements]) => ({
type,
count: elements.length,
examples: elements.slice(0, 3).map(el => el.text || el.type)
}))
.sort((a, b) => b.count - a.count)
.slice(0, 5);
// Топ критичных непокрытых
const criticalUncovered = uncoveredElements
.filter(el => el.critical)
.slice(0, 10);
// Топ интерактивных непокрытых
const interactiveUncovered = uncoveredElements
.filter(el => el.interactable)
.slice(0, 10);
return {
byType: topByType,
critical: criticalUncovered,
interactive: interactiveUncovered,
total: uncoveredElements.length
};
}
/**
* Генерация объединенных рекомендаций
*/
generateUnifiedRecommendations(baseReport, testsSummary) {
const recommendations = [...baseReport.recommendations];
// Рекомендации по тестам
const lowCoverageTests = testsSummary.filter(t => t.coveragePercentage < 50);
if (lowCoverageTests.length > 0) {
recommendations.push({
priority: 'HIGH',
category: 'Test Coverage',
message: `${lowCoverageTests.length} тестов имеют низкое покрытие (<50%)`,
tests: lowCoverageTests.map(t => t.name),
action: 'Добавьте больше взаимодействий в эти тесты'
});
}
// Рекомендации по неиспользуемым страницам
const allPages = new Set();
testsSummary.forEach(test => test.pages.forEach(page => allPages.add(page)));
if (allPages.size > testsSummary.length) {
recommendations.push({
priority: 'MEDIUM',
category: 'Page Coverage',
message: `Некоторые страницы анализируются несколькими тестами`,
pages: Array.from(allPages),
action: 'Рассмотрите оптимизацию распределения страниц по тестам'
});
}
return recommendations;
}
/**
* 💾 Сохранение единого отчета
*/
async saveUnifiedReport() {
const report = this.generateUnifiedReport();
if (!report) {
console.error('❌ Не удалось сгенерировать отчет');
return null;
}
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const outputDir = this.config.outputDir;
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
// JSON отчет
const jsonPath = path.join(outputDir, `unified-coverage-${timestamp}.json`);
fs.writeFileSync(jsonPath, JSON.stringify(report, null, 2));
// HTML отчет
const htmlReport = await this.generateUnifiedHTMLReport(report);
const htmlPath = path.join(outputDir, `unified-coverage-${timestamp}.html`);
fs.writeFileSync(htmlPath, htmlReport);
// Краткий отчет
const summaryPath = path.join(outputDir, `coverage-summary-${timestamp}.md`);
const summaryReport = this.generateSummaryReport(report);
fs.writeFileSync(summaryPath, summaryReport);
console.log('\n📊 === ЕДИНЫЙ ОТЧЕТ ПОКРЫТИЯ СОХРАНЕН ===');
console.log(`📄 JSON: ${jsonPath}`);
console.log(`🌐 HTML: ${htmlPath}`);
console.log(`📝 Summary: ${summaryPath}`);
// Вывод краткой статистики
this.printUnifiedSummary(report);
return {
json: jsonPath,
html: htmlPath,
summary: summaryPath
};
}
/**
* 📄 Генерация HTML отчета для единого покрытия
*/
async generateUnifiedHTMLReport(report) {
const html = `
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Unified UI Coverage Report - ${report.globalSession.sessionName}</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
.container { max-width: 1400px; margin: 0 auto; background: white; border-radius: 8px; padding: 30px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
.header { text-align: center; margin-bottom: 30px; }
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 30px; }
.stat-card { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 8px; text-align: center; }
.stat-card.success { background: linear-gradient(135deg, #4caf50 0%, #8bc34a 100%); }
.stat-card.warning { background: linear-gradient(135deg, #ff9800 0%, #ffc107 100%); }
.stat-card.danger { background: linear-gradient(135deg, #f44336 0%, #e91e63 100%); }
.stat-value { font-size: 2em; font-weight: bold; margin-bottom: 5px; }
.stat-label { opacity: 0.9; }
.section { margin: 30px 0; }
.section-title { font-size: 1.5em; margin-bottom: 15px; border-bottom: 2px solid #eee; padding-bottom: 5px; }
.tests-table { width: 100%; border-collapse: collapse; margin: 20px 0; }
.tests-table th, .tests-table td { padding: 12px; text-align: left; border-bottom: 1px solid #eee; }
.tests-table th { background: #f5f5f5; font-weight: bold; }
.coverage-bar { width: 100px; height: 20px; background: #eee; border-radius: 10px; overflow: hidden; }
.coverage-fill { height: 100%; transition: width 0.3s; }
.coverage-high { background: linear-gradient(90deg, #4caf50, #8bc34a); }
.coverage-medium { background: linear-gradient(90deg, #ff9800, #ffc107); }
.coverage-low { background: linear-gradient(90deg, #f44336, #e91e63); }
.recommendation { padding: 15px; margin: 10px 0; border-radius: 5px; border-left: 4px solid; }
.recommendation.high { border-color: #f44336; background: #ffebee; }
.recommendation.medium { border-color: #ff9800; background: #fff3e0; }
.recommendation.low { border-color: #4caf50; background: #e8f5e8; }
.element-list { max-height: 300px; overflow-y: auto; }
.element-item { padding: 8px; margin: 4px 0; border-radius: 4px; font-family: monospace; font-size: 0.9em; }
.element-item.uncovered { background: #ffebee; border-left: 3px solid #f44336; }
.element-item.critical { background: #fce4ec; border-left: 3px solid #e91e63; }
.tabs { display: flex; border-bottom: 2px solid #eee; margin-bottom: 20px; }
.tab { padding: 10px 20px; cursor: pointer; border-bottom: 2px solid transparent; }
.tab.active { border-bottom-color: #667eea; color: #667eea; font-weight: bold; }
.tab-content { display: none; }
.tab-content.active { display: block; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎯 Единый отчет покрытия UI элементов</h1>
<p>Сессия: ${report.globalSession.sessionName}</p>
<p>Тестов: ${report.globalSession.totalTests} | Пройдено: ${report.globalSession.testsPassed} | Провалено: ${report.globalSession.testsFailed}</p>
</div>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value">${report.summary.totalElements}</div>
<div class="stat-label">Всего элементов</div>
</div>
<div class="stat-card success">
<div class="stat-value">${report.summary.coveredElements}</div>
<div class="stat-label">Покрыто</div>
</div>
<div class="stat-card danger">
<div class="stat-value">${report.summary.uncoveredElements}</div>
<div class="stat-label">Не покрыто</div>
</div>
<div class="stat-card ${report.summary.coveragePercentage >= 80 ? 'success' : report.summary.coveragePercentage >= 50 ? 'warning' : 'danger'}">
<div class="stat-value">${report.summary.coveragePercentage}%</div>
<div class="stat-label">Покрытие</div>
</div>
<div class="stat-card">
<div class="stat-value">${report.aggregatedStats.totalInteractions}</div>
<div class="stat-label">Взаимодействий</div>
</div>
<div class="stat-card">
<div class="stat-value">${report.aggregatedStats.avgCoveragePerTest}%</div>
<div class="stat-label">Среднее по тестам</div>
</div>
</div>
<div class="tabs">
<div class="tab active" onclick="showTab('tests')">📊 Тесты</div>
<div class="tab" onclick="showTab('coverage')">📈 Покрытие</div>
<div class="tab" onclick="showTab('covered')">✅ Покрытые</div>
<div class="tab" onclick="showTab('uncovered')">❌ Непокрытые</div>
<div class="tab" onclick="showTab('pages')">🌐 Страницы</div>
<div class="tab" onclick="showTab('recommendations')">💡 Рекомендации</div>
</div>
<div id="tests" class="tab-content active">
<div class="section">
<h2 class="section-title">📊 Результаты тестов</h2>
<table class="tests-table">
<thead>
<tr>
<th>Тест</th>
<th>Статус</th>
<th>Страниц</th>
<th>Элементов</th>
<th>Покрыто</th>
<th>Покрытие</th>
<th>Взаимодействий</th>
</tr>
</thead>
<tbody>
${report.testsDetails.map(test => `
<tr>
<td><strong>${test.name}</strong></td>
<td>${test.status === 'passed' ? '✅' : test.status === 'failed' ? '❌' : '⏳'} ${test.status}</td>
<td>${test.pages.length}</td>
<td>${test.elementsFound}</td>
<td>${test.elementsCovered}</td>
<td>
<div class="coverage-bar">
<div class="coverage-fill ${test.coveragePercentage >= 80 ? 'coverage-high' : test.coveragePercentage >= 50 ? 'coverage-medium' : 'coverage-low'}"
style="width: ${test.coveragePercentage}%"></div>
</div>
${test.coveragePercentage}%
</td>
<td>${test.interactions}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
</div>
<div id="coverage" class="tab-content">
<div class="section">
<h2 class="section-title">📈 Покрытие по типам элементов</h2>
<table class="tests-table">
<thead>
<tr><th>Тип</th><th>Всего</th><th>Покрыто</th><th>Процент</th><th>Прогресс</th></tr>
</thead>
<tbody>
${Object.entries(report.coverageByType).map(([type, coverage]) => `
<tr>
<td><strong>${type}</strong></td>
<td>${coverage.total}</td>
<td>${coverage.covered}</td>
<td>${coverage.percentage}%</td>
<td>
<div class="coverage-bar">
<div class="coverage-fill ${coverage.percentage >= 80 ? 'coverage-high' : coverage.percentage >= 50 ? 'coverage-medium' : 'coverage-low'}"
style="width: ${coverage.percentage}%"></div>
</div>
</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
<div class="section">
<h2 class="section-title">🌐 Покрытие по страницам</h2>
<table class="tests-table">
<thead>
<tr><th>Страница</th><th>Всего</th><th>Покрыто</th><th>Процент</th><th>Прогресс</th></tr>
</thead>
<tbody>
${Object.entries(report.coverageByPage).map(([page, coverage]) => `
<tr>
<td><strong>${page}</strong></td>
<td>${coverage.total}</td>
<td>${coverage.covered}</td>
<td>${coverage.percentage}%</td>
<td>
<div class="coverage-bar">
<div class="coverage-fill ${coverage.percentage >= 80 ? 'coverage-high' : coverage.percentage >= 50 ? 'coverage-medium' : 'coverage-low'}"
style="width: ${coverage.percentage}%"></div>
</div>
</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
</div>
<div id="covered" class="tab-content">
<div class="section">
<h2 class="section-title">✅ Покрытые элементы</h2>
${report.globalSession.allElementsRegistry.filter(el => el.covered).length > 0 ? `
<div class="element-list">
${report.globalSession.allElementsRegistry
.filter(el => el.covered)
.map(element => `
<div class="element-item" style="background: #e8f5e8; border-left: 3px solid #4caf50;">
✅ ${element.type}: "${element.text}"
<div style="font-size: 0.8em; color: #666; margin-top: 4px;">
📄 ${element.pages ? element.pages.join(', ') : 'unknown'} | 🎯 ${element.tests ? element.tests.join(', ') : 'unknown'} | 🤝 ${element.interactions && element.interactions.length > 0 ? element.interactions[0].type : 'unknown'}
</div>
</div>
`).join('')}
</div>
` : '<p>Нет покрытых элементов</p>'}
</div>
<div class="section">
<h2 class="section-title">📊 Покрытые элементы по тестам</h2>
${report.testsDetails.map(testData => {
const coveredElements = report.globalSession.allElementsRegistry.filter(el =>
el.covered && el.tests && el.tests.includes(testData.name)
);
return `
<div style="margin: 20px 0; padding: 15px; border: 1px solid #ddd; border-radius: 8px;">
<h4>${testData.name} (${coveredElements.length} элементов)</h4>
${coveredElements.length > 0 ? `
<div style="margin-top: 10px;">
${coveredElements.map(el => `
<span style="display: inline-block; margin: 2px; padding: 4px 8px; background: #e8f5e8; border-radius: 4px; font-size: 0.9em;">
${el.type}: ${el.text}
</span>
`).join('')}
</div>
` : '<p style="color: #666;">Нет покрытых элементов</p>'}
</div>
`;
}).join('')}
</div>
</div>
<div id="uncovered" class="tab-content">
<div class="section">
<h2 class="section-title">❌ Топ непокрытых элементов по типам</h2>
${report.topUncoveredElements.byType.map(item => `
<div style="margin: 15px 0;">
<h4>${item.type} (${item.count} элементов)</h4>
<div style="color: #666;">${item.examples.join(', ')}</div>
</div>
`).join('')}
</div>
${report.topUncoveredElements.critical.length > 0 ? `
<div class="section">
<h2 class="section-title">🔴 Критичные непокрытые элементы</h2>
<div class="element-list">
${report.topUncoveredElements.critical.map(element => `
<div class="element-item critical">
🔴 ${element.type}: "${element.text}" (${element.selector})
</div>
`).join('')}
</div>
</div>
` : ''}
<div class="section">
<h2 class="section-title">🎯 Интерактивные непокрытые элементы</h2>
<div class="element-list">
${report.topUncoveredElements.interactive.slice(0, 20).map(element => `
<div class="element-item uncovered">
🎯 ${element.type}: "${element.text}" (${element.selector})
</div>
`).join('')}
</div>
</div>
</div>
<div id="pages" class="tab-content">
<div class="section">
<h2 class="section-title">🌐 Детализация по страницам</h2>
${Object.entries(report.globalSession.pageElementsMap).map(([pageName, pageElements]) => {
const coveredElements = pageElements.filter(el => el.covered);
const uncoveredElements = pageElements.filter(el => !el.covered);
const coveragePercent = pageElements.length > 0 ? Math.round((coveredElements.length / pageElements.length) * 100) : 0;
return `
<div style="margin: 20px 0; padding: 20px; border: 1px solid #ddd; border-radius: 8px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
<h3>${pageName}</h3>
<div style="text-align: right;">
<div style="font-size: 1.5em; font-weight: bold; color: ${coveragePercent >= 50 ? '#4caf50' : '#f44336'};">
${coveragePercent}%
</div>
<div style="font-size: 0.9em; color: #666;">
${coveredElements.length}/${pageElements.length} элементов
</div>
</div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px;">
<div>
<h4 style="color: #4caf50;">✅ Покрытые (${coveredElements.length})</h4>
<div style="max-height: 200px; overflow-y: auto;">
${coveredElements.length > 0 ? coveredElements.map(el => `
<div style="padding: 4px 8px; margin: 2px 0; background: #e8f5e8; border-radius: 4px; font-size: 0.9em;">
${el.type}: ${el.text}
<div style="font-size: 0.8em; color: #666;">🎯 ${el.testName}</div>
</div>
`).join('') : '<p style="color: #666;">Нет покрытых элементов</p>'}
</div>
</div>
<div>
<h4 style="color: #f44336;">❌ Непокрытые (${uncoveredElements.length})</h4>
<div style="max-height: 200px; overflow-y: auto;">
${uncoveredElements.length > 0 ? uncoveredElements.slice(0, 10).map(el => `
<div style="padding: 4px 8px; margin: 2px 0; background: #ffebee; border-radius: 4px; font-size: 0.9em;">
${el.type}: ${el.text}
</div>
`).join('') + (uncoveredElements.length > 10 ? `<div style="padding: 4px 8px; color: #666; font-style: italic;">... и еще ${uncoveredElements.length - 10} элементов</div>` : '') : '<p style="color: #666;">Все элементы покрыты!</p>'}
</div>
</div>
</div>
<div style="margin-top: 15px;">
<h4>📊 Статистика по типам элементов</h4>
<div style="display: flex; flex-wrap: wrap; gap: 10px;">
${Object.entries(pageElements.reduce((acc, el) => {
if (!acc[el.type]) acc[el.type] = { total: 0, covered: 0 };
acc[el.type].total++;
if (el.covered) acc[el.type].covered++;
return acc;
}, {})).map(([type, stats]) => `
<div style="padding: 8px 12px; background: #f5f5f5; border-radius: 4px; font-size: 0.9em;">
<strong>${type}</strong>: ${stats.covered}/${stats.total}
(${stats.total > 0 ? Math.round((stats.covered / stats.total) * 100) : 0}%)
</div>
`).join('')}
</div>
</div>
</div>
`;
}).join('')}
</div>
</div>
<div id="recommendations" class="tab-content">
<div class="section">
<h2 class="section-title">💡 Рекомендации по улучшению покрытия</h2>
${report.unifiedRecommendations.map(rec => `
<div class="recommendation ${rec.priority.toLowerCase()}">
<h4>${rec.priority === 'HIGH' ? '🔴' : rec.priority === 'MEDIUM' ? '🟡' : '🟢'} [${rec.priority}] ${rec.category}</h4>
<p><strong>Проблема:</strong> ${rec.message}</p>
<p><strong>Действие:</strong> ${rec.action}</p>
${rec.tests ? `<p><strong>Тесты:</strong> ${rec.tests.join(', ')}</p>` : ''}
${rec.elements ? `<p><strong>Элементы:</strong> ${rec.elements.slice(0, 5).join(', ')}${rec.elements.length > 5 ? '...' : ''}</p>` : ''}
</div>
`).join('')}
</div>
</div>
</div>
<script>
function showTab(tabName) {
document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
event.target.classList.add('active');
document.getElementById(tabName).classList.add('active');
}
</script>
</body>
</html>`;
return html;
}
/**
* 📝 Генерация краткого отчета
*/
generateSummaryReport(report) {
return `# 📊 Краткий отчет покрытия UI элементов
## 🎯 Общая информация
- **Сессия**: ${report.globalSession.sessionName}
- **Дата**: ${new Date().toLocaleString()}
- **Тестов выполнено**: ${report.globalSession.totalTests}
- **Успешных**: ${report.globalSession.testsPassed}
- **Провалившихся**: ${report.globalSession.testsFailed}
## 📈 Статистика покрытия
- **Всего элементов**: ${report.summary.totalElements}
- **Покрыто**: ${report.summary.coveredElements}
- **Не покрыто**: ${report.summary.uncoveredElements}
- **Процент покрытия**: ${report.summary.coveragePercentage}%
- **Взаимодействий**: ${report.aggregatedStats.totalInteractions}
## 🏆 Лучшие тесты
${report.testsDetails
.sort((a, b) => b.coveragePercentage - a.coveragePercentage)
.slice(0, 5)
.map(test => `- **${test.name}**: ${test.coveragePercentage}% (${test.elementsCovered}/${test.elementsFound})`)
.join('\n')}
## ⚠️ Проблемные области
${report.topUncoveredElements.byType
.slice(0, 3)
.map(item => `- **${item.type}**: ${item.count} непокрытых элементов`)
.join('\n')}
## 💡 Главные рекомендации
${report.unifiedRecommendations
.slice(0, 3)
.map(rec => `- **[${rec.priority}] ${rec.category}**: ${rec.message}`)
.join('\n')}
---
*Отчет сгенерирован автоматически системой детального покрытия UI элементов*`;
}
/**
* 📊 Вывод краткой статистики в консоль
*/
printUnifiedSummary(report) {
console.log('\n📊 === КРАТКАЯ СТАТИСТИКА ЕДИНОГО ОТЧЕТА ===');
console.log(`🎯 Сессия: ${report.globalSession.sessionName}`);
console.log(`🧪 Тестов: ${report.globalSession.totalTests} (✅${report.globalSession.testsPassed} ❌${report.globalSession.testsFailed})`);
console.log(`📊 Элементов: ${report.summary.totalElements} (✅${report.summary.coveredElements} ❌${report.summary.uncoveredElements})`);
console.log(`📈 Покрытие: ${report.summary.coveragePercentage}% (среднее по тестам: ${report.aggregatedStats.avgCoveragePerTest}%)`);
console.log(`🎯 Взаимодействий: ${report.aggregatedStats.totalInteractions}`);
if (report.aggregatedStats.mostProductiveTest) {
console.log(`🏆 Лучший тест: ${report.aggregatedStats.mostProductiveTest.name} (${report.aggregatedStats.mostProductiveTest.elementsCovered} элементов)`);
}
console.log(`💡 Рекомендаций: ${report.unifiedRecommendations.length}`);
}
/**
* 🧹 Очистка ресурсов
*/
cleanup() {
this.testResults.clear();
this.isInitialized = false;
console.log('🧹 Глобальный трекер покрытия очищен');
}
}
// Экспорт синглтона для использования во всех тестах
export const globalCoverageManager = new GlobalCoverageManager();
export default GlobalCoverageManager;