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
217 lines • 10.3 kB
JavaScript
/**
* Phoenix LiveView Screenshot Fix
*
* Addresses the 1x1 pixel screenshot issue by properly waiting for
* LiveView to fully render before taking screenshots.
*/
export class PhoenixLiveViewScreenshotFix {
/**
* Take a proper screenshot of a Phoenix LiveView page
* Addresses the 1x1 pixel issue by ensuring the page is fully loaded
*/
static async takeScreenshot(page, options = {}) {
const { fullPage = true, selector, timeout = 30000 } = options;
try {
// Step 1: Ensure viewport has reasonable size
const viewport = await page.viewportSize();
if (!viewport || viewport.width < 100 || viewport.height < 100) {
console.error('🔧 Setting default viewport size to avoid 1x1 issue');
await page.setViewportSize({ width: 1280, height: 720 });
}
// Step 2: Wait for Phoenix LiveView to be ready
const hasLiveView = await page.evaluate(() => {
return !!window.liveSocket || !!document.querySelector('[data-phx-main]');
});
if (hasLiveView) {
console.error('⚡ Phoenix LiveView detected, waiting for full initialization...');
// Wait for LiveSocket connection
await page.waitForFunction(() => {
if (window.liveSocket && window.liveSocket.isConnected) {
return window.liveSocket.isConnected();
}
// Fallback: check for phx-connected attribute
return document.body?.getAttribute('phx-connected') === 'true';
}, { timeout: timeout / 2 }).catch(() => {
console.error('⚠️ LiveSocket connection check timed out, proceeding anyway');
});
// Wait for any pending LiveView updates
await page.waitForFunction(() => {
// Check if there are no pending phx events
const pendingElements = document.querySelectorAll('[phx-pending]');
return pendingElements.length === 0;
}, { timeout: 5000 }).catch(() => {
console.error('⚠️ Some LiveView updates might still be pending');
});
// Additional wait for animations/transitions
await page.waitForTimeout(500);
}
// Step 3: Ensure page has actual content
const dimensions = await page.evaluate(() => {
return {
documentWidth: document.documentElement.scrollWidth,
documentHeight: document.documentElement.scrollHeight,
bodyWidth: document.body.scrollWidth,
bodyHeight: document.body.scrollHeight,
hasContent: (document.body.textContent || '').trim().length > 0
};
});
if (dimensions.documentHeight < 10 || dimensions.documentWidth < 10) {
console.error('⚠️ Page dimensions are too small:', dimensions);
console.error('🔄 Waiting for content to render...');
// Force a re-render by scrolling
await page.evaluate(() => window.scrollTo(0, 0));
await page.waitForTimeout(1000);
// Try to wait for any specific content
await page.waitForSelector('body > *', { timeout: 5000 }).catch(() => { });
}
// Step 4: Take the screenshot with retry logic
let screenshotBuffer = Buffer.alloc(0);
let attempts = 0;
const maxAttempts = 3;
while (attempts < maxAttempts) {
attempts++;
try {
if (selector) {
// Ensure the selector exists and is visible
await page.waitForSelector(selector, {
state: 'visible',
timeout: 5000
});
const element = await page.$(selector);
if (!element) {
throw new Error(`Selector "${selector}" not found`);
}
screenshotBuffer = await element.screenshot({ type: 'png' });
}
else {
screenshotBuffer = await page.screenshot({
fullPage,
type: 'png'
});
}
// Verify the screenshot is not 1x1
// PNG header + minimal content should be > 100 bytes
if (screenshotBuffer.length < 100) {
throw new Error(`Screenshot too small (${screenshotBuffer.length} bytes), likely 1x1 pixel`);
}
break; // Success
}
catch (error) {
console.error(`⚠️ Screenshot attempt ${attempts} failed:`, error.message);
if (attempts < maxAttempts) {
console.error('🔄 Retrying after additional wait...');
await page.waitForTimeout(2000);
// Try to trigger a re-render
await page.evaluate(() => {
window.dispatchEvent(new Event('resize'));
document.body.style.display = 'none';
document.body.offsetHeight; // Force reflow
document.body.style.display = '';
});
}
else {
throw error;
}
}
}
// Step 5: Get actual dimensions from the screenshot
// For now, we'll get page dimensions as a proxy
const finalDimensions = await page.evaluate(() => {
const rect = document.documentElement.getBoundingClientRect();
return {
width: Math.max(rect.width, window.innerWidth),
height: Math.max(rect.height, window.innerHeight)
};
});
// Step 6: Collect debug info
const debugInfo = await page.evaluate(() => {
return {
url: window.location.href,
title: document.title,
readyState: document.readyState,
liveViewConnected: window.liveSocket?.isConnected?.() || false,
elementCount: document.querySelectorAll('*').length,
hasPhxMain: !!document.querySelector('[data-phx-main]'),
viewportWidth: window.innerWidth,
viewportHeight: window.innerHeight,
scrollWidth: document.documentElement.scrollWidth,
scrollHeight: document.documentElement.scrollHeight
};
});
console.error('✅ Screenshot captured successfully:', {
size: `${screenshotBuffer.length} bytes`,
dimensions: `${finalDimensions.width}x${finalDimensions.height}`,
liveView: debugInfo.liveViewConnected ? 'connected' : 'not connected'
});
return {
data: screenshotBuffer.toString('base64'),
width: finalDimensions.width,
height: finalDimensions.height,
debugInfo
};
}
catch (error) {
console.error('❌ Phoenix LiveView screenshot failed:', error);
// Last resort: return a diagnostic image
const diagnosticHtml = `
<html>
<body style="padding: 20px; font-family: monospace;">
<h2>Screenshot Failed</h2>
<p>Error: ${error.message}</p>
<p>URL: ${page.url()}</p>
<p>This is a diagnostic image. The actual page screenshot failed.</p>
<details>
<summary>Debug Info</summary>
<pre>${JSON.stringify({
error: error.message,
url: page.url(),
timestamp: new Date().toISOString()
}, null, 2)}</pre>
</details>
</body>
</html>
`;
await page.setContent(diagnosticHtml);
const diagnosticScreenshot = await page.screenshot({ type: 'png' });
return {
data: diagnosticScreenshot.toString('base64'),
width: 800,
height: 600,
debugInfo: {
error: error instanceof Error ? error.message : String(error),
diagnostic: true
}
};
}
}
/**
* Wait for Phoenix LiveView to be fully loaded
*/
static async waitForLiveView(page, timeout = 10000) {
try {
// Method 1: Wait for LiveSocket connection
const hasLiveSocket = await page.waitForFunction(() => window.liveSocket?.isConnected?.() === true, { timeout: timeout / 2 }).then(() => true).catch(() => false);
if (hasLiveSocket) {
console.error('✅ LiveSocket connected');
return true;
}
// Method 2: Wait for phx-connected attribute
const hasPhxConnected = await page.waitForFunction(() => document.body.getAttribute('phx-connected') === 'true', { timeout: timeout / 2 }).then(() => true).catch(() => false);
if (hasPhxConnected) {
console.error('✅ Phoenix connected (phx-connected)');
return true;
}
// Method 3: Check for any LiveView elements
const hasLiveViewElements = await page.evaluate(() => {
return document.querySelector('[data-phx-main], [data-phx-session]') !== null;
});
return hasLiveViewElements;
}
catch {
return false;
}
}
}
// Type augmentation for window.liveSocket
// Window.liveSocket type already defined elsewhere
//# sourceMappingURL=phoenix-liveview-screenshot-fix.js.map