// Import helper functions from background-commands import { getFromContentScript, respondWith, respondWithError, attachDebugger, getElement } from './background-commands.js'; export async function click({tabId, mousePosition}, { selector, xpath }) { return await hover({ tabId, mousePosition }, { selector, xpath }, true); } export async function hover(tab, { selector, xpath }, click = false) { const { tabId, mousePosition } = tab; // Validate that either selector or xpath is provided if (!selector && !xpath) { return respondWithError(tabId, 'SELECTOR_OR_XPATH_REQUIRED', 'Either selector or xpath is required'); } // Get element and validate it exists and is visible const elementResult = await getElement(tabId, selector, xpath, true); if (elementResult.error) return elementResult; // Get current mouse position const currentPosition = mousePosition || { x: 0, y: 0 }; // Calculate target position (center of element) const targetX = elementResult.element.bounds.x + elementResult.element.bounds.width / 2; const targetY = elementResult.element.bounds.y + elementResult.element.bounds.height / 2; try { // Show cursor await getFromContentScript(tabId, '_cursor', { show: true }); // Set initial cursor position await getFromContentScript(tabId, '_moveMouseSVG', { x: currentPosition.x, y: currentPosition.y }); // Animation configuration const pixelsPerSecond = 1000; // Adjust for desired speed const frameInterval = 16; // ~60fps await attachDebugger(tabId, async () => { const sendCmd = (cmd, params) => chrome.debugger.sendCommand({ tabId }, cmd, params); const dispatchMouseEvent = (params) => sendCmd('Input.dispatchMouseEvent', params); // Enable Input domain for mouse events // await sendCmd('Input.enable'); // Function to animate to a position const animateToPosition = async (fromX, fromY, toX, toY) => { const distance = Math.sqrt(Math.pow(toX - fromX, 2) + Math.pow(toY - fromY, 2)); const duration = Math.min(1000, (distance / pixelsPerSecond) * 1000); const animSteps = Math.max(1, Math.ceil(duration / frameInterval)); const dx = (toX - fromX) / animSteps; const dy = (toY - fromY) / animSteps; for (let i = 1; i <= animSteps; i++) { const x = fromX + (dx * i); const y = fromY + (dy * i); // Move visual cursor await getFromContentScript(tabId, '_moveMouseSVG', { x, y }); // Send mouse move event await dispatchMouseEvent({ type: 'mouseMoved', x, y }); // Wait for next frame await new Promise(resolve => setTimeout(resolve, frameInterval)); } return { x: toX, y: toY }; }; // Initial animation to target let finalPosition = await animateToPosition(currentPosition.x, currentPosition.y, targetX, targetY); // Try up to 5 times to ensure we're over the target element const maxAttempts = 5; for (let attempt = 1; attempt <= maxAttempts; attempt++) { const finalCheck = await getElement(tabId, selector, xpath, true); if (!finalCheck.error && finalCheck.element) { const bounds = finalCheck.element.bounds; // Check if cursor is actually over the element const isOverElement = finalPosition.x >= bounds.x && finalPosition.x <= bounds.x + bounds.width && finalPosition.y >= bounds.y && finalPosition.y <= bounds.y + bounds.height; if (!isOverElement) { // Calculate new target center and animate to it const actualTargetX = bounds.x + bounds.width / 2; const actualTargetY = bounds.y + bounds.height / 2; finalPosition = await animateToPosition(finalPosition.x, finalPosition.y, actualTargetX, actualTargetY); } else { // Cursor is over the element, we're done break; } } else { // Element not found, stop trying break; } } const bounds = await getFromContentScript(tabId, '_elementPosition', { id: elementResult.element.id }); const actualTargetX = bounds.x + bounds.width / 2; const actualTargetY = bounds.y + bounds.height / 2; await getFromContentScript(tabId, '_moveMouseSVG', { x: actualTargetX, y: actualTargetY }); if (click) { await dispatchMouseEvent({type: 'mousePressed', x: actualTargetX, y: actualTargetY, button: 'left', clickCount: 1}); await dispatchMouseEvent({type: 'mouseReleased', x: actualTargetX, y: actualTargetY, button: 'left', clickCount: 1}); } }); // Hide cursor after 1 second setTimeout(async () => { await getFromContentScript(tabId, '_cursor', { show: false }); }, 1000); return respondWith(tabId, click ? { clicked: true } : { hovered: true }, selector, xpath); } catch (error) { // Make sure cursor is hidden on error await getFromContentScript(tabId, '_cursor', { show: false }); return respondWithError(tabId, 'CLICK_FAILED', error.message, selector, xpath); } }