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
JavaScript
/**
* 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