UNPKG

ai-debug-local-mcp

Version:

🎯 ENHANCED AI GUIDANCE v4.1.2: Dramatically improved tool descriptions help AI users choose the right tools instead of 'close enough' options. Ultra-fast keyboard automation (10x speed), universal recording, multi-ecosystem debugging support, and compreh

897 lines (854 loc) 38.1 kB
/** * Test Debugging Helper Handler * * Provides specialized tools for debugging test failures, particularly for: * - Registration flow redirects * - LiveView navigation tracking * - Form submission verification * - Visual regression timing * * Note: These tools require an existing debug session to be created first * using inject_debugging tool. */ import { BaseToolHandler } from './base-handler.js'; export class TestDebuggingHelperHandler extends BaseToolHandler { tools = [ { name: 'verify_registration_flow', description: `🔍 VERIFY REGISTRATION FLOW: Debug registration redirects by capturing the actual user flow, monitoring navigation events, and verifying the final destination. Perfect for fixing test expectations when redirects don't match assumptions. REQUIRES: Active debug session from inject_debugging tool.`, inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Debug session ID from inject_debugging' }, registrationData: { type: 'object', properties: { email: { type: 'string' }, password: { type: 'string' }, name: { type: 'string' }, additionalFields: { type: 'object' } }, required: ['email', 'password'] }, expectedRedirect: { type: 'string', description: 'Expected redirect URL pattern (regex supported)' }, captureScreenshots: { type: 'boolean', default: true, description: 'Capture screenshots at each step' }, timeout: { type: 'number', default: 10000, description: 'Navigation timeout in milliseconds' } }, required: ['sessionId', 'registrationData'] } }, { name: 'diagnose_liveview_death', description: `🩺 DIAGNOSE LIVEVIEW DEATH: Capture console errors, monitor GraphQL/WebSocket failures, and track disconnections when LiveView processes die. Essential for debugging async test failures and mock coverage issues. REQUIRES: Active debug session from inject_debugging tool.`, inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Debug session ID from inject_debugging' }, setupActions: { type: 'array', items: { type: 'object', properties: { action: { type: 'string', enum: ['click', 'fill', 'select', 'wait'] }, selector: { type: 'string' }, value: { type: 'string' }, timeout: { type: 'number' } } }, description: 'Actions to perform before monitoring' }, monitorDuration: { type: 'number', default: 5000, description: 'How long to monitor for issues (ms)' }, captureNetworkErrors: { type: 'boolean', default: true, description: 'Capture failed network requests' }, captureConsoleErrors: { type: 'boolean', default: true, description: 'Capture console errors and warnings' } }, required: ['sessionId'] } }, { name: 'capture_with_visual_stability', description: `📸 CAPTURE WITH VISUAL STABILITY: Take screenshots only after visual stability is achieved, preventing timing issues in visual regression tests. Waits for DOM changes to stop, animations to complete, and specific elements to render. REQUIRES: Active debug session from inject_debugging tool.`, inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Debug session ID from inject_debugging' }, stabilityThreshold: { type: 'number', default: 500, description: 'Milliseconds of no DOM changes to consider stable' }, waitConditions: { type: 'array', items: { type: 'object', properties: { type: { type: 'string', enum: ['element', 'text', 'animation', 'network', 'custom'] }, selector: { type: 'string' }, state: { type: 'string' }, text: { type: 'string' }, customScript: { type: 'string' } } }, description: 'Specific conditions to wait for' }, maxWaitTime: { type: 'number', default: 30000, description: 'Maximum time to wait for stability (ms)' }, annotations: { type: 'array', items: { type: 'object', properties: { type: { type: 'string' }, selector: { type: 'string' }, text: { type: 'string' } } }, description: 'Annotations to add to the screenshot' } }, required: ['sessionId'] } }, { name: 'debug_failed_test', description: `🐛 DEBUG FAILED TEST: Comprehensive test failure analysis with console logs, network activity, DOM snapshots, and AI-powered insights. Generates detailed reports for debugging complex test failures. REQUIRES: Active debug session from inject_debugging tool.`, inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Debug session ID from inject_debugging' }, testName: { type: 'string', description: 'Name of the failing test' }, setupScript: { type: 'string', description: 'Optional JavaScript to run before debugging' }, includeAIAnalysis: { type: 'boolean', default: true, description: 'Include AI-powered failure analysis' }, captureBeforeFailure: { type: 'boolean', default: true, description: 'Capture state before the failure occurs' }, focusAreas: { type: 'array', items: { type: 'string', enum: ['graphql', 'websocket', 'navigation', 'forms', 'async', 'dom'] }, description: 'Specific areas to focus debugging on' } }, required: ['sessionId', 'testName'] } } ]; async handle(toolName, args, sessions) { // Validate session exists const session = sessions.get(args.sessionId); if (!session) { return { content: [{ type: 'text', text: `❌ No active debug session found with ID: ${args.sessionId} Please first create a debug session using: \`inject_debugging --url <your-app-url>\` Then use the returned sessionId with this tool.` }] }; } switch (toolName) { case 'verify_registration_flow': return this.verifyRegistrationFlow(args, session); case 'diagnose_liveview_death': return this.diagnoseLiveViewDeath(args, session); case 'capture_with_visual_stability': return this.captureWithVisualStability(args, session); case 'debug_failed_test': return this.debugFailedTest(args, session); default: throw new Error(`Unknown test debugging tool: ${toolName}`); } } async verifyRegistrationFlow(args, session) { const { registrationData, expectedRedirect, captureScreenshots = true, timeout = 10000 } = args; try { const page = session.page; if (!page) { throw new Error('No page available in session'); } const navigationEvents = []; const screenshots = []; // Monitor navigation events const navigationHandler = (frame) => { if (frame === page.mainFrame()) { navigationEvents.push({ type: 'navigation', url: frame.url(), timestamp: Date.now() }); } }; page.on('framenavigated', navigationHandler); // Capture initial state if (captureScreenshots) { const initialScreenshot = await page.screenshot({ fullPage: true }); screenshots.push({ step: 'initial', url: page.url(), screenshot: initialScreenshot.toString('base64') }); } // Fill registration form for (const [field, value] of Object.entries(registrationData)) { if (field === 'additionalFields') continue; const selectors = [ `input[name="${field}"]`, `input[id*="${field}"]`, `input[placeholder*="${field}" i]` ]; let filled = false; for (const selector of selectors) { try { await page.fill(selector, value); filled = true; break; } catch (e) { // Try next selector } } if (!filled) { throw new Error(`Could not find input field for ${field}`); } } // Handle additional fields if (registrationData.additionalFields) { for (const [field, value] of Object.entries(registrationData.additionalFields)) { await page.fill(`input[name="${field}"]`, value); } } // Capture filled form if (captureScreenshots) { const filledScreenshot = await page.screenshot({ fullPage: true }); screenshots.push({ step: 'form_filled', url: page.url(), screenshot: filledScreenshot.toString('base64') }); } // Submit form const submitSelectors = [ 'button[type="submit"]', 'input[type="submit"]', 'button:has-text("Register")', 'button:has-text("Sign up")', 'button:has-text("Sign Up")', 'button:has-text("Create Account")' ]; let submitted = false; for (const selector of submitSelectors) { try { await page.click(selector); submitted = true; break; } catch (e) { // Try next selector } } if (!submitted) { throw new Error('Could not find submit button'); } // Wait for navigation or timeout try { await page.waitForNavigation({ timeout: timeout / 2 }); } catch (e) { // Navigation might not happen, check for errors } // Wait a bit more for any redirects await new Promise(resolve => setTimeout(resolve, 2000)); // Get final state const finalUrl = page.url(); // Capture final destination if (captureScreenshots) { const finalScreenshot = await page.screenshot({ fullPage: true }); screenshots.push({ step: 'final_destination', url: finalUrl, screenshot: finalScreenshot.toString('base64') }); } // Clean up event handler page.off('framenavigated', navigationHandler); // Analyze results const redirectMatches = expectedRedirect ? new RegExp(expectedRedirect).test(finalUrl) : null; return { content: [{ type: 'text', text: `## 🔍 Registration Flow Analysis **Initial URL**: ${session.url} **Final URL**: ${finalUrl} **Expected Pattern**: ${expectedRedirect || 'Not specified'} **Redirect Matches**: ${redirectMatches === null ? 'N/A' : redirectMatches ? '✅ Yes' : '❌ No'} ### Navigation Path ${navigationEvents.map((e, i) => `${i + 1}. ${e.url} (${new Date(e.timestamp).toISOString()})`).join('\n')} ### Timing - Total navigation events: ${navigationEvents.length} - Time to final destination: ${navigationEvents.length > 0 ? navigationEvents[navigationEvents.length - 1].timestamp - navigationEvents[0].timestamp : 0}ms ### Recommendations ${this.generateRegistrationFlowRecommendations(finalUrl, expectedRedirect, navigationEvents).map((r) => `- ${r}`).join('\n')} ### Screenshots Captured ${screenshots.map(s => `- ${s.step}: ${s.url}`).join('\n')}` }] }; } catch (error) { return { content: [{ type: 'text', text: `## ❌ Registration Flow Error **Error**: ${error instanceof Error ? error.message : 'Unknown error'} ### Recommendations - Check if the registration form structure has changed - Verify form field selectors match your HTML - Check for validation errors preventing submission - Ensure the page is fully loaded before testing` }] }; } } async diagnoseLiveViewDeath(args, session) { const { setupActions = [], monitorDuration = 5000, captureNetworkErrors = true, captureConsoleErrors = true } = args; try { const page = session.page; if (!page) { throw new Error('No page available in session'); } // Perform setup actions for (const action of setupActions) { switch (action.action) { case 'click': await page.click(action.selector); break; case 'fill': await page.fill(action.selector, action.value); break; case 'select': await page.selectOption(action.selector, action.value); break; case 'wait': await new Promise(resolve => setTimeout(resolve, action.timeout || 1000)); break; } } // Start monitoring const consoleErrors = []; const networkFailures = []; const websocketEvents = []; // Monitor console const consoleHandler = (msg) => { if (msg.type() === 'error' || msg.type() === 'warning') { consoleErrors.push({ level: msg.type(), message: msg.text(), timestamp: Date.now(), location: msg.location() }); } }; if (captureConsoleErrors) { page.on('console', consoleHandler); } // Monitor network const responseHandler = (response) => { const url = response.url(); const status = response.status(); if (status >= 400) { networkFailures.push({ url, method: response.request().method(), status, statusText: response.statusText(), timestamp: Date.now() }); } // Track WebSocket events if (url.includes('websocket') || url.includes('socket')) { websocketEvents.push({ type: 'response', url, status, timestamp: Date.now() }); } }; const requestFailedHandler = (request) => { networkFailures.push({ url: request.url(), method: request.method(), failure: request.failure()?.errorText, timestamp: Date.now() }); }; if (captureNetworkErrors) { page.on('response', responseHandler); page.on('requestfailed', requestFailedHandler); } // Wait for monitoring period await new Promise(resolve => setTimeout(resolve, monitorDuration)); // Clean up handlers if (captureConsoleErrors) { page.off('console', consoleHandler); } if (captureNetworkErrors) { page.off('response', responseHandler); page.off('requestfailed', requestFailedHandler); } // Analyze issues const analysis = this.analyzeLiveViewIssues(consoleErrors, networkFailures, websocketEvents); return { content: [{ type: 'text', text: `## 🩺 LiveView Death Diagnosis ### Console Errors (${consoleErrors.length}) ${consoleErrors.slice(0, 10).map(e => `- [${e.level}] ${e.message}`).join('\n')} ${consoleErrors.length > 10 ? `... and ${consoleErrors.length - 10} more` : ''} ### Network Failures (${networkFailures.length}) ${networkFailures.slice(0, 10).map(f => `- ${f.method || 'GET'} ${f.url} - ${f.status || f.failure}`).join('\n')} ${networkFailures.length > 10 ? `... and ${networkFailures.length - 10} more` : ''} ### GraphQL Errors (${analysis.graphqlProblems.length}) ${analysis.graphqlProblems.join('\n')} ### WebSocket Events (${websocketEvents.length}) ${websocketEvents.map(e => `- ${e.type} ${e.url} - Status: ${e.status}`).join('\n')} ### Analysis **Likely Issues**: ${analysis.likelyIssues.length > 0 ? analysis.likelyIssues.join(', ') : 'None detected'} **WebSocket Problems**: ${analysis.websocketProblems.join(', ') || 'None'} ### Recommendations ${analysis.recommendations.map((r) => `- ${r}`).join('\n')} ### Summary - Total errors: ${consoleErrors.length + networkFailures.length} - Monitoring duration: ${monitorDuration}ms - GraphQL failures: ${networkFailures.filter(f => f.url?.includes('graphql')).length} - WebSocket issues: ${websocketEvents.filter(e => e.status !== 200).length}` }] }; } catch (error) { return { content: [{ type: 'text', text: `## ❌ LiveView Diagnosis Error **Error**: ${error instanceof Error ? error.message : 'Unknown error'}` }] }; } } async captureWithVisualStability(args, session) { const { stabilityThreshold = 500, waitConditions = [], maxWaitTime = 30000, annotations = [] } = args; try { const page = session.page; if (!page) { throw new Error('No page available in session'); } const startTime = Date.now(); let isStable = false; // Inject DOM stability monitor await page.evaluate((threshold) => { let observer; window.__domStable = false; window.__lastDOMChange = Date.now(); observer = new MutationObserver(() => { window.__lastDOMChange = Date.now(); window.__domStable = false; }); observer.observe(document.body, { childList: true, subtree: true, attributes: true, characterData: true }); // Check stability periodically const checkStability = setInterval(() => { if (Date.now() - window.__lastDOMChange > threshold) { window.__domStable = true; clearInterval(checkStability); observer.disconnect(); } }, 100); }, stabilityThreshold); // Wait for specific conditions for (const condition of waitConditions) { await this.waitForCondition(page, condition, maxWaitTime - (Date.now() - startTime)); } // Wait for DOM stability while (!isStable && (Date.now() - startTime) < maxWaitTime) { isStable = await page.evaluate(() => window.__domStable); if (!isStable) { await new Promise(resolve => setTimeout(resolve, 100)); } } // Take screenshot const screenshot = await page.screenshot({ fullPage: true }); // Get timing information const timingInfo = { totalWaitTime: Date.now() - startTime, domStabilityAchieved: isStable, conditionsMet: waitConditions.map((c, i) => ({ condition: c, met: true // If we got here, they were met })) }; return { content: [{ type: 'text', text: `## 📸 Visual Stability Capture ### Stability Status - **DOM Stable**: ${isStable ? '✅ Yes' : '❌ No'} - **Wait Time**: ${timingInfo.totalWaitTime}ms - **Threshold**: ${stabilityThreshold}ms ### Wait Conditions ${waitConditions.length > 0 ? waitConditions.map((c, i) => `${i + 1}. ${c.type} - ${c.selector || c.text || 'custom'} ✅`).join('\n') : 'None specified'} ### Screenshot - **URL**: ${page.url()} - **Full Page**: Yes - **Captured**: ${new Date().toISOString()} ${!isStable ? ` ### ⚠️ Warning DOM stability was not achieved within ${maxWaitTime}ms. The page may still be changing. Consider: - Increasing maxWaitTime - Adjusting stabilityThreshold - Adding specific wait conditions ` : ''}` }], screenshot: screenshot.toString('base64') }; } catch (error) { return { content: [{ type: 'text', text: `## ❌ Visual Stability Capture Error **Error**: ${error instanceof Error ? error.message : 'Unknown error'} ### Recommendations - Increase maxWaitTime if the page loads slowly - Adjust stabilityThreshold for pages with animations - Add specific wait conditions for dynamic content` }] }; } } async debugFailedTest(args, session) { const { testName, setupScript, includeAIAnalysis = true, captureBeforeFailure = true, focusAreas = ['graphql', 'websocket', 'async'] } = args; try { const page = session.page; if (!page) { throw new Error('No page available in session'); } // Run setup script if provided if (setupScript) { await page.evaluate(setupScript); } // Capture initial state const initialState = captureBeforeFailure ? { screenshot: await page.screenshot({ fullPage: true }), url: page.url(), timestamp: Date.now() } : null; // Start comprehensive monitoring const capturedData = { console: [], network: [], errors: [], performance: {} }; // Set up console monitoring const consoleHandler = (msg) => { capturedData.console.push({ type: msg.type(), text: msg.text(), location: msg.location(), timestamp: Date.now() }); }; page.on('console', consoleHandler); // Set up network monitoring const responseHandler = (response) => { const entry = { url: response.url(), method: response.request().method(), status: response.status(), type: response.request().resourceType(), timestamp: Date.now() }; capturedData.network.push(entry); if (response.status() >= 400) { capturedData.errors.push({ ...entry, type: 'network' }); } }; page.on('response', responseHandler); // Monitor for 5 seconds to capture any async issues await new Promise(resolve => setTimeout(resolve, 5000)); // Get performance metrics capturedData.performance = await page.evaluate(() => { const perf = performance.getEntriesByType('navigation')[0]; return { domContentLoaded: perf?.domContentLoadedEventEnd - perf?.domContentLoadedEventStart, loadComplete: perf?.loadEventEnd - perf?.loadEventStart, totalTime: perf?.loadEventEnd - perf?.fetchStart }; }); // Clean up handlers page.off('console', consoleHandler); page.off('response', responseHandler); // Prepare analysis const summary = this.generateTestFailureSummary(capturedData, focusAreas); const recommendations = this.generateTestDebuggingRecommendations(capturedData, focusAreas); return { content: [{ type: 'text', text: `## 🐛 Failed Test Debug Report ### Test: ${testName} **URL**: ${page.url()} **Timestamp**: ${new Date().toISOString()} ### Summary - **Console Errors**: ${summary.consoleErrorCount} - **Network Failures**: ${summary.networkFailureCount} - **GraphQL Issues**: ${summary.graphqlIssues} - **WebSocket Issues**: ${summary.websocketIssues} - **Total Errors**: ${summary.errorCount} ### Focus Areas Analysis ${focusAreas.includes('graphql') ? ` #### GraphQL - Failed requests: ${capturedData.network.filter(n => n.url?.includes('graphql') && n.status >= 400).length} - GraphQL errors: ${capturedData.console.filter(c => c.text?.toLowerCase().includes('graphql')).length} ` : ''} ${focusAreas.includes('websocket') ? ` #### WebSocket - WebSocket errors: ${capturedData.network.filter(n => n.url?.includes('socket') && n.status >= 400).length} - Connection issues: ${capturedData.console.filter(c => c.text?.toLowerCase().includes('websocket') || c.text?.toLowerCase().includes('connection')).length} ` : ''} ${focusAreas.includes('async') ? ` #### Async Operations - Unhandled rejections: ${capturedData.console.filter(c => c.text?.includes('Unhandled') || c.text?.includes('rejection')).length} - Timeout errors: ${capturedData.console.filter(c => c.text?.toLowerCase().includes('timeout')).length} ` : ''} ### Console Output (Last 10 Errors) ${capturedData.console .filter(c => c.type === 'error') .slice(-10) .map(c => `- ${c.text}`) .join('\n') || 'No console errors captured'} ### Failed Network Requests ${capturedData.network .filter(n => n.status >= 400) .slice(0, 10) .map(n => `- ${n.method} ${n.url} - ${n.status}`) .join('\n') || 'No failed requests'} ### Performance Metrics - DOM Content Loaded: ${capturedData.performance.domContentLoaded}ms - Page Load Complete: ${capturedData.performance.loadComplete}ms - Total Load Time: ${capturedData.performance.totalTime}ms ### Recommendations ${recommendations.map((r) => `- ${r}`).join('\n')} ${includeAIAnalysis ? ` ### AI Analysis Note For deeper AI-powered analysis, use the \`analyze_with_ai\` tool with the captured data. ` : ''}` }] }; } catch (error) { return { content: [{ type: 'text', text: `## ❌ Test Debug Error **Error**: ${error instanceof Error ? error.message : 'Unknown error'} **Test**: ${testName}` }] }; } } // Helper methods async waitForCondition(page, condition, timeout) { const startTime = Date.now(); while ((Date.now() - startTime) < timeout) { let conditionMet = false; switch (condition.type) { case 'element': conditionMet = await page.evaluate((selector) => { return document.querySelector(selector) !== null; }, condition.selector); break; case 'text': conditionMet = await page.evaluate((args) => { const element = document.querySelector(args.selector); return element?.textContent?.includes(args.text) || false; }, { selector: condition.selector, text: condition.text }); break; case 'animation': conditionMet = await page.evaluate(() => { const animations = document.getAnimations(); return animations.every((a) => a.playState !== 'running'); }); break; case 'network': // For now, just wait - would need to track network activity await new Promise(resolve => setTimeout(resolve, 1000)); conditionMet = true; break; case 'custom': conditionMet = await page.evaluate(condition.customScript); break; } if (conditionMet) return; await new Promise(resolve => setTimeout(resolve, 100)); } throw new Error(`Condition timeout: ${JSON.stringify(condition)}`); } generateRegistrationFlowRecommendations(finalUrl, expectedRedirect, navigationEvents) { const recommendations = []; if (expectedRedirect && !new RegExp(expectedRedirect).test(finalUrl)) { recommendations.push(`Update test expectation: Registration redirects to "${finalUrl}" not "${expectedRedirect}"`); } if (navigationEvents.length > 2) { recommendations.push(`Multiple redirects detected (${navigationEvents.length}). Consider simplifying the flow.`); } if (navigationEvents.some(e => e.url?.includes('error') || e.url?.includes('fail'))) { recommendations.push('Registration may be failing. Check form validation and server responses.'); } if (navigationEvents.length === 1) { recommendations.push('No navigation occurred. Check if form submission is working correctly.'); } return recommendations; } analyzeLiveViewIssues(consoleErrors, networkFailures, websocketEvents) { const analysis = { likelyIssues: [], graphqlProblems: [], websocketProblems: [], recommendations: [] }; // Analyze GraphQL issues const graphqlErrors = networkFailures.filter(f => f.url?.includes('graphql')); if (graphqlErrors.length > 0) { analysis.graphqlProblems = graphqlErrors.map(e => `- GraphQL ${e.method} to ${e.url} failed with status ${e.status}`); analysis.recommendations.push('Check GraphQL mocks are available to async processes'); analysis.recommendations.push('Verify GraphQL schema matches between tests and application'); } // Analyze WebSocket issues const wsFailures = networkFailures.filter(f => f.url?.includes('socket')); if (wsFailures.length > 0 || websocketEvents.some(e => e.status !== 101 && e.status !== 200)) { analysis.websocketProblems.push('WebSocket connection issues detected'); analysis.recommendations.push('Ensure LiveView process isn\'t crashing'); analysis.recommendations.push('Check Phoenix endpoint configuration'); } // Analyze console errors const liveViewErrors = consoleErrors.filter(e => e.message?.toLowerCase().includes('liveview') || e.message?.toLowerCase().includes('phoenix') || e.message?.toLowerCase().includes('channel')); if (liveViewErrors.length > 0) { analysis.likelyIssues.push('LiveView JavaScript errors detected'); analysis.recommendations.push('Check LiveView hooks and event handlers'); } // Check for process crashes if (consoleErrors.some(e => e.message?.includes('disconnected') || e.message?.includes('terminated'))) { analysis.likelyIssues.push('Process disconnection/termination detected'); analysis.recommendations.push('Check server logs for process crashes'); } return analysis; } generateTestFailureSummary(capturedData, focusAreas) { return { errorCount: capturedData.errors.length, consoleErrorCount: capturedData.console.filter((c) => c.type === 'error').length, networkFailureCount: capturedData.network.filter((n) => n.status >= 400).length, graphqlIssues: focusAreas.includes('graphql') ? capturedData.network.filter((n) => n.url?.includes('graphql') && n.status >= 400).length : 0, websocketIssues: focusAreas.includes('websocket') ? capturedData.network.filter((n) => n.url?.includes('socket') && n.status >= 400).length : 0 }; } generateTestDebuggingRecommendations(capturedData, focusAreas) { const recommendations = []; if (capturedData.network.some((n) => n.url?.includes('graphql') && n.status >= 400)) { recommendations.push('GraphQL requests are failing - verify mocks are set up for all queries'); recommendations.push('Check if GraphQL mocks are accessible from async processes'); } if (capturedData.console.some((c) => c.text?.includes('channel') || c.text?.includes('socket'))) { recommendations.push('Phoenix channel errors detected - check LiveView process lifecycle'); recommendations.push('Verify WebSocket connection is established before interactions'); } if (capturedData.errors.length > 0) { recommendations.push(`${capturedData.errors.length} errors detected - review error messages in console`); } if (capturedData.console.some((c) => c.text?.includes('Unhandled'))) { recommendations.push('Unhandled promise rejections detected - add proper error handling'); } if (focusAreas.includes('async') && capturedData.console.some((c) => c.text?.includes('timeout'))) { recommendations.push('Timeout errors detected - consider increasing test timeouts'); } return recommendations; } } //# sourceMappingURL=test-debugging-helper-handler.js.map