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
248 lines • 10.9 kB
JavaScript
/**
* Flutter Click Handler
* Properly handles coordinate-based clicks on Flutter CanvasKit applications
*/
export class FlutterClickHandler {
/**
* Perform a reliable click on Flutter CanvasKit rendered UI
*/
static async performClick(page, options) {
const { x, y, retries = 3, verbose = true } = options;
if (verbose) {
// console.log(`🎯 Attempting Flutter click at (${x}, ${y})`);
}
// Try multiple click strategies
for (let attempt = 1; attempt <= retries; attempt++) {
if (verbose && attempt > 1) {
// console.log(`🔄 Retry attempt ${attempt}/${retries}`);
}
try {
// Strategy 1: Direct page click with proper options
await page.mouse.move(x, y);
await page.waitForTimeout(50); // Small delay for hover effects
await page.mouse.down();
await page.waitForTimeout(50); // Hold for a moment
await page.mouse.up();
// Strategy 2: Dispatch comprehensive events to Flutter view
const clicked = await page.evaluate(async ({ x, y }) => {
// Find the Flutter rendering surface
const flutterView = document.querySelector('flutter-view');
const canvas = document.querySelector('canvas');
const glassPane = document.querySelector('flt-glass-pane');
// Determine the target element (prefer flutter-view, then canvas)
const target = flutterView || canvas || glassPane || document.elementFromPoint(x, y);
if (!target) {
console.error('No Flutter rendering surface found');
return false;
}
// console.log(`Clicking on: ${target.tagName}${target.id ? '#' + target.id : ''}${target.className ? '.' + target.className : ''}`);
// Helper to create and dispatch event
const dispatchEvent = (type, props = {}) => {
const event = new PointerEvent(type, {
bubbles: true,
cancelable: true,
view: window,
clientX: x,
clientY: y,
screenX: x,
screenY: y,
pageX: x,
pageY: y,
pointerId: 1,
width: 1,
height: 1,
pressure: type.includes('down') ? 0.5 : 0,
pointerType: 'mouse',
isPrimary: true,
...props
});
target.dispatchEvent(event);
};
// Dispatch a complete sequence of events
// This mimics real user interaction more closely
dispatchEvent('pointerover');
dispatchEvent('pointerenter');
dispatchEvent('pointermove');
// Mouse events for compatibility
target.dispatchEvent(new MouseEvent('mouseover', {
bubbles: true,
cancelable: true,
clientX: x,
clientY: y
}));
target.dispatchEvent(new MouseEvent('mouseenter', {
bubbles: true,
cancelable: true,
clientX: x,
clientY: y
}));
target.dispatchEvent(new MouseEvent('mousemove', {
bubbles: true,
cancelable: true,
clientX: x,
clientY: y
}));
// Small delay to let Flutter process hover state
await new Promise(resolve => setTimeout(resolve, 50));
// Pointer down
dispatchEvent('pointerdown', {
button: 0,
buttons: 1
});
// Mouse down for compatibility
target.dispatchEvent(new MouseEvent('mousedown', {
bubbles: true,
cancelable: true,
clientX: x,
clientY: y,
button: 0,
buttons: 1
}));
// Small delay for down state
await new Promise(resolve => setTimeout(resolve, 50));
// Pointer up
dispatchEvent('pointerup', {
button: 0,
buttons: 0
});
// Mouse up for compatibility
target.dispatchEvent(new MouseEvent('mouseup', {
bubbles: true,
cancelable: true,
clientX: x,
clientY: y,
button: 0,
buttons: 0
}));
// Click event
target.dispatchEvent(new MouseEvent('click', {
bubbles: true,
cancelable: true,
clientX: x,
clientY: y,
button: 0
}));
// Touch events for Flutter mobile web support
const touch = new Touch({
identifier: 0,
target: target,
clientX: x,
clientY: y,
pageX: x,
pageY: y,
screenX: x,
screenY: y,
radiusX: 1,
radiusY: 1,
rotationAngle: 0,
force: 1
});
target.dispatchEvent(new TouchEvent('touchstart', {
bubbles: true,
cancelable: true,
touches: [touch],
targetTouches: [touch],
changedTouches: [touch]
}));
await new Promise(resolve => setTimeout(resolve, 50));
target.dispatchEvent(new TouchEvent('touchend', {
bubbles: true,
cancelable: true,
touches: [],
targetTouches: [],
changedTouches: [touch]
}));
return true;
}, { x, y });
if (!clicked) {
throw new Error('Failed to find Flutter rendering surface');
}
// Strategy 3: Try CDP if available (wrapped in try-catch)
try {
const cdpSession = await page.context().newCDPSession(page);
if (cdpSession) {
// Send synthetic mouse events via CDP
await cdpSession.send('Input.dispatchMouseEvent', {
type: 'mousePressed',
x: x,
y: y,
button: 'left',
clickCount: 1
});
await new Promise(resolve => setTimeout(resolve, 50));
await cdpSession.send('Input.dispatchMouseEvent', {
type: 'mouseReleased',
x: x,
y: y,
button: 'left',
clickCount: 1
});
}
}
catch (cdpError) {
// CDP not available, that's ok - we already tried other methods
if (verbose) {
// console.log('CDP not available, skipping low-level input strategy');
}
}
// Wait to see if the click had any effect
await page.waitForTimeout(500);
// Check if the URL changed (indicating navigation) with type safety
const currentUrl = page && typeof page.url === 'function' ? await page.url() : page.url;
if (verbose) {
// console.log(`✅ Click executed. Current URL: ${currentUrl}`);
}
return true;
}
catch (error) {
if (verbose) {
console.error(`❌ Click attempt ${attempt} failed:`, error);
}
if (attempt === retries) {
throw error;
}
// Wait before retry
await page.waitForTimeout(500);
}
}
return false;
}
/**
* Click on a Flutter element using its semantic information
*/
static async clickElement(page, element, options = {}) {
// Calculate center point
const centerX = element.bounds.x + element.bounds.width / 2;
const centerY = element.bounds.y + element.bounds.height / 2;
return await this.performClick(page, {
x: centerX,
y: centerY,
verbose: options.verbose
});
}
/**
* Verify if a click was successful by checking for changes
*/
static async verifyClickSuccess(page, beforeState, options = {}) {
const { timeout = 3000 } = options;
try {
// Wait for any of these conditions
await Promise.race([
// URL change
page.waitForURL(url => url.toString() !== beforeState.url, { timeout }),
// Title change
page.waitForFunction((beforeTitle) => document.title !== beforeTitle, beforeState.title, { timeout }),
// New elements appearing (form fields, etc)
page.waitForSelector('input:not([type="hidden"])', { timeout, state: 'visible' }),
// Loading indicators
page.waitForSelector('[class*="loading"], [class*="spinner"], [class*="progress"]', { timeout, state: 'visible' })
]);
return true;
}
catch {
// No changes detected
return false;
}
}
}
//# sourceMappingURL=flutter-click-handler.js.map