UNPKG

playwright-ai-auto-debug

Version:

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

613 lines (521 loc) 23.2 kB
// src/infrastructure/di/bindings.js import { Container } from './Container.js'; // Импорты реальных реализаций import { OpenAIProvider } from '../ai/OpenAIProvider.js'; import { MistralProvider } from '../ai/MistralProvider.js'; import { LocalAIProvider } from '../ai/LocalAIProvider.js'; import { FileErrorRepository } from '../repositories/FileErrorRepository.js'; // import { ReporterManager } from '../reporters/ReporterManager.js'; // import { McpClient } from '../mcp/McpClient.js'; import { ConfigLoader } from '../config/ConfigLoader.js'; // Импорты Use Cases import { AnalyzeTestErrorsUseCase } from '../../application/usecases/AnalyzeTestErrorsUseCase.js'; import { TestDebugService } from '../../application/services/TestDebugService.js'; /** * Конфигурирует DI контейнер со всеми зависимостями * @returns {Container} - настроенный контейнер */ export function configureContainer() { const container = new Container(); // ===== CONFIGURATION ===== container.singleton('configLoader', (c) => { return new ConfigLoader(); }); container.transient('config', async (c) => { const configLoader = c.get('configLoader'); return await configLoader.loadAiConfig(); }); // ===== REPOSITORIES ===== container.singleton('errorRepository', (c) => { return new FileErrorRepository(); }); // ===== AI PROVIDERS ===== container.singleton('aiProviderFactory', (c) => { return { create(providerType, config) { switch (providerType.toLowerCase()) { case 'openai': return new OpenAIProvider(); case 'mistral': return new MistralProvider(); case 'local': case 'ollama': case 'lmstudio': return new LocalAIProvider(); case 'auto': default: // Автоопределение по URL сервера if (config.ai_server && config.ai_server.includes('mistral.ai')) { return new MistralProvider(); } else if (config.ai_server && config.ai_server.includes('openai.com')) { return new OpenAIProvider(); } else if (config.ai_server && ( config.ai_server.includes('localhost') || config.ai_server.includes('127.0.0.1') || config.ai_server.match(/192\.168\.\d+\.\d+/) || config.ai_server.match(/10\.\d+\.\d+\.\d+/) || config.ai_server.match(/172\.\d+\.\d+\.\d+/) )) { return new LocalAIProvider(); } return new OpenAIProvider(); case 'claude': // TODO: Реализовать ClaudeProvider throw new Error('Claude provider not implemented yet. Use OpenAI, Mistral or Local provider.'); } } }; }); container.singleton('aiProvider', async (c) => { const config = await c.get('config'); const factory = c.get('aiProviderFactory'); // Определяем провайдера на основе конфигурации let providerType = 'auto'; // автоопределение if (config.ai_server && config.ai_server.includes('mistral.ai')) { providerType = 'mistral'; } else if (config.ai_server && config.ai_server.includes('openai.com')) { providerType = 'openai'; } else if (config.ai_server && config.ai_server.includes('claude')) { providerType = 'claude'; } else if (config.ai_server && ( config.ai_server.includes('localhost') || config.ai_server.includes('127.0.0.1') || config.ai_server.match(/192\.168\.\d+\.\d+/) || config.ai_server.match(/10\.\d+\.\d+\.\d+/) || config.ai_server.match(/172\.\d+\.\d+\.\d+/) )) { providerType = 'local'; } return factory.create(providerType, config); }); // ===== REPORTERS ===== container.singleton('htmlReporter', (c) => { return { async generate(results) { const { updateHtmlReport } = await import('../../../lib/updateHtml.js'); const config = await c.get('config'); const fs = await import('fs'); const path = await import('path'); console.log(`🌐 HTML Reporter: Processing ${results.length} results...`); // Создаем HTML отчет с результатами анализа const timestamp = Date.now(); const reportDir = config.report_dir || 'playwright-report'; const htmlPath = path.join(reportDir, `ai-analysis-${timestamp}.html`); // Создаем директорию если не существует if (!fs.existsSync(reportDir)) { fs.mkdirSync(reportDir, { recursive: true }); console.log(`📁 Created report directory: ${reportDir}`); } // Генерируем HTML отчет const htmlContent = this.generateHTMLReport(results, config); fs.writeFileSync(htmlPath, htmlContent, 'utf-8'); console.log(`📄 Created HTML report: ${htmlPath}`); // Проверяем, есть ли основной HTML отчет Playwright и интегрируем туда AI анализ const mainReportPath = path.join(reportDir, 'index.html'); if (fs.existsSync(mainReportPath) && results.length > 0) { console.log('🔗 Integrating AI analysis into main Playwright report...'); // Импортируем функцию извлечения имени теста const { extractTestName } = await import('../../../lib/updateHtml.js'); // Собираем все AI анализы для интеграции const aiAnalyses = results .filter(result => result.aiResponse?.content) .map(result => { // Извлекаем имя теста из пути к файлу const filePath = result.testError?.filePath || ''; let testName = extractTestName(result.testError?.content || '', filePath) || result.testError?.testName || 'Unknown Test'; return { testName, errorContent: result.testError?.content || '', aiResponse: result.aiResponse?.content || '', filePath }; }); if (aiAnalyses.length > 0) { await this.integrateIntoMainReport(mainReportPath, aiAnalyses); } } // Также обновляем существующие HTML файлы если они есть for (const result of results) { if (result.errorFile?.htmlPath) { await updateHtmlReport( result.errorFile.htmlPath, result.testError?.content || '', result.aiResponse?.content || '' ); console.log(`🔄 Updated existing HTML: ${result.errorFile.htmlPath}`); } } }, async integrateIntoMainReport(mainReportPath, aiAnalyses) { const fs = await import('fs'); const { updateHtmlReport } = await import('../../../lib/updateHtml.js'); try { // Интегрируем каждый AI анализ в основной отчет for (const analysis of aiAnalyses) { await updateHtmlReport( mainReportPath, analysis.errorContent, analysis.aiResponse, analysis.testName ); } console.log(`✅ Successfully integrated ${aiAnalyses.length} AI analyses into main report`); } catch (error) { console.error(`❌ Failed to integrate AI analyses: ${error.message}`); } }, generateHTMLReport(results, config) { const timestamp = new Date().toISOString(); const successCount = results.filter(r => r.success).length; return `<!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>AI Анализ Ошибок Тестов</title> <style> body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; margin: 20px; background: #f5f5f5; } .container { max-width: 1200px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } .header { text-align: center; margin-bottom: 30px; } .summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-bottom: 30px; } .stat-card { background: #f8f9fa; padding: 15px; border-radius: 6px; text-align: center; } .success { border-left: 4px solid #28a745; } .error { border-left: 4px solid #dc3545; } .result-item { margin-bottom: 20px; padding: 15px; border: 1px solid #ddd; border-radius: 6px; } .result-header { font-weight: bold; margin-bottom: 10px; } .ai-response { background: #f8f9fa; padding: 15px; border-radius: 4px; margin-top: 10px; } pre { background: #f4f4f4; padding: 10px; border-radius: 4px; overflow-x: auto; } </style> </head> <body> <div class="container"> <div class="header"> <h1>🤖 AI Анализ Ошибок Тестов</h1> <p>Отчет создан: ${timestamp}</p> </div> <div class="summary"> <div class="stat-card success"> <h3>✅ Успешно</h3> <div style="font-size: 24px; font-weight: bold;">${successCount}</div> </div> <div class="stat-card error"> <h3>❌ Ошибок</h3> <div style="font-size: 24px; font-weight: bold;">${results.length - successCount}</div> </div> <div class="stat-card"> <h3>📊 Всего</h3> <div style="font-size: 24px; font-weight: bold;">${results.length}</div> </div> </div> <div class="results"> ${results.map((result, index) => ` <div class="result-item ${result.success ? 'success' : 'error'}"> <div class="result-header"> ${result.success ? '✅' : '❌'} Файл ${index + 1}: ${result.testError?.filePath || 'Неизвестно'} </div> ${result.testError ? ` <p><strong>Тест:</strong> ${result.testError.testName || 'Не определен'}</p> <p><strong>Тип ошибки:</strong> ${result.testError.errorType || 'Неизвестно'}</p> ` : ''} ${result.aiResponse ? ` <div class="ai-response"> <h4>🤖 AI Рекомендации:</h4> <pre>${result.aiResponse.content}</pre> </div> ` : ''} ${result.error ? ` <div style="color: #dc3545; margin-top: 10px;"> <strong>Ошибка:</strong> ${result.error} </div> ` : ''} </div> `).join('')} </div> </div> </body> </html>`; }, getName() { return 'HTML Reporter'; } }; }); container.singleton('allureReporter', (c) => { return { async generate(results) { const { createAllureAttachment } = await import('../../../lib/sendToAI.js'); console.log(`🎯 Allure Reporter: Processing ${results.length} results...`); for (let i = 0; i < results.length; i++) { const result = results[i]; if (result.aiResponse && result.testError) { const config = await c.get('config'); console.log(`\n📋 Processing result ${i + 1}/${results.length}:`); console.log(` Test error file: ${result.testError.filePath}`); console.log(` Test error content length: ${result.testError.content?.length || 0} chars`); console.log(` AI response length: ${result.aiResponse.content?.length || 0} chars`); await createAllureAttachment( result.aiResponse.content, result.testError.content, config, i, result.testError.filePath ); } else { console.log(`⚠️ Skipping result ${i + 1}: missing aiResponse or testError`); if (!result.aiResponse) console.log(` Missing aiResponse`); if (!result.testError) console.log(` Missing testError`); } } console.log(`\n✅ Allure Reporter: Completed processing ${results.length} results`); }, getName() { return 'Allure Reporter'; } }; }); container.singleton('markdownReporter', (c) => { return { async generate(results) { const { saveResponseToMarkdown } = await import('../../../lib/sendToAI.js'); const config = await c.get('config'); const fs = await import('fs'); const path = await import('path'); console.log(`📝 Markdown Reporter: Processing ${results.length} results...`); const outputDir = config.ai_responses_dir || 'ai-responses'; // Создаем директорию если не существует if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); console.log(`📁 Created AI responses directory: ${outputDir}`); } // Сохраняем индивидуальные файлы for (let i = 0; i < results.length; i++) { const result = results[i]; if (result.aiResponse && result.testError) { saveResponseToMarkdown( result.aiResponse.content, result.testError.content, config, i ); } } // Создаем общий сводный отчет const timestamp = Date.now(); const summaryPath = path.join(outputDir, `analysis-summary-${timestamp}.md`); const summaryContent = this.generateSummaryReport(results, config); fs.writeFileSync(summaryPath, summaryContent, 'utf-8'); console.log(`📄 Created summary report: ${summaryPath}`); }, generateSummaryReport(results, config) { const timestamp = new Date().toISOString(); const successCount = results.filter(r => r.success).length; const errorCount = results.length - successCount; let summary = `# 🤖 AI Анализ Ошибок Тестов\n\n`; summary += `**Дата создания:** ${timestamp}\n\n`; summary += `## 📊 Сводка\n\n`; summary += `- ✅ **Успешно обработано:** ${successCount}/${results.length}\n`; summary += `- ❌ **Ошибок:** ${errorCount}/${results.length}\n`; summary += `- 📈 **Процент успеха:** ${Math.round((successCount / results.length) * 100)}%\n\n`; if (successCount > 0) { summary += `## ✅ Успешно проанализированные файлы\n\n`; results.filter(r => r.success).forEach((result, index) => { summary += `### ${index + 1}. ${result.testError?.testName || 'Неизвестный тест'}\n`; summary += `**Файл:** \`${result.testError?.filePath || 'Неизвестно'}\`\n`; summary += `**Тип ошибки:** ${result.testError?.errorType || 'Неизвестно'}\n\n`; if (result.aiResponse) { summary += `**AI Рекомендации:**\n\`\`\`\n${result.aiResponse.content}\n\`\`\`\n\n`; } summary += `---\n\n`; }); } if (errorCount > 0) { summary += `## ❌ Ошибки обработки\n\n`; results.filter(r => !r.success).forEach((result, index) => { summary += `### ${index + 1}. ${result.testError?.filePath || 'Неизвестный файл'}\n`; summary += `**Ошибка:** ${result.error || 'Неизвестная ошибка'}\n\n`; }); } return summary; }, getName() { return 'Markdown Reporter'; } }; }); container.singleton('reporterManager', async (c) => { const config = await c.get('config'); const reporters = []; // Добавляем репортеры на основе конфигурации reporters.push(c.get('htmlReporter')); // всегда включен if (config.allure_integration) { reporters.push(c.get('allureReporter')); } if (config.save_ai_responses) { reporters.push(c.get('markdownReporter')); } return { reporters, async createReports(results) { console.log(`📊 Creating reports using ${this.reporters.length} reporter(s)...`); console.log(`📁 Results to process: ${results.length}`); console.log(`⚙️ Config directories: ai_responses_dir="${config.ai_responses_dir}", allure_results_dir="${config.allure_results_dir}"`); for (const reporter of this.reporters) { try { console.log(`📄 Running ${reporter.getName()}...`); await reporter.generate(results); console.log(`✅ ${reporter.getName()} completed successfully`); } catch (error) { console.warn(`⚠️ ${reporter.getName()} failed: ${error.message}`); console.warn(` Stack: ${error.stack}`); } } console.log(`📊 Report generation completed. Check directories:`); console.log(` 📁 AI responses: ${config.ai_responses_dir || 'ai-responses'}`); console.log(` 📁 Allure results: ${config.allure_results_dir || 'allure-results'}`); }, addReporter(reporter) { this.reporters.push(reporter); } }; }); // ===== MCP CLIENT ===== container.transient('mcpClient', async (c) => { const config = await c.get('config'); if (!config.mcp_integration) { return null; } // TODO: Реализовать новый MCP клиент const { McpClient } = await import('../../../lib/mcpClient.js'); return new McpClient(config); }); // ===== USE CASES ===== container.singleton('analyzeTestErrorsUseCase', async (c) => { const errorRepository = c.get('errorRepository'); const aiProvider = await c.get('aiProvider'); const reporterManager = await c.get('reporterManager'); const mcpClient = await c.get('mcpClient'); return new AnalyzeTestErrorsUseCase( errorRepository, aiProvider, reporterManager, mcpClient ); }); // ===== SERVICES ===== container.singleton('testDebugService', async (c) => { const analyzeUseCase = await c.get('analyzeTestErrorsUseCase'); return { async debugTests(projectPath, options = {}) { const config = await c.get('config'); const request = { projectPath: projectPath || process.cwd(), config, useMcp: options.useMcp || false }; return await analyzeUseCase.execute(request); } }; }); // ===== MIDDLEWARE ===== // Middleware для логирования создания экземпляров container.addMiddleware((key, instance, container) => { if (process.env.DEBUG_DI) { console.log(`🔧 DI: Created instance of '${key}'`); } return instance; }); // Middleware для обработки ошибок container.addMiddleware((key, instance, container) => { if (instance && typeof instance === 'object') { // Добавляем обработку ошибок для сервисов const originalMethods = {}; for (const prop of Object.getOwnPropertyNames(Object.getPrototypeOf(instance))) { if (typeof instance[prop] === 'function' && prop !== 'constructor') { originalMethods[prop] = instance[prop]; instance[prop] = async (...args) => { try { return await originalMethods[prop].apply(instance, args); } catch (error) { console.error(`❌ Error in ${key}.${prop}:`, error.message); throw error; } }; } } } return instance; }); return container; } /** * Создает контейнер для тестирования с mock зависимостями * @returns {Container} */ export function configureTestContainer() { const container = new Container(); // Mock зависимости для тестирования container.constant('config', { api_key: 'test-key', model: 'test-model', max_prompt_length: 1000, request_delay: 0 }); container.singleton('errorRepository', () => ({ async findErrors() { return [ { path: 'test-error.txt', content: 'Test error content', htmlPath: 'test-report.html' } ]; } })); container.singleton('aiProvider', () => ({ async generateResponse(prompt) { return 'Mock AI response for: ' + prompt.substring(0, 50); }, getProviderName() { return 'Mock Provider'; }, getSupportedModels() { return ['mock-model']; }, async validateConfiguration() { return { isValid: true, issues: [] }; } })); container.singleton('reporterManager', () => ({ async createReports(results) { console.log(`Mock: Created reports for ${results.length} results`); } })); container.constant('mcpClient', null); // Use Cases container.singleton('analyzeTestErrorsUseCase', (c) => new AnalyzeTestErrorsUseCase( c.get('errorRepository'), c.get('aiProvider'), c.get('reporterManager'), c.get('mcpClient') ) ); // Application Services container.singleton('testDebugService', (c) => new TestDebugService( c.get('analyzeTestErrorsUseCase'), c.get('config') ) ); return container; } /** * Получает основной настроенный контейнер (singleton) * @returns {Container} */ let mainContainer = null; export function getContainer() { if (!mainContainer) { mainContainer = configureContainer(); } return mainContainer; } /** * Сбрасывает основной контейнер (для тестирования) */ export function resetContainer() { if (mainContainer) { mainContainer.clear(); mainContainer = null; } }