UNPKG

playwright-ai-auto-debug

Version:

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

1,089 lines (969 loc) 43.2 kB
// src/presentation/cli/CliApplication.js import { getContainer } from '../../infrastructure/di/bindings.js'; import fs from 'fs'; import path from 'path'; /** * Главное CLI приложение с новой архитектурой * Использует Dependency Injection для управления зависимостями */ export class CliApplication { constructor(container = null) { this.container = container || getContainer(); this.commands = new Map(); this.registerCommands(); } /** * Регистрирует доступные команды */ registerCommands() { this.commands.set('debug', this.createDebugCommand()); this.commands.set('analyze', this.createDebugCommand()); // alias this.commands.set('ui-coverage', this.createUICoverageCommand()); this.commands.set('coverage', this.createCoverageCommand()); // НОВОЕ! this.commands.set('setup', this.createSetupCommand()); this.commands.set('validate', this.createValidateCommand()); this.commands.set('info', this.createInfoCommand()); this.commands.set('help', this.createHelpCommand()); this.commands.set('version', this.createVersionCommand()); } /** * Создает команду отладки тестов * @returns {Object} */ createDebugCommand() { const self = this; return { description: 'Analyze test errors with AI assistance', usage: 'debug [options]', options: [ '--use-mcp Enable MCP DOM snapshots', '--project Project directory (default: current)', '--help Show help for this command' ], async execute(args, options) { try { console.log('🏗️ Using Clean Architecture implementation'); console.log('🔧 Initializing dependencies...'); // Получаем главный сервис через DI const testDebugService = await self.container.get('testDebugService'); // Извлекаем опции из аргументов const projectPath = self.extractOption(args, '--project') || process.cwd(); const useMcp = args.includes('--use-mcp'); console.log(`📁 Project path: ${projectPath}`); console.log(`🔗 MCP enabled: ${useMcp}`); // Выполняем анализ const results = await testDebugService.debugTests(projectPath, { useMcp }); // Выводим результаты await self.displayResults(results); return results; } catch (error) { console.error('❌ Analysis failed:', error.message); if (error.message.includes('api_key')) { console.error('💡 Run "playwright-ai setup" to configure API key'); } throw error; } } }; } /** * Создает команду UI coverage анализа * @returns {Object} */ createUICoverageCommand() { const self = this; return { description: 'Analyze UI coverage with MCP DOM snapshots', usage: 'ui-coverage [options]', options: [ '--project Project directory (default: current)', '--page Page name for report (default: auto-detect)', '--critical Path to critical elements config', '--golden Enable golden snapshot comparison', '--all-tests Analyze coverage across all test files', '--help Show help for this command' ], async execute(args, options) { try { console.log('🎯 UI Coverage Analysis'); console.log('🔧 Initializing UI Coverage analyzer...'); // Извлекаем опции const projectPath = self.extractOption(args, '--project') || process.cwd(); const pageName = self.extractOption(args, '--page') || 'current-page'; const criticalConfigPath = self.extractOption(args, '--critical'); const enableGolden = args.includes('--golden'); const analyzeAllTests = args.includes('--all-tests'); console.log(`📁 Project path: ${projectPath}`); console.log(`📄 Page name: ${pageName}`); console.log(`🔍 All tests analysis: ${analyzeAllTests ? 'Yes' : 'No'}`); // Импортируем UI coverage из DemoProject const { UICoverageAnalyzer } = await import('../../../DemoProject/lib/uiCoverageAnalyzer.js'); // Загружаем критичные элементы если указан путь let criticalElements = [ { type: 'button', name: 'Get started', selector: 'text=Get started' }, { type: 'button', name: 'Search', selector: 'button:has-text("Search")' }, { type: 'link', name: 'Docs', selector: 'text=Docs' }, { type: 'navigation', name: 'Main', selector: 'navigation' } ]; if (criticalConfigPath && fs.existsSync(criticalConfigPath)) { const criticalConfig = JSON.parse(fs.readFileSync(criticalConfigPath, 'utf8')); criticalElements = criticalConfig.criticalElements || criticalElements; } // Создаем анализатор const analyzer = new UICoverageAnalyzer({ criticalElements, coverageReportsDir: path.join(projectPath, 'ui-coverage-reports') }); // Получаем snapshots для анализа let snapshots = []; if (analyzeAllTests) { console.log('🔍 Analyzing all test scenarios...'); snapshots = [ { name: 'main-page', content: self.getDemoSnapshot() }, { name: 'navigation-test', content: self.getNavigationSnapshot() }, { name: 'form-test', content: self.getFormSnapshot() }, { name: 'api-test', content: self.getApiSnapshot() } ]; } else { snapshots = [{ name: pageName, content: self.getDemoSnapshot() }]; } // Выполняем анализ для каждого snapshot const allResults = []; for (const snapshot of snapshots) { console.log(`🌳 Parsing accessibility tree for ${snapshot.name}...`); const accessibilityTree = analyzer.parseAccessibilityTree(snapshot.content); console.log(`📊 Analyzing element coverage for ${snapshot.name}...`); const elementStats = analyzer.analyzeElementCoverage(accessibilityTree); console.log(`🔍 Checking critical elements for ${snapshot.name}...`); const criticalCheck = analyzer.checkCriticalElements(accessibilityTree, criticalElements); // Сравнение с эталоном если включено let goldenComparison = null; if (enableGolden) { const goldenSnapshot = self.getGoldenSnapshot(); const goldenTree = analyzer.parseAccessibilityTree(goldenSnapshot); goldenComparison = analyzer.compareWithGolden(accessibilityTree, goldenTree); } allResults.push({ name: snapshot.name, accessibilityTree, elementStats, criticalCheck, goldenComparison }); } // Агрегируем результаты const aggregatedResults = self.aggregateResults(allResults); const accessibilityTree = aggregatedResults.combinedTree; const elementStats = aggregatedResults.combinedStats; const criticalCheck = aggregatedResults.combinedCriticalCheck; const goldenComparison = aggregatedResults.combinedGoldenComparison; // Генерируем отчет const analysisResults = { accessibilityTree, elementStats, criticalCheck, goldenComparison }; const coverageReport = analyzer.generateCoverageReport(analysisResults, pageName); // Сохраняем отчет const timestamp = Date.now(); await analyzer.saveReport(coverageReport, `ui-coverage-${timestamp}.md`); // Генерируем HTML отчет const htmlReport = self.generateHTMLReport(coverageReport, analysisResults, timestamp); const htmlPath = path.join(projectPath, 'ui-coverage-reports', `ui-coverage-${timestamp}.html`); // Создаем директорию если не существует const reportsDir = path.join(projectPath, 'ui-coverage-reports'); if (!fs.existsSync(reportsDir)) { fs.mkdirSync(reportsDir, { recursive: true }); } fs.writeFileSync(htmlPath, htmlReport, 'utf8'); // Выводим результаты console.log('\n✅ UI Coverage analysis completed'); if (analyzeAllTests) { console.log(`📊 Analyzed ${allResults.length} test scenarios`); allResults.forEach(result => { console.log(` • ${result.name}: ${result.elementStats.summary.totalElements} elements`); }); } console.log(`📊 Total elements: ${coverageReport.summary.totalElements}`); console.log(`🎯 Interactive elements: ${coverageReport.summary.interactiveElements}`); console.log(`📈 Coverage: ${coverageReport.summary.coveragePercentage}%`); console.log(`♿ Accessibility Score: ${coverageReport.summary.accessibilityScore}%`); console.log(`🔍 Critical elements found: ${criticalCheck.foundCritical.length}/${criticalElements.length}`); if (criticalCheck.missingCritical.length > 0) { console.log('\n❌ Missing critical elements:'); criticalCheck.missingCritical.forEach(el => { console.log(` • ${el.name} (${el.type})`); }); } if (coverageReport.recommendations.length > 0) { console.log('\n💡 Recommendations:'); coverageReport.recommendations.forEach(rec => { console.log(` • ${rec}`); }); } console.log(`\n📄 Reports saved:`); console.log(` 📝 Markdown: ui-coverage-reports/ui-coverage-${timestamp}.md`); console.log(` 🌐 HTML: ui-coverage-reports/ui-coverage-${timestamp}.html`); console.log(` 📊 JSON: ui-coverage-reports/ui-coverage-${timestamp}.json`); return { success: true, report: coverageReport, recommendations: coverageReport.recommendations, htmlPath }; } catch (error) { console.error('❌ UI Coverage analysis failed:', error.message); throw error; } } }; } /** * Создает команду настройки * @returns {Object} */ createSetupCommand() { return { description: 'Interactive setup wizard', usage: 'setup [options]', options: [ '--help Show help for this command' ], async execute(args, options) { console.log('🧙‍♂️ Setup wizard (delegating to existing implementation)'); // Делегируем к временной реализации const { startSetupWizard } = await import('../../infrastructure/legacy/LegacyConfigWizard.js'); return await startSetupWizard(); } }; } /** * Создает команду валидации * @returns {Object} */ createValidateCommand() { return { description: 'Validate configuration', usage: 'validate [options]', options: [ '--help Show help for this command' ], async execute(args, options) { console.log('🔍 Configuration validation (delegating to existing implementation)'); // Делегируем к временной реализации const { validateConfiguration } = await import('../../infrastructure/legacy/LegacyConfigValidator.js'); return await validateConfiguration(); } }; } /** * Создает команду информации о системе * @returns {Object} */ createInfoCommand() { const self = this; return { description: 'Show system and dependency information', usage: 'info', options: [], async execute(args, options) { console.log('ℹ️ System Information\n'); try { // Информация о контейнере const info = self.container.getRegistrationInfo(); console.log('🔧 Dependency Injection Container:'); console.log(` 📦 Registered bindings: ${info.bindings.length}`); console.log(` 🔄 Singleton instances: ${info.singletons.length}`); console.log(` 📋 Constants: ${info.instances.length}`); // Информация о конфигурации try { const config = await self.container.get('config'); console.log('\n⚙️ Configuration:'); console.log(` 🤖 AI Server: ${config.ai_server}`); console.log(` 🧠 Model: ${config.model}`); console.log(` 📊 Allure Integration: ${config.allure_integration ? 'Enabled' : 'Disabled'}`); console.log(` 🔗 MCP Integration: ${config.mcp_integration ? 'Enabled' : 'Disabled'}`); } catch (error) { console.log('\n⚠️ Configuration: Not loaded or invalid'); } // Информация о AI провайдере try { const aiProvider = await self.container.get('aiProvider'); console.log('\n🤖 AI Provider:'); console.log(` 📛 Name: ${aiProvider.getProviderName()}`); console.log(` 🧠 Supported models: ${aiProvider.getSupportedModels().join(', ')}`); } catch (error) { console.log('\n⚠️ AI Provider: Not available'); } // Архитектурная информация console.log('\n🏗️ Architecture:'); console.log(' 📋 Pattern: Clean Architecture + Domain-Driven Design'); console.log(' 🔧 DI Container: Custom implementation'); console.log(' 🎯 Use Cases: Application layer orchestration'); console.log(' 📦 Entities: Rich domain models'); } catch (error) { console.error('❌ Error getting system info:', error.message); } } }; } /** * Создает команду помощи * @returns {Object} */ createHelpCommand() { const self = this; return { description: 'Show help information', usage: 'help [command]', options: [], execute(args, options) { const commandName = args[1]; if (commandName && self.commands.has(commandName)) { self.showCommandHelp(commandName); } else { self.showGeneralHelp(); } } }; } /** * Создает команду версии * @returns {Object} */ createVersionCommand() { return { description: 'Show version information', usage: 'version', options: [], async execute(args, options) { const packageJson = await import('../../../package.json', { assert: { type: 'json' } }); console.log(`playwright-ai-auto-debug v${packageJson.default.version}`); console.log('🏗️ Clean Architecture Edition'); } }; } /** * Создает команду UI Test Coverage * @returns {Object} */ createCoverageCommand() { return { description: 'UI Test Coverage system setup and management', usage: 'coverage <subcommand> [options]', options: [ 'init Setup UI Test Coverage in current project', 'info Show information about coverage system', '--help Show help for this command' ], async execute(args, options) { try { const { CoverageCommand } = await import('./CoverageCommand.js'); const coverageCmd = new CoverageCommand(); const subcommand = args[1]; // coverage <subcommand> switch (subcommand) { case 'init': await coverageCmd.init(options); break; case 'info': await coverageCmd.info(); break; default: console.log('🎯 UI Test Coverage'); console.log('\nДоступные команды:'); console.log(' npx playwright-ai coverage init # Настройка в проекте'); console.log(' npx playwright-ai coverage info # Информация о системе'); console.log('\n💡 После настройки:'); console.log(' npm run test:coverage # Запуск тестов с покрытием'); console.log(' npm run coverage:open # Открытие отчета'); } } catch (error) { console.error('❌ Ошибка команды coverage:', error.message); throw error; } } }; } /** * Запускает CLI приложение * @param {string[]} args - аргументы командной строки * @returns {Promise<*>} */ async run(args) { const [command = 'debug', ...params] = args; // Обработка флагов помощи и версии if (params.includes('--help') || params.includes('-h')) { if (this.commands.has(command)) { this.showCommandHelp(command); return; } } if (params.includes('--version') || params.includes('-v')) { await this.commands.get('version').execute([], {}); return; } // Выполнение команды const commandHandler = this.commands.get(command); if (!commandHandler) { console.error(`❌ Unknown command: ${command}`); this.showGeneralHelp(); process.exit(1); } try { return await commandHandler.execute([command, ...params], {}); } catch (error) { console.error(`❌ Command '${command}' failed:`, error.message); if (process.env.DEBUG) { console.error('Stack trace:', error.stack); } process.exit(1); } } /** * Показывает общую справку */ showGeneralHelp() { console.log('🎭 Playwright AI Auto-Debug - Clean Architecture Edition\n'); console.log('🏗️ Automatic Playwright test debugging with AI assistance\n'); console.log('Usage: playwright-ai <command> [options]\n'); console.log('Commands:'); for (const [name, command] of this.commands) { console.log(` ${name.padEnd(12)} ${command.description}`); } console.log('\nGlobal Options:'); console.log(' --help, -h Show help'); console.log(' --version, -v Show version'); console.log('\nExamples:'); console.log(' playwright-ai debug # Analyze errors in current directory'); console.log(' playwright-ai debug --use-mcp # Use MCP for DOM snapshots'); console.log(' playwright-ai setup # Interactive configuration'); console.log(' playwright-ai validate # Validate configuration'); console.log(' playwright-ai info # Show system information'); console.log('\n🏗️ Architecture Features:'); console.log(' • Clean Architecture with Domain-Driven Design'); console.log(' • Dependency Injection container'); console.log(' • Modular AI providers (Strategy pattern)'); console.log(' • Extensible reporters (Observer pattern)'); console.log(' • Rich domain entities with business logic'); console.log(' • Use Cases for application orchestration'); } /** * Показывает справку по конкретной команде * @param {string} commandName - имя команды */ showCommandHelp(commandName) { const command = this.commands.get(commandName); if (!command) { console.error(`❌ Unknown command: ${commandName}`); return; } console.log(`Command: ${commandName}`); console.log(`Description: ${command.description}`); console.log(`Usage: playwright-ai ${command.usage}\n`); if (command.options && command.options.length > 0) { console.log('Options:'); command.options.forEach(option => { console.log(` ${option}`); }); } } /** * Извлекает значение опции из аргументов * @param {string[]} args - аргументы * @param {string} option - имя опции * @returns {string|null} */ extractOption(args, option) { const index = args.indexOf(option); if (index !== -1 && index + 1 < args.length) { return args[index + 1]; } return null; } /** * Получает демо snapshot для UI coverage анализа * @returns {string} */ getDemoSnapshot() { return `# Page snapshot - region "Skip to main content": - link "Skip to main content": - /url: "#__docusaurus_skipToContent_fallback" - navigation "Main": - link "Playwright logo Playwright": - /url: / - img "Playwright logo" - text: Playwright - link "Docs": - /url: /docs/intro - link "API": - /url: /docs/api/class-playwright - button "Node.js" - link "Community": - /url: /community/welcome - link "GitHub repository": - /url: https://github.com/microsoft/playwright - button "Switch between dark and light mode" - button "Search (Command+K)": Search ⌘ K - banner: - heading "Playwright enables reliable end-to-end testing for modern web apps." [level=1] - link "Get started": - /url: /docs/intro - link "Star microsoft/playwright on GitHub": - /url: https://github.com/microsoft/playwright - main: - img "Browsers (Chromium, Firefox, WebKit)" - heading "Any browser • Any platform • One API" [level=3] - paragraph: Cross-browser. Playwright supports all modern rendering engines - link "TypeScript": - /url: https://playwright.dev/docs/intro - link "JavaScript": - /url: https://playwright.dev/docs/intro`; } /** * Получает золотой snapshot для сравнения * @returns {string} */ getGoldenSnapshot() { return `# Page snapshot - navigation "Main": - link "Playwright logo Playwright": - /url: / - link "Docs": - /url: /docs/intro - button "Node.js" - link "Community": - /url: /community/welcome - button "Search (Command+K)": Search ⌘ K - banner: - heading "Playwright enables reliable end-to-end testing" [level=1] - link "Get started": - /url: /docs/intro - main: - heading "Any browser • Any platform • One API" [level=3] - link "TypeScript": - /url: https://playwright.dev/docs/intro`; } /** * Генерирует подробный HTML отчет по UI coverage * @param {Object} coverageReport - отчет о покрытии * @param {Object} analysisResults - результаты анализа * @param {number} timestamp - временная метка * @returns {string} HTML содержимое */ generateHTMLReport(coverageReport, analysisResults, timestamp) { const { summary, elementBreakdown, criticalElementsCheck, recommendations } = coverageReport; const { accessibilityTree, elementStats, criticalCheck, goldenComparison } = analysisResults; return `<!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>UI Coverage Report - ${coverageReport.metadata.pageName}</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; } .elements-table { width: 100%; border-collapse: collapse; margin: 20px 0; } .elements-table th, .elements-table td { padding: 12px; text-align: left; border-bottom: 1px solid #eee; } .elements-table th { background: #f5f5f5; font-weight: bold; } .element-row.found { background: #e8f5e8; } .element-row.missing { background: #ffebee; } .element-row.critical { border-left: 4px solid #f44336; } .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; } .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; } .progress-bar { width: 100%; height: 20px; background: #eee; border-radius: 10px; overflow: hidden; margin: 10px 0; } .progress-fill { height: 100%; transition: width 0.3s; } .progress-high { background: linear-gradient(90deg, #4caf50, #8bc34a); } .progress-medium { background: linear-gradient(90deg, #ff9800, #ffc107); } .progress-low { background: linear-gradient(90deg, #f44336, #e91e63); } .element-tree { font-family: monospace; background: #f9f9f9; padding: 15px; border-radius: 5px; max-height: 400px; overflow-y: auto; } </style> </head> <body> <div class="container"> <div class="header"> <h1>🎯 UI Coverage Report</h1> <p><strong>Страница:</strong> ${coverageReport.metadata.pageName}</p> <p><strong>Время анализа:</strong> ${new Date(timestamp).toLocaleString('ru-RU')}</p> </div> <div class="stats-grid"> <div class="stat-card ${summary.coveragePercentage >= 80 ? 'success' : summary.coveragePercentage >= 50 ? 'warning' : 'danger'}"> <div class="stat-value">${summary.coveragePercentage}%</div> <div class="stat-label">Покрытие тестами</div> </div> <div class="stat-card"> <div class="stat-value">${summary.totalElements}</div> <div class="stat-label">Всего элементов</div> </div> <div class="stat-card"> <div class="stat-value">${summary.interactiveElements}</div> <div class="stat-label">Интерактивных</div> </div> <div class="stat-card ${summary.accessibilityScore >= 70 ? 'success' : summary.accessibilityScore >= 40 ? 'warning' : 'danger'}"> <div class="stat-value">${summary.accessibilityScore}%</div> <div class="stat-label">Доступность</div> </div> </div> <div class="tabs"> <div class="tab active" onclick="showTab('overview')">Обзор</div> <div class="tab" onclick="showTab('elements')">Элементы</div> <div class="tab" onclick="showTab('critical')">Критичные</div> <div class="tab" onclick="showTab('recommendations')">Рекомендации</div> ${goldenComparison ? '<div class="tab" onclick="showTab(\'comparison\')">Сравнение</div>' : ''} </div> <div id="overview" class="tab-content active"> <div class="section"> <h2 class="section-title">📊 Разбивка по типам элементов</h2> <table class="elements-table"> <thead> <tr> <th>Тип элемента</th> <th>Количество</th> <th>Покрытие</th> </tr> </thead> <tbody> <tr> <td>🔘 Кнопки</td> <td>${elementBreakdown.buttons || 0}</td> <td> <div class="progress-bar"> <div class="progress-fill progress-medium" style="width: ${Math.min(100, (elementBreakdown.buttons || 0) * 20)}%"></div> </div> </td> </tr> <tr> <td>🔗 Ссылки</td> <td>${elementBreakdown.links || 0}</td> <td> <div class="progress-bar"> <div class="progress-fill progress-high" style="width: ${Math.min(100, (elementBreakdown.links || 0) * 10)}%"></div> </div> </td> </tr> <tr> <td>📝 Поля ввода</td> <td>${elementBreakdown.inputs || 0}</td> <td> <div class="progress-bar"> <div class="progress-fill progress-low" style="width: ${Math.min(100, (elementBreakdown.inputs || 0) * 25)}%"></div> </div> </td> </tr> <tr> <td>📋 Формы</td> <td>${elementBreakdown.forms || 0}</td> <td> <div class="progress-bar"> <div class="progress-fill progress-medium" style="width: ${Math.min(100, (elementBreakdown.forms || 0) * 50)}%"></div> </div> </td> </tr> </tbody> </table> </div> </div> <div id="elements" class="tab-content"> <div class="section"> <h2 class="section-title">🌳 Дерево элементов страницы</h2> <div class="element-tree"> ${accessibilityTree.elements.map(el => ` <div class="element-item">${el.line}</div>` ).join('\n')} </div> </div> </div> <div id="critical" class="tab-content"> <div class="section"> <h2 class="section-title">🎯 Критичные элементы</h2> <table class="elements-table"> <thead> <tr> <th>Элемент</th> <th>Тип</th> <th>Селектор</th> <th>Статус</th> </tr> </thead> <tbody> ${criticalCheck.foundCritical.map(el => ` <tr class="element-row found"> <td>✅ ${el.name}</td> <td>${el.type}</td> <td><code>${el.selector}</code></td> <td><span style="color: #4caf50;">Найден</span></td> </tr>` ).join('\n')} ${criticalCheck.missingCritical.map(el => ` <tr class="element-row missing critical"> <td>❌ ${el.name}</td> <td>${el.type}</td> <td><code>${el.selector}</code></td> <td><span style="color: #f44336;">Отсутствует</span></td> </tr>` ).join('\n')} </tbody> </table> </div> </div> <div id="recommendations" class="tab-content"> <div class="section"> <h2 class="section-title">💡 Рекомендации</h2> ${recommendations.map(rec => ` <div class="recommendation ${rec.includes('🔴') ? 'high' : rec.includes('♿') ? 'medium' : 'low'}"> ${rec} </div>` ).join('\n')} </div> </div> ${goldenComparison ? ` <div id="comparison" class="tab-content"> <div class="section"> <h2 class="section-title">🔗 Сравнение с эталоном</h2> <p><strong>Идентичность:</strong> ${goldenComparison.identical ? '✅ Да' : '❌ Нет'}</p> <p><strong>Новых элементов:</strong> ${goldenComparison.newElements.length}</p> <p><strong>Удаленных элементов:</strong> ${goldenComparison.removedElements.length}</p> ${goldenComparison.differences.length > 0 ? ` <h3>Различия:</h3> <ul> ${goldenComparison.differences.map(diff => `<li>${diff}</li>`).join('\n')} </ul> ` : ''} </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'); } </script> </body> </html>`; } /** * Получает snapshot для навигационного теста */ getNavigationSnapshot() { return `# Navigation Test Page snapshot - navigation "Main": - link "Home": /url: / - link "Products": /url: /products - link "About": /url: /about - link "Contact": /url: /contact - button "Menu Toggle" - main: - heading "Navigation Test" [level=1] - button "Back to Home"`; } /** * Получает snapshot для формы */ getFormSnapshot() { return `# Form Test Page snapshot - main: - heading "Contact Form" [level=1] - form: - input "Name" type=text - input "Email" type=email - textarea "Message" - button "Submit" - button "Reset" - navigation: - link "Back": /url: /`; } /** * Получает snapshot для API теста */ getApiSnapshot() { return `# API Test Page snapshot - main: - heading "API Dashboard" [level=1] - button "Load Data" - button "Refresh" - div "Loading indicator" - section "Results": - table "Data Table" - button "Export CSV" - link "View Details": /url: /details`; } /** * Агрегирует результаты всех тестов */ aggregateResults(allResults) { const combinedElements = []; let totalElements = 0; let totalInteractive = 0; let allFoundCritical = []; let allMissingCritical = []; // Объединяем элементы из всех результатов allResults.forEach(result => { combinedElements.push(...result.accessibilityTree.elements); totalElements += result.elementStats.summary.totalElements; totalInteractive += result.elementStats.summary.interactive; allFoundCritical.push(...result.criticalCheck.foundCritical); allMissingCritical.push(...result.criticalCheck.missingCritical); }); // Удаляем дубликаты критичных элементов const uniqueFound = allFoundCritical.filter((item, index, self) => index === self.findIndex(el => el.name === item.name && el.type === item.type) ); const uniqueMissing = allMissingCritical.filter((item, index, self) => index === self.findIndex(el => el.name === item.name && el.type === item.type) ); return { combinedTree: { elements: combinedElements, totalCount: totalElements, byType: this.groupElementsByType(combinedElements) }, combinedStats: { summary: { totalElements, interactive: totalInteractive, buttons: combinedElements.filter(el => el.type === 'button').length, links: combinedElements.filter(el => el.type === 'link').length, inputs: combinedElements.filter(el => el.type === 'input').length, forms: combinedElements.filter(el => el.type === 'form').length, navigation: combinedElements.filter(el => el.type === 'navigation').length, withAriaLabel: 0, withRole: 0 } }, combinedCriticalCheck: { foundCritical: uniqueFound, missingCritical: uniqueMissing, allCriticalPresent: uniqueMissing.length === 0 }, combinedGoldenComparison: allResults[0]?.goldenComparison || null, testResults: allResults }; } /** * Группирует элементы по типу */ groupElementsByType(elements) { const grouped = {}; elements.forEach(el => { if (!grouped[el.type]) { grouped[el.type] = []; } grouped[el.type].push(el); }); return grouped; } /** * Отображает результаты анализа * @param {Object} results - результаты анализа */ async displayResults(results) { console.log('\n📊 Analysis Results Summary:'); console.log('─'.repeat(50)); if (results.summary) { const summary = results.summary; console.log(`📁 Total files: ${summary.totalFiles}`); console.log(`✅ Processed: ${summary.processedFiles}`); console.log(`❌ Errors: ${summary.errorFiles}`); console.log(`📈 Success rate: ${summary.successRate.toFixed(1)}%`); console.log(`🎯 Average confidence: ${summary.averageConfidence.toFixed(1)}%`); console.log(`⏱️ Processing time: ${summary.processingTimeMs}ms`); if (summary.topErrorTypes.length > 0) { console.log(`🔍 Top error types: ${summary.topErrorTypes.slice(0, 3).join(', ')}`); } if (summary.totalActions > 0) { console.log(`🎬 Total actions suggested: ${summary.totalActions}`); } if (summary.totalRecommendations > 0) { console.log(`💡 Total recommendations: ${summary.totalRecommendations}`); } } console.log('\n🏗️ Powered by Clean Architecture'); if (results.success) { console.log('✅ Analysis completed successfully'); // Показываем пользователю где найти отчеты await this.showReportLocations(); } else { console.log('⚠️ Analysis completed with some errors'); } } /** * Показывает пользователю где найти созданные отчеты */ async showReportLocations() { try { const config = await this.container.get('config'); const fs = await import('fs'); const path = await import('path'); const { glob } = await import('glob'); console.log('\n📄 Где найти отчеты:'); // Проверяем HTML отчеты const reportDir = config.report_dir || 'playwright-report'; if (fs.existsSync(reportDir)) { const htmlFiles = await glob(path.join(reportDir, 'ai-analysis-*.html')); if (htmlFiles.length > 0) { const latestHtml = htmlFiles[htmlFiles.length - 1]; console.log(` 🌐 HTML отчет: ${latestHtml}`); console.log(` 💡 Откройте в браузере: open ${latestHtml}`); } } // Проверяем Markdown отчеты const aiResponsesDir = config.ai_responses_dir || 'ai-responses'; if (fs.existsSync(aiResponsesDir)) { const markdownFiles = await glob(path.join(aiResponsesDir, 'analysis-summary-*.md')); if (markdownFiles.length > 0) { const latestSummary = markdownFiles[markdownFiles.length - 1]; console.log(` 📝 Сводный отчет: ${latestSummary}`); } const responseFiles = await glob(path.join(aiResponsesDir, 'ai-response-*.md')); if (responseFiles.length > 0) { console.log(` 📄 AI ответы: ${responseFiles.length} файлов в ${aiResponsesDir}/`); } } // Проверяем Allure if (config.allure_integration) { const allureDir = config.allure_results_dir || 'allure-results'; if (fs.existsSync(allureDir)) { console.log(` 📊 Allure attachments: ${allureDir}/`); console.log(` 💡 Запустите: npx allure serve ${allureDir}`); } } } catch (error) { console.warn(`⚠️ Не удалось проверить отчеты: ${error.message}`); } } /** * Очистка ресурсов */ async dispose() { if (this.container) { this.container.clear(); } } }