UNPKG

playwright-ai-auto-debug

Version:

Automatic Playwright test debugging with AI assistance + UI Test Coverage Analysis

966 lines (844 loc) 37.5 kB
// new-pw-ai/src/main/lib/globalCoverageTracker.js import fs from 'fs'; import path from 'path'; /** * Глобальный трекер покрытия для объединения данных всех тестов */ export class GlobalCoverageTracker { constructor(config = {}) { this.config = { outputDir: config.outputDir || 'test-coverage-reports', trackingEnabled: config.trackingEnabled ?? true, ...config }; // Глобальное хранилище данных покрытия this.globalData = { tests: new Map(), // testName -> testData pages: new Map(), // pageUrl -> pageData selectors: new Map(), // selector -> usage info sessions: [], // история запусков startTime: Date.now() }; this.currentSession = { id: `session-${Date.now()}`, startTime: Date.now(), tests: [], totalTests: 0, passedTests: 0, failedTests: 0 }; } /** * 🎯 Добавление данных теста в глобальную статистику */ addTestData(testName, testData) { // Добавляем или обновляем данные теста if (this.globalData.tests.has(testName)) { const existing = this.globalData.tests.get(testName); // Объединяем данные если тест запускался несколько раз existing.runs = existing.runs || []; existing.runs.push(testData); existing.lastRun = testData; } else { this.globalData.tests.set(testName, { ...testData, runs: [testData], firstRun: testData, lastRun: testData }); } // Добавляем в текущую сессию this.currentSession.tests.push({ name: testName, ...testData }); this.currentSession.totalTests++; } /** * 📄 Добавление данных страницы */ addPageData(pageUrl, pageData, testName) { if (!this.globalData.pages.has(pageUrl)) { this.globalData.pages.set(pageUrl, { elements: pageData.elements, visitedBy: [], firstVisited: Date.now(), lastAnalyzed: Date.now(), totalVisits: 0 }); } const existing = this.globalData.pages.get(pageUrl); if (!existing.visitedBy.includes(testName)) { existing.visitedBy.push(testName); } existing.totalVisits++; existing.lastAnalyzed = Date.now(); // Обновляем элементы если они изменились if (pageData.elements.length > existing.elements.length) { existing.elements = pageData.elements; } } /** * 🎯 Добавление информации о селекторе */ addSelectorUsage(testName, selector, method, options = {}) { const key = `${selector}::${method}`; if (!this.globalData.selectors.has(key)) { this.globalData.selectors.set(key, { selector, method, usedBy: [], totalUsage: 0, firstUsed: Date.now() }); } const existing = this.globalData.selectors.get(key); if (!existing.usedBy.includes(testName)) { existing.usedBy.push(testName); } existing.totalUsage++; existing.lastUsed = Date.now(); } /** * 📊 Генерация объединенного отчета */ generateUnifiedReport() { console.log('📊 Генерирую объединенный отчет покрытия...'); // Завершаем текущую сессию this.currentSession.endTime = Date.now(); this.currentSession.duration = this.currentSession.endTime - this.currentSession.startTime; this.globalData.sessions.push(this.currentSession); const report = { metadata: { generatedAt: new Date().toISOString(), sessionId: this.currentSession.id, totalSessions: this.globalData.sessions.length }, summary: this.calculateGlobalSummary(), currentSession: this.currentSession, pageAnalysis: this.generatePageAnalysis(), testAnalysis: this.generateTestAnalysis(), selectorAnalysis: this.generateSelectorAnalysis(), uncoveredElements: this.findGlobalUncoveredElements(), recommendations: this.generateGlobalRecommendations(), drillDown: this.generateDrillDownData() }; return report; } /** * 📈 Расчет глобальной статистики */ calculateGlobalSummary() { const totalTests = this.globalData.tests.size; const totalPages = this.globalData.pages.size; let totalElements = 0; let coveredElements = 0; // Подсчет элементов и покрытия for (const [pageUrl, pageData] of this.globalData.pages) { totalElements += pageData.elements.length; // Подсчет покрытых элементов для этой страницы const usedSelectors = this.getSelectorsForPage(pageUrl); for (const element of pageData.elements) { if (this.isElementCovered(element, usedSelectors)) { coveredElements++; } } } const coveragePercentage = totalElements > 0 ? Math.round((coveredElements / totalElements) * 100) : 0; return { totalTests, totalPages, totalElements, coveredElements, uncoveredElements: totalElements - coveredElements, coveragePercentage, totalSelectors: this.globalData.selectors.size, sessionDuration: Date.now() - this.globalData.startTime }; } /** * 🌐 Анализ по страницам */ generatePageAnalysis() { const pageAnalysis = []; for (const [pageUrl, pageData] of this.globalData.pages) { const usedSelectors = this.getSelectorsForPage(pageUrl); const coveredCount = pageData.elements.filter(el => this.isElementCovered(el, usedSelectors) ).length; pageAnalysis.push({ url: pageUrl, totalElements: pageData.elements.length, coveredElements: coveredCount, uncoveredElements: pageData.elements.length - coveredCount, coveragePercentage: Math.round((coveredCount / pageData.elements.length) * 100), visitedBy: pageData.visitedBy, totalVisits: pageData.totalVisits, elements: pageData.elements.map(el => ({ ...el, covered: this.isElementCovered(el, usedSelectors), usedInTests: this.getTestsUsingElement(el, usedSelectors) })) }); } return pageAnalysis.sort((a, b) => b.totalVisits - a.totalVisits); } /** * 🧪 Анализ по тестам */ generateTestAnalysis() { const testAnalysis = []; for (const [testName, testData] of this.globalData.tests) { const lastRun = testData.lastRun; testAnalysis.push({ testName, status: lastRun.status || 'unknown', pagesVisited: lastRun.pages?.length || 0, selectorsUsed: lastRun.selectors?.length || 0, uniqueSelectors: [...new Set(lastRun.selectors || [])].length, interactions: lastRun.interactions?.length || 0, duration: lastRun.duration || 0, runs: testData.runs?.length || 1, pages: lastRun.pages || [], mostUsedSelectors: this.getMostUsedSelectorsForTest(testName) }); } return testAnalysis.sort((a, b) => b.selectorsUsed - a.selectorsUsed); } /** * 🎯 Анализ селекторов */ generateSelectorAnalysis() { const selectorAnalysis = []; for (const [key, selectorData] of this.globalData.selectors) { selectorAnalysis.push({ selector: selectorData.selector, method: selectorData.method, usedBy: selectorData.usedBy, totalUsage: selectorData.totalUsage, testsCount: selectorData.usedBy.length, efficiency: this.calculateSelectorEfficiency(selectorData) }); } return selectorAnalysis.sort((a, b) => b.totalUsage - a.totalUsage); } /** * 🔍 Данные для drill-down навигации */ generateDrillDownData() { return { testsByPage: this.generateTestsByPageMap(), elementsByTest: this.generateElementsByTestMap(), selectorsByTest: this.generateSelectorsByTestMap(), pagesByTest: this.generatePagesByTestMap() }; } /** * 🗺️ Карта тестов по страницам */ generateTestsByPageMap() { const map = {}; for (const [pageUrl, pageData] of this.globalData.pages) { map[pageUrl] = pageData.visitedBy; } return map; } /** * 🎯 Карта элементов по тестам */ generateElementsByTestMap() { const map = {}; for (const [testName, testData] of this.globalData.tests) { map[testName] = []; for (const pageUrl of testData.lastRun.pages || []) { const pageData = this.globalData.pages.get(pageUrl); if (pageData) { const usedSelectors = this.getSelectorsForTest(testName); const coveredElements = pageData.elements.filter(el => this.isElementCovered(el, usedSelectors) ); map[testName].push(...coveredElements); } } } return map; } /** * 📝 Получение селекторов для конкретной страницы */ getSelectorsForPage(pageUrl) { const selectors = new Set(); for (const [testName, testData] of this.globalData.tests) { if (testData.lastRun.pages?.includes(pageUrl)) { for (const selector of testData.lastRun.selectors || []) { selectors.add(selector); } } } return selectors; } /** * 🎯 Получение селекторов для конкретного теста */ getSelectorsForTest(testName) { const testData = this.globalData.tests.get(testName); return new Set(testData?.lastRun.selectors || []); } /** * ✅ Проверка покрытия элемента */ isElementCovered(element, usedSelectors) { for (const selector of usedSelectors) { if (this.matchesSelector(element, selector)) { return true; } } return false; } /** * 🎯 Проверка соответствия селектора элементу */ matchesSelector(element, selector) { // Точное соответствие по тексту if (selector.includes('text=') && element.text) { const selectorText = selector.replace(/text=|locator\(|\)|"/g, ''); return element.text.includes(selectorText); } // Соответствие по aria-label if (selector.includes('aria-label') && element.ariaLabel) { return selector.includes(element.ariaLabel); } // Соответствие по ID if (selector.startsWith('#') && element.id) { return element.id === selector.substring(1); } // Соответствие по классу if (selector.includes('.') && element.className) { const classFromSelector = selector.match(/\.([a-zA-Z0-9_-]+)/)?.[1]; return classFromSelector && element.className.includes(classFromSelector); } // Соответствие по тегу if (element.tagName === selector.toLowerCase()) { return true; } // Соответствие по role if (selector.includes('role=') && element.role) { return selector.includes(element.role); } return false; } /** * 🔍 Поиск глобальных непокрытых элементов */ findGlobalUncoveredElements() { const uncovered = []; for (const [pageUrl, pageData] of this.globalData.pages) { const usedSelectors = this.getSelectorsForPage(pageUrl); for (const element of pageData.elements) { if (!this.isElementCovered(element, usedSelectors)) { uncovered.push({ ...element, pageUrl, suggestedSelectors: this.generateSuggestedSelectors(element), priority: this.calculateElementPriority(element), pageVisits: pageData.totalVisits }); } } } return uncovered.sort((a, b) => b.priority - a.priority); } /** * 🎯 Генерация предлагаемых селекторов */ generateSuggestedSelectors(element) { const selectors = []; if (element.text && element.text.length < 50) { selectors.push(`text=${element.text}`); } if (element.id) { selectors.push(`#${element.id}`); } if (element.ariaLabel) { selectors.push(`[aria-label="${element.ariaLabel}"]`); } if (element.className) { const classes = element.className.split(' ').filter(c => c.length > 2); if (classes.length > 0) { selectors.push(`.${classes[0]}`); } } if (element.role) { selectors.push(`[role="${element.role}"]`); } selectors.push(element.tagName); return selectors.slice(0, 4); // Ограничиваем 4 предложениями } /** * ⭐ Расчет приоритета элемента */ calculateElementPriority(element) { let priority = 1; if (['button', 'a', 'input'].includes(element.tagName)) priority += 5; if (element.text && element.text.length > 0) priority += 3; if (element.visible) priority += 2; if (element.id) priority += 2; if (element.ariaLabel) priority += 2; if (element.role) priority += 1; return priority; } /** * 💡 Генерация глобальных рекомендаций */ generateGlobalRecommendations() { const recommendations = []; const summary = this.calculateGlobalSummary(); // Рекомендации по покрытию if (summary.coveragePercentage < 30) { recommendations.push({ type: 'coverage', priority: 'high', message: `🔴 Очень низкое покрытие (${summary.coveragePercentage}%) - критически важно добавить тесты` }); } else if (summary.coveragePercentage < 60) { recommendations.push({ type: 'coverage', priority: 'medium', message: `🟡 Среднее покрытие (${summary.coveragePercentage}%) - рекомендуется улучшить` }); } else { recommendations.push({ type: 'coverage', priority: 'low', message: `🟢 Хорошее покрытие (${summary.coveragePercentage}%)` }); } // Рекомендации по непокрытым элементам const highPriorityUncovered = this.findGlobalUncoveredElements().filter(el => el.priority > 8); if (highPriorityUncovered.length > 0) { recommendations.push({ type: 'elements', priority: 'high', message: `🎯 ${highPriorityUncovered.length} критически важных элементов без покрытия` }); } // Рекомендации по тестам const testsWithLowCoverage = Array.from(this.globalData.tests.values()) .filter(test => (test.lastRun.selectors?.length || 0) < 3); if (testsWithLowCoverage.length > 0) { recommendations.push({ type: 'tests', priority: 'medium', message: `📝 ${testsWithLowCoverage.length} тестов используют мало селекторов` }); } return recommendations; } /** * 💾 Сохранение объединенного отчета */ async saveUnifiedReport(report) { const timestamp = Date.now(); const reportDir = this.config.outputDir; if (!fs.existsSync(reportDir)) { fs.mkdirSync(reportDir, { recursive: true }); } // Сохраняем главный объединенный отчет const jsonPath = path.join(reportDir, `unified-coverage-${timestamp}.json`); fs.writeFileSync(jsonPath, JSON.stringify(report, null, 2)); const htmlPath = path.join(reportDir, `unified-coverage-${timestamp}.html`); const htmlContent = this.generateUnifiedHTMLReport(report); fs.writeFileSync(htmlPath, htmlContent); const mdPath = path.join(reportDir, `unified-coverage-${timestamp}.md`); const mdContent = this.generateUnifiedMarkdownReport(report); fs.writeFileSync(mdPath, mdContent); // Создаем index.html для быстрого доступа const indexPath = path.join(reportDir, 'index.html'); const indexContent = this.generateIndexHTML(timestamp); fs.writeFileSync(indexPath, indexContent); console.log(`📊 Объединенный отчет сохранен:`); console.log(` 📝 JSON: ${jsonPath}`); console.log(` 🌐 HTML: ${htmlPath}`); console.log(` 📄 MD: ${mdPath}`); console.log(` 🏠 Index: ${indexPath}`); return { jsonPath, htmlPath, mdPath, indexPath }; } /** * 🌐 Генерация объединенного HTML отчета */ generateUnifiedHTMLReport(report) { return `<!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>🎯 Unified Test Coverage Report</title> <style> * { box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; margin: 0; padding: 20px; background: #f5f5f5; } .container { max-width: 1600px; margin: 0 auto; background: white; border-radius: 12px; padding: 30px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); } .header { text-align: center; margin-bottom: 30px; } .header h1 { color: #2c3e50; margin-bottom: 10px; } .header .subtitle { color: #7f8c8d; font-size: 1.1em; } .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px; margin-bottom: 30px; } .stat-card { padding: 25px; border-radius: 12px; text-align: center; color: white; position: relative; overflow: hidden; } .stat-card.coverage { background: linear-gradient(135deg, ${report.summary.coveragePercentage > 60 ? '#4caf50, #8bc34a' : report.summary.coveragePercentage > 30 ? '#ff9800, #ffc107' : '#f44336, #e91e63'}); } .stat-card.tests { background: linear-gradient(135deg, #667eea, #764ba2); } .stat-card.pages { background: linear-gradient(135deg, #00c9ff, #92fe9d); } .stat-card.elements { background: linear-gradient(135deg, #fc466b, #3f5efb); } .stat-value { font-size: 2.5em; font-weight: bold; margin-bottom: 5px; } .stat-label { opacity: 0.9; font-size: 1.1em; } .stat-detail { opacity: 0.8; font-size: 0.9em; margin-top: 5px; } .tabs { display: flex; border-bottom: 3px solid #eee; margin-bottom: 30px; background: #f8f9fa; border-radius: 8px 8px 0 0; } .tab { padding: 15px 25px; cursor: pointer; border-bottom: 3px solid transparent; font-weight: 500; transition: all 0.3s; } .tab:hover { background: #e9ecef; } .tab.active { border-bottom-color: #667eea; color: #667eea; background: white; } .tab-content { display: none; } .tab-content.active { display: block; } .section { margin: 30px 0; } .section-title { font-size: 1.8em; margin-bottom: 20px; color: #2c3e50; border-bottom: 2px solid #eee; padding-bottom: 10px; } .table { width: 100%; border-collapse: collapse; margin: 20px 0; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } .table th { background: #667eea; color: white; padding: 15px; text-align: left; font-weight: 600; } .table td { padding: 12px 15px; border-bottom: 1px solid #eee; } .table tr:hover { background: #f8f9fa; } .covered { background: #e8f5e8; } .uncovered { background: #ffebee; } .high-priority { border-left: 4px solid #f44336; } .medium-priority { border-left: 4px solid #ff9800; } .progress-bar { width: 100%; height: 8px; background: #eee; border-radius: 4px; overflow: hidden; margin: 5px 0; } .progress-fill { height: 100%; transition: width 0.3s; border-radius: 4px; } .progress-high { background: linear-gradient(90deg, #4caf50, #8bc34a); } .progress-medium { background: linear-gradient(90deg, #ff9800, #ffc107); } .progress-low { background: linear-gradient(90deg, #f44336, #e91e63); } .drill-down { cursor: pointer; color: #667eea; text-decoration: underline; } .drill-down:hover { color: #5a6fd8; } .expandable { cursor: pointer; } .expandable:hover { background: #f0f0f0; } .details { display: none; padding: 10px; background: #f8f9fa; border-left: 4px solid #667eea; margin: 5px 0; } .details.show { display: block; } .recommendation { padding: 15px; margin: 10px 0; border-radius: 8px; 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; } .badge { display: inline-block; padding: 4px 8px; border-radius: 12px; font-size: 0.8em; font-weight: bold; margin: 2px; } .badge.success { background: #4caf50; color: white; } .badge.warning { background: #ff9800; color: white; } .badge.danger { background: #f44336; color: white; } .badge.info { background: #2196f3; color: white; } </style> </head> <body> <div class="container"> <div class="header"> <h1>🎯 Unified Test Coverage Report</h1> <p class="subtitle">Объединенный анализ покрытия UI элементов тестами</p> <p><strong>Сессия:</strong> ${report.metadata.sessionId} | <strong>Создан:</strong> ${new Date(report.metadata.generatedAt).toLocaleString('ru')}</p> </div> <div class="stats-grid"> <div class="stat-card coverage"> <div class="stat-value">${report.summary.coveragePercentage}%</div> <div class="stat-label">Общее покрытие</div> <div class="stat-detail">${report.summary.coveredElements} из ${report.summary.totalElements}</div> </div> <div class="stat-card tests"> <div class="stat-value">${report.summary.totalTests}</div> <div class="stat-label">Всего тестов</div> <div class="stat-detail">${report.currentSession.totalTests} в сессии</div> </div> <div class="stat-card pages"> <div class="stat-value">${report.summary.totalPages}</div> <div class="stat-label">Страниц</div> <div class="stat-detail">${report.summary.totalSelectors} селекторов</div> </div> <div class="stat-card elements"> <div class="stat-value">${report.summary.uncoveredElements}</div> <div class="stat-label">Непокрыто</div> <div class="stat-detail">${Math.round((report.summary.sessionDuration / 1000))}s сессия</div> </div> </div> <div class="tabs"> <div class="tab active" onclick="showTab('summary')">📊 Сводка</div> <div class="tab" onclick="showTab('pages')">🌐 По страницам</div> <div class="tab" onclick="showTab('tests')">🧪 По тестам</div> <div class="tab" onclick="showTab('uncovered')">🎯 Непокрытые</div> <div class="tab" onclick="showTab('selectors')">📝 Селекторы</div> </div> <div id="summary" class="tab-content active"> <div class="section"> <h2 class="section-title">📊 Общая статистика</h2> <div class="progress-bar"> <div class="progress-fill ${report.summary.coveragePercentage > 60 ? 'progress-high' : report.summary.coveragePercentage > 30 ? 'progress-medium' : 'progress-low'}" style="width: ${report.summary.coveragePercentage}%"></div> </div> <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-top: 20px;"> <div> <h3>🎯 Топ-5 непокрытых элементов</h3> ${report.uncoveredElements.slice(0, 5).map((element, index) => ` <div class="expandable" onclick="toggleDetails('uncovered-${index}')"> <strong>${index + 1}. ${element.text || element.tagName}</strong> <span class="badge ${element.priority > 8 ? 'danger' : element.priority > 5 ? 'warning' : 'info'}">${element.priority}</span> </div> <div id="uncovered-${index}" class="details"> <strong>Страница:</strong> ${element.pageUrl}<br> <strong>Селекторы:</strong> <code>${element.suggestedSelectors.join(', ')}</code> </div> `).join('')} </div> <div> <h3>📈 Рекомендации</h3> ${report.recommendations.map(rec => ` <div class="recommendation ${rec.priority}"> ${rec.message} </div> `).join('')} </div> </div> </div> </div> <div id="pages" class="tab-content"> <div class="section"> <h2 class="section-title">🌐 Анализ по страницам</h2> <table class="table"> <thead> <tr> <th>Страница</th> <th>Элементов</th> <th>Покрыто</th> <th>Покрытие</th> <th>Тесты</th> <th>Детали</th> </tr> </thead> <tbody> ${report.pageAnalysis.map((page, index) => ` <tr> <td><strong>${page.url}</strong></td> <td>${page.totalElements}</td> <td>${page.coveredElements}</td> <td> <div class="progress-bar"> <div class="progress-fill ${page.coveragePercentage > 60 ? 'progress-high' : page.coveragePercentage > 30 ? 'progress-medium' : 'progress-low'}" style="width: ${page.coveragePercentage}%"></div> </div> ${page.coveragePercentage}% </td> <td>${page.visitedBy.length}</td> <td><span class="drill-down" onclick="toggleDetails('page-${index}')">Показать детали</span></td> </tr> <tr id="page-${index}" class="details" style="display: none;"> <td colspan="6"> <strong>Посещено тестами:</strong> ${page.visitedBy.join(', ')}<br> <strong>Всего посещений:</strong> ${page.totalVisits}<br> <strong>Непокрытые элементы:</strong> ${page.elements.filter(el => !el.covered).slice(0, 3).map(el => el.text || el.tagName).join(', ')} </td> </tr> `).join('')} </tbody> </table> </div> </div> <div id="tests" class="tab-content"> <div class="section"> <h2 class="section-title">🧪 Анализ по тестам</h2> <table class="table"> <thead> <tr> <th>Тест</th> <th>Статус</th> <th>Страниц</th> <th>Селекторов</th> <th>Взаимодействий</th> <th>Время</th> </tr> </thead> <tbody> ${report.testAnalysis.map(test => ` <tr> <td><strong>${test.testName}</strong></td> <td><span class="badge ${test.status === 'passed' ? 'success' : 'danger'}">${test.status}</span></td> <td>${test.pagesVisited}</td> <td>${test.uniqueSelectors}</td> <td>${test.interactions}</td> <td>${test.duration}ms</td> </tr> `).join('')} </tbody> </table> </div> </div> <div id="uncovered" class="tab-content"> <div class="section"> <h2 class="section-title">🎯 Непокрытые элементы</h2> <table class="table"> <thead> <tr> <th>Элемент</th> <th>Тип</th> <th>Страница</th> <th>Приоритет</th> <th>Предлагаемые селекторы</th> </tr> </thead> <tbody> ${report.uncoveredElements.slice(0, 20).map(element => ` <tr class="${element.priority > 8 ? 'high-priority' : element.priority > 5 ? 'medium-priority' : ''}"> <td><strong>${element.text || element.tagName}</strong></td> <td><span class="badge info">${element.tagName}</span></td> <td>${element.pageUrl.split('/').pop() || 'homepage'}</td> <td><span class="badge ${element.priority > 8 ? 'danger' : element.priority > 5 ? 'warning' : 'info'}">${element.priority}</span></td> <td><code>${element.suggestedSelectors.slice(0, 2).join(', ')}</code></td> </tr> `).join('')} </tbody> </table> </div> </div> <div id="selectors" class="tab-content"> <div class="section"> <h2 class="section-title">📝 Анализ селекторов</h2> <table class="table"> <thead> <tr> <th>Селектор</th> <th>Метод</th> <th>Использований</th> <th>Тестов</th> </tr> </thead> <tbody> ${report.selectorAnalysis.slice(0, 20).map(selector => ` <tr> <td><code>${selector.selector}</code></td> <td><span class="badge info">${selector.method}</span></td> <td>${selector.totalUsage}</td> <td>${selector.testsCount}</td> </tr> `).join('')} </tbody> </table> </div> </div> </div> <script> function showTab(tabName) { document.querySelectorAll('.tab-content').forEach(content => { content.classList.remove('active'); }); document.querySelectorAll('.tab').forEach(tab => { tab.classList.remove('active'); }); document.getElementById(tabName).classList.add('active'); event.target.classList.add('active'); } function toggleDetails(elementId) { const element = document.getElementById(elementId); if (element.style.display === 'none' || !element.style.display) { element.style.display = 'table-row'; } else { element.style.display = 'none'; } } </script> </body> </html>`; } /** * 📄 Генерация объединенного Markdown отчета */ generateUnifiedMarkdownReport(report) { return `# 🎯 Unified Test Coverage Report ## 📊 Общая статистика - **Покрытие:** ${report.summary.coveragePercentage}% (${report.summary.coveredElements} из ${report.summary.totalElements}) - **Тестов:** ${report.summary.totalTests} - **Страниц:** ${report.summary.totalPages} - **Селекторов:** ${report.summary.totalSelectors} - **Время сессии:** ${Math.round(report.summary.sessionDuration / 1000)}s - **Дата:** ${new Date(report.metadata.generatedAt).toLocaleString('ru')} ## 🌐 Топ страниц по покрытию ${report.pageAnalysis.slice(0, 10).map((page, index) => ` ${index + 1}. **${page.url}** - Элементов: ${page.totalElements} - Покрыто: ${page.coveredElements} (${page.coveragePercentage}%) - Тестов: ${page.visitedBy.length} - Посещений: ${page.totalVisits} `).join('')} ## 🧪 Анализ тестов ${report.testAnalysis.map(test => ` ### ${test.testName} - **Статус:** ${test.status} - **Страниц:** ${test.pagesVisited} - **Селекторов:** ${test.uniqueSelectors} - **Взаимодействий:** ${test.interactions} - **Время:** ${test.duration}ms `).join('')} ## 🎯 Топ-20 непокрытых элементов ${report.uncoveredElements.slice(0, 20).map((element, index) => ` ${index + 1}. **${element.text || element.tagName}** (${element.tagName}) - Приоритет: ${element.priority} - Страница: ${element.pageUrl} - Селекторы: \`${element.suggestedSelectors.slice(0, 3).join('`, `')}\` `).join('')} ## 💡 Рекомендации ${report.recommendations.map(rec => `- ${rec.message}`).join('\n')} --- *Объединенный отчет создан автоматически с помощью GlobalCoverageTracker* `; } /** * 🏠 Генерация index.html для быстрого доступа */ generateIndexHTML(timestamp) { return `<!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8"> <title>Test Coverage Reports</title> <meta http-equiv="refresh" content="0; url=unified-coverage-${timestamp}.html"> </head> <body> <p>Перенаправление на последний отчет покрытия...</p> <p><a href="unified-coverage-${timestamp}.html">Открыть отчет вручную</a></p> </body> </html>`; } // Вспомогательные методы getMostUsedSelectorsForTest(testName) { const testData = this.globalData.tests.get(testName); if (!testData?.lastRun.selectors) return []; const selectorCount = {}; testData.lastRun.selectors.forEach(selector => { selectorCount[selector] = (selectorCount[selector] || 0) + 1; }); return Object.entries(selectorCount) .sort(([,a], [,b]) => b - a) .slice(0, 5) .map(([selector, count]) => ({ selector, count })); } calculateSelectorEfficiency(selectorData) { // Эффективность = количество тестов / общее использование return selectorData.usedBy.length / selectorData.totalUsage; } generateSelectorsByTestMap() { const map = {}; for (const [testName, testData] of this.globalData.tests) { map[testName] = testData.lastRun.selectors || []; } return map; } generatePagesByTestMap() { const map = {}; for (const [testName, testData] of this.globalData.tests) { map[testName] = testData.lastRun.pages || []; } return map; } getTestsUsingElement(element, usedSelectors) { const tests = []; for (const [testName, testData] of this.globalData.tests) { const testSelectors = new Set(testData.lastRun.selectors || []); if (this.isElementCovered(element, testSelectors)) { tests.push(testName); } } return tests; } }