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

248 lines 10.9 kB
/** * 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