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

505 lines (501 loc) 19.5 kB
/** * Universal Screenshot Fix * * Addresses the 1x1 pixel screenshot issue across all frameworks by: * 1. Ensuring proper viewport size * 2. Waiting for content to load * 3. Validating screenshot size * 4. Framework-specific wait logic */ export class UniversalScreenshotFix { static MIN_VIEWPORT_WIDTH = 1280; static MIN_VIEWPORT_HEIGHT = 720; static MIN_SCREENSHOT_SIZE = 1000; // bytes - anything less is likely 1x1 static MAX_RETRIES = 3; /** * Take a screenshot with universal fixes for all frameworks */ static async takeScreenshot(page, options = {}) { const startTime = Date.now(); console.error('📸 Universal screenshot fix starting...'); try { // Step 1: Ensure viewport has proper size await this.ensureViewportSize(page); // Step 2: Detect framework and apply specific wait logic const framework = await this.detectFramework(page); await this.waitForFramework(page, framework, options.timeout); // Step 3: Ensure page has rendered content await this.ensureContentLoaded(page); // Step 4: Apply annotations if requested if (options.annotations && options.annotations.length > 0) { await this.applyAnnotations(page, options.annotations); } // Step 5: Take screenshot with retry logic const screenshot = await this.captureWithRetry(page, options); // Step 6: Validate and return result return await this.validateAndReturn(page, screenshot, startTime); } catch (error) { console.error('❌ Screenshot failed:', error); return await this.createDiagnosticScreenshot(page, error); } finally { // Clean up annotations if any await this.removeAnnotations(page); } } /** * Ensure viewport has minimum size to avoid 1x1 issue */ static async ensureViewportSize(page) { const viewport = await page.viewportSize(); if (!viewport || viewport.width < this.MIN_VIEWPORT_WIDTH || viewport.height < this.MIN_VIEWPORT_HEIGHT) { console.error(`🔧 Adjusting viewport from ${viewport?.width}x${viewport?.height} to ${this.MIN_VIEWPORT_WIDTH}x${this.MIN_VIEWPORT_HEIGHT}`); await page.setViewportSize({ width: this.MIN_VIEWPORT_WIDTH, height: this.MIN_VIEWPORT_HEIGHT }); } } /** * Detect the framework being used */ static async detectFramework(page) { try { // Try to use existing framework detector if available const framework = await page.evaluate(() => { // Check for common framework indicators if (window.liveSocket || document.querySelector('[data-phx-main]')) return 'phoenix-liveview'; if (window.React || document.querySelector('[data-reactroot]')) return 'react'; if (window.Vue || document.querySelector('#app[data-v-]')) return 'vue'; if (window.ng || document.querySelector('[ng-version]')) return 'angular'; if (window.flutter || document.querySelector('flt-scene')) return 'flutter'; if (window.Livewire || document.querySelector('[wire\\:id]')) return 'livewire'; if (document.querySelector('[data-svelte]')) return 'svelte'; if (document.querySelector('[data-server-rendered="true"]')) return 'nuxt'; if (window.__NEXT_DATA__) return 'nextjs'; return 'unknown'; }); console.error(`🔍 Detected framework: ${framework}`); return framework; } catch { return 'unknown'; } } /** * Apply framework-specific wait logic */ static async waitForFramework(page, framework, timeout) { const maxWait = timeout || 10000; switch (framework) { case 'phoenix-liveview': await this.waitForPhoenixLiveView(page, maxWait); break; case 'react': case 'nextjs': await this.waitForReact(page, maxWait); break; case 'vue': case 'nuxt': await this.waitForVue(page, maxWait); break; case 'angular': await this.waitForAngular(page, maxWait); break; case 'flutter': await this.waitForFlutter(page, maxWait); break; default: // Generic wait for any framework await this.waitForGeneric(page, maxWait); } } /** * Phoenix LiveView specific wait */ static async waitForPhoenixLiveView(page, timeout) { console.error('⚡ Waiting for Phoenix LiveView...'); try { // Wait for LiveSocket connection await page.waitForFunction(() => window.liveSocket?.isConnected?.() === true, { timeout: timeout / 2 }).catch(() => { }); // Wait for phx-connected await page.waitForFunction(() => document.body?.getAttribute('phx-connected') === 'true', { timeout: timeout / 2 }).catch(() => { }); // Wait for no pending updates await page.waitForFunction(() => document.querySelectorAll('[phx-pending]').length === 0, { timeout: 5000 }).catch(() => { }); } catch { console.error('⚠️ Phoenix LiveView wait incomplete, proceeding...'); } } /** * React specific wait */ static async waitForReact(page, timeout) { console.error('⚛️ Waiting for React...'); try { await page.waitForFunction(() => { const root = document.querySelector('[data-reactroot]') || document.getElementById('root'); return root && root.children.length > 0; }, { timeout }); } catch { console.error('⚠️ React wait incomplete, proceeding...'); } } /** * Vue specific wait */ static async waitForVue(page, timeout) { console.error('🟢 Waiting for Vue...'); try { await page.waitForFunction(() => { const app = document.getElementById('app') || document.querySelector('[data-v-]'); return app && app.children.length > 0; }, { timeout }); } catch { console.error('⚠️ Vue wait incomplete, proceeding...'); } } /** * Angular specific wait */ static async waitForAngular(page, timeout) { console.error('🅰️ Waiting for Angular...'); try { await page.waitForFunction(() => window.getAllAngularTestabilities?.()?.every(t => t.isStable()) ?? true, { timeout }); } catch { console.error('⚠️ Angular wait incomplete, proceeding...'); } } /** * Flutter specific wait */ static async waitForFlutter(page, timeout) { console.error('🐦 Waiting for Flutter...'); try { await page.waitForSelector('flt-scene-host', { timeout: timeout / 2 }); await page.waitForTimeout(1000); // Flutter needs extra time } catch { console.error('⚠️ Flutter wait incomplete, proceeding...'); } } /** * Generic wait for any framework */ static async waitForGeneric(page, timeout) { console.error('⏳ Waiting for page to stabilize...'); try { // Wait for basic loading await page.waitForLoadState('domcontentloaded', { timeout: timeout / 3 }); await page.waitForLoadState('networkidle', { timeout: timeout / 3 }); // Wait for common indicators await page.waitForFunction(() => { // Check if page has meaningful content const hasText = (document.body.textContent || '').trim().length > 10; const hasElements = document.querySelectorAll('*').length > 20; const noSpinners = document.querySelectorAll('[class*="spinner"], [class*="loading"], .loader').length === 0; return hasText && hasElements && noSpinners; }, { timeout: timeout / 3 }).catch(() => { }); } catch { console.error('⚠️ Generic wait incomplete, proceeding...'); } } /** * Ensure content is loaded and visible */ static async ensureContentLoaded(page) { const dimensions = await page.evaluate(() => ({ bodyWidth: document.body.scrollWidth, bodyHeight: document.body.scrollHeight, hasContent: (document.body.textContent?.trim() || '').length > 0, isVisible: window.getComputedStyle(document.body).visibility === 'visible' })); if (dimensions.bodyHeight < 10 || dimensions.bodyWidth < 10 || !dimensions.hasContent) { console.error('⚠️ Page appears empty, attempting recovery...'); // Try to trigger rendering await page.evaluate(() => { window.dispatchEvent(new Event('resize')); window.scrollTo(0, 0); }); await page.waitForTimeout(1000); } } /** * Capture screenshot with retry logic */ static async captureWithRetry(page, options) { let lastError; for (let attempt = 1; attempt <= this.MAX_RETRIES; attempt++) { try { console.error(`📸 Screenshot attempt ${attempt}/${this.MAX_RETRIES}...`); let screenshot; if (options.selector) { const element = await page.$(options.selector); if (!element) { throw new Error(`Selector "${options.selector}" not found`); } screenshot = await element.screenshot({ type: 'png' }); } else { screenshot = await page.screenshot({ fullPage: options.fullPage !== false, type: 'png' }); } // Validate size if (screenshot.length < this.MIN_SCREENSHOT_SIZE) { throw new Error(`Screenshot too small (${screenshot.length} bytes), likely 1x1 pixel`); } return screenshot; } catch (error) { lastError = error; console.error(`⚠️ Attempt ${attempt} failed:`, error instanceof Error ? error.message : String(error)); if (attempt < this.MAX_RETRIES) { await page.waitForTimeout(1000 * attempt); // Exponential backoff } } } throw lastError; } /** * Apply visual annotations to the page */ static async applyAnnotations(page, annotations) { await page.evaluate((annotations) => { const style = document.createElement('style'); style.id = 'screenshot-annotations'; style.textContent = ` .screenshot-annotation { position: absolute; z-index: 999999; pointer-events: none; } .screenshot-highlight { border: 3px solid #ff0000; background: rgba(255, 0, 0, 0.1); } .screenshot-arrow { width: 0; height: 0; border-left: 10px solid transparent; border-right: 10px solid transparent; border-bottom: 20px solid #ff0000; } .screenshot-text { background: #ff0000; color: white; padding: 5px 10px; border-radius: 3px; font-family: monospace; font-size: 14px; } `; document.head.appendChild(style); annotations.forEach((ann, index) => { const div = document.createElement('div'); div.className = `screenshot-annotation screenshot-${ann.type}`; div.id = `annotation-${index}`; if (ann.selector) { const element = document.querySelector(ann.selector); if (element) { const rect = element.getBoundingClientRect(); div.style.left = rect.left + 'px'; div.style.top = rect.top + 'px'; div.style.width = rect.width + 'px'; div.style.height = rect.height + 'px'; } } else if (ann.x !== undefined && ann.y !== undefined) { div.style.left = ann.x + 'px'; div.style.top = ann.y + 'px'; } if (ann.text && ann.type === 'text') { div.textContent = ann.text; } document.body.appendChild(div); }); }, annotations); // Wait for annotations to render await page.waitForTimeout(100); } /** * Remove annotations after screenshot */ static async removeAnnotations(page) { await page.evaluate(() => { document.getElementById('screenshot-annotations')?.remove(); document.querySelectorAll('.screenshot-annotation').forEach(el => el.remove()); }).catch(() => { }); } /** * Validate screenshot and create result */ static async validateAndReturn(page, screenshot, startTime) { // Get actual dimensions const dimensions = await page.evaluate(() => ({ viewport: { width: window.innerWidth, height: window.innerHeight }, document: { width: document.documentElement.scrollWidth, height: document.documentElement.scrollHeight } })); const debugInfo = { captureTime: Date.now() - startTime, screenshotSize: screenshot.length, viewport: dimensions.viewport, document: dimensions.document, url: page.url() }; console.error('✅ Screenshot captured:', { size: `${screenshot.length} bytes`, time: `${debugInfo.captureTime}ms`, dimensions: `${dimensions.document.width}x${dimensions.document.height}` }); return { data: screenshot.toString('base64'), width: dimensions.document.width, height: dimensions.document.height, format: 'PNG', debugInfo }; } /** * Create diagnostic screenshot when capture fails */ static async createDiagnosticScreenshot(page, error) { console.error('🚨 Creating diagnostic screenshot...'); const diagnosticHtml = ` <!DOCTYPE html> <html> <head> <style> body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, monospace; padding: 40px; background: #f5f5f5; margin: 0; } .container { max-width: 800px; margin: 0 auto; background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } h1 { color: #d32f2f; } .error-box { background: #ffebee; border: 1px solid #ffcdd2; padding: 15px; border-radius: 4px; margin: 20px 0; } .debug-info { background: #f5f5f5; padding: 15px; border-radius: 4px; font-family: monospace; font-size: 12px; overflow-x: auto; } .recommendation { background: #e3f2fd; border: 1px solid #bbdefb; padding: 15px; border-radius: 4px; margin-top: 20px; } </style> </head> <body> <div class="container"> <h1>🚨 Screenshot Capture Failed</h1> <div class="error-box"> <strong>Error:</strong> ${error.message || 'Unknown error'} </div> <h3>Debug Information:</h3> <div class="debug-info"> <strong>URL:</strong> ${page.url()}<br> <strong>Timestamp:</strong> ${new Date().toISOString()}<br> <strong>Error Stack:</strong><br> <pre>${error.stack || 'No stack trace available'}</pre> </div> <div class="recommendation"> <strong>💡 Recommendations:</strong> <ul> <li>Ensure the page has finished loading</li> <li>Check if the page requires authentication</li> <li>Verify the URL is accessible</li> <li>Try using a different selector or fullPage option</li> </ul> </div> <p><em>This is a diagnostic image. The actual screenshot could not be captured.</em></p> </div> </body> </html> `; try { await page.setContent(diagnosticHtml); const screenshot = await page.screenshot({ type: 'png' }); return { data: screenshot.toString('base64'), width: 800, height: 600, format: 'PNG', debugInfo: { error: error.message, diagnostic: true, url: page.url() } }; } catch (diagError) { // Ultimate fallback - return empty image data console.error('❌ Even diagnostic screenshot failed:', diagError); // Create a minimal 1x1 transparent PNG const minimalPng = Buffer.from([ 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x44, 0x41, 0x54, 0x08, 0x5B, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0D, 0x0A, 0x2D, 0xB4, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82 ]); return { data: minimalPng.toString('base64'), width: 1, height: 1, format: 'PNG', debugInfo: { error: 'Critical failure - returning minimal PNG', diagnostic: true } }; } } } //# sourceMappingURL=universal-screenshot-fix.js.map