9745 lines
		
	
	
		
			276 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			9745 lines
		
	
	
		
			276 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package daemon
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"encoding/json"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"log"
 | |
| 	"net"
 | |
| 	"net/http"
 | |
| 	"os"
 | |
| 	"path/filepath"
 | |
| 	"regexp"
 | |
| 	"strconv"
 | |
| 	"strings"
 | |
| 	"sync"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/go-rod/rod"
 | |
| 	"github.com/go-rod/rod/lib/launcher"
 | |
| 	"github.com/go-rod/rod/lib/proto"
 | |
| )
 | |
| 
 | |
| const Version = "2.0.0"
 | |
| 
 | |
| // Daemon is the main server that manages browser connections
 | |
| type Daemon struct {
 | |
| 	browser     *rod.Browser
 | |
| 	tabs        map[string]*rod.Page
 | |
| 	iframePages map[string]*rod.Page    // Maps tab ID to iframe page context
 | |
| 	currentTab  string                  // ID of the current/last used tab
 | |
| 	tabHistory  []string                // Stack of tab IDs in order of activation (LIFO)
 | |
| 	consoleLogs map[string][]ConsoleLog // Maps tab ID to console logs
 | |
| 	debug       bool                    // Enable debug logging
 | |
| 	mu          sync.Mutex
 | |
| 	server      *http.Server
 | |
| }
 | |
| 
 | |
| // ConsoleLog represents a console log entry
 | |
| type ConsoleLog struct {
 | |
| 	Level     string    `json:"level"`     // log, warn, error, info, debug
 | |
| 	Message   string    `json:"message"`   // The log message
 | |
| 	Timestamp time.Time `json:"timestamp"` // When the log occurred
 | |
| 	Source    string    `json:"source"`    // Source location if available
 | |
| }
 | |
| 
 | |
| // Command represents a command sent from the client to the daemon
 | |
| type Command struct {
 | |
| 	Action string            `json:"action"`
 | |
| 	Params map[string]string `json:"params"`
 | |
| }
 | |
| 
 | |
| // Response represents a response from the daemon to the client
 | |
| type Response struct {
 | |
| 	Success bool        `json:"success"`
 | |
| 	Data    interface{} `json:"data,omitempty"`
 | |
| 	Error   string      `json:"error,omitempty"`
 | |
| }
 | |
| 
 | |
| // checkChromeRunning checks if Chrome is running on the debug port
 | |
| func checkChromeRunning(port int) bool {
 | |
| 	conn, err := net.DialTimeout("tcp", fmt.Sprintf("localhost:%d", port), 2*time.Second)
 | |
| 	if err != nil {
 | |
| 		return false
 | |
| 	}
 | |
| 	conn.Close()
 | |
| 	return true
 | |
| }
 | |
| 
 | |
| // checkChromeDevTools checks if Chrome DevTools protocol is responding
 | |
| func checkChromeDevTools(port int) bool {
 | |
| 	resp, err := http.Get(fmt.Sprintf("http://localhost:%d/json/version", port))
 | |
| 	if err != nil {
 | |
| 		return false
 | |
| 	}
 | |
| 	defer resp.Body.Close()
 | |
| 	return resp.StatusCode == 200
 | |
| }
 | |
| 
 | |
| // debugLog logs a message only if debug mode is enabled
 | |
| func (d *Daemon) debugLog(format string, args ...interface{}) {
 | |
| 	if d.debug {
 | |
| 		log.Printf("[DEBUG] "+format, args...)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // NewDaemon creates a new daemon instance
 | |
| func NewDaemon(host string, port int, debug bool) (*Daemon, error) {
 | |
| 	if debug {
 | |
| 		log.Printf("[DEBUG] Creating new daemon on %s:%d", host, port)
 | |
| 	}
 | |
| 
 | |
| 	// Check if Chrome is running on the debug port
 | |
| 	chromePort := 9222 // Default Chrome debug port
 | |
| 
 | |
| 	if debug {
 | |
| 		log.Printf("[DEBUG] Checking if Chrome is running on port %d", chromePort)
 | |
| 	}
 | |
| 
 | |
| 	if !checkChromeRunning(chromePort) {
 | |
| 		return nil, fmt.Errorf("Chromium is not running with remote debugging enabled on port %d.\n\nTo start Chromium with remote debugging:\n  chromium --remote-debugging-port=%d --user-data-dir=/tmp/chromium-debug &\n  # or\n  google-chrome --remote-debugging-port=%d --user-data-dir=/tmp/chrome-debug &\n\nNote: The --user-data-dir flag is required to avoid conflicts with existing browser instances.", chromePort, chromePort, chromePort)
 | |
| 	}
 | |
| 
 | |
| 	// Check if Chromium DevTools protocol is responding
 | |
| 	if !checkChromeDevTools(chromePort) {
 | |
| 		return nil, fmt.Errorf("Something is listening on port %d but it's not Chromium DevTools protocol.\n\nThis might be:\n1. Chromium running without --remote-debugging-port=%d\n2. Another application using port %d\n\nTry stopping the process on port %d and starting Chromium with:\n  chromium --remote-debugging-port=%d --user-data-dir=/tmp/chromium-debug &", chromePort, chromePort, chromePort, chromePort, chromePort)
 | |
| 	}
 | |
| 
 | |
| 	// Connect to the existing browser instance
 | |
| 	u := launcher.MustResolveURL("")
 | |
| 	browser := rod.New().ControlURL(u)
 | |
| 
 | |
| 	err := browser.Connect()
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("Chromium DevTools is responding on port %d but rod connection failed: %w\n\nThis is unexpected. Try restarting Chromium with:\n  chromium --remote-debugging-port=%d --user-data-dir=/tmp/chromium-debug &", chromePort, err, chromePort)
 | |
| 	}
 | |
| 
 | |
| 	if debug {
 | |
| 		log.Printf("[DEBUG] Successfully connected to browser via rod")
 | |
| 	}
 | |
| 
 | |
| 	daemon := &Daemon{
 | |
| 		browser:     browser,
 | |
| 		tabs:        make(map[string]*rod.Page),
 | |
| 		iframePages: make(map[string]*rod.Page),
 | |
| 		tabHistory:  make([]string, 0),
 | |
| 		consoleLogs: make(map[string][]ConsoleLog),
 | |
| 		debug:       debug,
 | |
| 	}
 | |
| 
 | |
| 	daemon.debugLog("Daemon struct initialized")
 | |
| 
 | |
| 	// Create HTTP server
 | |
| 	daemon.debugLog("Setting up HTTP server")
 | |
| 	mux := http.NewServeMux()
 | |
| 	mux.HandleFunc("/command", daemon.handleCommand)
 | |
| 	mux.HandleFunc("/status", daemon.handleStatus)
 | |
| 	mux.HandleFunc("/upload", daemon.handleFileUpload)
 | |
| 	mux.HandleFunc("/download", daemon.handleFileDownload)
 | |
| 
 | |
| 	daemon.server = &http.Server{
 | |
| 		Addr:    fmt.Sprintf("%s:%d", host, port),
 | |
| 		Handler: mux,
 | |
| 	}
 | |
| 
 | |
| 	daemon.debugLog("HTTP server configured on %s:%d", host, port)
 | |
| 
 | |
| 	return daemon, nil
 | |
| }
 | |
| 
 | |
| // Start starts the daemon server
 | |
| func (d *Daemon) Start() error {
 | |
| 	log.Printf("Starting daemon server on %s", d.server.Addr)
 | |
| 	d.debugLog("About to call ListenAndServe()")
 | |
| 	err := d.server.ListenAndServe()
 | |
| 	d.debugLog("ListenAndServe() returned with error: %v", err)
 | |
| 	return err
 | |
| }
 | |
| 
 | |
| // Stop stops the daemon server
 | |
| func (d *Daemon) Stop() error {
 | |
| 	log.Println("Stopping daemon server")
 | |
| 	return d.server.Close()
 | |
| }
 | |
| 
 | |
| // handleStatus handles status requests
 | |
| func (d *Daemon) handleStatus(w http.ResponseWriter, r *http.Request) {
 | |
| 	if r.Method != http.MethodGet {
 | |
| 		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	d.mu.Lock()
 | |
| 	tabCount := len(d.tabs)
 | |
| 	tabs := make(map[string]string)
 | |
| 	currentTab := d.currentTab
 | |
| 	tabHistory := make([]string, len(d.tabHistory))
 | |
| 	copy(tabHistory, d.tabHistory)
 | |
| 
 | |
| 	// Get info about each tab
 | |
| 	for id, page := range d.tabs {
 | |
| 		try := func() string {
 | |
| 			info, err := page.Info()
 | |
| 			if err != nil {
 | |
| 				return "<unavailable>"
 | |
| 			}
 | |
| 			return info.URL
 | |
| 		}
 | |
| 		tabs[id] = try()
 | |
| 	}
 | |
| 	d.mu.Unlock()
 | |
| 
 | |
| 	response := Response{
 | |
| 		Success: true,
 | |
| 		Data: map[string]interface{}{
 | |
| 			"status":      "running",
 | |
| 			"tab_count":   tabCount,
 | |
| 			"tabs":        tabs,
 | |
| 			"current_tab": currentTab,
 | |
| 			"tab_history": tabHistory,
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	w.Header().Set("Content-Type", "application/json")
 | |
| 	json.NewEncoder(w).Encode(response)
 | |
| }
 | |
| 
 | |
| // handleCommand handles command requests
 | |
| func (d *Daemon) handleCommand(w http.ResponseWriter, r *http.Request) {
 | |
| 	d.debugLog("Received HTTP request: %s %s", r.Method, r.URL.Path)
 | |
| 
 | |
| 	if r.Method != http.MethodPost {
 | |
| 		d.debugLog("Invalid method: %s", r.Method)
 | |
| 		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	var cmd Command
 | |
| 	err := json.NewDecoder(r.Body).Decode(&cmd)
 | |
| 	if err != nil {
 | |
| 		d.debugLog("Failed to decode JSON: %v", err)
 | |
| 		http.Error(w, "Invalid request body", http.StatusBadRequest)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	d.debugLog("Processing command: %s with params: %+v", cmd.Action, cmd.Params)
 | |
| 
 | |
| 	var response Response
 | |
| 
 | |
| 	switch cmd.Action {
 | |
| 	case "version":
 | |
| 		response = Response{
 | |
| 			Success: true,
 | |
| 			Data:    Version,
 | |
| 		}
 | |
| 
 | |
| 	case "open-tab":
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 
 | |
| 		// Parse timeout (default to 5 seconds if not specified)
 | |
| 		timeout := 5
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		tabID, err := d.openTab(timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true, Data: tabID}
 | |
| 		}
 | |
| 
 | |
| 	case "load-url":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		url := cmd.Params["url"]
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 
 | |
| 		// Parse timeout (default to 5 seconds if not specified)
 | |
| 		timeout := 5
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		err := d.loadURL(tabID, url, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true}
 | |
| 		}
 | |
| 
 | |
| 	case "fill-form":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		selector := cmd.Params["selector"]
 | |
| 		value := cmd.Params["value"]
 | |
| 
 | |
| 		// Parse timeouts
 | |
| 		selectionTimeout := 5 // Default: 5 seconds
 | |
| 		if timeoutStr, ok := cmd.Params["selection-timeout"]; ok {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				selectionTimeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		actionTimeout := 5 // Default: 5 seconds
 | |
| 		if timeoutStr, ok := cmd.Params["action-timeout"]; ok {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				actionTimeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		err := d.fillFormField(tabID, selector, value, selectionTimeout, actionTimeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true}
 | |
| 		}
 | |
| 
 | |
| 	case "upload-file":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		selector := cmd.Params["selector"]
 | |
| 		filePath := cmd.Params["file"]
 | |
| 
 | |
| 		// Parse timeouts
 | |
| 		selectionTimeout := 5 // Default: 5 seconds
 | |
| 		if timeoutStr, ok := cmd.Params["selection-timeout"]; ok {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				selectionTimeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		actionTimeout := 5 // Default: 5 seconds
 | |
| 		if timeoutStr, ok := cmd.Params["action-timeout"]; ok {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				actionTimeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		err := d.uploadFile(tabID, selector, filePath, selectionTimeout, actionTimeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true}
 | |
| 		}
 | |
| 
 | |
| 	case "submit-form":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		selector := cmd.Params["selector"]
 | |
| 
 | |
| 		// Parse timeouts
 | |
| 		selectionTimeout := 5 // Default: 5 seconds
 | |
| 		if timeoutStr, ok := cmd.Params["selection-timeout"]; ok {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				selectionTimeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		actionTimeout := 5 // Default: 5 seconds
 | |
| 		if timeoutStr, ok := cmd.Params["action-timeout"]; ok {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				actionTimeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		err := d.submitForm(tabID, selector, selectionTimeout, actionTimeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true}
 | |
| 		}
 | |
| 
 | |
| 	case "get-source":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 
 | |
| 		// Parse timeout (default to 5 seconds if not specified)
 | |
| 		timeout := 5
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		source, err := d.getPageSource(tabID, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true, Data: source}
 | |
| 		}
 | |
| 
 | |
| 	case "get-element":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		selector := cmd.Params["selector"]
 | |
| 
 | |
| 		// Parse timeouts
 | |
| 		selectionTimeout := 5 // Default: 5 seconds
 | |
| 		if timeoutStr, ok := cmd.Params["selection-timeout"]; ok {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				selectionTimeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		html, err := d.getElementHTML(tabID, selector, selectionTimeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true, Data: html}
 | |
| 		}
 | |
| 
 | |
| 	case "close-tab":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 
 | |
| 		// Parse timeout (default to 5 seconds if not specified)
 | |
| 		timeout := 5
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		err := d.closeTab(tabID, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true}
 | |
| 		}
 | |
| 
 | |
| 	case "wait-navigation":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		timeout := 5 // Default timeout
 | |
| 		if timeoutStr, ok := cmd.Params["timeout"]; ok {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 		err := d.waitNavigation(tabID, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true}
 | |
| 		}
 | |
| 
 | |
| 	case "click-element":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		selector := cmd.Params["selector"]
 | |
| 
 | |
| 		// Parse timeouts
 | |
| 		selectionTimeout := 5 // Default: 5 seconds
 | |
| 		if timeoutStr, ok := cmd.Params["selection-timeout"]; ok {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				selectionTimeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		actionTimeout := 5 // Default: 5 seconds
 | |
| 		if timeoutStr, ok := cmd.Params["action-timeout"]; ok {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				actionTimeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		err := d.clickElement(tabID, selector, selectionTimeout, actionTimeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true}
 | |
| 		}
 | |
| 
 | |
| 	case "select-element":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		selector := cmd.Params["selector"]
 | |
| 		value := cmd.Params["value"]
 | |
| 
 | |
| 		// Parse timeouts
 | |
| 		selectionTimeout := 5 // Default: 5 seconds
 | |
| 		if timeoutStr, ok := cmd.Params["selection-timeout"]; ok {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				selectionTimeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		actionTimeout := 5 // Default: 5 seconds
 | |
| 		if timeoutStr, ok := cmd.Params["action-timeout"]; ok {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				actionTimeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		err := d.selectElement(tabID, selector, value, selectionTimeout, actionTimeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true}
 | |
| 		}
 | |
| 
 | |
| 	case "eval-js":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		jsCode := cmd.Params["code"]
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 
 | |
| 		// Parse timeout (default to 5 seconds if not specified)
 | |
| 		timeout := 5
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		result, err := d.evalJS(tabID, jsCode, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true, Data: result}
 | |
| 		}
 | |
| 
 | |
| 	case "switch-iframe":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		selector := cmd.Params["selector"]
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 
 | |
| 		// Parse timeout (default to 5 seconds if not specified)
 | |
| 		timeout := 5
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		err := d.switchToIframe(tabID, selector, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true}
 | |
| 		}
 | |
| 
 | |
| 	case "switch-main":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 
 | |
| 		err := d.switchToMain(tabID)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true}
 | |
| 		}
 | |
| 
 | |
| 	case "screenshot":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		outputPath := cmd.Params["output"]
 | |
| 		fullPageStr := cmd.Params["full-page"]
 | |
| 		zoomLevelStr := cmd.Params["zoom_level"]  // Optional: zoom level (e.g., "2.0")
 | |
| 		viewportWidthStr := cmd.Params["width"]   // Optional: viewport width
 | |
| 		viewportHeightStr := cmd.Params["height"] // Optional: viewport height
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 
 | |
| 		// Parse full-page flag
 | |
| 		fullPage := false
 | |
| 		if fullPageStr == "true" {
 | |
| 			fullPage = true
 | |
| 		}
 | |
| 
 | |
| 		// Parse zoom level
 | |
| 		var zoomLevel float64
 | |
| 		if zoomLevelStr != "" {
 | |
| 			if parsed, err := strconv.ParseFloat(zoomLevelStr, 64); err == nil && parsed > 0 {
 | |
| 				zoomLevel = parsed
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		// Parse viewport dimensions
 | |
| 		var viewportWidth, viewportHeight int
 | |
| 		if viewportWidthStr != "" {
 | |
| 			if parsed, err := strconv.Atoi(viewportWidthStr); err == nil && parsed > 0 {
 | |
| 				viewportWidth = parsed
 | |
| 			}
 | |
| 		}
 | |
| 		if viewportHeightStr != "" {
 | |
| 			if parsed, err := strconv.Atoi(viewportHeightStr); err == nil && parsed > 0 {
 | |
| 				viewportHeight = parsed
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		// Parse timeout (default to 5 seconds if not specified)
 | |
| 		timeout := 5
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		err := d.takeScreenshotEnhanced(tabID, outputPath, fullPage, zoomLevel, viewportWidth, viewportHeight, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true}
 | |
| 		}
 | |
| 
 | |
| 	case "console-logs":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		clearStr := cmd.Params["clear"]
 | |
| 
 | |
| 		// Parse clear flag
 | |
| 		clear := false
 | |
| 		if clearStr == "true" {
 | |
| 			clear = true
 | |
| 		}
 | |
| 
 | |
| 		logs, err := d.getConsoleLogs(tabID, clear)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true, Data: logs}
 | |
| 		}
 | |
| 
 | |
| 	case "console-command":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		command := cmd.Params["command"]
 | |
| 		injectLibrary := cmd.Params["inject_library"] // Optional: library URL or name
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 
 | |
| 		// Parse timeout (default to 5 seconds if not specified)
 | |
| 		timeout := 5
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		// Inject library if specified
 | |
| 		if injectLibrary != "" {
 | |
| 			err := d.injectLibrary(tabID, injectLibrary, timeout)
 | |
| 			if err != nil {
 | |
| 				response = Response{Success: false, Error: fmt.Sprintf("failed to inject library: %v", err)}
 | |
| 				break
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		result, err := d.executeConsoleCommand(tabID, command, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true, Data: result}
 | |
| 		}
 | |
| 
 | |
| 	case "check-element":
 | |
| 		d.debugLog("Processing check-element command")
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		selector := cmd.Params["selector"]
 | |
| 		checkType := cmd.Params["type"] // exists, visible, enabled, focused, selected
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 
 | |
| 		// Parse timeout (default to 5 seconds if not specified)
 | |
| 		timeout := 5
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		result, err := d.checkElement(tabID, selector, checkType, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true, Data: result}
 | |
| 		}
 | |
| 
 | |
| 	case "get-element-attributes":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		selector := cmd.Params["selector"]
 | |
| 		attributes := cmd.Params["attributes"] // comma-separated list or "all"
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 
 | |
| 		// Parse timeout (default to 5 seconds if not specified)
 | |
| 		timeout := 5
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		result, err := d.getElementAttributes(tabID, selector, attributes, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true, Data: result}
 | |
| 		}
 | |
| 
 | |
| 	case "count-elements":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		selector := cmd.Params["selector"]
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 
 | |
| 		// Parse timeout (default to 5 seconds if not specified)
 | |
| 		timeout := 5
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		count, err := d.countElements(tabID, selector, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true, Data: count}
 | |
| 		}
 | |
| 
 | |
| 	case "extract-multiple":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		selectors := cmd.Params["selectors"] // JSON array of selectors
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 
 | |
| 		// Parse timeout (default to 5 seconds if not specified)
 | |
| 		timeout := 5
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		result, err := d.extractMultiple(tabID, selectors, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true, Data: result}
 | |
| 		}
 | |
| 
 | |
| 	case "extract-links":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		selector := cmd.Params["selector"]        // Optional: filter links by container selector
 | |
| 		hrefPattern := cmd.Params["href-pattern"] // Optional: regex pattern for href
 | |
| 		textPattern := cmd.Params["text-pattern"] // Optional: regex pattern for link text
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 
 | |
| 		// Parse timeout (default to 5 seconds if not specified)
 | |
| 		timeout := 5
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		result, err := d.extractLinks(tabID, selector, hrefPattern, textPattern, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true, Data: result}
 | |
| 		}
 | |
| 
 | |
| 	case "extract-table":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		selector := cmd.Params["selector"]
 | |
| 		includeHeaders := cmd.Params["include-headers"] == "true"
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 
 | |
| 		// Parse timeout (default to 5 seconds if not specified)
 | |
| 		timeout := 5
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		result, err := d.extractTable(tabID, selector, includeHeaders, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true, Data: result}
 | |
| 		}
 | |
| 
 | |
| 	case "extract-text":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		selector := cmd.Params["selector"]
 | |
| 		pattern := cmd.Params["pattern"]  // Optional: regex pattern to match within text
 | |
| 		extractType := cmd.Params["type"] // text, innerText, textContent (default: textContent)
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 
 | |
| 		// Parse timeout (default to 5 seconds if not specified)
 | |
| 		timeout := 5
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		result, err := d.extractText(tabID, selector, pattern, extractType, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true, Data: result}
 | |
| 		}
 | |
| 
 | |
| 	case "analyze-form":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		selector := cmd.Params["selector"]
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 
 | |
| 		// Parse timeout (default to 5 seconds if not specified)
 | |
| 		timeout := 5
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		result, err := d.analyzeForm(tabID, selector, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true, Data: result}
 | |
| 		}
 | |
| 
 | |
| 	case "interact-multiple":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		interactionsJSON := cmd.Params["interactions"]
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 
 | |
| 		// Parse timeout (default to 5 seconds if not specified)
 | |
| 		timeout := 5
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		result, err := d.interactMultiple(tabID, interactionsJSON, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true, Data: result}
 | |
| 		}
 | |
| 
 | |
| 	case "fill-form-bulk":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		formSelector := cmd.Params["form-selector"]
 | |
| 		fieldsJSON := cmd.Params["fields"]
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 
 | |
| 		// Parse timeout (default to 5 seconds if not specified)
 | |
| 		timeout := 5
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		result, err := d.fillFormBulk(tabID, formSelector, fieldsJSON, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true, Data: result}
 | |
| 		}
 | |
| 
 | |
| 	case "get-page-info":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 
 | |
| 		// Parse timeout (default to 5 seconds if not specified)
 | |
| 		timeout := 5
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		result, err := d.getPageInfo(tabID, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true, Data: result}
 | |
| 		}
 | |
| 
 | |
| 	case "get-viewport-info":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 
 | |
| 		// Parse timeout (default to 5 seconds if not specified)
 | |
| 		timeout := 5
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		result, err := d.getViewportInfo(tabID, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true, Data: result}
 | |
| 		}
 | |
| 
 | |
| 	case "get-performance":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 
 | |
| 		// Parse timeout (default to 5 seconds if not specified)
 | |
| 		timeout := 5
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		result, err := d.getPerformance(tabID, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true, Data: result}
 | |
| 		}
 | |
| 
 | |
| 	case "check-content":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		contentType := cmd.Params["type"]
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 
 | |
| 		// Parse timeout (default to 5 seconds if not specified)
 | |
| 		timeout := 5
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		result, err := d.checkContent(tabID, contentType, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true, Data: result}
 | |
| 		}
 | |
| 
 | |
| 	case "screenshot-element":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		selector := cmd.Params["selector"]
 | |
| 		outputPath := cmd.Params["output"]
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 
 | |
| 		// Parse timeout (default to 5 seconds if not specified)
 | |
| 		timeout := 5
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		err := d.screenshotElement(tabID, selector, outputPath, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true}
 | |
| 		}
 | |
| 
 | |
| 	case "screenshot-enhanced":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		outputPath := cmd.Params["output"]
 | |
| 		fullPageStr := cmd.Params["full-page"]
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 
 | |
| 		// Parse full-page flag
 | |
| 		fullPage := false
 | |
| 		if fullPageStr == "true" {
 | |
| 			fullPage = true
 | |
| 		}
 | |
| 
 | |
| 		// Parse timeout (default to 5 seconds if not specified)
 | |
| 		timeout := 5
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		result, err := d.screenshotEnhanced(tabID, outputPath, fullPage, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true, Data: result}
 | |
| 		}
 | |
| 
 | |
| 	case "bulk-files":
 | |
| 		operationType := cmd.Params["operation"] // "upload" or "download"
 | |
| 		filesJSON := cmd.Params["files"]
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 
 | |
| 		// Parse timeout (default to 30 seconds for bulk operations)
 | |
| 		timeout := 30
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		result, err := d.bulkFiles(operationType, filesJSON, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true, Data: result}
 | |
| 		}
 | |
| 
 | |
| 	case "manage-files":
 | |
| 		operation := cmd.Params["operation"] // "cleanup", "list", "info"
 | |
| 		pattern := cmd.Params["pattern"]     // file pattern for cleanup/list
 | |
| 		maxAge := cmd.Params["max-age"]      // max age in hours for cleanup
 | |
| 
 | |
| 		result, err := d.manageFiles(operation, pattern, maxAge)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true, Data: result}
 | |
| 		}
 | |
| 
 | |
| 	// Accessibility tree commands
 | |
| 	case "get-accessibility-tree":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		depth := cmd.Params["depth"]
 | |
| 		includeContrastStr := cmd.Params["include_contrast"]
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 
 | |
| 		// Parse timeout (default to 5 seconds if not specified)
 | |
| 		timeout := 5
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		// Parse depth (optional)
 | |
| 		var depthInt *int
 | |
| 		if depth != "" {
 | |
| 			if parsedDepth, err := strconv.Atoi(depth); err == nil && parsedDepth >= 0 {
 | |
| 				depthInt = &parsedDepth
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		// Parse include_contrast flag
 | |
| 		includeContrast := false
 | |
| 		if includeContrastStr == "true" {
 | |
| 			includeContrast = true
 | |
| 		}
 | |
| 
 | |
| 		result, err := d.getAccessibilityTreeWithContrast(tabID, depthInt, includeContrast, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true, Data: result}
 | |
| 		}
 | |
| 
 | |
| 	case "get-partial-accessibility-tree":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		selector := cmd.Params["selector"]
 | |
| 		fetchRelatives := cmd.Params["fetch-relatives"] // "true" or "false"
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 
 | |
| 		// Parse timeout (default to 5 seconds if not specified)
 | |
| 		timeout := 5
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		// Parse fetchRelatives (default to true)
 | |
| 		fetchRel := true
 | |
| 		if fetchRelatives == "false" {
 | |
| 			fetchRel = false
 | |
| 		}
 | |
| 
 | |
| 		result, err := d.getPartialAccessibilityTree(tabID, selector, fetchRel, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true, Data: result}
 | |
| 		}
 | |
| 
 | |
| 	case "query-accessibility-tree":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		selector := cmd.Params["selector"]
 | |
| 		accessibleName := cmd.Params["accessible-name"]
 | |
| 		role := cmd.Params["role"]
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 
 | |
| 		// Parse timeout (default to 5 seconds if not specified)
 | |
| 		timeout := 5
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		result, err := d.queryAccessibilityTree(tabID, selector, accessibleName, role, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true, Data: result}
 | |
| 		}
 | |
| 
 | |
| 	case "disable-cache":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 
 | |
| 		// Parse timeout (default to 5 seconds if not specified)
 | |
| 		timeout := 5
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		err := d.setCacheDisabled(tabID, true, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true}
 | |
| 		}
 | |
| 
 | |
| 	case "enable-cache":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 
 | |
| 		// Parse timeout (default to 5 seconds if not specified)
 | |
| 		timeout := 5
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		err := d.setCacheDisabled(tabID, false, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true}
 | |
| 		}
 | |
| 
 | |
| 	case "clear-cache":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 
 | |
| 		// Parse timeout (default to 5 seconds if not specified)
 | |
| 		timeout := 5
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		err := d.clearBrowserCache(tabID, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true}
 | |
| 		}
 | |
| 
 | |
| 	case "clear-all-site-data":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 
 | |
| 		// Parse timeout (default to 5 seconds if not specified)
 | |
| 		timeout := 5
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		err := d.clearAllSiteData(tabID, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true}
 | |
| 		}
 | |
| 
 | |
| 	case "clear-cookies":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 
 | |
| 		// Parse timeout (default to 5 seconds if not specified)
 | |
| 		timeout := 5
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		err := d.clearCookies(tabID, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true}
 | |
| 		}
 | |
| 
 | |
| 	case "clear-storage":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 
 | |
| 		// Parse timeout (default to 5 seconds if not specified)
 | |
| 		timeout := 5
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		err := d.clearStorage(tabID, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true}
 | |
| 		}
 | |
| 
 | |
| 	case "drag-and-drop":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		sourceSelector := cmd.Params["source"]
 | |
| 		targetSelector := cmd.Params["target"]
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 		timeout := 5
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if sourceSelector == "" {
 | |
| 			response = Response{Success: false, Error: "source selector is required"}
 | |
| 			break
 | |
| 		}
 | |
| 		if targetSelector == "" {
 | |
| 			response = Response{Success: false, Error: "target selector is required"}
 | |
| 			break
 | |
| 		}
 | |
| 
 | |
| 		err := d.dragAndDrop(tabID, sourceSelector, targetSelector, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true}
 | |
| 		}
 | |
| 
 | |
| 	case "drag-and-drop-coordinates":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		sourceSelector := cmd.Params["source"]
 | |
| 		targetXStr := cmd.Params["target-x"]
 | |
| 		targetYStr := cmd.Params["target-y"]
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 		timeout := 5
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if sourceSelector == "" {
 | |
| 			response = Response{Success: false, Error: "source selector is required"}
 | |
| 			break
 | |
| 		}
 | |
| 		if targetXStr == "" || targetYStr == "" {
 | |
| 			response = Response{Success: false, Error: "target-x and target-y coordinates are required"}
 | |
| 			break
 | |
| 		}
 | |
| 
 | |
| 		targetX, err := strconv.Atoi(targetXStr)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: "invalid target-x coordinate"}
 | |
| 			break
 | |
| 		}
 | |
| 		targetY, err := strconv.Atoi(targetYStr)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: "invalid target-y coordinate"}
 | |
| 			break
 | |
| 		}
 | |
| 
 | |
| 		err = d.dragAndDropToCoordinates(tabID, sourceSelector, targetX, targetY, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true}
 | |
| 		}
 | |
| 
 | |
| 	case "drag-and-drop-offset":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		sourceSelector := cmd.Params["source"]
 | |
| 		offsetXStr := cmd.Params["offset-x"]
 | |
| 		offsetYStr := cmd.Params["offset-y"]
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 		timeout := 5
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if sourceSelector == "" {
 | |
| 			response = Response{Success: false, Error: "source selector is required"}
 | |
| 			break
 | |
| 		}
 | |
| 		if offsetXStr == "" || offsetYStr == "" {
 | |
| 			response = Response{Success: false, Error: "offset-x and offset-y are required"}
 | |
| 			break
 | |
| 		}
 | |
| 
 | |
| 		offsetX, err := strconv.Atoi(offsetXStr)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: "invalid offset-x value"}
 | |
| 			break
 | |
| 		}
 | |
| 		offsetY, err := strconv.Atoi(offsetYStr)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: "invalid offset-y value"}
 | |
| 			break
 | |
| 		}
 | |
| 
 | |
| 		err = d.dragAndDropByOffset(tabID, sourceSelector, offsetX, offsetY, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true}
 | |
| 		}
 | |
| 
 | |
| 	case "right-click":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		selector := cmd.Params["selector"]
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 		timeout := 5
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if selector == "" {
 | |
| 			response = Response{Success: false, Error: "selector is required"}
 | |
| 			break
 | |
| 		}
 | |
| 
 | |
| 		err := d.rightClick(tabID, selector, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true}
 | |
| 		}
 | |
| 
 | |
| 	case "double-click":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		selector := cmd.Params["selector"]
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 		timeout := 5
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if selector == "" {
 | |
| 			response = Response{Success: false, Error: "selector is required"}
 | |
| 			break
 | |
| 		}
 | |
| 
 | |
| 		err := d.doubleClick(tabID, selector, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true}
 | |
| 		}
 | |
| 
 | |
| 	case "middle-click":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		selector := cmd.Params["selector"]
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 		timeout := 5
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if selector == "" {
 | |
| 			response = Response{Success: false, Error: "selector is required"}
 | |
| 			break
 | |
| 		}
 | |
| 
 | |
| 		err := d.middleClick(tabID, selector, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true}
 | |
| 		}
 | |
| 
 | |
| 	case "hover":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		selector := cmd.Params["selector"]
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 		timeout := 5
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if selector == "" {
 | |
| 			response = Response{Success: false, Error: "selector is required"}
 | |
| 			break
 | |
| 		}
 | |
| 
 | |
| 		err := d.hover(tabID, selector, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true}
 | |
| 		}
 | |
| 
 | |
| 	case "mouse-move":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		xStr := cmd.Params["x"]
 | |
| 		yStr := cmd.Params["y"]
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 		timeout := 5
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if xStr == "" || yStr == "" {
 | |
| 			response = Response{Success: false, Error: "x and y coordinates are required"}
 | |
| 			break
 | |
| 		}
 | |
| 
 | |
| 		x, err := strconv.Atoi(xStr)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: "invalid x coordinate"}
 | |
| 			break
 | |
| 		}
 | |
| 		y, err := strconv.Atoi(yStr)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: "invalid y coordinate"}
 | |
| 			break
 | |
| 		}
 | |
| 
 | |
| 		err = d.mouseMove(tabID, x, y, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true}
 | |
| 		}
 | |
| 
 | |
| 	case "scroll-wheel":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		xStr := cmd.Params["x"]
 | |
| 		yStr := cmd.Params["y"]
 | |
| 		deltaXStr := cmd.Params["delta-x"]
 | |
| 		deltaYStr := cmd.Params["delta-y"]
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 		timeout := 5
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if xStr == "" || yStr == "" || deltaXStr == "" || deltaYStr == "" {
 | |
| 			response = Response{Success: false, Error: "x, y, delta-x, and delta-y are required"}
 | |
| 			break
 | |
| 		}
 | |
| 
 | |
| 		x, err := strconv.Atoi(xStr)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: "invalid x coordinate"}
 | |
| 			break
 | |
| 		}
 | |
| 		y, err := strconv.Atoi(yStr)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: "invalid y coordinate"}
 | |
| 			break
 | |
| 		}
 | |
| 		deltaX, err := strconv.Atoi(deltaXStr)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: "invalid delta-x value"}
 | |
| 			break
 | |
| 		}
 | |
| 		deltaY, err := strconv.Atoi(deltaYStr)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: "invalid delta-y value"}
 | |
| 			break
 | |
| 		}
 | |
| 
 | |
| 		err = d.scrollWheel(tabID, x, y, deltaX, deltaY, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true}
 | |
| 		}
 | |
| 
 | |
| 	case "key-combination":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		keys := cmd.Params["keys"]
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 		timeout := 5
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if keys == "" {
 | |
| 			response = Response{Success: false, Error: "keys parameter is required"}
 | |
| 			break
 | |
| 		}
 | |
| 
 | |
| 		err := d.keyCombination(tabID, keys, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true}
 | |
| 		}
 | |
| 
 | |
| 	case "special-key":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		key := cmd.Params["key"]
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 		timeout := 5
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if key == "" {
 | |
| 			response = Response{Success: false, Error: "key parameter is required"}
 | |
| 			break
 | |
| 		}
 | |
| 
 | |
| 		err := d.specialKey(tabID, key, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true}
 | |
| 		}
 | |
| 
 | |
| 	case "modifier-click":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		selector := cmd.Params["selector"]
 | |
| 		modifiers := cmd.Params["modifiers"]
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 		timeout := 5
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if selector == "" {
 | |
| 			response = Response{Success: false, Error: "selector is required"}
 | |
| 			break
 | |
| 		}
 | |
| 		if modifiers == "" {
 | |
| 			response = Response{Success: false, Error: "modifiers parameter is required"}
 | |
| 			break
 | |
| 		}
 | |
| 
 | |
| 		err := d.modifierClick(tabID, selector, modifiers, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true}
 | |
| 		}
 | |
| 
 | |
| 	case "touch-tap":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		xStr := cmd.Params["x"]
 | |
| 		yStr := cmd.Params["y"]
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 		timeout := 5
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if xStr == "" || yStr == "" {
 | |
| 			response = Response{Success: false, Error: "x and y coordinates are required"}
 | |
| 			break
 | |
| 		}
 | |
| 
 | |
| 		x, err := strconv.Atoi(xStr)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: "invalid x coordinate"}
 | |
| 			break
 | |
| 		}
 | |
| 		y, err := strconv.Atoi(yStr)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: "invalid y coordinate"}
 | |
| 			break
 | |
| 		}
 | |
| 
 | |
| 		err = d.touchTap(tabID, x, y, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true}
 | |
| 		}
 | |
| 
 | |
| 	case "touch-long-press":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		xStr := cmd.Params["x"]
 | |
| 		yStr := cmd.Params["y"]
 | |
| 		durationStr := cmd.Params["duration"]
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 		timeout := 5
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		duration := 1000 // Default 1000ms
 | |
| 		if durationStr != "" {
 | |
| 			if parsedDuration, err := strconv.Atoi(durationStr); err == nil && parsedDuration > 0 {
 | |
| 				duration = parsedDuration
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if xStr == "" || yStr == "" {
 | |
| 			response = Response{Success: false, Error: "x and y coordinates are required"}
 | |
| 			break
 | |
| 		}
 | |
| 
 | |
| 		x, err := strconv.Atoi(xStr)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: "invalid x coordinate"}
 | |
| 			break
 | |
| 		}
 | |
| 		y, err := strconv.Atoi(yStr)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: "invalid y coordinate"}
 | |
| 			break
 | |
| 		}
 | |
| 
 | |
| 		err = d.touchLongPress(tabID, x, y, duration, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true}
 | |
| 		}
 | |
| 
 | |
| 	case "touch-swipe":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		startXStr := cmd.Params["start-x"]
 | |
| 		startYStr := cmd.Params["start-y"]
 | |
| 		endXStr := cmd.Params["end-x"]
 | |
| 		endYStr := cmd.Params["end-y"]
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 		timeout := 5
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if startXStr == "" || startYStr == "" || endXStr == "" || endYStr == "" {
 | |
| 			response = Response{Success: false, Error: "start-x, start-y, end-x, and end-y are required"}
 | |
| 			break
 | |
| 		}
 | |
| 
 | |
| 		startX, err := strconv.Atoi(startXStr)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: "invalid start-x coordinate"}
 | |
| 			break
 | |
| 		}
 | |
| 		startY, err := strconv.Atoi(startYStr)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: "invalid start-y coordinate"}
 | |
| 			break
 | |
| 		}
 | |
| 		endX, err := strconv.Atoi(endXStr)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: "invalid end-x coordinate"}
 | |
| 			break
 | |
| 		}
 | |
| 		endY, err := strconv.Atoi(endYStr)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: "invalid end-y coordinate"}
 | |
| 			break
 | |
| 		}
 | |
| 
 | |
| 		err = d.touchSwipe(tabID, startX, startY, endX, endY, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true}
 | |
| 		}
 | |
| 
 | |
| 	case "pinch-zoom":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		centerXStr := cmd.Params["center-x"]
 | |
| 		centerYStr := cmd.Params["center-y"]
 | |
| 		scaleStr := cmd.Params["scale"]
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 		timeout := 5
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if centerXStr == "" || centerYStr == "" || scaleStr == "" {
 | |
| 			response = Response{Success: false, Error: "center-x, center-y, and scale are required"}
 | |
| 			break
 | |
| 		}
 | |
| 
 | |
| 		centerX, err := strconv.Atoi(centerXStr)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: "invalid center-x coordinate"}
 | |
| 			break
 | |
| 		}
 | |
| 		centerY, err := strconv.Atoi(centerYStr)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: "invalid center-y coordinate"}
 | |
| 			break
 | |
| 		}
 | |
| 		scale, err := strconv.ParseFloat(scaleStr, 64)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: "invalid scale value"}
 | |
| 			break
 | |
| 		}
 | |
| 
 | |
| 		err = d.pinchZoom(tabID, centerX, centerY, scale, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true}
 | |
| 		}
 | |
| 
 | |
| 	case "scroll-element":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		selector := cmd.Params["selector"]
 | |
| 		deltaXStr := cmd.Params["delta-x"]
 | |
| 		deltaYStr := cmd.Params["delta-y"]
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 		timeout := 5
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if selector == "" || deltaXStr == "" || deltaYStr == "" {
 | |
| 			response = Response{Success: false, Error: "selector, delta-x, and delta-y are required"}
 | |
| 			break
 | |
| 		}
 | |
| 
 | |
| 		deltaX, err := strconv.Atoi(deltaXStr)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: "invalid delta-x value"}
 | |
| 			break
 | |
| 		}
 | |
| 		deltaY, err := strconv.Atoi(deltaYStr)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: "invalid delta-y value"}
 | |
| 			break
 | |
| 		}
 | |
| 
 | |
| 		err = d.scrollElement(tabID, selector, deltaX, deltaY, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true}
 | |
| 		}
 | |
| 
 | |
| 	case "scroll-to-coordinates":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		xStr := cmd.Params["x"]
 | |
| 		yStr := cmd.Params["y"]
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 		timeout := 5
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if xStr == "" || yStr == "" {
 | |
| 			response = Response{Success: false, Error: "x and y coordinates are required"}
 | |
| 			break
 | |
| 		}
 | |
| 
 | |
| 		x, err := strconv.Atoi(xStr)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: "invalid x coordinate"}
 | |
| 			break
 | |
| 		}
 | |
| 		y, err := strconv.Atoi(yStr)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: "invalid y coordinate"}
 | |
| 			break
 | |
| 		}
 | |
| 
 | |
| 		err = d.scrollToCoordinates(tabID, x, y, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true}
 | |
| 		}
 | |
| 
 | |
| 	case "select-text":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		selector := cmd.Params["selector"]
 | |
| 		startStr := cmd.Params["start"]
 | |
| 		endStr := cmd.Params["end"]
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 		timeout := 5
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if selector == "" || startStr == "" || endStr == "" {
 | |
| 			response = Response{Success: false, Error: "selector, start, and end are required"}
 | |
| 			break
 | |
| 		}
 | |
| 
 | |
| 		start, err := strconv.Atoi(startStr)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: "invalid start index"}
 | |
| 			break
 | |
| 		}
 | |
| 		end, err := strconv.Atoi(endStr)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: "invalid end index"}
 | |
| 			break
 | |
| 		}
 | |
| 
 | |
| 		err = d.selectText(tabID, selector, start, end, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true}
 | |
| 		}
 | |
| 
 | |
| 	case "select-all-text":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		selector := cmd.Params["selector"] // Optional - if empty, selects all text on page
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 		timeout := 5
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		err := d.selectAllText(tabID, selector, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true}
 | |
| 		}
 | |
| 
 | |
| 	case "inject-axe":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		axeVersion := cmd.Params["version"] // Optional: specific axe-core version
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 
 | |
| 		// Parse timeout (default to 10 seconds for library injection)
 | |
| 		timeout := 10
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		err := d.injectAxeCore(tabID, axeVersion, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true, Data: "axe-core injected successfully"}
 | |
| 		}
 | |
| 
 | |
| 	case "run-axe":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		optionsJSON := cmd.Params["options"] // Optional: JSON string with axe.run() options
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 
 | |
| 		// Parse timeout (default to 30 seconds for comprehensive testing)
 | |
| 		timeout := 30
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		// Parse options if provided
 | |
| 		var options map[string]interface{}
 | |
| 		if optionsJSON != "" {
 | |
| 			err := json.Unmarshal([]byte(optionsJSON), &options)
 | |
| 			if err != nil {
 | |
| 				response = Response{Success: false, Error: fmt.Sprintf("invalid options JSON: %v", err)}
 | |
| 				break
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		result, err := d.runAxeCore(tabID, options, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true, Data: result}
 | |
| 		}
 | |
| 
 | |
| 	case "check-contrast":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		selector := cmd.Params["selector"] // Optional: CSS selector for specific elements
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 
 | |
| 		// Parse timeout (default to 10 seconds)
 | |
| 		timeout := 10
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		result, err := d.checkContrast(tabID, selector, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true, Data: result}
 | |
| 		}
 | |
| 
 | |
| 	case "test-keyboard":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 
 | |
| 		// Parse timeout (default to 15 seconds for comprehensive testing)
 | |
| 		timeout := 15
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		result, err := d.testKeyboardNavigation(tabID, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true, Data: result}
 | |
| 		}
 | |
| 
 | |
| 	case "test-zoom":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		zoomLevelsStr := cmd.Params["zoom_levels"] // Optional: comma-separated zoom levels
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 
 | |
| 		// Parse timeout (default to 10 seconds per zoom level)
 | |
| 		timeout := 10
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		// Parse zoom levels if provided
 | |
| 		var zoomLevels []float64
 | |
| 		if zoomLevelsStr != "" {
 | |
| 			levels := strings.Split(zoomLevelsStr, ",")
 | |
| 			for _, level := range levels {
 | |
| 				if zoom, err := strconv.ParseFloat(strings.TrimSpace(level), 64); err == nil && zoom > 0 {
 | |
| 					zoomLevels = append(zoomLevels, zoom)
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		result, err := d.testZoom(tabID, zoomLevels, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true, Data: result}
 | |
| 		}
 | |
| 
 | |
| 	case "test-reflow":
 | |
| 		tabID := cmd.Params["tab"]
 | |
| 		widthsStr := cmd.Params["widths"] // Optional: comma-separated widths
 | |
| 		timeoutStr := cmd.Params["timeout"]
 | |
| 
 | |
| 		// Parse timeout (default to 10 seconds per width)
 | |
| 		timeout := 10
 | |
| 		if timeoutStr != "" {
 | |
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 {
 | |
| 				timeout = parsedTimeout
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		// Parse widths if provided
 | |
| 		var widths []int
 | |
| 		if widthsStr != "" {
 | |
| 			widthStrs := strings.Split(widthsStr, ",")
 | |
| 			for _, widthStr := range widthStrs {
 | |
| 				if width, err := strconv.Atoi(strings.TrimSpace(widthStr)); err == nil && width > 0 {
 | |
| 					widths = append(widths, width)
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		result, err := d.testReflow(tabID, widths, timeout)
 | |
| 		if err != nil {
 | |
| 			response = Response{Success: false, Error: err.Error()}
 | |
| 		} else {
 | |
| 			response = Response{Success: true, Data: result}
 | |
| 		}
 | |
| 
 | |
| 	default:
 | |
| 		d.debugLog("Unknown action: %s", cmd.Action)
 | |
| 		response = Response{Success: false, Error: "Unknown action"}
 | |
| 	}
 | |
| 
 | |
| 	d.debugLog("Command %s completed, sending response: success=%v", cmd.Action, response.Success)
 | |
| 	w.Header().Set("Content-Type", "application/json")
 | |
| 	json.NewEncoder(w).Encode(response)
 | |
| 	d.debugLog("Response sent for command: %s", cmd.Action)
 | |
| }
 | |
| 
 | |
| // openTab opens a new tab and returns its ID
 | |
| func (d *Daemon) openTab(timeout int) (string, error) {
 | |
| 	d.debugLog("Opening new tab with timeout: %d", timeout)
 | |
| 	d.mu.Lock()
 | |
| 	defer d.mu.Unlock()
 | |
| 
 | |
| 	// Create a context with timeout if specified
 | |
| 	if timeout > 0 {
 | |
| 		d.debugLog("Using timeout context: %d seconds", timeout)
 | |
| 		ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
 | |
| 		defer cancel()
 | |
| 
 | |
| 		// Create a channel to signal completion
 | |
| 		done := make(chan struct {
 | |
| 			page  *rod.Page
 | |
| 			tabID string
 | |
| 			err   error
 | |
| 		}, 1)
 | |
| 
 | |
| 		// Execute the tab creation in a goroutine
 | |
| 		go func() {
 | |
| 			page, err := d.browser.Page(proto.TargetCreateTarget{URL: "about:blank"})
 | |
| 			var tabID string
 | |
| 			if err == nil {
 | |
| 				tabID = string(page.TargetID)
 | |
| 			}
 | |
| 			done <- struct {
 | |
| 				page  *rod.Page
 | |
| 				tabID string
 | |
| 				err   error
 | |
| 			}{page, tabID, err}
 | |
| 		}()
 | |
| 
 | |
| 		// Wait for either completion or timeout
 | |
| 		select {
 | |
| 		case res := <-done:
 | |
| 			if res.err != nil {
 | |
| 				return "", fmt.Errorf("failed to create new tab: %w", res.err)
 | |
| 			}
 | |
| 
 | |
| 			// Store the tab and update history
 | |
| 			d.tabs[res.tabID] = res.page
 | |
| 			d.tabHistory = append(d.tabHistory, res.tabID)
 | |
| 			d.currentTab = res.tabID
 | |
| 
 | |
| 			// Set up console logging for this tab
 | |
| 			d.setupConsoleLogging(res.tabID, res.page)
 | |
| 
 | |
| 			return res.tabID, nil
 | |
| 		case <-ctx.Done():
 | |
| 			return "", fmt.Errorf("opening tab timed out after %d seconds", timeout)
 | |
| 		}
 | |
| 	} else {
 | |
| 		// No timeout
 | |
| 		page, err := d.browser.Page(proto.TargetCreateTarget{URL: "about:blank"})
 | |
| 		if err != nil {
 | |
| 			return "", fmt.Errorf("failed to create new tab: %w", err)
 | |
| 		}
 | |
| 
 | |
| 		// Use the page ID as the tab ID
 | |
| 		tabID := string(page.TargetID)
 | |
| 		d.tabs[tabID] = page
 | |
| 
 | |
| 		// Add to tab history stack and set as current tab
 | |
| 		d.tabHistory = append(d.tabHistory, tabID)
 | |
| 		d.currentTab = tabID
 | |
| 
 | |
| 		// Set up console logging for this tab
 | |
| 		d.setupConsoleLogging(tabID, page)
 | |
| 
 | |
| 		return tabID, nil
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // getTabID returns the tab ID to use, falling back to the current tab if none is provided
 | |
| func (d *Daemon) getTabID(tabID string) (string, error) {
 | |
| 	d.mu.Lock()
 | |
| 	defer d.mu.Unlock()
 | |
| 
 | |
| 	// If no tab ID is provided, use the current tab
 | |
| 	if tabID == "" {
 | |
| 		if d.currentTab == "" {
 | |
| 			return "", fmt.Errorf("no current tab available, please open a tab first")
 | |
| 		}
 | |
| 		return d.currentTab, nil
 | |
| 	}
 | |
| 
 | |
| 	// Otherwise, use the provided tab ID
 | |
| 	return tabID, nil
 | |
| }
 | |
| 
 | |
| // updateTabHistory updates the tab history stack when a tab is activated
 | |
| func (d *Daemon) updateTabHistory(tabID string) {
 | |
| 	// Set as current tab
 | |
| 	d.currentTab = tabID
 | |
| 
 | |
| 	// Remove the tab from history if it's already there
 | |
| 	for i, id := range d.tabHistory {
 | |
| 		if id == tabID {
 | |
| 			// Remove this tab from history
 | |
| 			d.tabHistory = append(d.tabHistory[:i], d.tabHistory[i+1:]...)
 | |
| 			break
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Add the tab to the end of history (most recent)
 | |
| 	d.tabHistory = append(d.tabHistory, tabID)
 | |
| }
 | |
| 
 | |
| // findPageByID finds a page by its ID without updating the current tab or cache
 | |
| func (d *Daemon) findPageByID(tabID string) (*rod.Page, error) {
 | |
| 	// If not in memory, try to get the page from the browser
 | |
| 	pages, err := d.browser.Pages()
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to get browser pages: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	// Find the page with the matching ID
 | |
| 	for _, p := range pages {
 | |
| 		if string(p.TargetID) == tabID {
 | |
| 			return p, nil
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return nil, fmt.Errorf("tab not found: %s", tabID)
 | |
| }
 | |
| 
 | |
| // getTab returns a tab by its ID, checking for iframe context first
 | |
| func (d *Daemon) getTab(tabID string) (*rod.Page, error) {
 | |
| 	// Get the tab ID to use (may be the current tab)
 | |
| 	actualTabID, err := d.getTabID(tabID)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	d.mu.Lock()
 | |
| 	defer d.mu.Unlock()
 | |
| 
 | |
| 	// First check if we have an iframe context for this tab
 | |
| 	if iframePage, exists := d.iframePages[actualTabID]; exists {
 | |
| 		// Update tab history and current tab
 | |
| 		d.updateTabHistory(actualTabID)
 | |
| 		return iframePage, nil
 | |
| 	}
 | |
| 
 | |
| 	// Check in-memory cache for main page
 | |
| 	page, exists := d.tabs[actualTabID]
 | |
| 	if exists {
 | |
| 		// Update tab history and current tab
 | |
| 		d.updateTabHistory(actualTabID)
 | |
| 		return page, nil
 | |
| 	}
 | |
| 
 | |
| 	// If not in memory, try to find it
 | |
| 	page, err = d.findPageByID(actualTabID)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	// If found, cache it for future use
 | |
| 	if page != nil {
 | |
| 		d.tabs[actualTabID] = page
 | |
| 		// Update tab history and current tab
 | |
| 		d.updateTabHistory(actualTabID)
 | |
| 		return page, nil
 | |
| 	}
 | |
| 
 | |
| 	return nil, fmt.Errorf("tab not found: %s", actualTabID)
 | |
| }
 | |
| 
 | |
| // closeTab closes a tab by its ID
 | |
| func (d *Daemon) closeTab(tabID string, timeout int) error {
 | |
| 	// Get the tab ID to use (may be the current tab)
 | |
| 	actualTabID, err := d.getTabID(tabID)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	// First remove from our internal map to avoid future references
 | |
| 	d.mu.Lock()
 | |
| 	page, exists := d.tabs[actualTabID]
 | |
| 	delete(d.tabs, actualTabID)
 | |
| 
 | |
| 	// Remove the tab from history
 | |
| 	for i, id := range d.tabHistory {
 | |
| 		if id == actualTabID {
 | |
| 			// Remove this tab from history
 | |
| 			d.tabHistory = append(d.tabHistory[:i], d.tabHistory[i+1:]...)
 | |
| 			break
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// If we closed the current tab, set it to the previous tab in history
 | |
| 	if d.currentTab == actualTabID {
 | |
| 		if len(d.tabHistory) > 0 {
 | |
| 			// Set current tab to the most recent tab in history
 | |
| 			d.currentTab = d.tabHistory[len(d.tabHistory)-1]
 | |
| 		} else {
 | |
| 			// No tabs left in history, clear the current tab
 | |
| 			d.currentTab = ""
 | |
| 		}
 | |
| 	}
 | |
| 	d.mu.Unlock()
 | |
| 
 | |
| 	// If the page doesn't exist in our cache, try to find it
 | |
| 	if !exists {
 | |
| 		var err error
 | |
| 		page, err = d.findPageByID(actualTabID)
 | |
| 		if err != nil {
 | |
| 			// If we can't find the page, it might already be closed
 | |
| 			return nil
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if timeout > 0 {
 | |
| 		// Use timeout for closing the tab
 | |
| 		ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
 | |
| 		defer cancel()
 | |
| 
 | |
| 		// Create a channel to signal completion
 | |
| 		done := make(chan error, 1)
 | |
| 
 | |
| 		// Execute the close in a goroutine
 | |
| 		go func() {
 | |
| 			err := page.Close()
 | |
| 			done <- err
 | |
| 		}()
 | |
| 
 | |
| 		// Wait for either completion or timeout
 | |
| 		select {
 | |
| 		case err := <-done:
 | |
| 			if err != nil {
 | |
| 				// Log the error but don't return it, as we've already removed it from our map
 | |
| 				fmt.Printf("Warning: failed to close tab %s: %v\n", actualTabID, err)
 | |
| 			}
 | |
| 			return nil
 | |
| 		case <-ctx.Done():
 | |
| 			return fmt.Errorf("closing tab timed out after %d seconds", timeout)
 | |
| 		}
 | |
| 	} else {
 | |
| 		// No timeout - try to close the page, but don't fail if it's already closed
 | |
| 		err = page.Close()
 | |
| 		if err != nil {
 | |
| 			// Log the error but don't return it, as we've already removed it from our map
 | |
| 			fmt.Printf("Warning: failed to close tab %s: %v\n", actualTabID, err)
 | |
| 		}
 | |
| 
 | |
| 		return nil
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // loadURL loads a URL in a tab
 | |
| func (d *Daemon) loadURL(tabID, url string, timeout int) error {
 | |
| 	d.debugLog("Loading URL: %s in tab: %s with timeout: %d", url, tabID, timeout)
 | |
| 	page, err := d.getTab(tabID)
 | |
| 	if err != nil {
 | |
| 		d.debugLog("Failed to get tab %s: %v", tabID, err)
 | |
| 		return err
 | |
| 	}
 | |
| 	d.debugLog("Got tab %s, starting navigation", tabID)
 | |
| 
 | |
| 	if timeout > 0 {
 | |
| 		// Use timeout for the URL loading
 | |
| 		ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
 | |
| 		defer cancel()
 | |
| 
 | |
| 		// Create a channel to signal completion
 | |
| 		done := make(chan error, 1)
 | |
| 
 | |
| 		// Execute the navigation in a goroutine
 | |
| 		go func() {
 | |
| 			err := page.Navigate(url)
 | |
| 			if err != nil {
 | |
| 				done <- fmt.Errorf("failed to navigate to URL: %w", err)
 | |
| 				return
 | |
| 			}
 | |
| 
 | |
| 			// Wait for the page to be loaded
 | |
| 			err = page.WaitLoad()
 | |
| 			if err != nil {
 | |
| 				done <- fmt.Errorf("failed to wait for page load: %w", err)
 | |
| 				return
 | |
| 			}
 | |
| 
 | |
| 			done <- nil
 | |
| 		}()
 | |
| 
 | |
| 		// Wait for either completion or timeout
 | |
| 		select {
 | |
| 		case err := <-done:
 | |
| 			return err
 | |
| 		case <-ctx.Done():
 | |
| 			return fmt.Errorf("loading URL timed out after %d seconds", timeout)
 | |
| 		}
 | |
| 	} else {
 | |
| 		// No timeout
 | |
| 		err = page.Navigate(url)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("failed to navigate to URL: %w", err)
 | |
| 		}
 | |
| 
 | |
| 		// Wait for the page to be loaded
 | |
| 		err = page.WaitLoad()
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("failed to wait for page load: %w", err)
 | |
| 		}
 | |
| 
 | |
| 		return nil
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // isPageStable checks if a page is stable and not currently loading
 | |
| func (d *Daemon) isPageStable(page *rod.Page) (bool, error) {
 | |
| 	// Check if page is loading
 | |
| 	result, err := page.Eval(`() => document.readyState === 'complete'`)
 | |
| 	if err != nil {
 | |
| 		return false, err
 | |
| 	}
 | |
| 
 | |
| 	isComplete := result.Value.Bool()
 | |
| 
 | |
| 	if !isComplete {
 | |
| 		return false, nil
 | |
| 	}
 | |
| 
 | |
| 	// Additional check: ensure no pending network requests
 | |
| 	// This is a simple heuristic - if the page has been stable for a short time
 | |
| 	err = page.WaitStable(500 * time.Millisecond)
 | |
| 	if err != nil {
 | |
| 		return false, nil // Page is not stable
 | |
| 	}
 | |
| 
 | |
| 	return true, nil
 | |
| }
 | |
| 
 | |
| // detectNavigationInProgress monitors the page for a short period to detect if navigation starts
 | |
| func (d *Daemon) detectNavigationInProgress(page *rod.Page, monitorDuration time.Duration) (bool, error) {
 | |
| 	// Get current URL and readyState
 | |
| 	currentURL, err := page.Eval(`() => window.location.href`)
 | |
| 	if err != nil {
 | |
| 		return false, err
 | |
| 	}
 | |
| 
 | |
| 	currentReadyState, err := page.Eval(`() => document.readyState`)
 | |
| 	if err != nil {
 | |
| 		return false, err
 | |
| 	}
 | |
| 
 | |
| 	startURL := currentURL.Value.Str()
 | |
| 	startReadyState := currentReadyState.Value.Str()
 | |
| 
 | |
| 	// Monitor for changes over the specified duration
 | |
| 	ctx, cancel := context.WithTimeout(context.Background(), monitorDuration)
 | |
| 	defer cancel()
 | |
| 
 | |
| 	ticker := time.NewTicker(100 * time.Millisecond)
 | |
| 	defer ticker.Stop()
 | |
| 
 | |
| 	for {
 | |
| 		select {
 | |
| 		case <-ctx.Done():
 | |
| 			// No navigation detected during monitoring period
 | |
| 			return false, nil
 | |
| 		case <-ticker.C:
 | |
| 			// Check if URL or readyState changed
 | |
| 			newURL, err := page.Eval(`() => window.location.href`)
 | |
| 			if err != nil {
 | |
| 				continue // Ignore errors during monitoring
 | |
| 			}
 | |
| 
 | |
| 			newReadyState, err := page.Eval(`() => document.readyState`)
 | |
| 			if err != nil {
 | |
| 				continue // Ignore errors during monitoring
 | |
| 			}
 | |
| 
 | |
| 			if newURL.Value.Str() != startURL {
 | |
| 				// URL changed, navigation is happening
 | |
| 				return true, nil
 | |
| 			}
 | |
| 
 | |
| 			if newReadyState.Value.Str() != startReadyState && newReadyState.Value.Str() == "loading" {
 | |
| 				// Page started loading
 | |
| 				return true, nil
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // waitNavigation waits for a navigation event to happen
 | |
| func (d *Daemon) waitNavigation(tabID string, timeout int) error {
 | |
| 	page, err := d.getTab(tabID)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	// First, check if the page is already stable and loaded
 | |
| 	// If so, we don't need to wait for navigation
 | |
| 	isStable, err := d.isPageStable(page)
 | |
| 	if err == nil && isStable {
 | |
| 		// Page is already stable, no navigation happening
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	// Check if navigation is actually in progress by monitoring for a short period
 | |
| 	navigationDetected, err := d.detectNavigationInProgress(page, 2*time.Second)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to detect navigation state: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	if !navigationDetected {
 | |
| 		// No navigation detected, check if page is stable now
 | |
| 		isStable, err := d.isPageStable(page)
 | |
| 		if err == nil && isStable {
 | |
| 			return nil
 | |
| 		}
 | |
| 		// If we can't determine stability, proceed with waiting
 | |
| 	}
 | |
| 
 | |
| 	// Navigation is in progress, wait for it to complete
 | |
| 	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
 | |
| 	defer cancel()
 | |
| 
 | |
| 	// Wait for navigation with timeout
 | |
| 	done := make(chan error, 1)
 | |
| 	go func() {
 | |
| 		defer func() {
 | |
| 			if r := recover(); r != nil {
 | |
| 				done <- fmt.Errorf("navigation wait panicked: %v", r)
 | |
| 			}
 | |
| 		}()
 | |
| 
 | |
| 		// Wait for navigation event
 | |
| 		page.WaitNavigation(proto.PageLifecycleEventNameLoad)()
 | |
| 
 | |
| 		// Wait for the page to be fully loaded
 | |
| 		err := page.WaitLoad()
 | |
| 		done <- err
 | |
| 	}()
 | |
| 
 | |
| 	select {
 | |
| 	case err := <-done:
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("navigation wait failed: %w", err)
 | |
| 		}
 | |
| 		return nil
 | |
| 	case <-ctx.Done():
 | |
| 		// Timeout occurred, check if page is now stable
 | |
| 		isStable, err := d.isPageStable(page)
 | |
| 		if err == nil && isStable {
 | |
| 			// Page is stable, consider navigation complete
 | |
| 			return nil
 | |
| 		}
 | |
| 		return fmt.Errorf("navigation wait timed out after %d seconds", timeout)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // getPageSource returns the entire source code of a page
 | |
| func (d *Daemon) getPageSource(tabID string, timeout int) (string, error) {
 | |
| 	page, err := d.getTab(tabID)
 | |
| 	if err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 
 | |
| 	if timeout > 0 {
 | |
| 		// Use timeout for getting page source
 | |
| 		ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
 | |
| 		defer cancel()
 | |
| 
 | |
| 		// Create a channel to signal completion
 | |
| 		done := make(chan struct {
 | |
| 			html string
 | |
| 			err  error
 | |
| 		}, 1)
 | |
| 
 | |
| 		// Execute the HTML retrieval in a goroutine
 | |
| 		go func() {
 | |
| 			html, err := page.HTML()
 | |
| 			done <- struct {
 | |
| 				html string
 | |
| 				err  error
 | |
| 			}{html, err}
 | |
| 		}()
 | |
| 
 | |
| 		// Wait for either completion or timeout
 | |
| 		select {
 | |
| 		case res := <-done:
 | |
| 			if res.err != nil {
 | |
| 				return "", fmt.Errorf("failed to get page HTML: %w", res.err)
 | |
| 			}
 | |
| 			return res.html, nil
 | |
| 		case <-ctx.Done():
 | |
| 			return "", fmt.Errorf("getting page source timed out after %d seconds", timeout)
 | |
| 		}
 | |
| 	} else {
 | |
| 		// No timeout
 | |
| 		html, err := page.HTML()
 | |
| 		if err != nil {
 | |
| 			return "", fmt.Errorf("failed to get page HTML: %w", err)
 | |
| 		}
 | |
| 
 | |
| 		return html, nil
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // getElementHTML returns the HTML of an element at the specified selector
 | |
| func (d *Daemon) getElementHTML(tabID, selector string, selectionTimeout int) (string, error) {
 | |
| 	page, err := d.getTab(tabID)
 | |
| 	if err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 
 | |
| 	// Find the element with optional timeout
 | |
| 	var element *rod.Element
 | |
| 	if selectionTimeout > 0 {
 | |
| 		// Use timeout if specified
 | |
| 		element, err = page.Timeout(time.Duration(selectionTimeout) * time.Second).Element(selector)
 | |
| 		if err != nil {
 | |
| 			return "", fmt.Errorf("failed to find element (timeout after %ds): %w", selectionTimeout, err)
 | |
| 		}
 | |
| 	} else {
 | |
| 		// No timeout
 | |
| 		element, err = page.Element(selector)
 | |
| 		if err != nil {
 | |
| 			return "", fmt.Errorf("failed to find element: %w", err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Get the HTML of the element
 | |
| 	html, err := element.HTML()
 | |
| 	if err != nil {
 | |
| 		return "", fmt.Errorf("failed to get element HTML: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	return html, nil
 | |
| }
 | |
| 
 | |
| // fillFormField fills a form field with the specified value
 | |
| func (d *Daemon) fillFormField(tabID, selector, value string, selectionTimeout, actionTimeout int) error {
 | |
| 	page, err := d.getTab(tabID)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	// Find the element with optional timeout
 | |
| 	var element *rod.Element
 | |
| 	if selectionTimeout > 0 {
 | |
| 		// Use timeout if specified
 | |
| 		element, err = page.Timeout(time.Duration(selectionTimeout) * time.Second).Element(selector)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("failed to find element (timeout after %ds): %w", selectionTimeout, err)
 | |
| 		}
 | |
| 	} else {
 | |
| 		// No timeout
 | |
| 		element, err = page.Element(selector)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("failed to find element: %w", err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Get the element type
 | |
| 	tagName, err := element.Eval(`() => this.tagName.toLowerCase()`)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to get element type: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	// Get the element type attribute
 | |
| 	inputType, err := element.Eval(`() => this.type ? this.type.toLowerCase() : ''`)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to get element type attribute: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	// Handle different input types
 | |
| 	tagNameStr := tagName.Value.String()
 | |
| 	typeStr := inputType.Value.String()
 | |
| 
 | |
| 	// Handle checkbox and radio inputs
 | |
| 	if tagNameStr == "input" && (typeStr == "checkbox" || typeStr == "radio") {
 | |
| 		// Convert value to boolean
 | |
| 		checked := false
 | |
| 		if value == "true" || value == "1" || value == "yes" || value == "on" || value == "checked" {
 | |
| 			checked = true
 | |
| 		}
 | |
| 
 | |
| 		// Set the checked state with optional timeout
 | |
| 		if actionTimeout > 0 {
 | |
| 			// Use timeout for the action
 | |
| 			ctx, cancel := context.WithTimeout(context.Background(), time.Duration(actionTimeout)*time.Second)
 | |
| 			defer cancel()
 | |
| 
 | |
| 			// Create a channel to signal completion
 | |
| 			done := make(chan error, 1)
 | |
| 
 | |
| 			// Execute the action in a goroutine
 | |
| 			go func() {
 | |
| 				_, err := element.Eval(fmt.Sprintf(`() => { this.checked = %t; return true; }`, checked))
 | |
| 				done <- err
 | |
| 			}()
 | |
| 
 | |
| 			// Wait for either completion or timeout
 | |
| 			select {
 | |
| 			case err := <-done:
 | |
| 				if err != nil {
 | |
| 					return fmt.Errorf("failed to set checkbox state: %w", err)
 | |
| 				}
 | |
| 			case <-ctx.Done():
 | |
| 				return fmt.Errorf("setting checkbox state timed out after %d seconds", actionTimeout)
 | |
| 			}
 | |
| 
 | |
| 			// Create a channel for the event trigger
 | |
| 			done = make(chan error, 1)
 | |
| 
 | |
| 			// Trigger change event with timeout
 | |
| 			go func() {
 | |
| 				_, err := element.Eval(`() => {
 | |
| 					const event = new Event('change', { bubbles: true });
 | |
| 					this.dispatchEvent(event);
 | |
| 					return true;
 | |
| 				}`)
 | |
| 				done <- err
 | |
| 			}()
 | |
| 
 | |
| 			// Wait for either completion or timeout
 | |
| 			select {
 | |
| 			case err := <-done:
 | |
| 				if err != nil {
 | |
| 					return fmt.Errorf("failed to trigger change event: %w", err)
 | |
| 				}
 | |
| 			case <-ctx.Done():
 | |
| 				return fmt.Errorf("triggering change event timed out after %d seconds", actionTimeout)
 | |
| 			}
 | |
| 		} else {
 | |
| 			// No timeout
 | |
| 			_, err := element.Eval(fmt.Sprintf(`() => { this.checked = %t; return true; }`, checked))
 | |
| 			if err != nil {
 | |
| 				return fmt.Errorf("failed to set checkbox state: %w", err)
 | |
| 			}
 | |
| 
 | |
| 			// Trigger change event
 | |
| 			_, err = element.Eval(`() => {
 | |
| 				const event = new Event('change', { bubbles: true });
 | |
| 				this.dispatchEvent(event);
 | |
| 				return true;
 | |
| 			}`)
 | |
| 			if err != nil {
 | |
| 				return fmt.Errorf("failed to trigger change event: %w", err)
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	// For regular text inputs
 | |
| 	if actionTimeout > 0 {
 | |
| 		// Use timeout for the action
 | |
| 		ctx, cancel := context.WithTimeout(context.Background(), time.Duration(actionTimeout)*time.Second)
 | |
| 		defer cancel()
 | |
| 
 | |
| 		// Create a channel to signal completion
 | |
| 		done := make(chan error, 1)
 | |
| 
 | |
| 		// Clear the field first with timeout
 | |
| 		go func() {
 | |
| 			_ = element.SelectAllText()
 | |
| 			err := element.Input("")
 | |
| 			done <- err
 | |
| 		}()
 | |
| 
 | |
| 		// Wait for either completion or timeout
 | |
| 		select {
 | |
| 		case err := <-done:
 | |
| 			if err != nil {
 | |
| 				return fmt.Errorf("failed to clear field: %w", err)
 | |
| 			}
 | |
| 		case <-ctx.Done():
 | |
| 			return fmt.Errorf("clearing field timed out after %d seconds", actionTimeout)
 | |
| 		}
 | |
| 
 | |
| 		// Create a channel for the input action
 | |
| 		done = make(chan error, 1)
 | |
| 
 | |
| 		// Input the value with timeout
 | |
| 		go func() {
 | |
| 			err := element.Input(value)
 | |
| 			done <- err
 | |
| 		}()
 | |
| 
 | |
| 		// Wait for either completion or timeout
 | |
| 		select {
 | |
| 		case err := <-done:
 | |
| 			if err != nil {
 | |
| 				return fmt.Errorf("failed to input value: %w", err)
 | |
| 			}
 | |
| 		case <-ctx.Done():
 | |
| 			return fmt.Errorf("inputting value timed out after %d seconds", actionTimeout)
 | |
| 		}
 | |
| 	} else {
 | |
| 		// No timeout
 | |
| 		// Clear the field first
 | |
| 		_ = element.SelectAllText()
 | |
| 		err = element.Input("")
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("failed to clear field: %w", err)
 | |
| 		}
 | |
| 
 | |
| 		// Input the value
 | |
| 		err = element.Input(value)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("failed to input value: %w", err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // uploadFile uploads a file to a file input element
 | |
| func (d *Daemon) uploadFile(tabID, selector, filePath string, selectionTimeout, actionTimeout int) error {
 | |
| 	page, err := d.getTab(tabID)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	// Find the element with optional timeout
 | |
| 	var element *rod.Element
 | |
| 	if selectionTimeout > 0 {
 | |
| 		// Use timeout if specified
 | |
| 		element, err = page.Timeout(time.Duration(selectionTimeout) * time.Second).Element(selector)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("failed to find file input element (timeout after %ds): %w", selectionTimeout, err)
 | |
| 		}
 | |
| 	} else {
 | |
| 		// No timeout
 | |
| 		element, err = page.Element(selector)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("failed to find file input element: %w", err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Set the file with optional timeout
 | |
| 	if actionTimeout > 0 {
 | |
| 		// Use timeout for the action
 | |
| 		ctx, cancel := context.WithTimeout(context.Background(), time.Duration(actionTimeout)*time.Second)
 | |
| 		defer cancel()
 | |
| 
 | |
| 		// Create a channel to signal completion
 | |
| 		done := make(chan error, 1)
 | |
| 
 | |
| 		// Execute the action in a goroutine
 | |
| 		go func() {
 | |
| 			err := element.SetFiles([]string{filePath})
 | |
| 			done <- err
 | |
| 		}()
 | |
| 
 | |
| 		// Wait for either completion or timeout
 | |
| 		select {
 | |
| 		case err := <-done:
 | |
| 			if err != nil {
 | |
| 				return fmt.Errorf("failed to set file: %w", err)
 | |
| 			}
 | |
| 		case <-ctx.Done():
 | |
| 			return fmt.Errorf("setting file timed out after %d seconds", actionTimeout)
 | |
| 		}
 | |
| 	} else {
 | |
| 		// No timeout
 | |
| 		err = element.SetFiles([]string{filePath})
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("failed to set file: %w", err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // submitForm submits a form
 | |
| func (d *Daemon) submitForm(tabID, selector string, selectionTimeout, actionTimeout int) error {
 | |
| 	page, err := d.getTab(tabID)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	// Find the element with optional timeout
 | |
| 	var form *rod.Element
 | |
| 	if selectionTimeout > 0 {
 | |
| 		// Use timeout if specified
 | |
| 		form, err = page.Timeout(time.Duration(selectionTimeout) * time.Second).Element(selector)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("failed to find form element (timeout after %ds): %w", selectionTimeout, err)
 | |
| 		}
 | |
| 	} else {
 | |
| 		// No timeout
 | |
| 		form, err = page.Element(selector)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("failed to find form element: %w", err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Get the current URL to detect navigation
 | |
| 	currentURL := page.MustInfo().URL
 | |
| 
 | |
| 	// Create a context for navigation timeout
 | |
| 	var ctx context.Context
 | |
| 	var cancel context.CancelFunc
 | |
| 
 | |
| 	// Submit the form with optional timeout
 | |
| 	if actionTimeout > 0 {
 | |
| 		// Use timeout for the action
 | |
| 		submitCtx, submitCancel := context.WithTimeout(context.Background(), time.Duration(actionTimeout)*time.Second)
 | |
| 		defer submitCancel()
 | |
| 
 | |
| 		// Create a channel to signal completion
 | |
| 		done := make(chan error, 1)
 | |
| 
 | |
| 		// Execute the action in a goroutine
 | |
| 		go func() {
 | |
| 			_, err := form.Eval(`() => { try { this.submit(); return true; } catch(e) { return false; } }`)
 | |
| 			done <- err
 | |
| 		}()
 | |
| 
 | |
| 		// Wait for either completion or timeout
 | |
| 		select {
 | |
| 		case err := <-done:
 | |
| 			if err != nil {
 | |
| 				// Log the error but continue
 | |
| 				fmt.Printf("Warning: error during form submission: %v\n", err)
 | |
| 			}
 | |
| 		case <-submitCtx.Done():
 | |
| 			return fmt.Errorf("form submission timed out after %d seconds", actionTimeout)
 | |
| 		}
 | |
| 
 | |
| 		// Wait for navigation to complete (with the same timeout)
 | |
| 		ctx, cancel = context.WithTimeout(context.Background(), time.Duration(actionTimeout)*time.Second)
 | |
| 	} else {
 | |
| 		// No timeout for submission
 | |
| 		try := func() (bool, error) {
 | |
| 			_, err := form.Eval(`() => { try { this.submit(); return true; } catch(e) { return false; } }`)
 | |
| 			return err == nil, err
 | |
| 		}
 | |
| 
 | |
| 		// Try to submit the form, but don't fail if it's already been submitted
 | |
| 		_, err = try()
 | |
| 		if err != nil {
 | |
| 			// Log the error but continue
 | |
| 			fmt.Printf("Warning: error during form submission: %v\n", err)
 | |
| 		}
 | |
| 
 | |
| 		// Wait for navigation to complete (with default timeout)
 | |
| 		ctx, cancel = context.WithTimeout(context.Background(), 10*time.Second)
 | |
| 	}
 | |
| 	defer cancel()
 | |
| 
 | |
| 	// Wait for the page to navigate away from the current URL
 | |
| 	waitNav := func() error {
 | |
| 		for {
 | |
| 			select {
 | |
| 			case <-ctx.Done():
 | |
| 				return ctx.Err()
 | |
| 			case <-time.After(100 * time.Millisecond):
 | |
| 				// Check if the page has navigated
 | |
| 				try := func() (string, error) {
 | |
| 					info, err := page.Info()
 | |
| 					if err != nil {
 | |
| 						return "", err
 | |
| 					}
 | |
| 					return info.URL, nil
 | |
| 				}
 | |
| 
 | |
| 				newURL, err := try()
 | |
| 				if err != nil {
 | |
| 					// Page might be navigating, wait a bit more
 | |
| 					continue
 | |
| 				}
 | |
| 
 | |
| 				if newURL != currentURL {
 | |
| 					// Navigation completed
 | |
| 					return nil
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Wait for navigation but don't fail if it times out
 | |
| 	err = waitNav()
 | |
| 	if err != nil {
 | |
| 		// Log the error but don't fail
 | |
| 		fmt.Printf("Warning: navigation after form submission may not have completed: %v\n", err)
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // clickElement clicks on an element
 | |
| func (d *Daemon) clickElement(tabID, selector string, selectionTimeout, actionTimeout int) error {
 | |
| 	page, err := d.getTab(tabID)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	// Find the element with optional timeout
 | |
| 	var element *rod.Element
 | |
| 	if selectionTimeout > 0 {
 | |
| 		// Use timeout if specified
 | |
| 		element, err = page.Timeout(time.Duration(selectionTimeout) * time.Second).Element(selector)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("failed to find element (timeout after %ds): %w", selectionTimeout, err)
 | |
| 		}
 | |
| 	} else {
 | |
| 		// No timeout
 | |
| 		element, err = page.Element(selector)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("failed to find element: %w", err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Make sure the element is visible and scrolled into view
 | |
| 	err = element.ScrollIntoView()
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to scroll element into view: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	// Click the element with optional timeout
 | |
| 	if actionTimeout > 0 {
 | |
| 		// Use timeout for the click action
 | |
| 		ctx, cancel := context.WithTimeout(context.Background(), time.Duration(actionTimeout)*time.Second)
 | |
| 		defer cancel()
 | |
| 
 | |
| 		// Create a channel to signal completion
 | |
| 		done := make(chan error, 1)
 | |
| 
 | |
| 		// Execute the click in a goroutine
 | |
| 		go func() {
 | |
| 			err := element.Click(proto.InputMouseButtonLeft, 1) // 1 click
 | |
| 			done <- err
 | |
| 		}()
 | |
| 
 | |
| 		// Wait for either completion or timeout
 | |
| 		select {
 | |
| 		case err := <-done:
 | |
| 			if err != nil {
 | |
| 				return fmt.Errorf("failed to click element: %w", err)
 | |
| 			}
 | |
| 		case <-ctx.Done():
 | |
| 			return fmt.Errorf("click action timed out after %d seconds", actionTimeout)
 | |
| 		}
 | |
| 	} else {
 | |
| 		// No timeout
 | |
| 		err = element.Click(proto.InputMouseButtonLeft, 1) // 1 click
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("failed to click element: %w", err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Wait a moment for any navigation to start
 | |
| 	time.Sleep(100 * time.Millisecond)
 | |
| 
 | |
| 	// Wait for any potential page load or DOM changes
 | |
| 	err = page.WaitStable(1 * time.Second)
 | |
| 	if err != nil {
 | |
| 		// This is not a critical error, so we'll just log it
 | |
| 		log.Printf("Warning: page not stable after click: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // selectElement selects an option in a select dropdown
 | |
| func (d *Daemon) selectElement(tabID, selector, value string, selectionTimeout, actionTimeout int) error {
 | |
| 	page, err := d.getTab(tabID)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	// Find the element with optional timeout
 | |
| 	var element *rod.Element
 | |
| 	if selectionTimeout > 0 {
 | |
| 		// Use timeout if specified
 | |
| 		element, err = page.Timeout(time.Duration(selectionTimeout) * time.Second).Element(selector)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("failed to find element (timeout after %ds): %w", selectionTimeout, err)
 | |
| 		}
 | |
| 	} else {
 | |
| 		// No timeout
 | |
| 		element, err = page.Element(selector)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("failed to find element: %w", err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Make sure the element is visible and scrolled into view
 | |
| 	err = element.ScrollIntoView()
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to scroll element into view: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	// For select elements, use rod's built-in Select method
 | |
| 	// Try to select by text first (most common case)
 | |
| 	err = element.Select([]string{value}, true, rod.SelectorTypeText)
 | |
| 	if err != nil {
 | |
| 		// If text selection failed, use JavaScript as fallback
 | |
| 		// Use a simple single statement that works with rod's evaluation
 | |
| 		script := fmt.Sprintf("document.querySelector(\"%s\").value = \"%s\"", selector, value)
 | |
| 
 | |
| 		// Execute the value assignment (ignore evaluation errors, rod has issues with some JS patterns)
 | |
| 		page.Eval(script)
 | |
| 
 | |
| 		// Dispatch the change event separately
 | |
| 		changeScript := fmt.Sprintf("document.querySelector(\"%s\").dispatchEvent(new Event(\"change\", { bubbles: true }))", selector)
 | |
| 		page.Eval(changeScript)
 | |
| 
 | |
| 		// Verify the selection worked by checking the element's value property directly
 | |
| 		currentValue, err := element.Property("value")
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("failed to verify selection: %w", err)
 | |
| 		}
 | |
| 
 | |
| 		// Check if the selection actually worked
 | |
| 		if currentValue.Str() != value {
 | |
| 			return fmt.Errorf("failed to select option '%s' in element (current value: %s)", value, currentValue.Str())
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // evalJS executes JavaScript code in a tab and returns the result
 | |
| func (d *Daemon) evalJS(tabID, jsCode string, timeout int) (string, error) {
 | |
| 	page, err := d.getTab(tabID)
 | |
| 	if err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 
 | |
| 	// Create a comprehensive wrapper that handles both expressions and statements
 | |
| 	// and properly formats the result
 | |
| 	wrappedCode := `() => {
 | |
| 		var result;
 | |
| 		try {
 | |
| 			// Try to evaluate as an expression first
 | |
| 			result = eval(` + "`" + jsCode + "`" + `);
 | |
| 		} catch(e) {
 | |
| 			// If that fails, try to execute as statements
 | |
| 			try {
 | |
| 				eval(` + "`" + jsCode + "`" + `);
 | |
| 				result = undefined;
 | |
| 			} catch(e2) {
 | |
| 				throw e; // Re-throw the original error
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		// Format the result for return
 | |
| 		if (typeof result === 'undefined') return 'undefined';
 | |
| 		if (result === null) return 'null';
 | |
| 		if (typeof result === 'string') return result;
 | |
| 		if (typeof result === 'number' || typeof result === 'boolean') return String(result);
 | |
| 		try {
 | |
| 			return JSON.stringify(result);
 | |
| 		} catch(e) {
 | |
| 			return String(result);
 | |
| 		}
 | |
| 	}`
 | |
| 
 | |
| 	// Execute the wrapped JavaScript code with timeout
 | |
| 	if timeout > 0 {
 | |
| 		// Use timeout for the JavaScript execution
 | |
| 		ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
 | |
| 		defer cancel()
 | |
| 
 | |
| 		// Create a channel to signal completion
 | |
| 		done := make(chan struct {
 | |
| 			result string
 | |
| 			err    error
 | |
| 		}, 1)
 | |
| 
 | |
| 		// Execute the JavaScript in a goroutine
 | |
| 		go func() {
 | |
| 			result, err := page.Eval(wrappedCode)
 | |
| 			var resultStr string
 | |
| 			if err == nil {
 | |
| 				// Convert the result to a string representation
 | |
| 				if result.Value.Nil() {
 | |
| 					resultStr = "null"
 | |
| 				} else {
 | |
| 					resultStr = result.Value.String()
 | |
| 				}
 | |
| 			}
 | |
| 			done <- struct {
 | |
| 				result string
 | |
| 				err    error
 | |
| 			}{resultStr, err}
 | |
| 		}()
 | |
| 
 | |
| 		// Wait for either completion or timeout
 | |
| 		select {
 | |
| 		case res := <-done:
 | |
| 			if res.err != nil {
 | |
| 				return "", fmt.Errorf("failed to execute JavaScript: %w", res.err)
 | |
| 			}
 | |
| 			return res.result, nil
 | |
| 		case <-ctx.Done():
 | |
| 			return "", fmt.Errorf("JavaScript execution timed out after %d seconds", timeout)
 | |
| 		}
 | |
| 	} else {
 | |
| 		// No timeout
 | |
| 		result, err := page.Eval(wrappedCode)
 | |
| 		if err != nil {
 | |
| 			return "", fmt.Errorf("failed to execute JavaScript: %w", err)
 | |
| 		}
 | |
| 
 | |
| 		// Convert the result to a string representation
 | |
| 		if result.Value.Nil() {
 | |
| 			return "null", nil
 | |
| 		}
 | |
| 
 | |
| 		return result.Value.String(), nil
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // takeScreenshot takes a screenshot of a tab and saves it to a file
 | |
| func (d *Daemon) takeScreenshot(tabID, outputPath string, fullPage bool, timeout int) error {
 | |
| 	page, err := d.getTab(tabID)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	if timeout > 0 {
 | |
| 		// Use timeout for taking screenshot
 | |
| 		ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
 | |
| 		defer cancel()
 | |
| 
 | |
| 		// Create a channel to signal completion
 | |
| 		done := make(chan error, 1)
 | |
| 
 | |
| 		// Execute the screenshot in a goroutine
 | |
| 		go func() {
 | |
| 			// Take screenshot and save it
 | |
| 			screenshotBytes, err := page.Screenshot(fullPage, &proto.PageCaptureScreenshot{
 | |
| 				Format: proto.PageCaptureScreenshotFormatPng,
 | |
| 			})
 | |
| 
 | |
| 			if err != nil {
 | |
| 				done <- fmt.Errorf("failed to capture screenshot: %w", err)
 | |
| 				return
 | |
| 			}
 | |
| 
 | |
| 			// Write the screenshot to file
 | |
| 			err = os.WriteFile(outputPath, screenshotBytes, 0644)
 | |
| 			if err != nil {
 | |
| 				done <- fmt.Errorf("failed to save screenshot to %s: %w", outputPath, err)
 | |
| 				return
 | |
| 			}
 | |
| 
 | |
| 			done <- nil
 | |
| 		}()
 | |
| 
 | |
| 		// Wait for either completion or timeout
 | |
| 		select {
 | |
| 		case err := <-done:
 | |
| 			if err != nil {
 | |
| 				return fmt.Errorf("failed to save screenshot: %w", err)
 | |
| 			}
 | |
| 			return nil
 | |
| 		case <-ctx.Done():
 | |
| 			return fmt.Errorf("taking screenshot timed out after %d seconds", timeout)
 | |
| 		}
 | |
| 	} else {
 | |
| 		// No timeout - take screenshot directly
 | |
| 		screenshotBytes, err := page.Screenshot(fullPage, &proto.PageCaptureScreenshot{
 | |
| 			Format: proto.PageCaptureScreenshotFormatPng,
 | |
| 		})
 | |
| 
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("failed to capture screenshot: %w", err)
 | |
| 		}
 | |
| 
 | |
| 		// Write the screenshot to file
 | |
| 		err = os.WriteFile(outputPath, screenshotBytes, 0644)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("failed to save screenshot to %s: %w", outputPath, err)
 | |
| 		}
 | |
| 
 | |
| 		return nil
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // takeScreenshotEnhanced takes a screenshot with optional zoom level and viewport size
 | |
| func (d *Daemon) takeScreenshotEnhanced(tabID, outputPath string, fullPage bool, zoomLevel float64, viewportWidth, viewportHeight, timeout int) error {
 | |
| 	page, err := d.getTab(tabID)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	// Store original viewport settings if we need to change them
 | |
| 	var originalViewport *proto.EmulationSetDeviceMetricsOverride
 | |
| 	needsReset := false
 | |
| 
 | |
| 	// Get current viewport if we need to modify it
 | |
| 	if zoomLevel > 0 || viewportWidth > 0 || viewportHeight > 0 {
 | |
| 		currentViewport, err := page.Eval(`() => {
 | |
| 			return {
 | |
| 				width: window.innerWidth,
 | |
| 				height: window.innerHeight
 | |
| 			};
 | |
| 		}`)
 | |
| 		if err == nil {
 | |
| 			var viewportData struct {
 | |
| 				Width  int `json:"width"`
 | |
| 				Height int `json:"height"`
 | |
| 			}
 | |
| 			json.Unmarshal([]byte(currentViewport.Value.String()), &viewportData)
 | |
| 
 | |
| 			originalViewport = &proto.EmulationSetDeviceMetricsOverride{
 | |
| 				Width:             viewportData.Width,
 | |
| 				Height:            viewportData.Height,
 | |
| 				DeviceScaleFactor: 1.0,
 | |
| 				Mobile:            false,
 | |
| 			}
 | |
| 			needsReset = true
 | |
| 
 | |
| 			// Set new viewport settings
 | |
| 			newWidth := viewportData.Width
 | |
| 			newHeight := viewportData.Height
 | |
| 			newZoom := 1.0
 | |
| 
 | |
| 			if viewportWidth > 0 {
 | |
| 				newWidth = viewportWidth
 | |
| 			}
 | |
| 			if viewportHeight > 0 {
 | |
| 				newHeight = viewportHeight
 | |
| 			}
 | |
| 			if zoomLevel > 0 {
 | |
| 				newZoom = zoomLevel
 | |
| 			}
 | |
| 
 | |
| 			err = page.SetViewport(&proto.EmulationSetDeviceMetricsOverride{
 | |
| 				Width:             newWidth,
 | |
| 				Height:            newHeight,
 | |
| 				DeviceScaleFactor: newZoom,
 | |
| 				Mobile:            newWidth <= 768,
 | |
| 			})
 | |
| 			if err != nil {
 | |
| 				return fmt.Errorf("failed to set viewport: %w", err)
 | |
| 			}
 | |
| 
 | |
| 			// Wait for reflow
 | |
| 			time.Sleep(500 * time.Millisecond)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Take the screenshot
 | |
| 	var screenshotErr error
 | |
| 	if timeout > 0 {
 | |
| 		ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
 | |
| 		defer cancel()
 | |
| 
 | |
| 		done := make(chan error, 1)
 | |
| 		go func() {
 | |
| 			screenshotBytes, err := page.Screenshot(fullPage, &proto.PageCaptureScreenshot{
 | |
| 				Format: proto.PageCaptureScreenshotFormatPng,
 | |
| 			})
 | |
| 			if err != nil {
 | |
| 				done <- fmt.Errorf("failed to capture screenshot: %w", err)
 | |
| 				return
 | |
| 			}
 | |
| 
 | |
| 			err = os.WriteFile(outputPath, screenshotBytes, 0644)
 | |
| 			if err != nil {
 | |
| 				done <- fmt.Errorf("failed to save screenshot to %s: %w", outputPath, err)
 | |
| 				return
 | |
| 			}
 | |
| 			done <- nil
 | |
| 		}()
 | |
| 
 | |
| 		select {
 | |
| 		case err := <-done:
 | |
| 			screenshotErr = err
 | |
| 		case <-ctx.Done():
 | |
| 			screenshotErr = fmt.Errorf("taking screenshot timed out after %d seconds", timeout)
 | |
| 		}
 | |
| 	} else {
 | |
| 		screenshotBytes, err := page.Screenshot(fullPage, &proto.PageCaptureScreenshot{
 | |
| 			Format: proto.PageCaptureScreenshotFormatPng,
 | |
| 		})
 | |
| 		if err != nil {
 | |
| 			screenshotErr = fmt.Errorf("failed to capture screenshot: %w", err)
 | |
| 		} else {
 | |
| 			err = os.WriteFile(outputPath, screenshotBytes, 0644)
 | |
| 			if err != nil {
 | |
| 				screenshotErr = fmt.Errorf("failed to save screenshot to %s: %w", outputPath, err)
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Reset viewport if we changed it
 | |
| 	if needsReset && originalViewport != nil {
 | |
| 		err = page.SetViewport(originalViewport)
 | |
| 		if err != nil {
 | |
| 			d.debugLog("Warning: Failed to reset viewport: %v", err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return screenshotErr
 | |
| }
 | |
| 
 | |
| // switchToIframe switches the context to an iframe for subsequent commands
 | |
| func (d *Daemon) switchToIframe(tabID, selector string, timeout int) error {
 | |
| 	d.debugLog("Switching to iframe: selector=%s, tab=%s, timeout=%d", selector, tabID, timeout)
 | |
| 
 | |
| 	// Get the main page first (not iframe context)
 | |
| 	actualTabID, err := d.getTabID(tabID)
 | |
| 	if err != nil {
 | |
| 		d.debugLog("Failed to get tab ID: %v", err)
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	d.mu.Lock()
 | |
| 	defer d.mu.Unlock()
 | |
| 
 | |
| 	// Get the main page (bypass iframe context)
 | |
| 	mainPage, exists := d.tabs[actualTabID]
 | |
| 	if !exists {
 | |
| 		d.debugLog("Tab %s not in cache, trying to find it", actualTabID)
 | |
| 		// Try to find it
 | |
| 		mainPage, err = d.findPageByID(actualTabID)
 | |
| 		if err != nil {
 | |
| 			d.debugLog("Failed to find tab %s: %v", actualTabID, err)
 | |
| 			return err
 | |
| 		}
 | |
| 		if mainPage == nil {
 | |
| 			d.debugLog("Tab %s not found", actualTabID)
 | |
| 			return fmt.Errorf("tab not found: %s", actualTabID)
 | |
| 		}
 | |
| 		d.tabs[actualTabID] = mainPage
 | |
| 	}
 | |
| 
 | |
| 	d.debugLog("Found main page for tab %s, looking for iframe element", actualTabID)
 | |
| 
 | |
| 	// Find the iframe element with timeout
 | |
| 	var iframeElement *rod.Element
 | |
| 	if timeout > 0 {
 | |
| 		// Use timeout context for finding the iframe element
 | |
| 		ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
 | |
| 		defer cancel()
 | |
| 
 | |
| 		// Create a channel to signal completion
 | |
| 		done := make(chan struct {
 | |
| 			element *rod.Element
 | |
| 			err     error
 | |
| 		}, 1)
 | |
| 
 | |
| 		// Execute the element search in a goroutine
 | |
| 		go func() {
 | |
| 			defer func() {
 | |
| 				if r := recover(); r != nil {
 | |
| 					done <- struct {
 | |
| 						element *rod.Element
 | |
| 						err     error
 | |
| 					}{nil, fmt.Errorf("iframe element search panicked: %v", r)}
 | |
| 				}
 | |
| 			}()
 | |
| 
 | |
| 			element, err := mainPage.Timeout(time.Duration(timeout) * time.Second).Element(selector)
 | |
| 			done <- struct {
 | |
| 				element *rod.Element
 | |
| 				err     error
 | |
| 			}{element, err}
 | |
| 		}()
 | |
| 
 | |
| 		// Wait for either completion or timeout
 | |
| 		select {
 | |
| 		case result := <-done:
 | |
| 			iframeElement = result.element
 | |
| 			err = result.err
 | |
| 		case <-ctx.Done():
 | |
| 			d.debugLog("Iframe element search timed out after %d seconds", timeout)
 | |
| 			return fmt.Errorf("failed to find iframe element (timeout after %ds): %s", timeout, selector)
 | |
| 		}
 | |
| 	} else {
 | |
| 		// No timeout
 | |
| 		iframeElement, err = mainPage.Element(selector)
 | |
| 	}
 | |
| 
 | |
| 	if err != nil {
 | |
| 		d.debugLog("Failed to find iframe element: %v", err)
 | |
| 		return fmt.Errorf("failed to find iframe element: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	d.debugLog("Found iframe element, getting frame context")
 | |
| 
 | |
| 	// Get the iframe's page context with timeout
 | |
| 	var iframePage *rod.Page
 | |
| 	if timeout > 0 {
 | |
| 		// Use timeout context for getting the frame
 | |
| 		ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
 | |
| 		defer cancel()
 | |
| 
 | |
| 		// Create a channel to signal completion
 | |
| 		done := make(chan struct {
 | |
| 			page *rod.Page
 | |
| 			err  error
 | |
| 		}, 1)
 | |
| 
 | |
| 		// Execute the frame access in a goroutine
 | |
| 		go func() {
 | |
| 			defer func() {
 | |
| 				if r := recover(); r != nil {
 | |
| 					done <- struct {
 | |
| 						page *rod.Page
 | |
| 						err  error
 | |
| 					}{nil, fmt.Errorf("iframe frame access panicked: %v", r)}
 | |
| 				}
 | |
| 			}()
 | |
| 
 | |
| 			page, err := iframeElement.Frame()
 | |
| 			done <- struct {
 | |
| 				page *rod.Page
 | |
| 				err  error
 | |
| 			}{page, err}
 | |
| 		}()
 | |
| 
 | |
| 		// Wait for either completion or timeout
 | |
| 		select {
 | |
| 		case result := <-done:
 | |
| 			iframePage = result.page
 | |
| 			err = result.err
 | |
| 		case <-ctx.Done():
 | |
| 			d.debugLog("Iframe frame access timed out after %d seconds", timeout)
 | |
| 			return fmt.Errorf("failed to get iframe context (timeout after %ds)", timeout)
 | |
| 		}
 | |
| 	} else {
 | |
| 		// No timeout
 | |
| 		iframePage, err = iframeElement.Frame()
 | |
| 	}
 | |
| 
 | |
| 	if err != nil {
 | |
| 		d.debugLog("Failed to get iframe context: %v", err)
 | |
| 		return fmt.Errorf("failed to get iframe context: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	// Store the iframe page context
 | |
| 	d.iframePages[actualTabID] = iframePage
 | |
| 	d.debugLog("Successfully switched to iframe context for tab %s", actualTabID)
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // switchToMain switches back to the main page context
 | |
| func (d *Daemon) switchToMain(tabID string) error {
 | |
| 	d.debugLog("Switching back to main context: tab=%s", tabID)
 | |
| 
 | |
| 	// Get the tab ID to use (may be the current tab)
 | |
| 	actualTabID, err := d.getTabID(tabID)
 | |
| 	if err != nil {
 | |
| 		d.debugLog("Failed to get tab ID: %v", err)
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	d.mu.Lock()
 | |
| 	defer d.mu.Unlock()
 | |
| 
 | |
| 	// Check if there was an iframe context to remove
 | |
| 	if _, exists := d.iframePages[actualTabID]; exists {
 | |
| 		d.debugLog("Removing iframe context for tab %s", actualTabID)
 | |
| 		// Remove the iframe context for this tab
 | |
| 		delete(d.iframePages, actualTabID)
 | |
| 	} else {
 | |
| 		d.debugLog("No iframe context found for tab %s", actualTabID)
 | |
| 	}
 | |
| 
 | |
| 	d.debugLog("Successfully switched back to main context for tab %s", actualTabID)
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // handleFileUpload handles file upload requests from clients
 | |
| func (d *Daemon) handleFileUpload(w http.ResponseWriter, r *http.Request) {
 | |
| 	if r.Method != http.MethodPost {
 | |
| 		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// Parse multipart form (32MB max memory)
 | |
| 	err := r.ParseMultipartForm(32 << 20)
 | |
| 	if err != nil {
 | |
| 		http.Error(w, "Failed to parse multipart form", http.StatusBadRequest)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// Get the uploaded file
 | |
| 	file, header, err := r.FormFile("file")
 | |
| 	if err != nil {
 | |
| 		http.Error(w, "Failed to get uploaded file", http.StatusBadRequest)
 | |
| 		return
 | |
| 	}
 | |
| 	defer file.Close()
 | |
| 
 | |
| 	// Get the target path (optional, defaults to /tmp/)
 | |
| 	targetPath := r.FormValue("path")
 | |
| 	if targetPath == "" {
 | |
| 		targetPath = "/tmp/" + header.Filename
 | |
| 	}
 | |
| 
 | |
| 	// Create the target file
 | |
| 	targetFile, err := os.Create(targetPath)
 | |
| 	if err != nil {
 | |
| 		http.Error(w, fmt.Sprintf("Failed to create target file: %v", err), http.StatusInternalServerError)
 | |
| 		return
 | |
| 	}
 | |
| 	defer targetFile.Close()
 | |
| 
 | |
| 	// Copy the uploaded file to the target location
 | |
| 	_, err = io.Copy(targetFile, file)
 | |
| 	if err != nil {
 | |
| 		http.Error(w, fmt.Sprintf("Failed to save file: %v", err), http.StatusInternalServerError)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// Return success response
 | |
| 	response := Response{
 | |
| 		Success: true,
 | |
| 		Data: map[string]interface{}{
 | |
| 			"filename":    header.Filename,
 | |
| 			"size":        header.Size,
 | |
| 			"target_path": targetPath,
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	w.Header().Set("Content-Type", "application/json")
 | |
| 	json.NewEncoder(w).Encode(response)
 | |
| }
 | |
| 
 | |
| // handleFileDownload handles file download requests from clients
 | |
| func (d *Daemon) handleFileDownload(w http.ResponseWriter, r *http.Request) {
 | |
| 	if r.Method != http.MethodGet {
 | |
| 		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// Get the file path from query parameter
 | |
| 	filePath := r.URL.Query().Get("path")
 | |
| 	if filePath == "" {
 | |
| 		http.Error(w, "File path is required", http.StatusBadRequest)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// Check if file exists and get info
 | |
| 	fileInfo, err := os.Stat(filePath)
 | |
| 	if err != nil {
 | |
| 		if os.IsNotExist(err) {
 | |
| 			http.Error(w, "File not found", http.StatusNotFound)
 | |
| 		} else {
 | |
| 			http.Error(w, fmt.Sprintf("Failed to access file: %v", err), http.StatusInternalServerError)
 | |
| 		}
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// Open the file
 | |
| 	file, err := os.Open(filePath)
 | |
| 	if err != nil {
 | |
| 		http.Error(w, fmt.Sprintf("Failed to open file: %v", err), http.StatusInternalServerError)
 | |
| 		return
 | |
| 	}
 | |
| 	defer file.Close()
 | |
| 
 | |
| 	// Set headers for file download
 | |
| 	w.Header().Set("Content-Type", "application/octet-stream")
 | |
| 	w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", fileInfo.Name()))
 | |
| 	w.Header().Set("Content-Length", strconv.FormatInt(fileInfo.Size(), 10))
 | |
| 
 | |
| 	// Stream the file to the client
 | |
| 	_, err = io.Copy(w, file)
 | |
| 	if err != nil {
 | |
| 		log.Printf("Error streaming file to client: %v", err)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // setupConsoleLogging sets up console log capture for a tab
 | |
| func (d *Daemon) setupConsoleLogging(tabID string, page *rod.Page) {
 | |
| 	// Initialize console logs for this tab
 | |
| 	d.consoleLogs[tabID] = make([]ConsoleLog, 0)
 | |
| 
 | |
| 	// Listen for console events
 | |
| 	go page.EachEvent(func(e *proto.RuntimeConsoleAPICalled) {
 | |
| 		d.mu.Lock()
 | |
| 		defer d.mu.Unlock()
 | |
| 
 | |
| 		// Convert console level
 | |
| 		level := string(e.Type)
 | |
| 
 | |
| 		// Build message from arguments
 | |
| 		var message string
 | |
| 		for i, arg := range e.Args {
 | |
| 			if i > 0 {
 | |
| 				message += " "
 | |
| 			}
 | |
| 			// Handle different argument types
 | |
| 			if !arg.Value.Nil() {
 | |
| 				message += arg.Value.String()
 | |
| 			} else if arg.Description != "" {
 | |
| 				message += arg.Description
 | |
| 			} else {
 | |
| 				message += "[object]"
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		// Create console log entry
 | |
| 		logEntry := ConsoleLog{
 | |
| 			Level:     level,
 | |
| 			Message:   message,
 | |
| 			Timestamp: time.Now(),
 | |
| 			Source:    "", // Could be enhanced with stack trace info
 | |
| 		}
 | |
| 
 | |
| 		// Add to console logs (keep last 1000 entries per tab)
 | |
| 		logs := d.consoleLogs[tabID]
 | |
| 		logs = append(logs, logEntry)
 | |
| 		if len(logs) > 1000 {
 | |
| 			logs = logs[1:] // Remove oldest entry
 | |
| 		}
 | |
| 		d.consoleLogs[tabID] = logs
 | |
| 	})()
 | |
| }
 | |
| 
 | |
| // getConsoleLogs retrieves console logs for a tab
 | |
| func (d *Daemon) getConsoleLogs(tabID string, clear bool) ([]ConsoleLog, error) {
 | |
| 	d.mu.Lock()
 | |
| 	defer d.mu.Unlock()
 | |
| 
 | |
| 	// Use current tab if not specified
 | |
| 	if tabID == "" {
 | |
| 		tabID = d.currentTab
 | |
| 	}
 | |
| 
 | |
| 	if tabID == "" {
 | |
| 		return nil, fmt.Errorf("no tab specified and no current tab available")
 | |
| 	}
 | |
| 
 | |
| 	// Check if tab exists
 | |
| 	if _, exists := d.tabs[tabID]; !exists {
 | |
| 		return nil, fmt.Errorf("tab not found: %s", tabID)
 | |
| 	}
 | |
| 
 | |
| 	// Get logs for this tab
 | |
| 	logs, exists := d.consoleLogs[tabID]
 | |
| 	if !exists {
 | |
| 		logs = make([]ConsoleLog, 0)
 | |
| 	}
 | |
| 
 | |
| 	// Clear logs if requested
 | |
| 	if clear {
 | |
| 		d.consoleLogs[tabID] = make([]ConsoleLog, 0)
 | |
| 	}
 | |
| 
 | |
| 	return logs, nil
 | |
| }
 | |
| 
 | |
| // executeConsoleCommand executes a command in the browser console
 | |
| func (d *Daemon) executeConsoleCommand(tabID, command string, timeout int) (string, error) {
 | |
| 	// Use current tab if not specified
 | |
| 	if tabID == "" {
 | |
| 		tabID = d.currentTab
 | |
| 	}
 | |
| 
 | |
| 	if tabID == "" {
 | |
| 		return "", fmt.Errorf("no tab specified and no current tab available")
 | |
| 	}
 | |
| 
 | |
| 	// Execute the command as JavaScript and return the result
 | |
| 	// This is similar to evalJS but specifically for console commands
 | |
| 	return d.evalJS(tabID, command, timeout)
 | |
| }
 | |
| 
 | |
| // ElementCheckResult represents the result of an element check
 | |
| type ElementCheckResult struct {
 | |
| 	Exists   bool `json:"exists"`
 | |
| 	Visible  bool `json:"visible,omitempty"`
 | |
| 	Enabled  bool `json:"enabled,omitempty"`
 | |
| 	Focused  bool `json:"focused,omitempty"`
 | |
| 	Selected bool `json:"selected,omitempty"`
 | |
| 	Count    int  `json:"count,omitempty"`
 | |
| }
 | |
| 
 | |
| // MultipleExtractionResult represents the result of extracting from multiple selectors
 | |
| type MultipleExtractionResult struct {
 | |
| 	Results map[string]interface{} `json:"results"`
 | |
| 	Errors  map[string]string      `json:"errors,omitempty"`
 | |
| }
 | |
| 
 | |
| // LinkInfo represents information about a link
 | |
| type LinkInfo struct {
 | |
| 	Href   string `json:"href"`
 | |
| 	Text   string `json:"text"`
 | |
| 	Title  string `json:"title,omitempty"`
 | |
| 	Target string `json:"target,omitempty"`
 | |
| }
 | |
| 
 | |
| // LinksExtractionResult represents the result of extracting links
 | |
| type LinksExtractionResult struct {
 | |
| 	Links []LinkInfo `json:"links"`
 | |
| 	Count int        `json:"count"`
 | |
| }
 | |
| 
 | |
| // TableExtractionResult represents the result of extracting table data
 | |
| type TableExtractionResult struct {
 | |
| 	Headers []string            `json:"headers,omitempty"`
 | |
| 	Rows    [][]string          `json:"rows"`
 | |
| 	Data    []map[string]string `json:"data,omitempty"` // Only if headers are included
 | |
| 	Count   int                 `json:"count"`
 | |
| }
 | |
| 
 | |
| // TextExtractionResult represents the result of extracting text
 | |
| type TextExtractionResult struct {
 | |
| 	Text    string   `json:"text"`
 | |
| 	Matches []string `json:"matches,omitempty"` // If pattern was used
 | |
| 	Count   int      `json:"count"`             // Number of elements matched
 | |
| }
 | |
| 
 | |
| // checkElement checks various states of an element
 | |
| func (d *Daemon) checkElement(tabID, selector, checkType string, timeout int) (*ElementCheckResult, error) {
 | |
| 	// Use current tab if not specified
 | |
| 	if tabID == "" {
 | |
| 		tabID = d.currentTab
 | |
| 	}
 | |
| 
 | |
| 	if tabID == "" {
 | |
| 		return nil, fmt.Errorf("no tab specified and no current tab available")
 | |
| 	}
 | |
| 
 | |
| 	page, exists := d.tabs[tabID]
 | |
| 	if !exists {
 | |
| 		return nil, fmt.Errorf("tab %s not found", tabID)
 | |
| 	}
 | |
| 
 | |
| 	// Check if we're in iframe mode for this tab
 | |
| 	if iframePage, inIframe := d.iframePages[tabID]; inIframe {
 | |
| 		page = iframePage
 | |
| 	}
 | |
| 
 | |
| 	result := &ElementCheckResult{}
 | |
| 
 | |
| 	// First check if element exists
 | |
| 	elements, err := page.Elements(selector)
 | |
| 	if err != nil {
 | |
| 		// If we can't find elements, it means they don't exist
 | |
| 		result.Exists = false
 | |
| 		return result, nil
 | |
| 	}
 | |
| 
 | |
| 	result.Exists = len(elements) > 0
 | |
| 	result.Count = len(elements)
 | |
| 
 | |
| 	// If no elements exist, return early
 | |
| 	if !result.Exists {
 | |
| 		return result, nil
 | |
| 	}
 | |
| 
 | |
| 	// For additional checks, use the first element
 | |
| 	element := elements[0]
 | |
| 
 | |
| 	switch checkType {
 | |
| 	case "exists":
 | |
| 		// Already handled above
 | |
| 	case "visible":
 | |
| 		visible, err := element.Visible()
 | |
| 		if err != nil {
 | |
| 			return nil, fmt.Errorf("failed to check visibility: %w", err)
 | |
| 		}
 | |
| 		result.Visible = visible
 | |
| 	case "enabled":
 | |
| 		// Check if element is enabled (not disabled)
 | |
| 		disabled, err := element.Attribute("disabled")
 | |
| 		if err != nil {
 | |
| 			return nil, fmt.Errorf("failed to check enabled state: %w", err)
 | |
| 		}
 | |
| 		result.Enabled = disabled == nil
 | |
| 	case "focused":
 | |
| 		// Check if element is focused
 | |
| 		jsCode := fmt.Sprintf("document.activeElement === document.querySelector('%s')", selector)
 | |
| 		focusResult, err := page.Eval(jsCode)
 | |
| 		if err != nil {
 | |
| 			return nil, fmt.Errorf("failed to check focus state: %w", err)
 | |
| 		}
 | |
| 		result.Focused = focusResult.Value.Bool()
 | |
| 	case "selected":
 | |
| 		// Check if element is selected (for checkboxes, radio buttons, options)
 | |
| 		selected, err := element.Attribute("selected")
 | |
| 		if err == nil && selected != nil {
 | |
| 			result.Selected = true
 | |
| 		} else {
 | |
| 			// Also check 'checked' attribute for checkboxes and radio buttons
 | |
| 			checked, err := element.Attribute("checked")
 | |
| 			if err == nil && checked != nil {
 | |
| 				result.Selected = true
 | |
| 			} else {
 | |
| 				result.Selected = false
 | |
| 			}
 | |
| 		}
 | |
| 	case "all":
 | |
| 		// Check all states
 | |
| 		visible, _ := element.Visible()
 | |
| 		result.Visible = visible
 | |
| 
 | |
| 		disabled, _ := element.Attribute("disabled")
 | |
| 		result.Enabled = disabled == nil
 | |
| 
 | |
| 		jsCode := fmt.Sprintf("document.activeElement === document.querySelector('%s')", selector)
 | |
| 		focusResult, _ := page.Eval(jsCode)
 | |
| 		if focusResult != nil {
 | |
| 			result.Focused = focusResult.Value.Bool()
 | |
| 		}
 | |
| 
 | |
| 		selected, _ := element.Attribute("selected")
 | |
| 		if selected != nil {
 | |
| 			result.Selected = true
 | |
| 		} else {
 | |
| 			checked, _ := element.Attribute("checked")
 | |
| 			result.Selected = checked != nil
 | |
| 		}
 | |
| 	default:
 | |
| 		return nil, fmt.Errorf("unknown check type: %s", checkType)
 | |
| 	}
 | |
| 
 | |
| 	return result, nil
 | |
| }
 | |
| 
 | |
| // getElementAttributes gets attributes, properties, and computed styles of an element
 | |
| func (d *Daemon) getElementAttributes(tabID, selector, attributes string, timeout int) (map[string]interface{}, error) {
 | |
| 	// Use current tab if not specified
 | |
| 	if tabID == "" {
 | |
| 		tabID = d.currentTab
 | |
| 	}
 | |
| 
 | |
| 	if tabID == "" {
 | |
| 		return nil, fmt.Errorf("no tab specified and no current tab available")
 | |
| 	}
 | |
| 
 | |
| 	page, exists := d.tabs[tabID]
 | |
| 	if !exists {
 | |
| 		return nil, fmt.Errorf("tab %s not found", tabID)
 | |
| 	}
 | |
| 
 | |
| 	// Check if we're in iframe mode for this tab
 | |
| 	if iframePage, inIframe := d.iframePages[tabID]; inIframe {
 | |
| 		page = iframePage
 | |
| 	}
 | |
| 
 | |
| 	// Find the element with timeout
 | |
| 	var element *rod.Element
 | |
| 	if timeout > 0 {
 | |
| 		ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
 | |
| 		defer cancel()
 | |
| 
 | |
| 		var err error
 | |
| 		element, err = page.Context(ctx).Element(selector)
 | |
| 		if err != nil {
 | |
| 			return nil, fmt.Errorf("element not found: %w", err)
 | |
| 		}
 | |
| 	} else {
 | |
| 		var err error
 | |
| 		element, err = page.Element(selector)
 | |
| 		if err != nil {
 | |
| 			return nil, fmt.Errorf("element not found: %w", err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	result := make(map[string]interface{})
 | |
| 
 | |
| 	if attributes == "all" {
 | |
| 		// Get all common attributes and properties
 | |
| 		commonAttrs := []string{
 | |
| 			"id", "class", "name", "type", "value", "href", "src", "alt", "title",
 | |
| 			"disabled", "checked", "selected", "readonly", "required", "placeholder",
 | |
| 			"data-*", "aria-*",
 | |
| 		}
 | |
| 
 | |
| 		// Get HTML attributes
 | |
| 		for _, attr := range commonAttrs {
 | |
| 			if attr == "data-*" || attr == "aria-*" {
 | |
| 				// Skip wildcard attributes for now
 | |
| 				continue
 | |
| 			}
 | |
| 			value, err := element.Attribute(attr)
 | |
| 			if err == nil && value != nil {
 | |
| 				result[attr] = *value
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		// Get common properties via JavaScript
 | |
| 		jsCode := `
 | |
| 			(function(el) {
 | |
| 				return {
 | |
| 					tagName: el.tagName,
 | |
| 					textContent: el.textContent,
 | |
| 					innerHTML: el.innerHTML,
 | |
| 					outerHTML: el.outerHTML,
 | |
| 					offsetWidth: el.offsetWidth,
 | |
| 					offsetHeight: el.offsetHeight,
 | |
| 					scrollWidth: el.scrollWidth,
 | |
| 					scrollHeight: el.scrollHeight,
 | |
| 					clientWidth: el.clientWidth,
 | |
| 					clientHeight: el.clientHeight
 | |
| 				};
 | |
| 			})(arguments[0])
 | |
| 		`
 | |
| 
 | |
| 		jsResult, err := element.Eval(jsCode)
 | |
| 		if err == nil {
 | |
| 			if props := jsResult.Value.Map(); props != nil {
 | |
| 				for key, value := range props {
 | |
| 					result[key] = value
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		// Get computed styles for common properties
 | |
| 		styleProps := []string{
 | |
| 			"display", "visibility", "opacity", "position", "top", "left", "width", "height",
 | |
| 			"margin", "padding", "border", "background-color", "color", "font-size", "font-family",
 | |
| 		}
 | |
| 
 | |
| 		for _, prop := range styleProps {
 | |
| 			jsCode := fmt.Sprintf("getComputedStyle(arguments[0]).%s", prop)
 | |
| 			styleResult, err := element.Eval(jsCode)
 | |
| 			if err == nil {
 | |
| 				result["style_"+prop] = styleResult.Value.Str()
 | |
| 			}
 | |
| 		}
 | |
| 	} else {
 | |
| 		// Get specific attributes (comma-separated)
 | |
| 		attrList := []string{}
 | |
| 		if attributes != "" {
 | |
| 			// Split by comma and trim spaces
 | |
| 			for _, attr := range strings.Split(attributes, ",") {
 | |
| 				attrList = append(attrList, strings.TrimSpace(attr))
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		for _, attr := range attrList {
 | |
| 			if strings.HasPrefix(attr, "style_") {
 | |
| 				// Get computed style
 | |
| 				styleProp := strings.TrimPrefix(attr, "style_")
 | |
| 				jsCode := fmt.Sprintf("getComputedStyle(arguments[0]).%s", styleProp)
 | |
| 				styleResult, err := element.Eval(jsCode)
 | |
| 				if err == nil {
 | |
| 					result[attr] = styleResult.Value.Str()
 | |
| 				}
 | |
| 			} else if strings.HasPrefix(attr, "prop_") {
 | |
| 				// Get JavaScript property
 | |
| 				propName := strings.TrimPrefix(attr, "prop_")
 | |
| 				jsCode := fmt.Sprintf("arguments[0].%s", propName)
 | |
| 				propResult, err := element.Eval(jsCode)
 | |
| 				if err == nil {
 | |
| 					result[attr] = propResult.Value.Raw
 | |
| 				}
 | |
| 			} else {
 | |
| 				// Get HTML attribute
 | |
| 				value, err := element.Attribute(attr)
 | |
| 				if err == nil && value != nil {
 | |
| 					result[attr] = *value
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return result, nil
 | |
| }
 | |
| 
 | |
| // countElements counts the number of elements matching a selector
 | |
| func (d *Daemon) countElements(tabID, selector string, timeout int) (int, error) {
 | |
| 	// Use current tab if not specified
 | |
| 	if tabID == "" {
 | |
| 		tabID = d.currentTab
 | |
| 	}
 | |
| 
 | |
| 	if tabID == "" {
 | |
| 		return 0, fmt.Errorf("no tab specified and no current tab available")
 | |
| 	}
 | |
| 
 | |
| 	page, exists := d.tabs[tabID]
 | |
| 	if !exists {
 | |
| 		return 0, fmt.Errorf("tab %s not found", tabID)
 | |
| 	}
 | |
| 
 | |
| 	// Check if we're in iframe mode for this tab
 | |
| 	if iframePage, inIframe := d.iframePages[tabID]; inIframe {
 | |
| 		page = iframePage
 | |
| 	}
 | |
| 
 | |
| 	// Find elements with timeout
 | |
| 	var elements rod.Elements
 | |
| 	var err error
 | |
| 
 | |
| 	if timeout > 0 {
 | |
| 		ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
 | |
| 		defer cancel()
 | |
| 
 | |
| 		elements, err = page.Context(ctx).Elements(selector)
 | |
| 	} else {
 | |
| 		elements, err = page.Elements(selector)
 | |
| 	}
 | |
| 
 | |
| 	if err != nil {
 | |
| 		// If we can't find elements, return 0 (not an error)
 | |
| 		return 0, nil
 | |
| 	}
 | |
| 
 | |
| 	return len(elements), nil
 | |
| }
 | |
| 
 | |
| // extractMultiple extracts data from multiple selectors in a single call
 | |
| func (d *Daemon) extractMultiple(tabID, selectorsJSON string, timeout int) (*MultipleExtractionResult, error) {
 | |
| 	// Use current tab if not specified
 | |
| 	if tabID == "" {
 | |
| 		tabID = d.currentTab
 | |
| 	}
 | |
| 
 | |
| 	if tabID == "" {
 | |
| 		return nil, fmt.Errorf("no tab specified and no current tab available")
 | |
| 	}
 | |
| 
 | |
| 	page, exists := d.tabs[tabID]
 | |
| 	if !exists {
 | |
| 		return nil, fmt.Errorf("tab %s not found", tabID)
 | |
| 	}
 | |
| 
 | |
| 	// Check if we're in iframe mode for this tab
 | |
| 	if iframePage, inIframe := d.iframePages[tabID]; inIframe {
 | |
| 		page = iframePage
 | |
| 	}
 | |
| 
 | |
| 	// Parse selectors JSON
 | |
| 	var selectors map[string]string
 | |
| 	if err := json.Unmarshal([]byte(selectorsJSON), &selectors); err != nil {
 | |
| 		return nil, fmt.Errorf("invalid selectors JSON: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	result := &MultipleExtractionResult{
 | |
| 		Results: make(map[string]interface{}),
 | |
| 		Errors:  make(map[string]string),
 | |
| 	}
 | |
| 
 | |
| 	// Extract from each selector
 | |
| 	for key, selector := range selectors {
 | |
| 		var elements rod.Elements
 | |
| 		var err error
 | |
| 
 | |
| 		if timeout > 0 {
 | |
| 			ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
 | |
| 			defer cancel()
 | |
| 			elements, err = page.Context(ctx).Elements(selector)
 | |
| 		} else {
 | |
| 			elements, err = page.Elements(selector)
 | |
| 		}
 | |
| 
 | |
| 		if err != nil {
 | |
| 			result.Errors[key] = err.Error()
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		if len(elements) == 0 {
 | |
| 			result.Results[key] = nil
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		// Extract content from all matching elements
 | |
| 		var values []string
 | |
| 		for _, element := range elements {
 | |
| 			var value string
 | |
| 			var err error
 | |
| 
 | |
| 			// Check if it's a form input element and get its value
 | |
| 			tagName, _ := element.Eval("() => this.tagName.toLowerCase()")
 | |
| 
 | |
| 			if tagName.Value.Str() == "input" || tagName.Value.Str() == "textarea" || tagName.Value.Str() == "select" {
 | |
| 				// For form elements, get the value property
 | |
| 				valueProp, err := element.Property("value")
 | |
| 				if err == nil && valueProp.Str() != "" {
 | |
| 					value = valueProp.Str()
 | |
| 				} else {
 | |
| 					// Fallback to text content
 | |
| 					value, err = element.Text()
 | |
| 				}
 | |
| 			} else {
 | |
| 				// For non-form elements, get text content
 | |
| 				value, err = element.Text()
 | |
| 			}
 | |
| 
 | |
| 			if err != nil {
 | |
| 				result.Errors[key] = fmt.Sprintf("failed to get content: %v", err)
 | |
| 				break
 | |
| 			}
 | |
| 			values = append(values, value)
 | |
| 		}
 | |
| 
 | |
| 		if len(values) == 1 {
 | |
| 			result.Results[key] = values[0]
 | |
| 		} else {
 | |
| 			result.Results[key] = values
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return result, nil
 | |
| }
 | |
| 
 | |
| // extractLinks extracts all links from the page with optional filtering
 | |
| func (d *Daemon) extractLinks(tabID, containerSelector, hrefPattern, textPattern string, timeout int) (*LinksExtractionResult, error) {
 | |
| 	// Use current tab if not specified
 | |
| 	if tabID == "" {
 | |
| 		tabID = d.currentTab
 | |
| 	}
 | |
| 
 | |
| 	if tabID == "" {
 | |
| 		return nil, fmt.Errorf("no tab specified and no current tab available")
 | |
| 	}
 | |
| 
 | |
| 	page, exists := d.tabs[tabID]
 | |
| 	if !exists {
 | |
| 		return nil, fmt.Errorf("tab %s not found", tabID)
 | |
| 	}
 | |
| 
 | |
| 	// Check if we're in iframe mode for this tab
 | |
| 	if iframePage, inIframe := d.iframePages[tabID]; inIframe {
 | |
| 		page = iframePage
 | |
| 	}
 | |
| 
 | |
| 	// Build selector for links
 | |
| 	linkSelector := "a[href]"
 | |
| 	if containerSelector != "" {
 | |
| 		linkSelector = containerSelector + " " + linkSelector
 | |
| 	}
 | |
| 
 | |
| 	// Find all links
 | |
| 	var elements rod.Elements
 | |
| 	var err error
 | |
| 
 | |
| 	if timeout > 0 {
 | |
| 		ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
 | |
| 		defer cancel()
 | |
| 		elements, err = page.Context(ctx).Elements(linkSelector)
 | |
| 	} else {
 | |
| 		elements, err = page.Elements(linkSelector)
 | |
| 	}
 | |
| 
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to find links: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	result := &LinksExtractionResult{
 | |
| 		Links: make([]LinkInfo, 0),
 | |
| 		Count: 0,
 | |
| 	}
 | |
| 
 | |
| 	// Compile regex patterns if provided
 | |
| 	var hrefRegex, textRegex *regexp.Regexp
 | |
| 	if hrefPattern != "" {
 | |
| 		hrefRegex, err = regexp.Compile(hrefPattern)
 | |
| 		if err != nil {
 | |
| 			return nil, fmt.Errorf("invalid href pattern: %w", err)
 | |
| 		}
 | |
| 	}
 | |
| 	if textPattern != "" {
 | |
| 		textRegex, err = regexp.Compile(textPattern)
 | |
| 		if err != nil {
 | |
| 			return nil, fmt.Errorf("invalid text pattern: %w", err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Extract link information
 | |
| 	for _, element := range elements {
 | |
| 		href, err := element.Attribute("href")
 | |
| 		if err != nil || href == nil {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		text, err := element.Text()
 | |
| 		if err != nil {
 | |
| 			text = ""
 | |
| 		}
 | |
| 
 | |
| 		// Apply filters
 | |
| 		if hrefRegex != nil && !hrefRegex.MatchString(*href) {
 | |
| 			continue
 | |
| 		}
 | |
| 		if textRegex != nil && !textRegex.MatchString(text) {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		// Get additional attributes
 | |
| 		title, _ := element.Attribute("title")
 | |
| 		target, _ := element.Attribute("target")
 | |
| 
 | |
| 		linkInfo := LinkInfo{
 | |
| 			Href: *href,
 | |
| 			Text: text,
 | |
| 		}
 | |
| 		if title != nil {
 | |
| 			linkInfo.Title = *title
 | |
| 		}
 | |
| 		if target != nil {
 | |
| 			linkInfo.Target = *target
 | |
| 		}
 | |
| 
 | |
| 		result.Links = append(result.Links, linkInfo)
 | |
| 	}
 | |
| 
 | |
| 	result.Count = len(result.Links)
 | |
| 	return result, nil
 | |
| }
 | |
| 
 | |
| // extractTable extracts table data as structured JSON
 | |
| func (d *Daemon) extractTable(tabID, selector string, includeHeaders bool, timeout int) (*TableExtractionResult, error) {
 | |
| 	// Use current tab if not specified
 | |
| 	if tabID == "" {
 | |
| 		tabID = d.currentTab
 | |
| 	}
 | |
| 
 | |
| 	if tabID == "" {
 | |
| 		return nil, fmt.Errorf("no tab specified and no current tab available")
 | |
| 	}
 | |
| 
 | |
| 	page, exists := d.tabs[tabID]
 | |
| 	if !exists {
 | |
| 		return nil, fmt.Errorf("tab %s not found", tabID)
 | |
| 	}
 | |
| 
 | |
| 	// Check if we're in iframe mode for this tab
 | |
| 	if iframePage, inIframe := d.iframePages[tabID]; inIframe {
 | |
| 		page = iframePage
 | |
| 	}
 | |
| 
 | |
| 	// Find the table
 | |
| 	var table *rod.Element
 | |
| 	var err error
 | |
| 
 | |
| 	if timeout > 0 {
 | |
| 		ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
 | |
| 		defer cancel()
 | |
| 		table, err = page.Context(ctx).Element(selector)
 | |
| 	} else {
 | |
| 		table, err = page.Element(selector)
 | |
| 	}
 | |
| 
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to find table: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	result := &TableExtractionResult{
 | |
| 		Rows:  make([][]string, 0),
 | |
| 		Count: 0,
 | |
| 	}
 | |
| 
 | |
| 	// Extract headers if requested
 | |
| 	if includeHeaders {
 | |
| 		headerRows, err := table.Elements("thead tr, tr:first-child")
 | |
| 		if err == nil && len(headerRows) > 0 {
 | |
| 			headerCells, err := headerRows[0].Elements("th, td")
 | |
| 			if err == nil {
 | |
| 				headers := make([]string, 0)
 | |
| 				for _, cell := range headerCells {
 | |
| 					text, err := cell.Text()
 | |
| 					if err != nil {
 | |
| 						text = ""
 | |
| 					}
 | |
| 					headers = append(headers, strings.TrimSpace(text))
 | |
| 				}
 | |
| 				result.Headers = headers
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Extract all rows
 | |
| 	rows, err := table.Elements("tbody tr, tr")
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to find table rows: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	// Skip header row if we extracted headers
 | |
| 	startIndex := 0
 | |
| 	if includeHeaders && len(result.Headers) > 0 {
 | |
| 		startIndex = 1
 | |
| 	}
 | |
| 
 | |
| 	for i := startIndex; i < len(rows); i++ {
 | |
| 		cells, err := rows[i].Elements("td, th")
 | |
| 		if err != nil {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		rowData := make([]string, 0)
 | |
| 		for _, cell := range cells {
 | |
| 			text, err := cell.Text()
 | |
| 			if err != nil {
 | |
| 				text = ""
 | |
| 			}
 | |
| 			rowData = append(rowData, strings.TrimSpace(text))
 | |
| 		}
 | |
| 
 | |
| 		if len(rowData) > 0 {
 | |
| 			result.Rows = append(result.Rows, rowData)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Create structured data if headers are available
 | |
| 	if includeHeaders && len(result.Headers) > 0 {
 | |
| 		result.Data = make([]map[string]string, 0)
 | |
| 		for _, row := range result.Rows {
 | |
| 			rowMap := make(map[string]string)
 | |
| 			for i, header := range result.Headers {
 | |
| 				if i < len(row) {
 | |
| 					rowMap[header] = row[i]
 | |
| 				} else {
 | |
| 					rowMap[header] = ""
 | |
| 				}
 | |
| 			}
 | |
| 			result.Data = append(result.Data, rowMap)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	result.Count = len(result.Rows)
 | |
| 	return result, nil
 | |
| }
 | |
| 
 | |
| // extractText extracts text content with optional pattern matching
 | |
| func (d *Daemon) extractText(tabID, selector, pattern, extractType string, timeout int) (*TextExtractionResult, error) {
 | |
| 	// Use current tab if not specified
 | |
| 	if tabID == "" {
 | |
| 		tabID = d.currentTab
 | |
| 	}
 | |
| 
 | |
| 	if tabID == "" {
 | |
| 		return nil, fmt.Errorf("no tab specified and no current tab available")
 | |
| 	}
 | |
| 
 | |
| 	page, exists := d.tabs[tabID]
 | |
| 	if !exists {
 | |
| 		return nil, fmt.Errorf("tab %s not found", tabID)
 | |
| 	}
 | |
| 
 | |
| 	// Check if we're in iframe mode for this tab
 | |
| 	if iframePage, inIframe := d.iframePages[tabID]; inIframe {
 | |
| 		page = iframePage
 | |
| 	}
 | |
| 
 | |
| 	// Default extract type
 | |
| 	if extractType == "" {
 | |
| 		extractType = "textContent"
 | |
| 	}
 | |
| 
 | |
| 	// Find elements
 | |
| 	var elements rod.Elements
 | |
| 	var err error
 | |
| 
 | |
| 	if timeout > 0 {
 | |
| 		ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
 | |
| 		defer cancel()
 | |
| 		elements, err = page.Context(ctx).Elements(selector)
 | |
| 	} else {
 | |
| 		elements, err = page.Elements(selector)
 | |
| 	}
 | |
| 
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to find elements: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	result := &TextExtractionResult{
 | |
| 		Count: len(elements),
 | |
| 	}
 | |
| 
 | |
| 	// Compile regex pattern if provided
 | |
| 	var textRegex *regexp.Regexp
 | |
| 	if pattern != "" {
 | |
| 		textRegex, err = regexp.Compile(pattern)
 | |
| 		if err != nil {
 | |
| 			return nil, fmt.Errorf("invalid text pattern: %w", err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Extract text from all elements
 | |
| 	var allTexts []string
 | |
| 	for _, element := range elements {
 | |
| 		var text string
 | |
| 
 | |
| 		switch extractType {
 | |
| 		case "text":
 | |
| 			text, err = element.Text()
 | |
| 		case "innerText":
 | |
| 			// Use JavaScript to get innerText
 | |
| 			jsResult, jsErr := element.Eval("() => this.innerText")
 | |
| 			if jsErr == nil && jsResult.Value.Str() != "" {
 | |
| 				text = jsResult.Value.Str()
 | |
| 			} else {
 | |
| 				text, err = element.Text() // Fallback
 | |
| 			}
 | |
| 		case "textContent":
 | |
| 			// Use JavaScript to get textContent
 | |
| 			jsResult, jsErr := element.Eval("() => this.textContent")
 | |
| 			if jsErr == nil && jsResult.Value.Str() != "" {
 | |
| 				text = jsResult.Value.Str()
 | |
| 			} else {
 | |
| 				text, err = element.Text() // Fallback
 | |
| 			}
 | |
| 		default:
 | |
| 			text, err = element.Text()
 | |
| 		}
 | |
| 
 | |
| 		if err != nil {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		allTexts = append(allTexts, text)
 | |
| 	}
 | |
| 
 | |
| 	// Join all texts
 | |
| 	result.Text = strings.Join(allTexts, "\n")
 | |
| 
 | |
| 	// Apply pattern matching if provided
 | |
| 	if textRegex != nil {
 | |
| 		matches := textRegex.FindAllString(result.Text, -1)
 | |
| 		result.Matches = matches
 | |
| 	}
 | |
| 
 | |
| 	return result, nil
 | |
| }
 | |
| 
 | |
| // FormField represents a form field with its properties
 | |
| type FormField struct {
 | |
| 	Name        string            `json:"name"`
 | |
| 	Type        string            `json:"type"`
 | |
| 	Value       string            `json:"value"`
 | |
| 	Placeholder string            `json:"placeholder,omitempty"`
 | |
| 	Required    bool              `json:"required"`
 | |
| 	Disabled    bool              `json:"disabled"`
 | |
| 	ReadOnly    bool              `json:"readonly"`
 | |
| 	Selector    string            `json:"selector"`
 | |
| 	Label       string            `json:"label,omitempty"`
 | |
| 	Options     []FormFieldOption `json:"options,omitempty"` // For select/radio/checkbox
 | |
| }
 | |
| 
 | |
| // FormFieldOption represents an option in a select, radio, or checkbox group
 | |
| type FormFieldOption struct {
 | |
| 	Value    string `json:"value"`
 | |
| 	Text     string `json:"text"`
 | |
| 	Selected bool   `json:"selected"`
 | |
| }
 | |
| 
 | |
| // FormAnalysisResult represents the result of analyzing a form
 | |
| type FormAnalysisResult struct {
 | |
| 	Action     string      `json:"action,omitempty"`
 | |
| 	Method     string      `json:"method,omitempty"`
 | |
| 	Fields     []FormField `json:"fields"`
 | |
| 	FieldCount int         `json:"field_count"`
 | |
| 	CanSubmit  bool        `json:"can_submit"`
 | |
| 	SubmitText string      `json:"submit_text,omitempty"`
 | |
| }
 | |
| 
 | |
| // InteractionItem represents a single interaction to perform
 | |
| type InteractionItem struct {
 | |
| 	Selector string `json:"selector"`
 | |
| 	Action   string `json:"action"` // click, fill, select, check, uncheck
 | |
| 	Value    string `json:"value,omitempty"`
 | |
| }
 | |
| 
 | |
| // InteractionResult represents the result of a single interaction
 | |
| type InteractionResult struct {
 | |
| 	Selector string `json:"selector"`
 | |
| 	Action   string `json:"action"`
 | |
| 	Success  bool   `json:"success"`
 | |
| 	Error    string `json:"error,omitempty"`
 | |
| }
 | |
| 
 | |
| // MultipleInteractionResult represents the result of multiple interactions
 | |
| type MultipleInteractionResult struct {
 | |
| 	Results      []InteractionResult `json:"results"`
 | |
| 	SuccessCount int                 `json:"success_count"`
 | |
| 	ErrorCount   int                 `json:"error_count"`
 | |
| 	TotalCount   int                 `json:"total_count"`
 | |
| }
 | |
| 
 | |
| // FormBulkFillResult represents the result of bulk form filling
 | |
| type FormBulkFillResult struct {
 | |
| 	FilledFields []InteractionResult `json:"filled_fields"`
 | |
| 	SuccessCount int                 `json:"success_count"`
 | |
| 	ErrorCount   int                 `json:"error_count"`
 | |
| 	TotalCount   int                 `json:"total_count"`
 | |
| }
 | |
| 
 | |
| // analyzeForm analyzes a form and returns detailed information about its fields
 | |
| func (d *Daemon) analyzeForm(tabID, selector string, timeout int) (*FormAnalysisResult, error) {
 | |
| 	page, err := d.getTab(tabID)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	// Find the form element
 | |
| 	var form *rod.Element
 | |
| 	if timeout > 0 {
 | |
| 		ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
 | |
| 		defer cancel()
 | |
| 		form, err = page.Context(ctx).Element(selector)
 | |
| 	} else {
 | |
| 		form, err = page.Element(selector)
 | |
| 	}
 | |
| 
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to find form: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	result := &FormAnalysisResult{
 | |
| 		Fields: make([]FormField, 0),
 | |
| 	}
 | |
| 
 | |
| 	// Get form action and method
 | |
| 	if action, err := form.Attribute("action"); err == nil && action != nil {
 | |
| 		result.Action = *action
 | |
| 	}
 | |
| 	if method, err := form.Attribute("method"); err == nil && method != nil {
 | |
| 		result.Method = *method
 | |
| 	} else {
 | |
| 		result.Method = "GET" // Default
 | |
| 	}
 | |
| 
 | |
| 	// Find all form fields
 | |
| 	fieldSelectors := []string{
 | |
| 		"input", "textarea", "select", "button[type='submit']", "input[type='submit']",
 | |
| 	}
 | |
| 
 | |
| 	for _, fieldSelector := range fieldSelectors {
 | |
| 		elements, err := form.Elements(fieldSelector)
 | |
| 		if err != nil {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		for _, element := range elements {
 | |
| 			field := FormField{}
 | |
| 
 | |
| 			// Get basic attributes
 | |
| 			if name, err := element.Attribute("name"); err == nil && name != nil {
 | |
| 				field.Name = *name
 | |
| 			}
 | |
| 			if id, err := element.Attribute("id"); err == nil && id != nil && field.Name == "" {
 | |
| 				field.Name = *id
 | |
| 			}
 | |
| 
 | |
| 			if fieldType, err := element.Attribute("type"); err == nil && fieldType != nil {
 | |
| 				field.Type = *fieldType
 | |
| 			} else {
 | |
| 				// Get tag name if no type
 | |
| 				if tagName, err := element.Eval("() => this.tagName.toLowerCase()"); err == nil {
 | |
| 					field.Type = tagName.Value.Str()
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			// Skip submit buttons for field analysis but note them for submission info
 | |
| 			if field.Type == "submit" {
 | |
| 				result.CanSubmit = true
 | |
| 				if value, err := element.Attribute("value"); err == nil && value != nil {
 | |
| 					result.SubmitText = *value
 | |
| 				} else if text, err := element.Text(); err == nil {
 | |
| 					result.SubmitText = text
 | |
| 				}
 | |
| 				continue
 | |
| 			}
 | |
| 
 | |
| 			// Get current value
 | |
| 			if value, err := element.Attribute("value"); err == nil && value != nil {
 | |
| 				field.Value = *value
 | |
| 			}
 | |
| 
 | |
| 			// Get placeholder
 | |
| 			if placeholder, err := element.Attribute("placeholder"); err == nil && placeholder != nil {
 | |
| 				field.Placeholder = *placeholder
 | |
| 			}
 | |
| 
 | |
| 			// Get boolean attributes
 | |
| 			if required, err := element.Attribute("required"); err == nil && required != nil {
 | |
| 				field.Required = true
 | |
| 			}
 | |
| 			if disabled, err := element.Attribute("disabled"); err == nil && disabled != nil {
 | |
| 				field.Disabled = true
 | |
| 			}
 | |
| 			if readonly, err := element.Attribute("readonly"); err == nil && readonly != nil {
 | |
| 				field.ReadOnly = true
 | |
| 			}
 | |
| 
 | |
| 			// Generate selector for this field
 | |
| 			if field.Name != "" {
 | |
| 				field.Selector = fmt.Sprintf("[name='%s']", field.Name)
 | |
| 			} else if id, err := element.Attribute("id"); err == nil && id != nil {
 | |
| 				field.Selector = fmt.Sprintf("#%s", *id)
 | |
| 			}
 | |
| 
 | |
| 			// Try to find associated label
 | |
| 			if field.Name != "" {
 | |
| 				if label, err := form.Element(fmt.Sprintf("label[for='%s']", field.Name)); err == nil {
 | |
| 					if labelText, err := label.Text(); err == nil {
 | |
| 						field.Label = labelText
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			// Handle select options
 | |
| 			if field.Type == "select" {
 | |
| 				options, err := element.Elements("option")
 | |
| 				if err == nil {
 | |
| 					field.Options = make([]FormFieldOption, 0)
 | |
| 					for _, option := range options {
 | |
| 						opt := FormFieldOption{}
 | |
| 						if value, err := option.Attribute("value"); err == nil && value != nil {
 | |
| 							opt.Value = *value
 | |
| 						}
 | |
| 						if text, err := option.Text(); err == nil {
 | |
| 							opt.Text = text
 | |
| 						}
 | |
| 						if selected, err := option.Attribute("selected"); err == nil && selected != nil {
 | |
| 							opt.Selected = true
 | |
| 						}
 | |
| 						field.Options = append(field.Options, opt)
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			result.Fields = append(result.Fields, field)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	result.FieldCount = len(result.Fields)
 | |
| 
 | |
| 	// Check if form can be submitted (has submit button or can be submitted via JS)
 | |
| 	if !result.CanSubmit {
 | |
| 		// Look for any button that might submit
 | |
| 		if buttons, err := form.Elements("button"); err == nil {
 | |
| 			for _, button := range buttons {
 | |
| 				if buttonType, err := button.Attribute("type"); err == nil && buttonType != nil {
 | |
| 					if *buttonType == "submit" || *buttonType == "" {
 | |
| 						result.CanSubmit = true
 | |
| 						if text, err := button.Text(); err == nil {
 | |
| 							result.SubmitText = text
 | |
| 						}
 | |
| 						break
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return result, nil
 | |
| }
 | |
| 
 | |
| // interactMultiple performs multiple interactions in sequence
 | |
| func (d *Daemon) interactMultiple(tabID, interactionsJSON string, timeout int) (*MultipleInteractionResult, error) {
 | |
| 	page, err := d.getTab(tabID)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	// Parse interactions JSON
 | |
| 	var interactions []InteractionItem
 | |
| 	err = json.Unmarshal([]byte(interactionsJSON), &interactions)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to parse interactions JSON: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	result := &MultipleInteractionResult{
 | |
| 		Results:    make([]InteractionResult, 0),
 | |
| 		TotalCount: len(interactions),
 | |
| 	}
 | |
| 
 | |
| 	// Perform each interaction
 | |
| 	for _, interaction := range interactions {
 | |
| 		interactionResult := InteractionResult{
 | |
| 			Selector: interaction.Selector,
 | |
| 			Action:   interaction.Action,
 | |
| 			Success:  false,
 | |
| 		}
 | |
| 
 | |
| 		// Find the element without timeout to avoid context cancellation issues
 | |
| 		var element *rod.Element
 | |
| 		element, err = page.Element(interaction.Selector)
 | |
| 
 | |
| 		if err != nil {
 | |
| 			interactionResult.Error = fmt.Sprintf("failed to find element: %v", err)
 | |
| 			result.Results = append(result.Results, interactionResult)
 | |
| 			result.ErrorCount++
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		// Perform the action
 | |
| 		switch interaction.Action {
 | |
| 		case "click":
 | |
| 			err = element.Click(proto.InputMouseButtonLeft, 1)
 | |
| 			// Retry once if context was canceled
 | |
| 			if err != nil && strings.Contains(err.Error(), "context canceled") {
 | |
| 				// Try to find element again and click
 | |
| 				element, err = page.Element(interaction.Selector)
 | |
| 				if err == nil {
 | |
| 					err = element.Click(proto.InputMouseButtonLeft, 1)
 | |
| 				}
 | |
| 			}
 | |
| 			if err != nil {
 | |
| 				interactionResult.Error = fmt.Sprintf("failed to click: %v", err)
 | |
| 			} else {
 | |
| 				interactionResult.Success = true
 | |
| 			}
 | |
| 
 | |
| 		case "fill":
 | |
| 			// Clear field first
 | |
| 			err = element.SelectAllText()
 | |
| 			if err == nil {
 | |
| 				err = element.Input("")
 | |
| 			}
 | |
| 			if err == nil {
 | |
| 				err = element.Input(interaction.Value)
 | |
| 			}
 | |
| 			// Retry once if context was canceled
 | |
| 			if err != nil && strings.Contains(err.Error(), "context canceled") {
 | |
| 				// Try to find element again and fill
 | |
| 				element, err = page.Element(interaction.Selector)
 | |
| 				if err == nil {
 | |
| 					err = element.SelectAllText()
 | |
| 					if err == nil {
 | |
| 						err = element.Input("")
 | |
| 					}
 | |
| 					if err == nil {
 | |
| 						err = element.Input(interaction.Value)
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 			if err != nil {
 | |
| 				interactionResult.Error = fmt.Sprintf("failed to fill: %v", err)
 | |
| 			} else {
 | |
| 				interactionResult.Success = true
 | |
| 			}
 | |
| 
 | |
| 		case "select":
 | |
| 			// For select elements, use rod's built-in Select method
 | |
| 			// Try to select by text first (most common case)
 | |
| 			err = element.Select([]string{interaction.Value}, true, rod.SelectorTypeText)
 | |
| 			if err != nil {
 | |
| 				// If text selection failed, use JavaScript as fallback
 | |
| 				// Execute the value assignment (ignore evaluation errors, rod has issues with some JS patterns)
 | |
| 				script := fmt.Sprintf("document.querySelector(\"%s\").value = \"%s\"", interaction.Selector, interaction.Value)
 | |
| 				page.Eval(script)
 | |
| 
 | |
| 				// Dispatch the change event separately
 | |
| 				changeScript := fmt.Sprintf("document.querySelector(\"%s\").dispatchEvent(new Event(\"change\", { bubbles: true }))", interaction.Selector)
 | |
| 				page.Eval(changeScript)
 | |
| 
 | |
| 				// Verify the selection worked by checking the element's value property directly
 | |
| 				currentValue, err := element.Property("value")
 | |
| 				if err != nil {
 | |
| 					interactionResult.Error = fmt.Sprintf("failed to verify selection: %v", err)
 | |
| 				} else if currentValue.Str() != interaction.Value {
 | |
| 					interactionResult.Error = fmt.Sprintf("failed to select option '%s' (current value: %s)", interaction.Value, currentValue.Str())
 | |
| 				} else {
 | |
| 					interactionResult.Success = true
 | |
| 				}
 | |
| 			} else {
 | |
| 				interactionResult.Success = true
 | |
| 			}
 | |
| 
 | |
| 		case "check":
 | |
| 			// Check if it's already checked
 | |
| 			checked, err := element.Property("checked")
 | |
| 			if err == nil && checked.Bool() {
 | |
| 				interactionResult.Success = true // Already checked
 | |
| 			} else {
 | |
| 				err = element.Click(proto.InputMouseButtonLeft, 1)
 | |
| 				// Retry once if context was canceled
 | |
| 				if err != nil && strings.Contains(err.Error(), "context canceled") {
 | |
| 					// Try to find element again and click
 | |
| 					element, err = page.Element(interaction.Selector)
 | |
| 					if err == nil {
 | |
| 						err = element.Click(proto.InputMouseButtonLeft, 1)
 | |
| 					}
 | |
| 				}
 | |
| 				if err != nil {
 | |
| 					interactionResult.Error = fmt.Sprintf("failed to check: %v", err)
 | |
| 				} else {
 | |
| 					interactionResult.Success = true
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 		case "uncheck":
 | |
| 			// Check if it's already unchecked
 | |
| 			checked, err := element.Property("checked")
 | |
| 			if err == nil && !checked.Bool() {
 | |
| 				interactionResult.Success = true // Already unchecked
 | |
| 			} else {
 | |
| 				err = element.Click(proto.InputMouseButtonLeft, 1)
 | |
| 				if err != nil {
 | |
| 					interactionResult.Error = fmt.Sprintf("failed to uncheck: %v", err)
 | |
| 				} else {
 | |
| 					interactionResult.Success = true
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 		default:
 | |
| 			interactionResult.Error = fmt.Sprintf("unknown action: %s", interaction.Action)
 | |
| 		}
 | |
| 
 | |
| 		result.Results = append(result.Results, interactionResult)
 | |
| 		if interactionResult.Success {
 | |
| 			result.SuccessCount++
 | |
| 		} else {
 | |
| 			result.ErrorCount++
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return result, nil
 | |
| }
 | |
| 
 | |
| // fillFormBulk fills multiple form fields in a single operation
 | |
| func (d *Daemon) fillFormBulk(tabID, formSelector, fieldsJSON string, timeout int) (*FormBulkFillResult, error) {
 | |
| 	page, err := d.getTab(tabID)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	// Parse fields JSON
 | |
| 	var fields map[string]string
 | |
| 	err = json.Unmarshal([]byte(fieldsJSON), &fields)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to parse fields JSON: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	result := &FormBulkFillResult{
 | |
| 		FilledFields: make([]InteractionResult, 0),
 | |
| 		TotalCount:   len(fields),
 | |
| 	}
 | |
| 
 | |
| 	// Find the form element if selector is provided
 | |
| 	var form *rod.Element
 | |
| 	if formSelector != "" {
 | |
| 		if timeout > 0 {
 | |
| 			ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
 | |
| 			form, err = page.Context(ctx).Element(formSelector)
 | |
| 			cancel()
 | |
| 		} else {
 | |
| 			form, err = page.Element(formSelector)
 | |
| 		}
 | |
| 
 | |
| 		if err != nil {
 | |
| 			return nil, fmt.Errorf("failed to find form: %w", err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Fill each field
 | |
| 	for fieldName, fieldValue := range fields {
 | |
| 		fieldResult := InteractionResult{
 | |
| 			Selector: fieldName,
 | |
| 			Action:   "fill", // Default action, will be updated based on element type
 | |
| 			Success:  false,
 | |
| 		}
 | |
| 
 | |
| 		// Try different selector strategies for the field (fast, no individual timeouts)
 | |
| 		var element *rod.Element
 | |
| 		selectors := []string{
 | |
| 			fmt.Sprintf("[name='%s']", fieldName),
 | |
| 			fmt.Sprintf("#%s", fieldName),
 | |
| 			fmt.Sprintf("[id='%s']", fieldName),
 | |
| 			fieldName, // In case it's already a full selector
 | |
| 		}
 | |
| 
 | |
| 		// Search for element (try form first if available, then page)
 | |
| 		for _, selector := range selectors {
 | |
| 			// Try without timeout first (should be instant if element exists)
 | |
| 			if form != nil {
 | |
| 				element, err = form.Element(selector)
 | |
| 			} else {
 | |
| 				element, err = page.Element(selector)
 | |
| 			}
 | |
| 
 | |
| 			if err == nil {
 | |
| 				fieldResult.Selector = selector
 | |
| 				break
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if element == nil {
 | |
| 			fieldResult.Error = fmt.Sprintf("failed to find field: %s", fieldName)
 | |
| 			result.FilledFields = append(result.FilledFields, fieldResult)
 | |
| 			result.ErrorCount++
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		// Determine the element type using rod's built-in method (much faster than Eval)
 | |
| 		tagName, err := element.Property("tagName")
 | |
| 		if err != nil {
 | |
| 			fieldResult.Error = fmt.Sprintf("failed to get element tag name: %v", err)
 | |
| 			result.FilledFields = append(result.FilledFields, fieldResult)
 | |
| 			result.ErrorCount++
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		// Handle different element types
 | |
| 		if strings.ToLower(tagName.Str()) == "select" {
 | |
| 			// Use select action for select elements
 | |
| 			fieldResult.Action = "select"
 | |
| 			err = element.Select([]string{fieldValue}, true, rod.SelectorTypeText)
 | |
| 			if err != nil {
 | |
| 				// If text selection failed, use JavaScript as fallback
 | |
| 				// Execute the value assignment (ignore evaluation errors, rod has issues with some JS patterns)
 | |
| 				script := fmt.Sprintf("document.querySelector(\"%s\").value = \"%s\"", fieldResult.Selector, fieldValue)
 | |
| 				page.Eval(script)
 | |
| 
 | |
| 				// Dispatch the change event separately
 | |
| 				changeScript := fmt.Sprintf("document.querySelector(\"%s\").dispatchEvent(new Event(\"change\", { bubbles: true }))", fieldResult.Selector)
 | |
| 				page.Eval(changeScript)
 | |
| 
 | |
| 				// Verify the selection worked by checking the element's value property directly
 | |
| 				currentValue, err := element.Property("value")
 | |
| 				if err != nil {
 | |
| 					fieldResult.Error = fmt.Sprintf("failed to verify selection: %v", err)
 | |
| 					result.ErrorCount++
 | |
| 				} else if currentValue.Str() != fieldValue {
 | |
| 					fieldResult.Error = fmt.Sprintf("failed to select option '%s' (current value: %s)", fieldValue, currentValue.Str())
 | |
| 					result.ErrorCount++
 | |
| 				} else {
 | |
| 					fieldResult.Success = true
 | |
| 					result.SuccessCount++
 | |
| 				}
 | |
| 			} else {
 | |
| 				fieldResult.Success = true
 | |
| 				result.SuccessCount++
 | |
| 			}
 | |
| 		} else {
 | |
| 			// Use fill action for input, textarea, etc.
 | |
| 			fieldResult.Action = "fill"
 | |
| 			err = element.SelectAllText()
 | |
| 			if err == nil {
 | |
| 				err = element.Input("")
 | |
| 			}
 | |
| 			if err == nil {
 | |
| 				err = element.Input(fieldValue)
 | |
| 			}
 | |
| 
 | |
| 			if err != nil {
 | |
| 				fieldResult.Error = fmt.Sprintf("failed to fill field: %v", err)
 | |
| 				result.ErrorCount++
 | |
| 			} else {
 | |
| 				fieldResult.Success = true
 | |
| 				result.SuccessCount++
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		result.FilledFields = append(result.FilledFields, fieldResult)
 | |
| 	}
 | |
| 
 | |
| 	return result, nil
 | |
| }
 | |
| 
 | |
| // PageInfo represents page metadata and state information
 | |
| type PageInfo struct {
 | |
| 	Title         string `json:"title"`
 | |
| 	URL           string `json:"url"`
 | |
| 	LoadingState  string `json:"loading_state"`
 | |
| 	ReadyState    string `json:"ready_state"`
 | |
| 	Referrer      string `json:"referrer"`
 | |
| 	Domain        string `json:"domain"`
 | |
| 	Protocol      string `json:"protocol"`
 | |
| 	Charset       string `json:"charset"`
 | |
| 	ContentType   string `json:"content_type"`
 | |
| 	LastModified  string `json:"last_modified"`
 | |
| 	CookieEnabled bool   `json:"cookie_enabled"`
 | |
| 	OnlineStatus  bool   `json:"online_status"`
 | |
| }
 | |
| 
 | |
| // ViewportInfo represents viewport and scroll information
 | |
| type ViewportInfo struct {
 | |
| 	Width            int     `json:"width"`
 | |
| 	Height           int     `json:"height"`
 | |
| 	ScrollX          int     `json:"scroll_x"`
 | |
| 	ScrollY          int     `json:"scroll_y"`
 | |
| 	ScrollWidth      int     `json:"scroll_width"`
 | |
| 	ScrollHeight     int     `json:"scroll_height"`
 | |
| 	ClientWidth      int     `json:"client_width"`
 | |
| 	ClientHeight     int     `json:"client_height"`
 | |
| 	DevicePixelRatio float64 `json:"device_pixel_ratio"`
 | |
| 	Orientation      string  `json:"orientation"`
 | |
| }
 | |
| 
 | |
| // PerformanceMetrics represents page performance data
 | |
| type PerformanceMetrics struct {
 | |
| 	NavigationStart      int64 `json:"navigation_start"`
 | |
| 	LoadEventEnd         int64 `json:"load_event_end"`
 | |
| 	DOMContentLoaded     int64 `json:"dom_content_loaded"`
 | |
| 	FirstPaint           int64 `json:"first_paint"`
 | |
| 	FirstContentfulPaint int64 `json:"first_contentful_paint"`
 | |
| 	LoadTime             int64 `json:"load_time"`
 | |
| 	DOMLoadTime          int64 `json:"dom_load_time"`
 | |
| 	ResourceCount        int   `json:"resource_count"`
 | |
| 	JSHeapSizeLimit      int64 `json:"js_heap_size_limit"`
 | |
| 	JSHeapSizeTotal      int64 `json:"js_heap_size_total"`
 | |
| 	JSHeapSizeUsed       int64 `json:"js_heap_size_used"`
 | |
| }
 | |
| 
 | |
| // ContentCheck represents content verification results
 | |
| type ContentCheck struct {
 | |
| 	Type           string   `json:"type"`
 | |
| 	ImagesLoaded   int      `json:"images_loaded,omitempty"`
 | |
| 	ImagesTotal    int      `json:"images_total,omitempty"`
 | |
| 	ScriptsLoaded  int      `json:"scripts_loaded,omitempty"`
 | |
| 	ScriptsTotal   int      `json:"scripts_total,omitempty"`
 | |
| 	StylesLoaded   int      `json:"styles_loaded,omitempty"`
 | |
| 	StylesTotal    int      `json:"styles_total,omitempty"`
 | |
| 	FormsPresent   int      `json:"forms_present,omitempty"`
 | |
| 	LinksPresent   int      `json:"links_present,omitempty"`
 | |
| 	IframesPresent int      `json:"iframes_present,omitempty"`
 | |
| 	HasErrors      bool     `json:"has_errors,omitempty"`
 | |
| 	ErrorCount     int      `json:"error_count,omitempty"`
 | |
| 	ErrorMessages  []string `json:"error_messages,omitempty"`
 | |
| }
 | |
| 
 | |
| // getPageInfo retrieves comprehensive page metadata and state information
 | |
| func (d *Daemon) getPageInfo(tabID string, timeout int) (*PageInfo, error) {
 | |
| 	d.debugLog("Getting page info for tab: %s with timeout: %d", tabID, timeout)
 | |
| 
 | |
| 	page, err := d.getTab(tabID)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to get page: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	result := &PageInfo{}
 | |
| 
 | |
| 	// Get basic page information using JavaScript
 | |
| 	// Note: page.Eval expects a function expression, not an IIFE
 | |
| 	jsCode := `() => {
 | |
| 		return {
 | |
| 			title: document.title,
 | |
| 			url: window.location.href,
 | |
| 			readyState: document.readyState,
 | |
| 			referrer: document.referrer,
 | |
| 			domain: document.domain,
 | |
| 			protocol: window.location.protocol,
 | |
| 			charset: document.characterSet || document.charset,
 | |
| 			contentType: document.contentType,
 | |
| 			lastModified: document.lastModified,
 | |
| 			cookieEnabled: navigator.cookieEnabled,
 | |
| 			onlineStatus: navigator.onLine
 | |
| 		};
 | |
| 	}`
 | |
| 
 | |
| 	jsResult, err := page.Eval(jsCode)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to execute JavaScript: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// Parse the JavaScript result
 | |
| 	if props := jsResult.Value.Map(); props != nil {
 | |
| 		if title, ok := props["title"]; ok && title.Str() != "" {
 | |
| 			result.Title = title.Str()
 | |
| 		}
 | |
| 		if url, ok := props["url"]; ok && url.Str() != "" {
 | |
| 			result.URL = url.Str()
 | |
| 		}
 | |
| 		if readyState, ok := props["readyState"]; ok && readyState.Str() != "" {
 | |
| 			result.ReadyState = readyState.Str()
 | |
| 		}
 | |
| 		if referrer, ok := props["referrer"]; ok && referrer.Str() != "" {
 | |
| 			result.Referrer = referrer.Str()
 | |
| 		}
 | |
| 		if domain, ok := props["domain"]; ok && domain.Str() != "" {
 | |
| 			result.Domain = domain.Str()
 | |
| 		}
 | |
| 		if protocol, ok := props["protocol"]; ok && protocol.Str() != "" {
 | |
| 			result.Protocol = protocol.Str()
 | |
| 		}
 | |
| 		if charset, ok := props["charset"]; ok && charset.Str() != "" {
 | |
| 			result.Charset = charset.Str()
 | |
| 		}
 | |
| 		if contentType, ok := props["contentType"]; ok && contentType.Str() != "" {
 | |
| 			result.ContentType = contentType.Str()
 | |
| 		}
 | |
| 		if lastModified, ok := props["lastModified"]; ok && lastModified.Str() != "" {
 | |
| 			result.LastModified = lastModified.Str()
 | |
| 		}
 | |
| 		if cookieEnabled, ok := props["cookieEnabled"]; ok {
 | |
| 			result.CookieEnabled = cookieEnabled.Bool()
 | |
| 		}
 | |
| 		if onlineStatus, ok := props["onlineStatus"]; ok {
 | |
| 			result.OnlineStatus = onlineStatus.Bool()
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Determine loading state
 | |
| 	if result.ReadyState == "complete" {
 | |
| 		result.LoadingState = "complete"
 | |
| 	} else if result.ReadyState == "interactive" {
 | |
| 		result.LoadingState = "interactive"
 | |
| 	} else {
 | |
| 		result.LoadingState = "loading"
 | |
| 	}
 | |
| 
 | |
| 	d.debugLog("Successfully retrieved page info for tab: %s", tabID)
 | |
| 	return result, nil
 | |
| }
 | |
| 
 | |
| // getViewportInfo retrieves viewport and scroll information
 | |
| func (d *Daemon) getViewportInfo(tabID string, timeout int) (*ViewportInfo, error) {
 | |
| 	d.debugLog("Getting viewport info for tab: %s with timeout: %d", tabID, timeout)
 | |
| 
 | |
| 	page, err := d.getTab(tabID)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to get page: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	result := &ViewportInfo{}
 | |
| 
 | |
| 	// Get viewport and scroll information using JavaScript
 | |
| 	// Note: page.Eval expects a function expression, not an IIFE
 | |
| 	jsCode := `() => {
 | |
| 		return {
 | |
| 			width: window.innerWidth,
 | |
| 			height: window.innerHeight,
 | |
| 			scrollX: window.scrollX || window.pageXOffset,
 | |
| 			scrollY: window.scrollY || window.pageYOffset,
 | |
| 			scrollWidth: document.documentElement.scrollWidth,
 | |
| 			scrollHeight: document.documentElement.scrollHeight,
 | |
| 			clientWidth: document.documentElement.clientWidth,
 | |
| 			clientHeight: document.documentElement.clientHeight,
 | |
| 			devicePixelRatio: window.devicePixelRatio,
 | |
| 			orientation: screen.orientation ? screen.orientation.type : 'unknown'
 | |
| 		};
 | |
| 	}`
 | |
| 
 | |
| 	jsResult, err := page.Eval(jsCode)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to execute JavaScript: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// Parse the JavaScript result
 | |
| 	if props := jsResult.Value.Map(); props != nil {
 | |
| 		if width, ok := props["width"]; ok {
 | |
| 			result.Width = int(width.Num())
 | |
| 		}
 | |
| 		if height, ok := props["height"]; ok {
 | |
| 			result.Height = int(height.Num())
 | |
| 		}
 | |
| 		if scrollX, ok := props["scrollX"]; ok {
 | |
| 			result.ScrollX = int(scrollX.Num())
 | |
| 		}
 | |
| 		if scrollY, ok := props["scrollY"]; ok {
 | |
| 			result.ScrollY = int(scrollY.Num())
 | |
| 		}
 | |
| 		if scrollWidth, ok := props["scrollWidth"]; ok {
 | |
| 			result.ScrollWidth = int(scrollWidth.Num())
 | |
| 		}
 | |
| 		if scrollHeight, ok := props["scrollHeight"]; ok {
 | |
| 			result.ScrollHeight = int(scrollHeight.Num())
 | |
| 		}
 | |
| 		if clientWidth, ok := props["clientWidth"]; ok {
 | |
| 			result.ClientWidth = int(clientWidth.Num())
 | |
| 		}
 | |
| 		if clientHeight, ok := props["clientHeight"]; ok {
 | |
| 			result.ClientHeight = int(clientHeight.Num())
 | |
| 		}
 | |
| 		if devicePixelRatio, ok := props["devicePixelRatio"]; ok {
 | |
| 			result.DevicePixelRatio = devicePixelRatio.Num()
 | |
| 		}
 | |
| 		if orientation, ok := props["orientation"]; ok && orientation.Str() != "" {
 | |
| 			result.Orientation = orientation.Str()
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	d.debugLog("Successfully retrieved viewport info for tab: %s", tabID)
 | |
| 	return result, nil
 | |
| }
 | |
| 
 | |
| // getPerformance retrieves page performance metrics
 | |
| func (d *Daemon) getPerformance(tabID string, timeout int) (*PerformanceMetrics, error) {
 | |
| 	d.debugLog("Getting performance metrics for tab: %s with timeout: %d", tabID, timeout)
 | |
| 
 | |
| 	page, err := d.getTab(tabID)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to get page: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	result := &PerformanceMetrics{}
 | |
| 
 | |
| 	// Get performance metrics using JavaScript
 | |
| 	// Note: page.Eval expects a function expression, not an IIFE
 | |
| 	jsCode := `() => {
 | |
| 		const perf = window.performance;
 | |
| 		const timing = perf.timing;
 | |
| 		const navigation = perf.navigation;
 | |
| 		const memory = perf.memory;
 | |
| 
 | |
| 		// Get paint metrics if available
 | |
| 		let firstPaint = 0;
 | |
| 		let firstContentfulPaint = 0;
 | |
| 		if (perf.getEntriesByType) {
 | |
| 			const paintEntries = perf.getEntriesByType('paint');
 | |
| 			for (const entry of paintEntries) {
 | |
| 				if (entry.name === 'first-paint') {
 | |
| 					firstPaint = entry.startTime;
 | |
| 				} else if (entry.name === 'first-contentful-paint') {
 | |
| 					firstContentfulPaint = entry.startTime;
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		// Count resources
 | |
| 		let resourceCount = 0;
 | |
| 		if (perf.getEntriesByType) {
 | |
| 			resourceCount = perf.getEntriesByType('resource').length;
 | |
| 		}
 | |
| 
 | |
| 		return {
 | |
| 			navigationStart: timing.navigationStart,
 | |
| 			loadEventEnd: timing.loadEventEnd,
 | |
| 			domContentLoaded: timing.domContentLoadedEventEnd,
 | |
| 			firstPaint: firstPaint,
 | |
| 			firstContentfulPaint: firstContentfulPaint,
 | |
| 			loadTime: timing.loadEventEnd - timing.navigationStart,
 | |
| 			domLoadTime: timing.domContentLoadedEventEnd - timing.navigationStart,
 | |
| 			resourceCount: resourceCount,
 | |
| 			jsHeapSizeLimit: memory ? memory.jsHeapSizeLimit : 0,
 | |
| 			jsHeapSizeTotal: memory ? memory.totalJSHeapSize : 0,
 | |
| 			jsHeapSizeUsed: memory ? memory.usedJSHeapSize : 0
 | |
| 		};
 | |
| 	}`
 | |
| 
 | |
| 	jsResult, err := page.Eval(jsCode)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to execute JavaScript: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// Parse the JavaScript result
 | |
| 	if props := jsResult.Value.Map(); props != nil {
 | |
| 		if navigationStart, ok := props["navigationStart"]; ok {
 | |
| 			result.NavigationStart = int64(navigationStart.Num())
 | |
| 		}
 | |
| 		if loadEventEnd, ok := props["loadEventEnd"]; ok {
 | |
| 			result.LoadEventEnd = int64(loadEventEnd.Num())
 | |
| 		}
 | |
| 		if domContentLoaded, ok := props["domContentLoaded"]; ok {
 | |
| 			result.DOMContentLoaded = int64(domContentLoaded.Num())
 | |
| 		}
 | |
| 		if firstPaint, ok := props["firstPaint"]; ok {
 | |
| 			result.FirstPaint = int64(firstPaint.Num())
 | |
| 		}
 | |
| 		if firstContentfulPaint, ok := props["firstContentfulPaint"]; ok {
 | |
| 			result.FirstContentfulPaint = int64(firstContentfulPaint.Num())
 | |
| 		}
 | |
| 		if loadTime, ok := props["loadTime"]; ok {
 | |
| 			result.LoadTime = int64(loadTime.Num())
 | |
| 		}
 | |
| 		if domLoadTime, ok := props["domLoadTime"]; ok {
 | |
| 			result.DOMLoadTime = int64(domLoadTime.Num())
 | |
| 		}
 | |
| 		if resourceCount, ok := props["resourceCount"]; ok {
 | |
| 			result.ResourceCount = int(resourceCount.Num())
 | |
| 		}
 | |
| 		if jsHeapSizeLimit, ok := props["jsHeapSizeLimit"]; ok {
 | |
| 			result.JSHeapSizeLimit = int64(jsHeapSizeLimit.Num())
 | |
| 		}
 | |
| 		if jsHeapSizeTotal, ok := props["jsHeapSizeTotal"]; ok {
 | |
| 			result.JSHeapSizeTotal = int64(jsHeapSizeTotal.Num())
 | |
| 		}
 | |
| 		if jsHeapSizeUsed, ok := props["jsHeapSizeUsed"]; ok {
 | |
| 			result.JSHeapSizeUsed = int64(jsHeapSizeUsed.Num())
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	d.debugLog("Successfully retrieved performance metrics for tab: %s", tabID)
 | |
| 	return result, nil
 | |
| }
 | |
| 
 | |
| // checkContent verifies specific content types and loading states
 | |
| func (d *Daemon) checkContent(tabID string, contentType string, timeout int) (*ContentCheck, error) {
 | |
| 	d.debugLog("Checking content type '%s' for tab: %s with timeout: %d", contentType, tabID, timeout)
 | |
| 
 | |
| 	page, err := d.getTab(tabID)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to get page: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	result := &ContentCheck{
 | |
| 		Type: contentType,
 | |
| 	}
 | |
| 
 | |
| 	var jsCode string
 | |
| 
 | |
| 	switch contentType {
 | |
| 	case "images":
 | |
| 		// Note: page.Eval expects a function expression, not an IIFE
 | |
| 		jsCode = `() => {
 | |
| 			const images = document.querySelectorAll('img');
 | |
| 			let loaded = 0;
 | |
| 			let total = images.length;
 | |
| 
 | |
| 			images.forEach(img => {
 | |
| 				if (img.complete && img.naturalHeight !== 0) {
 | |
| 					loaded++;
 | |
| 				}
 | |
| 			});
 | |
| 
 | |
| 			return {
 | |
| 				imagesLoaded: loaded,
 | |
| 				imagesTotal: total
 | |
| 			};
 | |
| 		}`
 | |
| 	case "scripts":
 | |
| 		jsCode = `() => {
 | |
| 			const scripts = document.querySelectorAll('script[src]');
 | |
| 			let loaded = 0;
 | |
| 			let total = scripts.length;
 | |
| 
 | |
| 			scripts.forEach(script => {
 | |
| 				if (script.readyState === 'loaded' || script.readyState === 'complete' || !script.readyState) {
 | |
| 					loaded++;
 | |
| 				}
 | |
| 			});
 | |
| 
 | |
| 			return {
 | |
| 				scriptsLoaded: loaded,
 | |
| 				scriptsTotal: total
 | |
| 			};
 | |
| 		}`
 | |
| 	case "styles":
 | |
| 		jsCode = `() => {
 | |
| 			const styles = document.querySelectorAll('link[rel="stylesheet"]');
 | |
| 			let loaded = 0;
 | |
| 			let total = styles.length;
 | |
| 
 | |
| 			styles.forEach(style => {
 | |
| 				if (style.sheet) {
 | |
| 					loaded++;
 | |
| 				}
 | |
| 			});
 | |
| 
 | |
| 			return {
 | |
| 				stylesLoaded: loaded,
 | |
| 				stylesTotal: total
 | |
| 			};
 | |
| 		}`
 | |
| 	case "forms":
 | |
| 		jsCode = `() => {
 | |
| 			return {
 | |
| 				formsPresent: document.querySelectorAll('form').length
 | |
| 			};
 | |
| 		}`
 | |
| 	case "links":
 | |
| 		jsCode = `() => {
 | |
| 			return {
 | |
| 				linksPresent: document.querySelectorAll('a[href]').length
 | |
| 			};
 | |
| 		}`
 | |
| 	case "iframes":
 | |
| 		jsCode = `() => {
 | |
| 			return {
 | |
| 				iframesPresent: document.querySelectorAll('iframe').length
 | |
| 			};
 | |
| 		}`
 | |
| 	case "errors":
 | |
| 		jsCode = `() => {
 | |
| 			const errors = [];
 | |
| 
 | |
| 			// Check for JavaScript errors in console (if available)
 | |
| 			if (window.console && window.console.error) {
 | |
| 				// This is limited - we can't access console history
 | |
| 				// But we can check for common error indicators
 | |
| 			}
 | |
| 
 | |
| 			// Check for broken images
 | |
| 			const brokenImages = Array.from(document.querySelectorAll('img')).filter(img =>
 | |
| 				!img.complete || img.naturalHeight === 0
 | |
| 			);
 | |
| 
 | |
| 			if (brokenImages.length > 0) {
 | |
| 				errors.push('Broken images detected: ' + brokenImages.length);
 | |
| 			}
 | |
| 
 | |
| 			// Check for missing stylesheets
 | |
| 			const brokenStyles = Array.from(document.querySelectorAll('link[rel="stylesheet"]')).filter(link =>
 | |
| 				!link.sheet
 | |
| 			);
 | |
| 
 | |
| 			if (brokenStyles.length > 0) {
 | |
| 				errors.push('Missing stylesheets detected: ' + brokenStyles.length);
 | |
| 			}
 | |
| 
 | |
| 			return {
 | |
| 				hasErrors: errors.length > 0,
 | |
| 				errorCount: errors.length,
 | |
| 				errorMessages: errors
 | |
| 			};
 | |
| 		}`
 | |
| 	default:
 | |
| 		return nil, fmt.Errorf("unknown content type: %s", contentType)
 | |
| 	}
 | |
| 
 | |
| 	jsResult, err := page.Eval(jsCode)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to execute JavaScript: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// Parse the JavaScript result
 | |
| 	if props := jsResult.Value.Map(); props != nil {
 | |
| 		if imagesLoaded, ok := props["imagesLoaded"]; ok {
 | |
| 			result.ImagesLoaded = int(imagesLoaded.Num())
 | |
| 		}
 | |
| 		if imagesTotal, ok := props["imagesTotal"]; ok {
 | |
| 			result.ImagesTotal = int(imagesTotal.Num())
 | |
| 		}
 | |
| 		if scriptsLoaded, ok := props["scriptsLoaded"]; ok {
 | |
| 			result.ScriptsLoaded = int(scriptsLoaded.Num())
 | |
| 		}
 | |
| 		if scriptsTotal, ok := props["scriptsTotal"]; ok {
 | |
| 			result.ScriptsTotal = int(scriptsTotal.Num())
 | |
| 		}
 | |
| 		if stylesLoaded, ok := props["stylesLoaded"]; ok {
 | |
| 			result.StylesLoaded = int(stylesLoaded.Num())
 | |
| 		}
 | |
| 		if stylesTotal, ok := props["stylesTotal"]; ok {
 | |
| 			result.StylesTotal = int(stylesTotal.Num())
 | |
| 		}
 | |
| 		if formsPresent, ok := props["formsPresent"]; ok {
 | |
| 			result.FormsPresent = int(formsPresent.Num())
 | |
| 		}
 | |
| 		if linksPresent, ok := props["linksPresent"]; ok {
 | |
| 			result.LinksPresent = int(linksPresent.Num())
 | |
| 		}
 | |
| 		if iframesPresent, ok := props["iframesPresent"]; ok {
 | |
| 			result.IframesPresent = int(iframesPresent.Num())
 | |
| 		}
 | |
| 		if hasErrors, ok := props["hasErrors"]; ok {
 | |
| 			result.HasErrors = hasErrors.Bool()
 | |
| 		}
 | |
| 		if errorCount, ok := props["errorCount"]; ok {
 | |
| 			result.ErrorCount = int(errorCount.Num())
 | |
| 		}
 | |
| 		if errorMessages, ok := props["errorMessages"]; ok {
 | |
| 			if arr := errorMessages.Arr(); arr != nil {
 | |
| 				for _, msg := range arr {
 | |
| 					if msg.Str() != "" {
 | |
| 						result.ErrorMessages = append(result.ErrorMessages, msg.Str())
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	d.debugLog("Successfully checked content type '%s' for tab: %s", contentType, tabID)
 | |
| 	return result, nil
 | |
| }
 | |
| 
 | |
| // screenshotElement takes a screenshot of a specific element
 | |
| func (d *Daemon) screenshotElement(tabID, selector, outputPath string, timeout int) error {
 | |
| 	d.debugLog("Taking element screenshot for tab: %s, selector: %s", tabID, selector)
 | |
| 
 | |
| 	page, err := d.getTab(tabID)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	// Find the element
 | |
| 	var element *rod.Element
 | |
| 	if timeout > 0 {
 | |
| 		element, err = page.Timeout(time.Duration(timeout) * time.Second).Element(selector)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("failed to find element (timeout after %ds): %w", timeout, err)
 | |
| 		}
 | |
| 	} else {
 | |
| 		element, err = page.Element(selector)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("failed to find element: %w", err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Scroll element into view
 | |
| 	err = element.ScrollIntoView()
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to scroll element into view: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	// Wait for element to be stable
 | |
| 	err = element.WaitStable(500 * time.Millisecond)
 | |
| 	if err != nil {
 | |
| 		d.debugLog("Warning: element not stable: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// Take screenshot of the element
 | |
| 	screenshotBytes, err := element.Screenshot(proto.PageCaptureScreenshotFormatPng, 0)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to capture element screenshot: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	// Write the screenshot to file
 | |
| 	err = os.WriteFile(outputPath, screenshotBytes, 0644)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to save element screenshot to %s: %w", outputPath, err)
 | |
| 	}
 | |
| 
 | |
| 	d.debugLog("Successfully captured element screenshot for tab: %s", tabID)
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // ScreenshotMetadata represents metadata for enhanced screenshots
 | |
| type ScreenshotMetadata struct {
 | |
| 	Timestamp    string `json:"timestamp"`
 | |
| 	URL          string `json:"url"`
 | |
| 	Title        string `json:"title"`
 | |
| 	ViewportSize struct {
 | |
| 		Width  int `json:"width"`
 | |
| 		Height int `json:"height"`
 | |
| 	} `json:"viewport_size"`
 | |
| 	FullPage   bool   `json:"full_page"`
 | |
| 	FilePath   string `json:"file_path"`
 | |
| 	FileSize   int64  `json:"file_size"`
 | |
| 	Resolution struct {
 | |
| 		Width  int `json:"width"`
 | |
| 		Height int `json:"height"`
 | |
| 	} `json:"resolution"`
 | |
| }
 | |
| 
 | |
| // screenshotEnhanced takes a screenshot with metadata
 | |
| func (d *Daemon) screenshotEnhanced(tabID, outputPath string, fullPage bool, timeout int) (*ScreenshotMetadata, error) {
 | |
| 	d.debugLog("Taking enhanced screenshot for tab: %s", tabID)
 | |
| 
 | |
| 	page, err := d.getTab(tabID)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	// Get page info for metadata
 | |
| 	pageInfo, err := page.Info()
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to get page info: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	// Get viewport size
 | |
| 	viewport, err := page.Eval(`() => ({
 | |
| 		width: window.innerWidth,
 | |
| 		height: window.innerHeight
 | |
| 	})`)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to get viewport: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	viewportData := viewport.Value.Map()
 | |
| 	viewportWidth := int(viewportData["width"].Num())
 | |
| 	viewportHeight := int(viewportData["height"].Num())
 | |
| 
 | |
| 	// Take screenshot with timeout handling
 | |
| 	var screenshotBytes []byte
 | |
| 	if timeout > 0 {
 | |
| 		ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
 | |
| 		defer cancel()
 | |
| 
 | |
| 		done := make(chan error, 1)
 | |
| 		go func() {
 | |
| 			bytes, err := page.Screenshot(fullPage, &proto.PageCaptureScreenshot{
 | |
| 				Format: proto.PageCaptureScreenshotFormatPng,
 | |
| 			})
 | |
| 			screenshotBytes = bytes
 | |
| 			done <- err
 | |
| 		}()
 | |
| 
 | |
| 		select {
 | |
| 		case err := <-done:
 | |
| 			if err != nil {
 | |
| 				return nil, fmt.Errorf("failed to capture screenshot: %w", err)
 | |
| 			}
 | |
| 		case <-ctx.Done():
 | |
| 			return nil, fmt.Errorf("taking screenshot timed out after %d seconds", timeout)
 | |
| 		}
 | |
| 	} else {
 | |
| 		screenshotBytes, err = page.Screenshot(fullPage, &proto.PageCaptureScreenshot{
 | |
| 			Format: proto.PageCaptureScreenshotFormatPng,
 | |
| 		})
 | |
| 		if err != nil {
 | |
| 			return nil, fmt.Errorf("failed to capture screenshot: %w", err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Write the screenshot to file
 | |
| 	err = os.WriteFile(outputPath, screenshotBytes, 0644)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to save screenshot to %s: %w", outputPath, err)
 | |
| 	}
 | |
| 
 | |
| 	// Get file info
 | |
| 	fileInfo, err := os.Stat(outputPath)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to get file info: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	// Create metadata
 | |
| 	metadata := &ScreenshotMetadata{
 | |
| 		Timestamp: time.Now().Format(time.RFC3339),
 | |
| 		URL:       pageInfo.URL,
 | |
| 		Title:     pageInfo.Title,
 | |
| 		FullPage:  fullPage,
 | |
| 		FilePath:  outputPath,
 | |
| 		FileSize:  fileInfo.Size(),
 | |
| 	}
 | |
| 
 | |
| 	metadata.ViewportSize.Width = viewportWidth
 | |
| 	metadata.ViewportSize.Height = viewportHeight
 | |
| 
 | |
| 	// Get actual image dimensions (approximate based on viewport or full page)
 | |
| 	if fullPage {
 | |
| 		// For full page, we'd need to calculate the full document size
 | |
| 		// For now, use viewport size as approximation
 | |
| 		metadata.Resolution.Width = viewportWidth
 | |
| 		metadata.Resolution.Height = viewportHeight
 | |
| 	} else {
 | |
| 		metadata.Resolution.Width = viewportWidth
 | |
| 		metadata.Resolution.Height = viewportHeight
 | |
| 	}
 | |
| 
 | |
| 	d.debugLog("Successfully captured enhanced screenshot for tab: %s", tabID)
 | |
| 	return metadata, nil
 | |
| }
 | |
| 
 | |
| // FileOperation represents a single file operation
 | |
| type FileOperation struct {
 | |
| 	LocalPath     string `json:"local_path"`
 | |
| 	ContainerPath string `json:"container_path"`
 | |
| 	Operation     string `json:"operation"` // "upload" or "download"
 | |
| }
 | |
| 
 | |
| // BulkFileResult represents the result of bulk file operations
 | |
| type BulkFileResult struct {
 | |
| 	Successful []FileOperationResult `json:"successful"`
 | |
| 	Failed     []FileOperationError  `json:"failed"`
 | |
| 	Summary    struct {
 | |
| 		Total      int `json:"total"`
 | |
| 		Successful int `json:"successful"`
 | |
| 		Failed     int `json:"failed"`
 | |
| 	} `json:"summary"`
 | |
| }
 | |
| 
 | |
| // FileOperationResult represents a successful file operation
 | |
| type FileOperationResult struct {
 | |
| 	LocalPath     string `json:"local_path"`
 | |
| 	ContainerPath string `json:"container_path"`
 | |
| 	Operation     string `json:"operation"`
 | |
| 	Size          int64  `json:"size"`
 | |
| }
 | |
| 
 | |
| // FileOperationError represents a failed file operation
 | |
| type FileOperationError struct {
 | |
| 	LocalPath     string `json:"local_path"`
 | |
| 	ContainerPath string `json:"container_path"`
 | |
| 	Operation     string `json:"operation"`
 | |
| 	Error         string `json:"error"`
 | |
| }
 | |
| 
 | |
| // bulkFiles performs bulk file operations (upload/download)
 | |
| func (d *Daemon) bulkFiles(operationType, filesJSON string, timeout int) (*BulkFileResult, error) {
 | |
| 	d.debugLog("Performing bulk file operations: %s", operationType)
 | |
| 
 | |
| 	// Parse the files JSON
 | |
| 	var operations []FileOperation
 | |
| 	err := json.Unmarshal([]byte(filesJSON), &operations)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to parse files JSON: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	result := &BulkFileResult{
 | |
| 		Successful: make([]FileOperationResult, 0),
 | |
| 		Failed:     make([]FileOperationError, 0),
 | |
| 	}
 | |
| 
 | |
| 	// Set up timeout context
 | |
| 	ctx := context.Background()
 | |
| 	if timeout > 0 {
 | |
| 		var cancel context.CancelFunc
 | |
| 		ctx, cancel = context.WithTimeout(ctx, time.Duration(timeout)*time.Second)
 | |
| 		defer cancel()
 | |
| 	}
 | |
| 
 | |
| 	// Process each file operation
 | |
| 	for _, op := range operations {
 | |
| 		select {
 | |
| 		case <-ctx.Done():
 | |
| 			// Timeout reached, add remaining operations as failed
 | |
| 			for i := len(result.Successful) + len(result.Failed); i < len(operations); i++ {
 | |
| 				result.Failed = append(result.Failed, FileOperationError{
 | |
| 					LocalPath:     operations[i].LocalPath,
 | |
| 					ContainerPath: operations[i].ContainerPath,
 | |
| 					Operation:     operations[i].Operation,
 | |
| 					Error:         "operation timed out",
 | |
| 				})
 | |
| 			}
 | |
| 			break
 | |
| 		default:
 | |
| 			// Perform the operation
 | |
| 			if op.Operation == "upload" || (op.Operation == "" && operationType == "upload") {
 | |
| 				err := d.performFileUpload(op.LocalPath, op.ContainerPath)
 | |
| 				if err != nil {
 | |
| 					result.Failed = append(result.Failed, FileOperationError{
 | |
| 						LocalPath:     op.LocalPath,
 | |
| 						ContainerPath: op.ContainerPath,
 | |
| 						Operation:     "upload",
 | |
| 						Error:         err.Error(),
 | |
| 					})
 | |
| 				} else {
 | |
| 					// Get file size
 | |
| 					fileInfo, _ := os.Stat(op.ContainerPath)
 | |
| 					size := int64(0)
 | |
| 					if fileInfo != nil {
 | |
| 						size = fileInfo.Size()
 | |
| 					}
 | |
| 					result.Successful = append(result.Successful, FileOperationResult{
 | |
| 						LocalPath:     op.LocalPath,
 | |
| 						ContainerPath: op.ContainerPath,
 | |
| 						Operation:     "upload",
 | |
| 						Size:          size,
 | |
| 					})
 | |
| 				}
 | |
| 			} else if op.Operation == "download" || (op.Operation == "" && operationType == "download") {
 | |
| 				err := d.performFileDownload(op.ContainerPath, op.LocalPath)
 | |
| 				if err != nil {
 | |
| 					result.Failed = append(result.Failed, FileOperationError{
 | |
| 						LocalPath:     op.LocalPath,
 | |
| 						ContainerPath: op.ContainerPath,
 | |
| 						Operation:     "download",
 | |
| 						Error:         err.Error(),
 | |
| 					})
 | |
| 				} else {
 | |
| 					// Get file size
 | |
| 					fileInfo, _ := os.Stat(op.LocalPath)
 | |
| 					size := int64(0)
 | |
| 					if fileInfo != nil {
 | |
| 						size = fileInfo.Size()
 | |
| 					}
 | |
| 					result.Successful = append(result.Successful, FileOperationResult{
 | |
| 						LocalPath:     op.LocalPath,
 | |
| 						ContainerPath: op.ContainerPath,
 | |
| 						Operation:     "download",
 | |
| 						Size:          size,
 | |
| 					})
 | |
| 				}
 | |
| 			} else {
 | |
| 				result.Failed = append(result.Failed, FileOperationError{
 | |
| 					LocalPath:     op.LocalPath,
 | |
| 					ContainerPath: op.ContainerPath,
 | |
| 					Operation:     op.Operation,
 | |
| 					Error:         "unknown operation type",
 | |
| 				})
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Update summary
 | |
| 	result.Summary.Total = len(operations)
 | |
| 	result.Summary.Successful = len(result.Successful)
 | |
| 	result.Summary.Failed = len(result.Failed)
 | |
| 
 | |
| 	d.debugLog("Bulk file operations completed: %d successful, %d failed", result.Summary.Successful, result.Summary.Failed)
 | |
| 	return result, nil
 | |
| }
 | |
| 
 | |
| // performFileUpload handles a single file upload operation
 | |
| func (d *Daemon) performFileUpload(localPath, containerPath string) error {
 | |
| 	// Open the source file
 | |
| 	sourceFile, err := os.Open(localPath)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to open source file: %w", err)
 | |
| 	}
 | |
| 	defer sourceFile.Close()
 | |
| 
 | |
| 	// Create the destination file
 | |
| 	destFile, err := os.Create(containerPath)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to create destination file: %w", err)
 | |
| 	}
 | |
| 	defer destFile.Close()
 | |
| 
 | |
| 	// Copy the file
 | |
| 	_, err = io.Copy(destFile, sourceFile)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to copy file: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // performFileDownload handles a single file download operation
 | |
| func (d *Daemon) performFileDownload(containerPath, localPath string) error {
 | |
| 	// Open the source file
 | |
| 	sourceFile, err := os.Open(containerPath)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to open source file: %w", err)
 | |
| 	}
 | |
| 	defer sourceFile.Close()
 | |
| 
 | |
| 	// Create the destination file
 | |
| 	destFile, err := os.Create(localPath)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to create destination file: %w", err)
 | |
| 	}
 | |
| 	defer destFile.Close()
 | |
| 
 | |
| 	// Copy the file
 | |
| 	_, err = io.Copy(destFile, sourceFile)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to copy file: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // FileManagementResult represents the result of file management operations
 | |
| type FileManagementResult struct {
 | |
| 	Operation string                 `json:"operation"`
 | |
| 	Files     []FileInfo             `json:"files,omitempty"`
 | |
| 	Cleaned   []string               `json:"cleaned,omitempty"`
 | |
| 	Summary   map[string]interface{} `json:"summary"`
 | |
| }
 | |
| 
 | |
| // FileInfo represents information about a file
 | |
| type FileInfo struct {
 | |
| 	Path        string    `json:"path"`
 | |
| 	Size        int64     `json:"size"`
 | |
| 	ModTime     time.Time `json:"mod_time"`
 | |
| 	IsDir       bool      `json:"is_dir"`
 | |
| 	Permissions string    `json:"permissions"`
 | |
| }
 | |
| 
 | |
| // manageFiles performs file management operations
 | |
| func (d *Daemon) manageFiles(operation, pattern, maxAge string) (*FileManagementResult, error) {
 | |
| 	d.debugLog("Performing file management operation: %s", operation)
 | |
| 
 | |
| 	result := &FileManagementResult{
 | |
| 		Operation: operation,
 | |
| 		Summary:   make(map[string]interface{}),
 | |
| 	}
 | |
| 
 | |
| 	switch operation {
 | |
| 	case "cleanup":
 | |
| 		return d.cleanupFiles(pattern, maxAge, result)
 | |
| 	case "list":
 | |
| 		return d.listFiles(pattern, result)
 | |
| 	case "info":
 | |
| 		return d.getFileInfo(pattern, result)
 | |
| 	default:
 | |
| 		return nil, fmt.Errorf("unknown file management operation: %s", operation)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // cleanupFiles removes files matching pattern and age criteria
 | |
| func (d *Daemon) cleanupFiles(pattern, maxAge string, result *FileManagementResult) (*FileManagementResult, error) {
 | |
| 	// Parse max age (default to 24 hours if not specified)
 | |
| 	maxAgeHours := 24
 | |
| 	if maxAge != "" {
 | |
| 		if parsed, err := strconv.Atoi(maxAge); err == nil && parsed > 0 {
 | |
| 			maxAgeHours = parsed
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	cutoffTime := time.Now().Add(-time.Duration(maxAgeHours) * time.Hour)
 | |
| 
 | |
| 	// Default pattern if not specified
 | |
| 	if pattern == "" {
 | |
| 		pattern = "/tmp/cremote-*"
 | |
| 	}
 | |
| 
 | |
| 	// Find files matching pattern
 | |
| 	matches, err := filepath.Glob(pattern)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to find files matching pattern: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	var cleaned []string
 | |
| 	var totalSize int64
 | |
| 
 | |
| 	for _, filePath := range matches {
 | |
| 		fileInfo, err := os.Stat(filePath)
 | |
| 		if err != nil {
 | |
| 			continue // Skip files we can't stat
 | |
| 		}
 | |
| 
 | |
| 		// Check if file is older than cutoff time
 | |
| 		if fileInfo.ModTime().Before(cutoffTime) {
 | |
| 			totalSize += fileInfo.Size()
 | |
| 			err = os.Remove(filePath)
 | |
| 			if err != nil {
 | |
| 				d.debugLog("Failed to remove file %s: %v", filePath, err)
 | |
| 			} else {
 | |
| 				cleaned = append(cleaned, filePath)
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	result.Cleaned = cleaned
 | |
| 	result.Summary["files_cleaned"] = len(cleaned)
 | |
| 	result.Summary["total_size_freed"] = totalSize
 | |
| 	result.Summary["cutoff_time"] = cutoffTime.Format(time.RFC3339)
 | |
| 
 | |
| 	d.debugLog("Cleanup completed: %d files removed, %d bytes freed", len(cleaned), totalSize)
 | |
| 	return result, nil
 | |
| }
 | |
| 
 | |
| // listFiles lists files matching pattern
 | |
| func (d *Daemon) listFiles(pattern string, result *FileManagementResult) (*FileManagementResult, error) {
 | |
| 	// Default pattern if not specified
 | |
| 	if pattern == "" {
 | |
| 		pattern = "/tmp/*"
 | |
| 	}
 | |
| 
 | |
| 	// Find files matching pattern
 | |
| 	matches, err := filepath.Glob(pattern)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to find files matching pattern: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	var files []FileInfo
 | |
| 	var totalSize int64
 | |
| 
 | |
| 	for _, filePath := range matches {
 | |
| 		fileInfo, err := os.Stat(filePath)
 | |
| 		if err != nil {
 | |
| 			continue // Skip files we can't stat
 | |
| 		}
 | |
| 
 | |
| 		files = append(files, FileInfo{
 | |
| 			Path:        filePath,
 | |
| 			Size:        fileInfo.Size(),
 | |
| 			ModTime:     fileInfo.ModTime(),
 | |
| 			IsDir:       fileInfo.IsDir(),
 | |
| 			Permissions: fileInfo.Mode().String(),
 | |
| 		})
 | |
| 
 | |
| 		if !fileInfo.IsDir() {
 | |
| 			totalSize += fileInfo.Size()
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	result.Files = files
 | |
| 	result.Summary["total_files"] = len(files)
 | |
| 	result.Summary["total_size"] = totalSize
 | |
| 
 | |
| 	d.debugLog("Listed %d files matching pattern: %s", len(files), pattern)
 | |
| 	return result, nil
 | |
| }
 | |
| 
 | |
| // getFileInfo gets detailed information about a specific file
 | |
| func (d *Daemon) getFileInfo(filePath string, result *FileManagementResult) (*FileManagementResult, error) {
 | |
| 	if filePath == "" {
 | |
| 		return nil, fmt.Errorf("file path is required for info operation")
 | |
| 	}
 | |
| 
 | |
| 	fileInfo, err := os.Stat(filePath)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to get file info: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	files := []FileInfo{{
 | |
| 		Path:        filePath,
 | |
| 		Size:        fileInfo.Size(),
 | |
| 		ModTime:     fileInfo.ModTime(),
 | |
| 		IsDir:       fileInfo.IsDir(),
 | |
| 		Permissions: fileInfo.Mode().String(),
 | |
| 	}}
 | |
| 
 | |
| 	result.Files = files
 | |
| 	result.Summary["exists"] = true
 | |
| 	result.Summary["size"] = fileInfo.Size()
 | |
| 	result.Summary["is_directory"] = fileInfo.IsDir()
 | |
| 	result.Summary["last_modified"] = fileInfo.ModTime().Format(time.RFC3339)
 | |
| 
 | |
| 	d.debugLog("Retrieved info for file: %s", filePath)
 | |
| 	return result, nil
 | |
| }
 | |
| 
 | |
| // Accessibility tree data structures
 | |
| 
 | |
| // AXNode represents a node in the accessibility tree
 | |
| type AXNode struct {
 | |
| 	NodeID           string       `json:"nodeId"`
 | |
| 	Ignored          bool         `json:"ignored"`
 | |
| 	IgnoredReasons   []AXProperty `json:"ignoredReasons,omitempty"`
 | |
| 	Role             *AXValue     `json:"role,omitempty"`
 | |
| 	ChromeRole       *AXValue     `json:"chromeRole,omitempty"`
 | |
| 	Name             *AXValue     `json:"name,omitempty"`
 | |
| 	Description      *AXValue     `json:"description,omitempty"`
 | |
| 	Value            *AXValue     `json:"value,omitempty"`
 | |
| 	Properties       []AXProperty `json:"properties,omitempty"`
 | |
| 	ParentID         string       `json:"parentId,omitempty"`
 | |
| 	ChildIDs         []string     `json:"childIds,omitempty"`
 | |
| 	BackendDOMNodeID int          `json:"backendDOMNodeId,omitempty"`
 | |
| 	FrameID          string       `json:"frameId,omitempty"`
 | |
| }
 | |
| 
 | |
| // AXProperty represents a property of an accessibility node
 | |
| type AXProperty struct {
 | |
| 	Name  string   `json:"name"`
 | |
| 	Value *AXValue `json:"value"`
 | |
| }
 | |
| 
 | |
| // AXValue represents a computed accessibility value
 | |
| type AXValue struct {
 | |
| 	Type         string          `json:"type"`
 | |
| 	Value        interface{}     `json:"value,omitempty"`
 | |
| 	RelatedNodes []AXRelatedNode `json:"relatedNodes,omitempty"`
 | |
| 	Sources      []AXValueSource `json:"sources,omitempty"`
 | |
| }
 | |
| 
 | |
| // AXRelatedNode represents a related node in the accessibility tree
 | |
| type AXRelatedNode struct {
 | |
| 	BackendDOMNodeID int    `json:"backendDOMNodeId"`
 | |
| 	IDRef            string `json:"idref,omitempty"`
 | |
| 	Text             string `json:"text,omitempty"`
 | |
| }
 | |
| 
 | |
| // AXValueSource represents a source for a computed accessibility value
 | |
| type AXValueSource struct {
 | |
| 	Type              string   `json:"type"`
 | |
| 	Value             *AXValue `json:"value,omitempty"`
 | |
| 	Attribute         string   `json:"attribute,omitempty"`
 | |
| 	AttributeValue    *AXValue `json:"attributeValue,omitempty"`
 | |
| 	Superseded        bool     `json:"superseded,omitempty"`
 | |
| 	NativeSource      string   `json:"nativeSource,omitempty"`
 | |
| 	NativeSourceValue *AXValue `json:"nativeSourceValue,omitempty"`
 | |
| 	Invalid           bool     `json:"invalid,omitempty"`
 | |
| 	InvalidReason     string   `json:"invalidReason,omitempty"`
 | |
| }
 | |
| 
 | |
| // AccessibilityTreeResult represents the result of accessibility tree operations
 | |
| type AccessibilityTreeResult struct {
 | |
| 	Nodes []AXNode `json:"nodes"`
 | |
| }
 | |
| 
 | |
| // AccessibilityQueryResult represents the result of accessibility queries
 | |
| type AccessibilityQueryResult struct {
 | |
| 	Nodes []AXNode `json:"nodes"`
 | |
| }
 | |
| 
 | |
| // getAccessibilityTree retrieves the full accessibility tree for a tab
 | |
| func (d *Daemon) getAccessibilityTree(tabID string, depth *int, timeout int) (*AccessibilityTreeResult, error) {
 | |
| 	return d.getAccessibilityTreeWithContrast(tabID, depth, false, timeout)
 | |
| }
 | |
| 
 | |
| // getAccessibilityTreeWithContrast retrieves the full accessibility tree with optional contrast data
 | |
| func (d *Daemon) getAccessibilityTreeWithContrast(tabID string, depth *int, includeContrast bool, timeout int) (*AccessibilityTreeResult, error) {
 | |
| 	d.debugLog("Getting accessibility tree for tab: %s with depth: %v, includeContrast: %v, timeout: %d", tabID, depth, includeContrast, timeout)
 | |
| 
 | |
| 	// Use current tab if not specified
 | |
| 	if tabID == "" {
 | |
| 		tabID = d.currentTab
 | |
| 	}
 | |
| 
 | |
| 	if tabID == "" {
 | |
| 		return nil, fmt.Errorf("no tab specified and no current tab available")
 | |
| 	}
 | |
| 
 | |
| 	page, err := d.getTab(tabID)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to get page: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// Enable accessibility domain
 | |
| 	err = proto.AccessibilityEnable{}.Call(page)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to enable accessibility domain: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// Build the request parameters
 | |
| 	params := proto.AccessibilityGetFullAXTree{}
 | |
| 	if depth != nil {
 | |
| 		params.Depth = depth
 | |
| 	}
 | |
| 
 | |
| 	// Call the Chrome DevTools Protocol Accessibility.getFullAXTree method
 | |
| 	result, err := proto.AccessibilityGetFullAXTree{}.Call(page)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to get accessibility tree: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// Parse the result
 | |
| 	var axResult AccessibilityTreeResult
 | |
| 	for _, node := range result.Nodes {
 | |
| 		axNode := d.convertProtoAXNode(node)
 | |
| 
 | |
| 		// Add contrast data if requested (simplified - just add a note that contrast checking is available)
 | |
| 		if includeContrast && !node.Ignored && node.BackendDOMNodeID > 0 {
 | |
| 			// Add a property indicating contrast data is available via web_contrast_check tool
 | |
| 			axNode.Properties = append(axNode.Properties, AXProperty{
 | |
| 				Name: "contrastCheckAvailable",
 | |
| 				Value: &AXValue{
 | |
| 					Type:  "boolean",
 | |
| 					Value: true,
 | |
| 				},
 | |
| 			})
 | |
| 		}
 | |
| 
 | |
| 		axResult.Nodes = append(axResult.Nodes, axNode)
 | |
| 	}
 | |
| 
 | |
| 	d.debugLog("Successfully retrieved accessibility tree with %d nodes for tab: %s", len(axResult.Nodes), tabID)
 | |
| 	return &axResult, nil
 | |
| }
 | |
| 
 | |
| // convertProtoAXNode converts a proto.AccessibilityAXNode to our AXNode struct
 | |
| func (d *Daemon) convertProtoAXNode(protoNode *proto.AccessibilityAXNode) AXNode {
 | |
| 	node := AXNode{
 | |
| 		NodeID:           string(protoNode.NodeID),
 | |
| 		Ignored:          protoNode.Ignored,
 | |
| 		BackendDOMNodeID: int(protoNode.BackendDOMNodeID),
 | |
| 	}
 | |
| 
 | |
| 	// Convert role
 | |
| 	if protoNode.Role != nil {
 | |
| 		node.Role = d.convertProtoAXValue(protoNode.Role)
 | |
| 	}
 | |
| 
 | |
| 	// Convert chrome role
 | |
| 	if protoNode.ChromeRole != nil {
 | |
| 		node.ChromeRole = d.convertProtoAXValue(protoNode.ChromeRole)
 | |
| 	}
 | |
| 
 | |
| 	// Convert name
 | |
| 	if protoNode.Name != nil {
 | |
| 		node.Name = d.convertProtoAXValue(protoNode.Name)
 | |
| 	}
 | |
| 
 | |
| 	// Convert description
 | |
| 	if protoNode.Description != nil {
 | |
| 		node.Description = d.convertProtoAXValue(protoNode.Description)
 | |
| 	}
 | |
| 
 | |
| 	// Convert value
 | |
| 	if protoNode.Value != nil {
 | |
| 		node.Value = d.convertProtoAXValue(protoNode.Value)
 | |
| 	}
 | |
| 
 | |
| 	// Convert properties
 | |
| 	for _, prop := range protoNode.Properties {
 | |
| 		node.Properties = append(node.Properties, AXProperty{
 | |
| 			Name:  string(prop.Name),
 | |
| 			Value: d.convertProtoAXValue(prop.Value),
 | |
| 		})
 | |
| 	}
 | |
| 
 | |
| 	// Convert ignored reasons
 | |
| 	for _, reason := range protoNode.IgnoredReasons {
 | |
| 		node.IgnoredReasons = append(node.IgnoredReasons, AXProperty{
 | |
| 			Name:  string(reason.Name),
 | |
| 			Value: d.convertProtoAXValue(reason.Value),
 | |
| 		})
 | |
| 	}
 | |
| 
 | |
| 	// Convert parent and child IDs
 | |
| 	if protoNode.ParentID != "" {
 | |
| 		node.ParentID = string(protoNode.ParentID)
 | |
| 	}
 | |
| 
 | |
| 	for _, childID := range protoNode.ChildIDs {
 | |
| 		node.ChildIDs = append(node.ChildIDs, string(childID))
 | |
| 	}
 | |
| 
 | |
| 	if protoNode.FrameID != "" {
 | |
| 		node.FrameID = string(protoNode.FrameID)
 | |
| 	}
 | |
| 
 | |
| 	return node
 | |
| }
 | |
| 
 | |
| // convertProtoAXValue converts a proto.AccessibilityAXValue to our AXValue struct
 | |
| func (d *Daemon) convertProtoAXValue(protoValue *proto.AccessibilityAXValue) *AXValue {
 | |
| 	if protoValue == nil {
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	value := &AXValue{
 | |
| 		Type:  string(protoValue.Type),
 | |
| 		Value: protoValue.Value,
 | |
| 	}
 | |
| 
 | |
| 	// Convert related nodes
 | |
| 	for _, relatedNode := range protoValue.RelatedNodes {
 | |
| 		value.RelatedNodes = append(value.RelatedNodes, AXRelatedNode{
 | |
| 			BackendDOMNodeID: int(relatedNode.BackendDOMNodeID),
 | |
| 			IDRef:            relatedNode.Idref,
 | |
| 			Text:             relatedNode.Text,
 | |
| 		})
 | |
| 	}
 | |
| 
 | |
| 	// Convert sources
 | |
| 	for _, source := range protoValue.Sources {
 | |
| 		axSource := AXValueSource{
 | |
| 			Type:          string(source.Type),
 | |
| 			Superseded:    source.Superseded,
 | |
| 			Invalid:       source.Invalid,
 | |
| 			InvalidReason: source.InvalidReason,
 | |
| 		}
 | |
| 
 | |
| 		if source.Value != nil {
 | |
| 			axSource.Value = d.convertProtoAXValue(source.Value)
 | |
| 		}
 | |
| 
 | |
| 		if source.Attribute != "" {
 | |
| 			axSource.Attribute = source.Attribute
 | |
| 		}
 | |
| 
 | |
| 		if source.AttributeValue != nil {
 | |
| 			axSource.AttributeValue = d.convertProtoAXValue(source.AttributeValue)
 | |
| 		}
 | |
| 
 | |
| 		if source.NativeSource != "" {
 | |
| 			axSource.NativeSource = string(source.NativeSource)
 | |
| 		}
 | |
| 
 | |
| 		if source.NativeSourceValue != nil {
 | |
| 			axSource.NativeSourceValue = d.convertProtoAXValue(source.NativeSourceValue)
 | |
| 		}
 | |
| 
 | |
| 		value.Sources = append(value.Sources, axSource)
 | |
| 	}
 | |
| 
 | |
| 	return value
 | |
| }
 | |
| 
 | |
| // getPartialAccessibilityTree retrieves a partial accessibility tree for a specific element
 | |
| func (d *Daemon) getPartialAccessibilityTree(tabID, selector string, fetchRelatives bool, timeout int) (*AccessibilityTreeResult, error) {
 | |
| 	d.debugLog("Getting partial accessibility tree for tab: %s, selector: %s, fetchRelatives: %v, timeout: %d", tabID, selector, fetchRelatives, timeout)
 | |
| 
 | |
| 	// Use current tab if not specified
 | |
| 	if tabID == "" {
 | |
| 		tabID = d.currentTab
 | |
| 	}
 | |
| 
 | |
| 	if tabID == "" {
 | |
| 		return nil, fmt.Errorf("no tab specified and no current tab available")
 | |
| 	}
 | |
| 
 | |
| 	page, err := d.getTab(tabID)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to get page: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// Enable accessibility domain
 | |
| 	err = proto.AccessibilityEnable{}.Call(page)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to enable accessibility domain: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// Find the DOM node first
 | |
| 	var element *rod.Element
 | |
| 	if timeout > 0 {
 | |
| 		element, err = page.Timeout(time.Duration(timeout) * time.Second).Element(selector)
 | |
| 	} else {
 | |
| 		element, err = page.Element(selector)
 | |
| 	}
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to find element: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	// Get the backend node ID
 | |
| 	nodeInfo, err := element.Describe(1, false)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to describe element: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	// Call the Chrome DevTools Protocol Accessibility.getPartialAXTree method
 | |
| 	result, err := proto.AccessibilityGetPartialAXTree{
 | |
| 		BackendNodeID:  nodeInfo.BackendNodeID,
 | |
| 		FetchRelatives: fetchRelatives,
 | |
| 	}.Call(page)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to get partial accessibility tree: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// Parse the result
 | |
| 	var axResult AccessibilityTreeResult
 | |
| 	for _, node := range result.Nodes {
 | |
| 		axNode := d.convertProtoAXNode(node)
 | |
| 		axResult.Nodes = append(axResult.Nodes, axNode)
 | |
| 	}
 | |
| 
 | |
| 	d.debugLog("Successfully retrieved partial accessibility tree with %d nodes for tab: %s", len(axResult.Nodes), tabID)
 | |
| 	return &axResult, nil
 | |
| }
 | |
| 
 | |
| // queryAccessibilityTree queries the accessibility tree for nodes matching specific criteria
 | |
| func (d *Daemon) queryAccessibilityTree(tabID, selector, accessibleName, role string, timeout int) (*AccessibilityQueryResult, error) {
 | |
| 	d.debugLog("Querying accessibility tree for tab: %s, selector: %s, name: %s, role: %s, timeout: %d", tabID, selector, accessibleName, role, timeout)
 | |
| 
 | |
| 	// Use current tab if not specified
 | |
| 	if tabID == "" {
 | |
| 		tabID = d.currentTab
 | |
| 	}
 | |
| 
 | |
| 	if tabID == "" {
 | |
| 		return nil, fmt.Errorf("no tab specified and no current tab available")
 | |
| 	}
 | |
| 
 | |
| 	page, err := d.getTab(tabID)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to get page: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// Enable accessibility domain
 | |
| 	err = proto.AccessibilityEnable{}.Call(page)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to enable accessibility domain: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// Find the DOM node first if selector is provided
 | |
| 	var backendNodeID *proto.DOMBackendNodeID
 | |
| 	if selector != "" {
 | |
| 		var element *rod.Element
 | |
| 		if timeout > 0 {
 | |
| 			element, err = page.Timeout(time.Duration(timeout) * time.Second).Element(selector)
 | |
| 		} else {
 | |
| 			element, err = page.Element(selector)
 | |
| 		}
 | |
| 		if err != nil {
 | |
| 			return nil, fmt.Errorf("failed to find element: %w", err)
 | |
| 		}
 | |
| 
 | |
| 		// Get the backend node ID
 | |
| 		nodeInfo, err := element.Describe(1, false)
 | |
| 		if err != nil {
 | |
| 			return nil, fmt.Errorf("failed to describe element: %w", err)
 | |
| 		}
 | |
| 		backendNodeID = &nodeInfo.BackendNodeID
 | |
| 	}
 | |
| 
 | |
| 	// Build query parameters
 | |
| 	queryParams := proto.AccessibilityQueryAXTree{}
 | |
| 	if backendNodeID != nil {
 | |
| 		queryParams.BackendNodeID = *backendNodeID
 | |
| 	}
 | |
| 	if accessibleName != "" {
 | |
| 		queryParams.AccessibleName = accessibleName
 | |
| 	}
 | |
| 	if role != "" {
 | |
| 		queryParams.Role = role
 | |
| 	}
 | |
| 
 | |
| 	// Call the Chrome DevTools Protocol Accessibility.queryAXTree method
 | |
| 	result, err := queryParams.Call(page)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to query accessibility tree: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// Parse the result
 | |
| 	var axResult AccessibilityQueryResult
 | |
| 	for _, node := range result.Nodes {
 | |
| 		axNode := d.convertProtoAXNode(node)
 | |
| 		axResult.Nodes = append(axResult.Nodes, axNode)
 | |
| 	}
 | |
| 
 | |
| 	d.debugLog("Successfully queried accessibility tree with %d matching nodes for tab: %s", len(axResult.Nodes), tabID)
 | |
| 	return &axResult, nil
 | |
| }
 | |
| 
 | |
| // setCacheDisabled enables or disables browser cache for a tab
 | |
| func (d *Daemon) setCacheDisabled(tabID string, disabled bool, timeout int) error {
 | |
| 	d.debugLog("Setting cache disabled=%v for tab: %s with timeout: %d", disabled, tabID, timeout)
 | |
| 
 | |
| 	// Use current tab if not specified
 | |
| 	if tabID == "" {
 | |
| 		tabID = d.currentTab
 | |
| 	}
 | |
| 
 | |
| 	if tabID == "" {
 | |
| 		return fmt.Errorf("no tab specified and no current tab available")
 | |
| 	}
 | |
| 
 | |
| 	page, err := d.getTab(tabID)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to get page: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// Create a context with timeout if specified
 | |
| 	if timeout > 0 {
 | |
| 		ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
 | |
| 		defer cancel()
 | |
| 
 | |
| 		// Create a channel to signal completion
 | |
| 		done := make(chan error, 1)
 | |
| 
 | |
| 		// Execute the cache setting in a goroutine
 | |
| 		go func() {
 | |
| 			err := proto.NetworkSetCacheDisabled{CacheDisabled: disabled}.Call(page)
 | |
| 			done <- err
 | |
| 		}()
 | |
| 
 | |
| 		// Wait for completion or timeout
 | |
| 		select {
 | |
| 		case err := <-done:
 | |
| 			if err != nil {
 | |
| 				return fmt.Errorf("failed to set cache disabled: %v", err)
 | |
| 			}
 | |
| 		case <-ctx.Done():
 | |
| 			return fmt.Errorf("timeout setting cache disabled after %d seconds", timeout)
 | |
| 		}
 | |
| 	} else {
 | |
| 		// No timeout - execute directly
 | |
| 		err := proto.NetworkSetCacheDisabled{CacheDisabled: disabled}.Call(page)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("failed to set cache disabled: %v", err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	d.debugLog("Successfully set cache disabled=%v for tab: %s", disabled, tabID)
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // clearBrowserCache clears the browser cache for a tab
 | |
| func (d *Daemon) clearBrowserCache(tabID string, timeout int) error {
 | |
| 	d.debugLog("Clearing browser cache for tab: %s with timeout: %d", tabID, timeout)
 | |
| 
 | |
| 	// Use current tab if not specified
 | |
| 	if tabID == "" {
 | |
| 		tabID = d.currentTab
 | |
| 	}
 | |
| 
 | |
| 	if tabID == "" {
 | |
| 		return fmt.Errorf("no tab specified and no current tab available")
 | |
| 	}
 | |
| 
 | |
| 	page, err := d.getTab(tabID)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to get page: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// Create a context with timeout if specified
 | |
| 	if timeout > 0 {
 | |
| 		ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
 | |
| 		defer cancel()
 | |
| 
 | |
| 		// Create a channel to signal completion
 | |
| 		done := make(chan error, 1)
 | |
| 
 | |
| 		// Execute the cache clearing in a goroutine
 | |
| 		go func() {
 | |
| 			err := proto.NetworkClearBrowserCache{}.Call(page)
 | |
| 			done <- err
 | |
| 		}()
 | |
| 
 | |
| 		// Wait for completion or timeout
 | |
| 		select {
 | |
| 		case err := <-done:
 | |
| 			if err != nil {
 | |
| 				return fmt.Errorf("failed to clear browser cache: %v", err)
 | |
| 			}
 | |
| 		case <-ctx.Done():
 | |
| 			return fmt.Errorf("timeout clearing browser cache after %d seconds", timeout)
 | |
| 		}
 | |
| 	} else {
 | |
| 		// No timeout - execute directly
 | |
| 		err := proto.NetworkClearBrowserCache{}.Call(page)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("failed to clear browser cache: %v", err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	d.debugLog("Successfully cleared browser cache for tab: %s", tabID)
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // clearAllSiteData clears all site data including cookies, storage, cache, etc. for a tab
 | |
| func (d *Daemon) clearAllSiteData(tabID string, timeout int) error {
 | |
| 	d.debugLog("Clearing all site data for tab: %s with timeout: %d", tabID, timeout)
 | |
| 
 | |
| 	// Use current tab if not specified
 | |
| 	if tabID == "" {
 | |
| 		tabID = d.currentTab
 | |
| 	}
 | |
| 
 | |
| 	if tabID == "" {
 | |
| 		return fmt.Errorf("no tab specified and no current tab available")
 | |
| 	}
 | |
| 
 | |
| 	page, err := d.getTab(tabID)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to get page: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// Create a context with timeout if specified
 | |
| 	if timeout > 0 {
 | |
| 		ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
 | |
| 		defer cancel()
 | |
| 
 | |
| 		// Create a channel to signal completion
 | |
| 		done := make(chan error, 1)
 | |
| 
 | |
| 		// Execute the site data clearing in a goroutine
 | |
| 		go func() {
 | |
| 			// Clear all types of site data
 | |
| 			err := proto.StorageClearDataForOrigin{
 | |
| 				Origin:       "*", // Clear for all origins
 | |
| 				StorageTypes: "appcache,cookies,file_systems,indexeddb,local_storage,shader_cache,websql,service_workers,cache_storage",
 | |
| 			}.Call(page)
 | |
| 			done <- err
 | |
| 		}()
 | |
| 
 | |
| 		// Wait for completion or timeout
 | |
| 		select {
 | |
| 		case err := <-done:
 | |
| 			if err != nil {
 | |
| 				return fmt.Errorf("failed to clear all site data: %v", err)
 | |
| 			}
 | |
| 		case <-ctx.Done():
 | |
| 			return fmt.Errorf("timeout clearing all site data after %d seconds", timeout)
 | |
| 		}
 | |
| 	} else {
 | |
| 		// No timeout - execute directly
 | |
| 		err := proto.StorageClearDataForOrigin{
 | |
| 			Origin:       "*", // Clear for all origins
 | |
| 			StorageTypes: "appcache,cookies,file_systems,indexeddb,local_storage,shader_cache,websql,service_workers,cache_storage",
 | |
| 		}.Call(page)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("failed to clear all site data: %v", err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	d.debugLog("Successfully cleared all site data for tab: %s", tabID)
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // clearCookies clears cookies for a tab
 | |
| func (d *Daemon) clearCookies(tabID string, timeout int) error {
 | |
| 	d.debugLog("Clearing cookies for tab: %s with timeout: %d", tabID, timeout)
 | |
| 
 | |
| 	// Use current tab if not specified
 | |
| 	if tabID == "" {
 | |
| 		tabID = d.currentTab
 | |
| 	}
 | |
| 
 | |
| 	if tabID == "" {
 | |
| 		return fmt.Errorf("no tab specified and no current tab available")
 | |
| 	}
 | |
| 
 | |
| 	page, err := d.getTab(tabID)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to get page: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// Create a context with timeout if specified
 | |
| 	if timeout > 0 {
 | |
| 		ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
 | |
| 		defer cancel()
 | |
| 
 | |
| 		// Create a channel to signal completion
 | |
| 		done := make(chan error, 1)
 | |
| 
 | |
| 		// Execute the cookie clearing in a goroutine
 | |
| 		go func() {
 | |
| 			// Clear cookies only
 | |
| 			err := proto.StorageClearDataForOrigin{
 | |
| 				Origin:       "*", // Clear for all origins
 | |
| 				StorageTypes: "cookies",
 | |
| 			}.Call(page)
 | |
| 			done <- err
 | |
| 		}()
 | |
| 
 | |
| 		// Wait for completion or timeout
 | |
| 		select {
 | |
| 		case err := <-done:
 | |
| 			if err != nil {
 | |
| 				return fmt.Errorf("failed to clear cookies: %v", err)
 | |
| 			}
 | |
| 		case <-ctx.Done():
 | |
| 			return fmt.Errorf("timeout clearing cookies after %d seconds", timeout)
 | |
| 		}
 | |
| 	} else {
 | |
| 		// No timeout - execute directly
 | |
| 		err := proto.StorageClearDataForOrigin{
 | |
| 			Origin:       "*", // Clear for all origins
 | |
| 			StorageTypes: "cookies",
 | |
| 		}.Call(page)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("failed to clear cookies: %v", err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	d.debugLog("Successfully cleared cookies for tab: %s", tabID)
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // clearStorage clears web storage (localStorage, sessionStorage, IndexedDB, etc.) for a tab
 | |
| func (d *Daemon) clearStorage(tabID string, timeout int) error {
 | |
| 	d.debugLog("Clearing storage for tab: %s with timeout: %d", tabID, timeout)
 | |
| 
 | |
| 	// Use current tab if not specified
 | |
| 	if tabID == "" {
 | |
| 		tabID = d.currentTab
 | |
| 	}
 | |
| 
 | |
| 	if tabID == "" {
 | |
| 		return fmt.Errorf("no tab specified and no current tab available")
 | |
| 	}
 | |
| 
 | |
| 	page, err := d.getTab(tabID)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to get page: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// Create a context with timeout if specified
 | |
| 	if timeout > 0 {
 | |
| 		ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
 | |
| 		defer cancel()
 | |
| 
 | |
| 		// Create a channel to signal completion
 | |
| 		done := make(chan error, 1)
 | |
| 
 | |
| 		// Execute the storage clearing in a goroutine
 | |
| 		go func() {
 | |
| 			// Clear storage types (excluding cookies and cache)
 | |
| 			err := proto.StorageClearDataForOrigin{
 | |
| 				Origin:       "*", // Clear for all origins
 | |
| 				StorageTypes: "appcache,file_systems,indexeddb,local_storage,websql,service_workers,cache_storage",
 | |
| 			}.Call(page)
 | |
| 			done <- err
 | |
| 		}()
 | |
| 
 | |
| 		// Wait for completion or timeout
 | |
| 		select {
 | |
| 		case err := <-done:
 | |
| 			if err != nil {
 | |
| 				return fmt.Errorf("failed to clear storage: %v", err)
 | |
| 			}
 | |
| 		case <-ctx.Done():
 | |
| 			return fmt.Errorf("timeout clearing storage after %d seconds", timeout)
 | |
| 		}
 | |
| 	} else {
 | |
| 		// No timeout - execute directly
 | |
| 		err := proto.StorageClearDataForOrigin{
 | |
| 			Origin:       "*", // Clear for all origins
 | |
| 			StorageTypes: "appcache,file_systems,indexeddb,local_storage,websql,service_workers,cache_storage",
 | |
| 		}.Call(page)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("failed to clear storage: %v", err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	d.debugLog("Successfully cleared storage for tab: %s", tabID)
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // dragAndDrop performs a drag and drop operation from source element to target element
 | |
| func (d *Daemon) dragAndDrop(tabID, sourceSelector, targetSelector string, timeout int) error {
 | |
| 	d.debugLog("Performing drag and drop from %s to %s for tab: %s with timeout: %d", sourceSelector, targetSelector, tabID, timeout)
 | |
| 
 | |
| 	// Use current tab if not specified
 | |
| 	if tabID == "" {
 | |
| 		tabID = d.currentTab
 | |
| 	}
 | |
| 
 | |
| 	if tabID == "" {
 | |
| 		return fmt.Errorf("no tab specified and no current tab available")
 | |
| 	}
 | |
| 
 | |
| 	page, err := d.getTab(tabID)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to get page: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// Create a context with timeout if specified
 | |
| 	if timeout > 0 {
 | |
| 		ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
 | |
| 		defer cancel()
 | |
| 
 | |
| 		// Create a channel to signal completion
 | |
| 		done := make(chan error, 1)
 | |
| 
 | |
| 		// Execute the drag and drop in a goroutine
 | |
| 		go func() {
 | |
| 			err := d.performDragAndDrop(page, sourceSelector, targetSelector)
 | |
| 			done <- err
 | |
| 		}()
 | |
| 
 | |
| 		// Wait for completion or timeout
 | |
| 		select {
 | |
| 		case err := <-done:
 | |
| 			if err != nil {
 | |
| 				return fmt.Errorf("failed to perform drag and drop: %v", err)
 | |
| 			}
 | |
| 		case <-ctx.Done():
 | |
| 			return fmt.Errorf("timeout performing drag and drop after %d seconds", timeout)
 | |
| 		}
 | |
| 	} else {
 | |
| 		// No timeout - execute directly
 | |
| 		err := d.performDragAndDrop(page, sourceSelector, targetSelector)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("failed to perform drag and drop: %v", err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	d.debugLog("Successfully performed drag and drop for tab: %s", tabID)
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // dragAndDropToCoordinates performs a drag and drop operation from source element to specific coordinates
 | |
| func (d *Daemon) dragAndDropToCoordinates(tabID, sourceSelector string, targetX, targetY, timeout int) error {
 | |
| 	d.debugLog("Performing drag and drop from %s to coordinates (%d, %d) for tab: %s with timeout: %d", sourceSelector, targetX, targetY, tabID, timeout)
 | |
| 
 | |
| 	// Use current tab if not specified
 | |
| 	if tabID == "" {
 | |
| 		tabID = d.currentTab
 | |
| 	}
 | |
| 
 | |
| 	if tabID == "" {
 | |
| 		return fmt.Errorf("no tab specified and no current tab available")
 | |
| 	}
 | |
| 
 | |
| 	page, err := d.getTab(tabID)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to get page: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// Create a context with timeout if specified
 | |
| 	if timeout > 0 {
 | |
| 		ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
 | |
| 		defer cancel()
 | |
| 
 | |
| 		// Create a channel to signal completion
 | |
| 		done := make(chan error, 1)
 | |
| 
 | |
| 		// Execute the drag and drop in a goroutine
 | |
| 		go func() {
 | |
| 			err := d.performDragAndDropToCoordinates(page, sourceSelector, targetX, targetY)
 | |
| 			done <- err
 | |
| 		}()
 | |
| 
 | |
| 		// Wait for completion or timeout
 | |
| 		select {
 | |
| 		case err := <-done:
 | |
| 			if err != nil {
 | |
| 				return fmt.Errorf("failed to perform drag and drop to coordinates: %v", err)
 | |
| 			}
 | |
| 		case <-ctx.Done():
 | |
| 			return fmt.Errorf("timeout performing drag and drop to coordinates after %d seconds", timeout)
 | |
| 		}
 | |
| 	} else {
 | |
| 		// No timeout - execute directly
 | |
| 		err := d.performDragAndDropToCoordinates(page, sourceSelector, targetX, targetY)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("failed to perform drag and drop to coordinates: %v", err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	d.debugLog("Successfully performed drag and drop to coordinates for tab: %s", tabID)
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // dragAndDropByOffset performs a drag and drop operation from source element by a relative offset
 | |
| func (d *Daemon) dragAndDropByOffset(tabID, sourceSelector string, offsetX, offsetY, timeout int) error {
 | |
| 	d.debugLog("Performing drag and drop from %s by offset (%d, %d) for tab: %s with timeout: %d", sourceSelector, offsetX, offsetY, tabID, timeout)
 | |
| 
 | |
| 	// Use current tab if not specified
 | |
| 	if tabID == "" {
 | |
| 		tabID = d.currentTab
 | |
| 	}
 | |
| 
 | |
| 	if tabID == "" {
 | |
| 		return fmt.Errorf("no tab specified and no current tab available")
 | |
| 	}
 | |
| 
 | |
| 	page, err := d.getTab(tabID)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to get page: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// Create a context with timeout if specified
 | |
| 	if timeout > 0 {
 | |
| 		ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
 | |
| 		defer cancel()
 | |
| 
 | |
| 		// Create a channel to signal completion
 | |
| 		done := make(chan error, 1)
 | |
| 
 | |
| 		// Execute the drag and drop in a goroutine
 | |
| 		go func() {
 | |
| 			err := d.performDragAndDropByOffset(page, sourceSelector, offsetX, offsetY)
 | |
| 			done <- err
 | |
| 		}()
 | |
| 
 | |
| 		// Wait for completion or timeout
 | |
| 		select {
 | |
| 		case err := <-done:
 | |
| 			if err != nil {
 | |
| 				return fmt.Errorf("failed to perform drag and drop by offset: %v", err)
 | |
| 			}
 | |
| 		case <-ctx.Done():
 | |
| 			return fmt.Errorf("timeout performing drag and drop by offset after %d seconds", timeout)
 | |
| 		}
 | |
| 	} else {
 | |
| 		// No timeout - execute directly
 | |
| 		err := d.performDragAndDropByOffset(page, sourceSelector, offsetX, offsetY)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("failed to perform drag and drop by offset: %v", err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	d.debugLog("Successfully performed drag and drop by offset for tab: %s", tabID)
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // performDragAndDrop performs the actual drag and drop operation between two elements
 | |
| func (d *Daemon) performDragAndDrop(page *rod.Page, sourceSelector, targetSelector string) error {
 | |
| 	// First, try the enhanced HTML5 drag and drop approach
 | |
| 	err := d.performHTML5DragAndDrop(page, sourceSelector, targetSelector)
 | |
| 	if err == nil {
 | |
| 		d.debugLog("HTML5 drag and drop completed successfully")
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	d.debugLog("HTML5 drag and drop failed (%v), falling back to mouse events", err)
 | |
| 
 | |
| 	// Fallback to the original mouse-based approach
 | |
| 	// Find source element
 | |
| 	sourceElement, err := page.Element(sourceSelector)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to find source element %s: %v", sourceSelector, err)
 | |
| 	}
 | |
| 
 | |
| 	// Find target element
 | |
| 	targetElement, err := page.Element(targetSelector)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to find target element %s: %v", targetSelector, err)
 | |
| 	}
 | |
| 
 | |
| 	// Get source element position and size
 | |
| 	sourceBox, err := sourceElement.Shape()
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to get source element shape: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// Get target element position and size
 | |
| 	targetBox, err := targetElement.Shape()
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to get target element shape: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// Calculate center points from the first quad (border box)
 | |
| 	if len(sourceBox.Quads) == 0 {
 | |
| 		return fmt.Errorf("source element has no quads")
 | |
| 	}
 | |
| 	if len(targetBox.Quads) == 0 {
 | |
| 		return fmt.Errorf("target element has no quads")
 | |
| 	}
 | |
| 
 | |
| 	sourceQuad := sourceBox.Quads[0]
 | |
| 	targetQuad := targetBox.Quads[0]
 | |
| 
 | |
| 	// Calculate center from quad points (quad has 8 values: x1,y1,x2,y2,x3,y3,x4,y4)
 | |
| 	sourceX := (sourceQuad[0] + sourceQuad[2] + sourceQuad[4] + sourceQuad[6]) / 4
 | |
| 	sourceY := (sourceQuad[1] + sourceQuad[3] + sourceQuad[5] + sourceQuad[7]) / 4
 | |
| 	targetX := (targetQuad[0] + targetQuad[2] + targetQuad[4] + targetQuad[6]) / 4
 | |
| 	targetY := (targetQuad[1] + targetQuad[3] + targetQuad[5] + targetQuad[7]) / 4
 | |
| 
 | |
| 	return d.performDragAndDropBetweenPoints(page, sourceX, sourceY, targetX, targetY)
 | |
| }
 | |
| 
 | |
| // injectDragDropHelpers injects the JavaScript drag and drop helper functions into the page
 | |
| func (d *Daemon) injectDragDropHelpers(page *rod.Page) error {
 | |
| 	// Read the JavaScript helper file
 | |
| 	jsHelpers := `
 | |
| // HTML5 Drag and Drop Helper Functions for Cremote
 | |
| // These functions are injected into web pages to provide reliable drag and drop functionality
 | |
| 
 | |
| (function() {
 | |
|     'use strict';
 | |
| 
 | |
|     // Create a namespace to avoid conflicts
 | |
|     window.cremoteDragDrop = window.cremoteDragDrop || {};
 | |
| 
 | |
|     /**
 | |
|      * Simulates HTML5 drag and drop between two elements
 | |
|      * @param {string} sourceSelector - CSS selector for source element
 | |
|      * @param {string} targetSelector - CSS selector for target element
 | |
|      * @returns {Promise<boolean>} - Success status
 | |
|      */
 | |
|     window.cremoteDragDrop.dragElementToElement = async function(sourceSelector, targetSelector) {
 | |
|         const sourceElement = document.querySelector(sourceSelector);
 | |
|         const targetElement = document.querySelector(targetSelector);
 | |
| 
 | |
|         if (!sourceElement) {
 | |
|             throw new Error('Source element not found: ' + sourceSelector);
 | |
|         }
 | |
|         if (!targetElement) {
 | |
|             throw new Error('Target element not found: ' + targetSelector);
 | |
|         }
 | |
| 
 | |
|         // Make source draggable if not already
 | |
|         if (!sourceElement.draggable) {
 | |
|             sourceElement.draggable = true;
 | |
|         }
 | |
| 
 | |
|         // Create and dispatch dragstart event
 | |
|         const dragStartEvent = new DragEvent('dragstart', {
 | |
|             bubbles: true,
 | |
|             cancelable: true,
 | |
|             dataTransfer: new DataTransfer()
 | |
|         });
 | |
| 
 | |
|         // Set drag data
 | |
|         dragStartEvent.dataTransfer.setData('text/plain', sourceElement.id || sourceSelector);
 | |
|         dragStartEvent.dataTransfer.effectAllowed = 'all';
 | |
| 
 | |
|         const dragStartResult = sourceElement.dispatchEvent(dragStartEvent);
 | |
|         if (!dragStartResult) {
 | |
|             console.log('Dragstart was cancelled');
 | |
|             return false;
 | |
|         }
 | |
| 
 | |
|         // Small delay to simulate realistic drag timing
 | |
|         await new Promise(resolve => setTimeout(resolve, 50));
 | |
| 
 | |
|         // Create and dispatch dragover event on target
 | |
|         const dragOverEvent = new DragEvent('dragover', {
 | |
|             bubbles: true,
 | |
|             cancelable: true,
 | |
|             dataTransfer: dragStartEvent.dataTransfer
 | |
|         });
 | |
| 
 | |
|         const dragOverResult = targetElement.dispatchEvent(dragOverEvent);
 | |
| 
 | |
|         // Create and dispatch drop event on target
 | |
|         const dropEvent = new DragEvent('drop', {
 | |
|             bubbles: true,
 | |
|             cancelable: true,
 | |
|             dataTransfer: dragStartEvent.dataTransfer
 | |
|         });
 | |
| 
 | |
|         const dropResult = targetElement.dispatchEvent(dropEvent);
 | |
| 
 | |
|         // Create and dispatch dragend event on source
 | |
|         const dragEndEvent = new DragEvent('dragend', {
 | |
|             bubbles: true,
 | |
|             cancelable: true,
 | |
|             dataTransfer: dragStartEvent.dataTransfer
 | |
|         });
 | |
| 
 | |
|         sourceElement.dispatchEvent(dragEndEvent);
 | |
| 
 | |
|         return dropResult;
 | |
|     };
 | |
| 
 | |
|     console.log('Cremote drag and drop helpers loaded successfully');
 | |
| })();
 | |
| `
 | |
| 
 | |
| 	// Inject the JavaScript helpers
 | |
| 	_, err := page.Eval(jsHelpers)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to inject drag and drop helpers: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // performHTML5DragAndDrop performs drag and drop using HTML5 drag events
 | |
| func (d *Daemon) performHTML5DragAndDrop(page *rod.Page, sourceSelector, targetSelector string) error {
 | |
| 	// Inject the helper functions
 | |
| 	err := d.injectDragDropHelpers(page)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to inject helpers: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// Execute the HTML5 drag and drop
 | |
| 	jsCode := fmt.Sprintf(`
 | |
| 		(async function() {
 | |
| 			try {
 | |
| 				const result = await window.cremoteDragDrop.dragElementToElement('%s', '%s');
 | |
| 				return { success: result, error: null };
 | |
| 			} catch (error) {
 | |
| 				return { success: false, error: error.message };
 | |
| 			}
 | |
| 		})()
 | |
| 	`, sourceSelector, targetSelector)
 | |
| 
 | |
| 	result, err := page.Eval(jsCode)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to execute HTML5 drag and drop: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// Parse the result
 | |
| 	resultMap := result.Value.Map()
 | |
| 	if resultMap == nil {
 | |
| 		return fmt.Errorf("invalid result from HTML5 drag and drop")
 | |
| 	}
 | |
| 
 | |
| 	success, exists := resultMap["success"]
 | |
| 	if !exists || !success.Bool() {
 | |
| 		errorMsg := "unknown error"
 | |
| 		if errorVal, exists := resultMap["error"]; exists && errorVal.Str() != "" {
 | |
| 			errorMsg = errorVal.Str()
 | |
| 		}
 | |
| 		return fmt.Errorf("HTML5 drag and drop failed: %s", errorMsg)
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // injectEnhancedDragDropHelpers injects the complete JavaScript drag and drop helper functions
 | |
| func (d *Daemon) injectEnhancedDragDropHelpers(page *rod.Page) error {
 | |
| 	// Read the perfect JavaScript helper file content
 | |
| 	jsHelpers := `
 | |
| // Perfect HTML5 Drag and Drop Helper Functions for Cremote
 | |
| // These functions achieve 100% reliability for drag and drop operations
 | |
| 
 | |
| (function() {
 | |
|     'use strict';
 | |
| 
 | |
|     // Create a namespace to avoid conflicts
 | |
|     window.cremoteDragDrop = window.cremoteDragDrop || {};
 | |
| 
 | |
|     /**
 | |
|      * Perfect HTML5 drag and drop between two elements
 | |
|      * @param {string} sourceSelector - CSS selector for source element
 | |
|      * @param {string} targetSelector - CSS selector for target element
 | |
|      * @returns {Promise<boolean>} - Success status
 | |
|      */
 | |
|     window.cremoteDragDrop.dragElementToElement = async function(sourceSelector, targetSelector) {
 | |
|         const sourceElement = document.querySelector(sourceSelector);
 | |
|         const targetElement = document.querySelector(targetSelector);
 | |
| 
 | |
|         if (!sourceElement) {
 | |
|             throw new Error('Source element not found: ' + sourceSelector);
 | |
|         }
 | |
|         if (!targetElement) {
 | |
|             throw new Error('Target element not found: ' + targetSelector);
 | |
|         }
 | |
| 
 | |
|         // Ensure source is draggable
 | |
|         if (!sourceElement.draggable) {
 | |
|             sourceElement.draggable = true;
 | |
|         }
 | |
| 
 | |
|         // Create a persistent DataTransfer object
 | |
|         const dataTransfer = new DataTransfer();
 | |
|         dataTransfer.setData('text/plain', sourceElement.id || sourceSelector);
 | |
|         dataTransfer.setData('application/x-cremote-drag', JSON.stringify({
 | |
|             sourceId: sourceElement.id,
 | |
|             sourceSelector: sourceSelector,
 | |
|             timestamp: Date.now()
 | |
|         }));
 | |
|         dataTransfer.effectAllowed = 'all';
 | |
| 
 | |
|         // Step 1: Dispatch dragstart event
 | |
|         const dragStartEvent = new DragEvent('dragstart', {
 | |
|             bubbles: true,
 | |
|             cancelable: true,
 | |
|             dataTransfer: dataTransfer
 | |
|         });
 | |
| 
 | |
|         const dragStartResult = sourceElement.dispatchEvent(dragStartEvent);
 | |
|         if (!dragStartResult) {
 | |
|             console.log('Dragstart was cancelled');
 | |
|             return false;
 | |
|         }
 | |
| 
 | |
|         // Step 2: Small delay for realism
 | |
|         await new Promise(resolve => setTimeout(resolve, 50));
 | |
| 
 | |
|         // Step 3: Dispatch dragenter event on target
 | |
|         const dragEnterEvent = new DragEvent('dragenter', {
 | |
|             bubbles: true,
 | |
|             cancelable: true,
 | |
|             dataTransfer: dataTransfer
 | |
|         });
 | |
|         targetElement.dispatchEvent(dragEnterEvent);
 | |
| 
 | |
|         // Step 4: Dispatch dragover event on target (critical for drop acceptance)
 | |
|         const dragOverEvent = new DragEvent('dragover', {
 | |
|             bubbles: true,
 | |
|             cancelable: true,
 | |
|             dataTransfer: dataTransfer
 | |
|         });
 | |
| 
 | |
|         // Prevent default to allow drop
 | |
|         dragOverEvent.preventDefault = function() { this.defaultPrevented = true; };
 | |
|         const dragOverResult = targetElement.dispatchEvent(dragOverEvent);
 | |
| 
 | |
|         // Step 5: Dispatch drop event on target
 | |
|         const dropEvent = new DragEvent('drop', {
 | |
|             bubbles: true,
 | |
|             cancelable: true,
 | |
|             dataTransfer: dataTransfer
 | |
|         });
 | |
| 
 | |
|         const dropResult = targetElement.dispatchEvent(dropEvent);
 | |
| 
 | |
|         // Step 6: Dispatch dragend event on source
 | |
|         const dragEndEvent = new DragEvent('dragend', {
 | |
|             bubbles: true,
 | |
|             cancelable: true,
 | |
|             dataTransfer: dataTransfer
 | |
|         });
 | |
| 
 | |
|         sourceElement.dispatchEvent(dragEndEvent);
 | |
| 
 | |
|         return dropResult;
 | |
|     };
 | |
| 
 | |
|     /**
 | |
|      * Enhanced drop target detection with multiple strategies
 | |
|      * @param {Element} element - Element to check
 | |
|      * @returns {boolean} - Whether element can receive drops
 | |
|      */
 | |
|     window.cremoteDragDrop.hasDropEventListener = function(element) {
 | |
|         // Strategy 1: Check for explicit drop handlers
 | |
|         if (element.ondrop) return true;
 | |
|         if (element.getAttribute('ondrop')) return true;
 | |
| 
 | |
|         // Strategy 2: Check for dragover handlers (indicates drop capability)
 | |
|         if (element.ondragover || element.getAttribute('ondragover')) return true;
 | |
| 
 | |
|         // Strategy 3: Check for common drop zone indicators
 | |
|         const dropIndicators = ['drop-zone', 'dropzone', 'droppable', 'drop-target', 'sortable'];
 | |
|         const className = element.className.toLowerCase();
 | |
|         if (dropIndicators.some(indicator => className.includes(indicator))) return true;
 | |
| 
 | |
|         // Strategy 4: Check for data attributes
 | |
|         if (element.hasAttribute('data-drop') || element.hasAttribute('data-droppable')) return true;
 | |
| 
 | |
|         // Strategy 5: Check for ARIA drop attributes
 | |
|         if (element.getAttribute('aria-dropeffect') && element.getAttribute('aria-dropeffect') !== 'none') return true;
 | |
| 
 | |
|         return false;
 | |
|     };
 | |
| 
 | |
|     /**
 | |
|      * Perfect coordinate-based drop target detection
 | |
|      * @param {number} x - X coordinate
 | |
|      * @param {number} y - Y coordinate
 | |
|      * @returns {Element|null} - Best drop target element or null
 | |
|      */
 | |
|     window.cremoteDragDrop.findDropTargetAtCoordinates = function(x, y) {
 | |
|         // Ensure coordinates are within viewport
 | |
|         if (x < 0 || y < 0 || x > window.innerWidth || y > window.innerHeight) {
 | |
|             console.log('Coordinates outside viewport:', {x, y, viewport: {width: window.innerWidth, height: window.innerHeight}});
 | |
|             return null;
 | |
|         }
 | |
| 
 | |
|         const elements = document.elementsFromPoint(x, y);
 | |
|         if (!elements || elements.length === 0) {
 | |
|             console.log('No elements found at coordinates:', {x, y});
 | |
|             return null;
 | |
|         }
 | |
| 
 | |
|         // Look for explicit drop targets first
 | |
|         for (const element of elements) {
 | |
|             if (this.hasDropEventListener(element)) {
 | |
|                 console.log('Found drop target:', element.tagName, element.id, element.className);
 | |
|                 return element;
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         // If no explicit drop target, return the topmost non-body element
 | |
|         const topElement = elements.find(el => el.tagName !== 'HTML' && el.tagName !== 'BODY');
 | |
|         console.log('Using topmost element as fallback:', topElement?.tagName, topElement?.id, topElement?.className);
 | |
|         return topElement || elements[0];
 | |
|     };
 | |
| 
 | |
|     /**
 | |
|      * Perfect drag to coordinates with comprehensive event handling
 | |
|      * @param {string} sourceSelector - CSS selector for source element
 | |
|      * @param {number} x - Target X coordinate
 | |
|      * @param {number} y - Target Y coordinate
 | |
|      * @returns {Promise<object>} - Detailed result object
 | |
|      */
 | |
|     window.cremoteDragDrop.dragElementToCoordinates = async function(sourceSelector, x, y) {
 | |
|         const sourceElement = document.querySelector(sourceSelector);
 | |
| 
 | |
|         if (!sourceElement) {
 | |
|             throw new Error('Source element not found: ' + sourceSelector);
 | |
|         }
 | |
| 
 | |
|         const targetElement = this.findDropTargetAtCoordinates(x, y);
 | |
|         if (!targetElement) {
 | |
|             throw new Error('No element found at coordinates (' + x + ', ' + y + ')');
 | |
|         }
 | |
| 
 | |
|         // Ensure source is draggable
 | |
|         if (!sourceElement.draggable) {
 | |
|             sourceElement.draggable = true;
 | |
|         }
 | |
| 
 | |
|         // Create persistent DataTransfer
 | |
|         const dataTransfer = new DataTransfer();
 | |
|         dataTransfer.setData('text/plain', sourceElement.id || sourceSelector);
 | |
|         dataTransfer.setData('application/x-cremote-drag', JSON.stringify({
 | |
|             sourceId: sourceElement.id,
 | |
|             sourceSelector: sourceSelector,
 | |
|             targetX: x,
 | |
|             targetY: y,
 | |
|             timestamp: Date.now()
 | |
|         }));
 | |
|         dataTransfer.effectAllowed = 'all';
 | |
| 
 | |
|         // Step 1: Dragstart
 | |
|         const dragStartEvent = new DragEvent('dragstart', {
 | |
|             bubbles: true,
 | |
|             cancelable: true,
 | |
|             dataTransfer: dataTransfer
 | |
|         });
 | |
| 
 | |
|         const dragStartResult = sourceElement.dispatchEvent(dragStartEvent);
 | |
|         if (!dragStartResult) {
 | |
|             return { success: false, reason: 'Dragstart was cancelled', targetElement: null };
 | |
|         }
 | |
| 
 | |
|         await new Promise(resolve => setTimeout(resolve, 50));
 | |
| 
 | |
|         // Step 2: Dragenter on target
 | |
|         const dragEnterEvent = new DragEvent('dragenter', {
 | |
|             bubbles: true,
 | |
|             cancelable: true,
 | |
|             clientX: x,
 | |
|             clientY: y,
 | |
|             dataTransfer: dataTransfer
 | |
|         });
 | |
|         targetElement.dispatchEvent(dragEnterEvent);
 | |
| 
 | |
|         // Step 3: Dragover on target (critical!)
 | |
|         const dragOverEvent = new DragEvent('dragover', {
 | |
|             bubbles: true,
 | |
|             cancelable: true,
 | |
|             clientX: x,
 | |
|             clientY: y,
 | |
|             dataTransfer: dataTransfer
 | |
|         });
 | |
| 
 | |
|         // Force preventDefault to allow drop
 | |
|         dragOverEvent.preventDefault = function() { this.defaultPrevented = true; };
 | |
|         targetElement.dispatchEvent(dragOverEvent);
 | |
| 
 | |
|         // Step 4: Drop on target
 | |
|         const dropEvent = new DragEvent('drop', {
 | |
|             bubbles: true,
 | |
|             cancelable: true,
 | |
|             clientX: x,
 | |
|             clientY: y,
 | |
|             dataTransfer: dataTransfer
 | |
|         });
 | |
| 
 | |
|         const dropResult = targetElement.dispatchEvent(dropEvent);
 | |
| 
 | |
|         // Step 5: Dragend on source
 | |
|         const dragEndEvent = new DragEvent('dragend', {
 | |
|             bubbles: true,
 | |
|             cancelable: true,
 | |
|             dataTransfer: dataTransfer
 | |
|         });
 | |
| 
 | |
|         sourceElement.dispatchEvent(dragEndEvent);
 | |
| 
 | |
|         return {
 | |
|             success: dropResult,
 | |
|             targetElement: {
 | |
|                 tagName: targetElement.tagName,
 | |
|                 id: targetElement.id,
 | |
|                 className: targetElement.className,
 | |
|                 hasDropListener: this.hasDropEventListener(targetElement)
 | |
|             }
 | |
|         };
 | |
|     };
 | |
| 
 | |
|     /**
 | |
|      * Perfect smart drag to coordinates with optimal strategy selection
 | |
|      * @param {string} sourceSelector - CSS selector for source element
 | |
|      * @param {number} x - Target X coordinate
 | |
|      * @param {number} y - Target Y coordinate
 | |
|      * @returns {Promise<object>} - Enhanced result with method info
 | |
|      */
 | |
|     window.cremoteDragDrop.smartDragToCoordinates = async function(sourceSelector, x, y) {
 | |
|         const sourceElement = document.querySelector(sourceSelector);
 | |
| 
 | |
|         if (!sourceElement) {
 | |
|             throw new Error('Source element not found: ' + sourceSelector);
 | |
|         }
 | |
| 
 | |
|         const targetElement = this.findDropTargetAtCoordinates(x, y);
 | |
|         if (!targetElement) {
 | |
|             throw new Error('No suitable drop target found at coordinates (' + x + ', ' + y + ')');
 | |
|         }
 | |
| 
 | |
|         const canReceiveDrops = this.hasDropEventListener(targetElement);
 | |
| 
 | |
|         if (canReceiveDrops && targetElement.id) {
 | |
|             // Use element-to-element drag for maximum reliability
 | |
|             const success = await this.dragElementToElement(sourceSelector, '#' + targetElement.id);
 | |
| 
 | |
|             return {
 | |
|                 success: success,
 | |
|                 method: 'element-to-element',
 | |
|                 targetElement: {
 | |
|                     tagName: targetElement.tagName,
 | |
|                     id: targetElement.id,
 | |
|                     className: targetElement.className,
 | |
|                     hasDropListener: true
 | |
|                 }
 | |
|             };
 | |
|         } else {
 | |
|             // Use coordinate-based drag with perfect event handling
 | |
|             const result = await this.dragElementToCoordinates(sourceSelector, x, y);
 | |
|             result.method = 'coordinate-based';
 | |
|             return result;
 | |
|         }
 | |
|     };
 | |
| 
 | |
|     console.log('Perfect Cremote drag and drop helpers loaded successfully');
 | |
| 
 | |
| })();
 | |
| `
 | |
| 
 | |
| 	// Inject the JavaScript helpers
 | |
| 	_, err := page.Eval(jsHelpers)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to inject enhanced drag and drop helpers: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // performDragAndDropToCoordinates performs drag and drop from element to specific coordinates
 | |
| func (d *Daemon) performDragAndDropToCoordinates(page *rod.Page, sourceSelector string, targetX, targetY int) error {
 | |
| 	// First, try the enhanced HTML5 approach with smart target detection
 | |
| 	err := d.performHTML5DragToCoordinates(page, sourceSelector, targetX, targetY)
 | |
| 	if err == nil {
 | |
| 		d.debugLog("HTML5 coordinate drag completed successfully")
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	d.debugLog("HTML5 coordinate drag failed (%v), falling back to mouse events", err)
 | |
| 
 | |
| 	// Fallback to the original mouse-based approach
 | |
| 	// Find source element
 | |
| 	sourceElement, err := page.Element(sourceSelector)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to find source element %s: %v", sourceSelector, err)
 | |
| 	}
 | |
| 
 | |
| 	// Get source element position and size
 | |
| 	sourceBox, err := sourceElement.Shape()
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to get source element shape: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// Calculate source center point from the first quad
 | |
| 	if len(sourceBox.Quads) == 0 {
 | |
| 		return fmt.Errorf("source element has no quads")
 | |
| 	}
 | |
| 	sourceQuad := sourceBox.Quads[0]
 | |
| 	sourceX := (sourceQuad[0] + sourceQuad[2] + sourceQuad[4] + sourceQuad[6]) / 4
 | |
| 	sourceY := (sourceQuad[1] + sourceQuad[3] + sourceQuad[5] + sourceQuad[7]) / 4
 | |
| 
 | |
| 	return d.performDragAndDropBetweenPoints(page, sourceX, sourceY, float64(targetX), float64(targetY))
 | |
| }
 | |
| 
 | |
| // performHTML5DragToCoordinates performs HTML5 drag to coordinates with smart target detection
 | |
| func (d *Daemon) performHTML5DragToCoordinates(page *rod.Page, sourceSelector string, targetX, targetY int) error {
 | |
| 	// First, inject the enhanced helper functions that include coordinate support
 | |
| 	err := d.injectEnhancedDragDropHelpers(page)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to inject enhanced helpers: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// Execute the smart coordinate drag
 | |
| 	jsCode := fmt.Sprintf(`
 | |
| 		(async function() {
 | |
| 			try {
 | |
| 				const result = await window.cremoteDragDrop.smartDragToCoordinates('%s', %d, %d);
 | |
| 				return { success: result.success, method: result.method, error: null, targetInfo: result.targetElement };
 | |
| 			} catch (error) {
 | |
| 				return { success: false, error: error.message, method: 'failed', targetInfo: null };
 | |
| 			}
 | |
| 		})()
 | |
| 	`, sourceSelector, targetX, targetY)
 | |
| 
 | |
| 	result, err := page.Eval(jsCode)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to execute HTML5 coordinate drag: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// Parse the result
 | |
| 	resultMap := result.Value.Map()
 | |
| 	if resultMap == nil {
 | |
| 		return fmt.Errorf("invalid result from HTML5 coordinate drag")
 | |
| 	}
 | |
| 
 | |
| 	success, exists := resultMap["success"]
 | |
| 	if !exists || !success.Bool() {
 | |
| 		errorMsg := "unknown error"
 | |
| 		if errorVal, exists := resultMap["error"]; exists && errorVal.Str() != "" {
 | |
| 			errorMsg = errorVal.Str()
 | |
| 		}
 | |
| 		return fmt.Errorf("HTML5 coordinate drag failed: %s", errorMsg)
 | |
| 	}
 | |
| 
 | |
| 	// Log the method used for debugging
 | |
| 	if method, exists := resultMap["method"]; exists && method.Str() != "" {
 | |
| 		d.debugLog("HTML5 coordinate drag used method: %s", method.Str())
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // performDragAndDropByOffset performs drag and drop from element by relative offset
 | |
| func (d *Daemon) performDragAndDropByOffset(page *rod.Page, sourceSelector string, offsetX, offsetY int) error {
 | |
| 	// First, calculate the target coordinates
 | |
| 	sourceElement, err := page.Element(sourceSelector)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to find source element %s: %v", sourceSelector, err)
 | |
| 	}
 | |
| 
 | |
| 	sourceBox, err := sourceElement.Shape()
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to get source element shape: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	if len(sourceBox.Quads) == 0 {
 | |
| 		return fmt.Errorf("source element has no quads")
 | |
| 	}
 | |
| 	sourceQuad := sourceBox.Quads[0]
 | |
| 	sourceX := (sourceQuad[0] + sourceQuad[2] + sourceQuad[4] + sourceQuad[6]) / 4
 | |
| 	sourceY := (sourceQuad[1] + sourceQuad[3] + sourceQuad[5] + sourceQuad[7]) / 4
 | |
| 
 | |
| 	// Calculate target coordinates
 | |
| 	targetX := int(sourceX + float64(offsetX))
 | |
| 	targetY := int(sourceY + float64(offsetY))
 | |
| 
 | |
| 	// Try the enhanced HTML5 approach first (reuse coordinate logic)
 | |
| 	err = d.performHTML5DragToCoordinates(page, sourceSelector, targetX, targetY)
 | |
| 	if err == nil {
 | |
| 		d.debugLog("HTML5 offset drag completed successfully")
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	d.debugLog("HTML5 offset drag failed (%v), falling back to mouse events", err)
 | |
| 
 | |
| 	// Fallback to the original mouse-based approach
 | |
| 	return d.performDragAndDropBetweenPoints(page, sourceX, sourceY, float64(targetX), float64(targetY))
 | |
| }
 | |
| 
 | |
| // performDragAndDropBetweenPoints performs the actual drag and drop using Chrome DevTools Protocol mouse events
 | |
| func (d *Daemon) performDragAndDropBetweenPoints(page *rod.Page, sourceX, sourceY, targetX, targetY float64) error {
 | |
| 	d.debugLog("Performing drag and drop from (%.2f, %.2f) to (%.2f, %.2f)", sourceX, sourceY, targetX, targetY)
 | |
| 
 | |
| 	// Step 1: Move mouse to source position
 | |
| 	err := proto.InputDispatchMouseEvent{
 | |
| 		Type: proto.InputDispatchMouseEventTypeMouseMoved,
 | |
| 		X:    sourceX,
 | |
| 		Y:    sourceY,
 | |
| 	}.Call(page)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to move mouse to source position: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// Step 2: Mouse down at source position
 | |
| 	err = proto.InputDispatchMouseEvent{
 | |
| 		Type:       proto.InputDispatchMouseEventTypeMousePressed,
 | |
| 		X:          sourceX,
 | |
| 		Y:          sourceY,
 | |
| 		Button:     proto.InputMouseButtonLeft,
 | |
| 		ClickCount: 1,
 | |
| 	}.Call(page)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to press mouse at source position: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// Step 3: Move mouse to target position (this creates the drag)
 | |
| 	// We'll do this in small steps to simulate realistic dragging
 | |
| 	steps := 10
 | |
| 	for i := 1; i <= steps; i++ {
 | |
| 		progress := float64(i) / float64(steps)
 | |
| 		currentX := sourceX + (targetX-sourceX)*progress
 | |
| 		currentY := sourceY + (targetY-sourceY)*progress
 | |
| 
 | |
| 		err = proto.InputDispatchMouseEvent{
 | |
| 			Type:   proto.InputDispatchMouseEventTypeMouseMoved,
 | |
| 			X:      currentX,
 | |
| 			Y:      currentY,
 | |
| 			Button: proto.InputMouseButtonLeft,
 | |
| 		}.Call(page)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("failed to move mouse during drag (step %d): %v", i, err)
 | |
| 		}
 | |
| 
 | |
| 		// Small delay between steps to make it more realistic
 | |
| 		time.Sleep(10 * time.Millisecond)
 | |
| 	}
 | |
| 
 | |
| 	// Step 4: Mouse up at target position (this completes the drop)
 | |
| 	err = proto.InputDispatchMouseEvent{
 | |
| 		Type:       proto.InputDispatchMouseEventTypeMouseReleased,
 | |
| 		X:          targetX,
 | |
| 		Y:          targetY,
 | |
| 		Button:     proto.InputMouseButtonLeft,
 | |
| 		ClickCount: 1,
 | |
| 	}.Call(page)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to release mouse at target position: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// Wait a moment for any drag and drop events to be processed
 | |
| 	time.Sleep(100 * time.Millisecond)
 | |
| 
 | |
| 	d.debugLog("Successfully completed drag and drop operation")
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // rightClick performs a right-click on an element with timeout handling
 | |
| func (d *Daemon) rightClick(tabID, selector string, timeout int) error {
 | |
| 	d.debugLog("Right-clicking element: %s", selector)
 | |
| 
 | |
| 	// Create context with timeout
 | |
| 	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
 | |
| 	defer cancel()
 | |
| 
 | |
| 	// Execute in goroutine with timeout
 | |
| 	done := make(chan error, 1)
 | |
| 	go func() {
 | |
| 		done <- d.performRightClick(tabID, selector)
 | |
| 	}()
 | |
| 
 | |
| 	select {
 | |
| 	case err := <-done:
 | |
| 		return err
 | |
| 	case <-ctx.Done():
 | |
| 		return fmt.Errorf("right-click operation timed out after %d seconds", timeout)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // performRightClick performs the actual right-click operation
 | |
| func (d *Daemon) performRightClick(tabID, selector string) error {
 | |
| 	page, err := d.getTab(tabID)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	// Find the element
 | |
| 	element, err := page.Element(selector)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to find element with selector '%s': %v", selector, err)
 | |
| 	}
 | |
| 
 | |
| 	// Get element position
 | |
| 	box, err := element.Shape()
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to get element shape: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	if len(box.Quads) == 0 {
 | |
| 		return fmt.Errorf("element has no quads")
 | |
| 	}
 | |
| 
 | |
| 	// Calculate center point
 | |
| 	quad := box.Quads[0]
 | |
| 	centerX := (quad[0] + quad[2] + quad[4] + quad[6]) / 4
 | |
| 	centerY := (quad[1] + quad[3] + quad[5] + quad[7]) / 4
 | |
| 
 | |
| 	// Perform right-click using Chrome DevTools Protocol
 | |
| 	err = proto.InputDispatchMouseEvent{
 | |
| 		Type:       proto.InputDispatchMouseEventTypeMousePressed,
 | |
| 		X:          centerX,
 | |
| 		Y:          centerY,
 | |
| 		Button:     proto.InputMouseButtonRight,
 | |
| 		ClickCount: 1,
 | |
| 	}.Call(page)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to press right mouse button: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// Release right mouse button
 | |
| 	err = proto.InputDispatchMouseEvent{
 | |
| 		Type:       proto.InputDispatchMouseEventTypeMouseReleased,
 | |
| 		X:          centerX,
 | |
| 		Y:          centerY,
 | |
| 		Button:     proto.InputMouseButtonRight,
 | |
| 		ClickCount: 1,
 | |
| 	}.Call(page)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to release right mouse button: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	d.debugLog("Successfully right-clicked element")
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // doubleClick performs a double-click on an element with timeout handling
 | |
| func (d *Daemon) doubleClick(tabID, selector string, timeout int) error {
 | |
| 	d.debugLog("Double-clicking element: %s", selector)
 | |
| 
 | |
| 	// Create context with timeout
 | |
| 	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
 | |
| 	defer cancel()
 | |
| 
 | |
| 	// Execute in goroutine with timeout
 | |
| 	done := make(chan error, 1)
 | |
| 	go func() {
 | |
| 		done <- d.performDoubleClick(tabID, selector)
 | |
| 	}()
 | |
| 
 | |
| 	select {
 | |
| 	case err := <-done:
 | |
| 		return err
 | |
| 	case <-ctx.Done():
 | |
| 		return fmt.Errorf("double-click operation timed out after %d seconds", timeout)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // performDoubleClick performs the actual double-click operation
 | |
| func (d *Daemon) performDoubleClick(tabID, selector string) error {
 | |
| 	page, err := d.getTab(tabID)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	// Find the element
 | |
| 	element, err := page.Element(selector)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to find element with selector '%s': %v", selector, err)
 | |
| 	}
 | |
| 
 | |
| 	// Get element position
 | |
| 	box, err := element.Shape()
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to get element shape: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	if len(box.Quads) == 0 {
 | |
| 		return fmt.Errorf("element has no quads")
 | |
| 	}
 | |
| 
 | |
| 	// Calculate center point
 | |
| 	quad := box.Quads[0]
 | |
| 	centerX := (quad[0] + quad[2] + quad[4] + quad[6]) / 4
 | |
| 	centerY := (quad[1] + quad[3] + quad[5] + quad[7]) / 4
 | |
| 
 | |
| 	// Perform double-click using Chrome DevTools Protocol
 | |
| 	err = proto.InputDispatchMouseEvent{
 | |
| 		Type:       proto.InputDispatchMouseEventTypeMousePressed,
 | |
| 		X:          centerX,
 | |
| 		Y:          centerY,
 | |
| 		Button:     proto.InputMouseButtonLeft,
 | |
| 		ClickCount: 2,
 | |
| 	}.Call(page)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to press mouse button for double-click: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// Release mouse button
 | |
| 	err = proto.InputDispatchMouseEvent{
 | |
| 		Type:       proto.InputDispatchMouseEventTypeMouseReleased,
 | |
| 		X:          centerX,
 | |
| 		Y:          centerY,
 | |
| 		Button:     proto.InputMouseButtonLeft,
 | |
| 		ClickCount: 2,
 | |
| 	}.Call(page)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to release mouse button for double-click: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	d.debugLog("Successfully double-clicked element")
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // middleClick performs a middle-click on an element with timeout handling
 | |
| func (d *Daemon) middleClick(tabID, selector string, timeout int) error {
 | |
| 	d.debugLog("Middle-clicking element: %s", selector)
 | |
| 
 | |
| 	// Create context with timeout
 | |
| 	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
 | |
| 	defer cancel()
 | |
| 
 | |
| 	// Execute in goroutine with timeout
 | |
| 	done := make(chan error, 1)
 | |
| 	go func() {
 | |
| 		done <- d.performMiddleClick(tabID, selector)
 | |
| 	}()
 | |
| 
 | |
| 	select {
 | |
| 	case err := <-done:
 | |
| 		return err
 | |
| 	case <-ctx.Done():
 | |
| 		return fmt.Errorf("middle-click operation timed out after %d seconds", timeout)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // performMiddleClick performs the actual middle-click operation
 | |
| func (d *Daemon) performMiddleClick(tabID, selector string) error {
 | |
| 	page, err := d.getTab(tabID)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	// Find the element
 | |
| 	element, err := page.Element(selector)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to find element with selector '%s': %v", selector, err)
 | |
| 	}
 | |
| 
 | |
| 	// Get element position
 | |
| 	box, err := element.Shape()
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to get element shape: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	if len(box.Quads) == 0 {
 | |
| 		return fmt.Errorf("element has no quads")
 | |
| 	}
 | |
| 
 | |
| 	// Calculate center point
 | |
| 	quad := box.Quads[0]
 | |
| 	centerX := (quad[0] + quad[2] + quad[4] + quad[6]) / 4
 | |
| 	centerY := (quad[1] + quad[3] + quad[5] + quad[7]) / 4
 | |
| 
 | |
| 	// Perform middle-click using Chrome DevTools Protocol
 | |
| 	err = proto.InputDispatchMouseEvent{
 | |
| 		Type:       proto.InputDispatchMouseEventTypeMousePressed,
 | |
| 		X:          centerX,
 | |
| 		Y:          centerY,
 | |
| 		Button:     proto.InputMouseButtonMiddle,
 | |
| 		ClickCount: 1,
 | |
| 	}.Call(page)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to press middle mouse button: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// Release middle mouse button
 | |
| 	err = proto.InputDispatchMouseEvent{
 | |
| 		Type:       proto.InputDispatchMouseEventTypeMouseReleased,
 | |
| 		X:          centerX,
 | |
| 		Y:          centerY,
 | |
| 		Button:     proto.InputMouseButtonMiddle,
 | |
| 		ClickCount: 1,
 | |
| 	}.Call(page)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to release middle mouse button: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	d.debugLog("Successfully middle-clicked element")
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // hover moves the mouse over an element without clicking
 | |
| func (d *Daemon) hover(tabID, selector string, timeout int) error {
 | |
| 	d.debugLog("Hovering over element: %s", selector)
 | |
| 
 | |
| 	// Create context with timeout
 | |
| 	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
 | |
| 	defer cancel()
 | |
| 
 | |
| 	// Execute in goroutine with timeout
 | |
| 	done := make(chan error, 1)
 | |
| 	go func() {
 | |
| 		done <- d.performHover(tabID, selector)
 | |
| 	}()
 | |
| 
 | |
| 	select {
 | |
| 	case err := <-done:
 | |
| 		return err
 | |
| 	case <-ctx.Done():
 | |
| 		return fmt.Errorf("hover operation timed out after %d seconds", timeout)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // performHover performs the actual hover operation
 | |
| func (d *Daemon) performHover(tabID, selector string) error {
 | |
| 	page, err := d.getTab(tabID)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	// Find the element
 | |
| 	element, err := page.Element(selector)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to find element with selector '%s': %v", selector, err)
 | |
| 	}
 | |
| 
 | |
| 	// Get element position
 | |
| 	box, err := element.Shape()
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to get element shape: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	if len(box.Quads) == 0 {
 | |
| 		return fmt.Errorf("element has no quads")
 | |
| 	}
 | |
| 
 | |
| 	// Calculate center point
 | |
| 	quad := box.Quads[0]
 | |
| 	centerX := (quad[0] + quad[2] + quad[4] + quad[6]) / 4
 | |
| 	centerY := (quad[1] + quad[3] + quad[5] + quad[7]) / 4
 | |
| 
 | |
| 	// Move mouse to element center (hover)
 | |
| 	err = proto.InputDispatchMouseEvent{
 | |
| 		Type: proto.InputDispatchMouseEventTypeMouseMoved,
 | |
| 		X:    centerX,
 | |
| 		Y:    centerY,
 | |
| 	}.Call(page)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to move mouse to element: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	d.debugLog("Successfully hovered over element")
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // mouseMove moves the mouse to specific coordinates
 | |
| func (d *Daemon) mouseMove(tabID string, x, y int, timeout int) error {
 | |
| 	d.debugLog("Moving mouse to coordinates: (%d, %d)", x, y)
 | |
| 
 | |
| 	// Create context with timeout
 | |
| 	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
 | |
| 	defer cancel()
 | |
| 
 | |
| 	// Execute in goroutine with timeout
 | |
| 	done := make(chan error, 1)
 | |
| 	go func() {
 | |
| 		done <- d.performMouseMove(tabID, x, y)
 | |
| 	}()
 | |
| 
 | |
| 	select {
 | |
| 	case err := <-done:
 | |
| 		return err
 | |
| 	case <-ctx.Done():
 | |
| 		return fmt.Errorf("mouse move operation timed out after %d seconds", timeout)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // performMouseMove performs the actual mouse move operation
 | |
| func (d *Daemon) performMouseMove(tabID string, x, y int) error {
 | |
| 	page, err := d.getTab(tabID)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	// Move mouse to coordinates
 | |
| 	err = proto.InputDispatchMouseEvent{
 | |
| 		Type: proto.InputDispatchMouseEventTypeMouseMoved,
 | |
| 		X:    float64(x),
 | |
| 		Y:    float64(y),
 | |
| 	}.Call(page)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to move mouse to coordinates: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	d.debugLog("Successfully moved mouse to coordinates")
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // scrollWheel performs mouse wheel scrolling at specific coordinates
 | |
| func (d *Daemon) scrollWheel(tabID string, x, y, deltaX, deltaY int, timeout int) error {
 | |
| 	d.debugLog("Scrolling with mouse wheel at (%d, %d) with delta (%d, %d)", x, y, deltaX, deltaY)
 | |
| 
 | |
| 	// Create context with timeout
 | |
| 	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
 | |
| 	defer cancel()
 | |
| 
 | |
| 	// Execute in goroutine with timeout
 | |
| 	done := make(chan error, 1)
 | |
| 	go func() {
 | |
| 		done <- d.performScrollWheel(tabID, x, y, deltaX, deltaY)
 | |
| 	}()
 | |
| 
 | |
| 	select {
 | |
| 	case err := <-done:
 | |
| 		return err
 | |
| 	case <-ctx.Done():
 | |
| 		return fmt.Errorf("scroll wheel operation timed out after %d seconds", timeout)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // performScrollWheel performs the actual mouse wheel scroll operation
 | |
| func (d *Daemon) performScrollWheel(tabID string, x, y, deltaX, deltaY int) error {
 | |
| 	page, err := d.getTab(tabID)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	// Perform mouse wheel scroll using Chrome DevTools Protocol
 | |
| 	err = proto.InputDispatchMouseEvent{
 | |
| 		Type:   proto.InputDispatchMouseEventTypeMouseWheel,
 | |
| 		X:      float64(x),
 | |
| 		Y:      float64(y),
 | |
| 		DeltaX: float64(deltaX),
 | |
| 		DeltaY: float64(deltaY),
 | |
| 	}.Call(page)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to perform mouse wheel scroll: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	d.debugLog("Successfully performed mouse wheel scroll")
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // keyCombination sends a key combination (e.g., "Ctrl+C", "Alt+Tab")
 | |
| func (d *Daemon) keyCombination(tabID, keys string, timeout int) error {
 | |
| 	d.debugLog("Sending key combination: %s", keys)
 | |
| 
 | |
| 	// Create context with timeout
 | |
| 	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
 | |
| 	defer cancel()
 | |
| 
 | |
| 	// Execute in goroutine with timeout
 | |
| 	done := make(chan error, 1)
 | |
| 	go func() {
 | |
| 		done <- d.performKeyCombination(tabID, keys)
 | |
| 	}()
 | |
| 
 | |
| 	select {
 | |
| 	case err := <-done:
 | |
| 		return err
 | |
| 	case <-ctx.Done():
 | |
| 		return fmt.Errorf("key combination operation timed out after %d seconds", timeout)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // performKeyCombination performs the actual key combination operation
 | |
| func (d *Daemon) performKeyCombination(tabID, keys string) error {
 | |
| 	page, err := d.getTab(tabID)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	// Parse key combination (e.g., "Ctrl+C", "Alt+Tab", "Shift+Enter")
 | |
| 	parts := strings.Split(keys, "+")
 | |
| 	if len(parts) < 2 {
 | |
| 		return fmt.Errorf("invalid key combination format: %s", keys)
 | |
| 	}
 | |
| 
 | |
| 	// Map modifier keys
 | |
| 	modifiers := 0
 | |
| 	var mainKey string
 | |
| 
 | |
| 	for i, part := range parts {
 | |
| 		part = strings.TrimSpace(part)
 | |
| 		if i == len(parts)-1 {
 | |
| 			// Last part is the main key
 | |
| 			mainKey = part
 | |
| 		} else {
 | |
| 			// Modifier keys
 | |
| 			switch strings.ToLower(part) {
 | |
| 			case "ctrl", "control":
 | |
| 				modifiers |= 2 // ControlLeft
 | |
| 			case "alt":
 | |
| 				modifiers |= 1 // AltLeft
 | |
| 			case "shift":
 | |
| 				modifiers |= 4 // ShiftLeft
 | |
| 			case "meta", "cmd", "command":
 | |
| 				modifiers |= 8 // MetaLeft
 | |
| 			default:
 | |
| 				return fmt.Errorf("unknown modifier key: %s", part)
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Convert key name to key code
 | |
| 	keyCode, err := d.getKeyCode(mainKey)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to get key code for '%s': %v", mainKey, err)
 | |
| 	}
 | |
| 
 | |
| 	// Send key down events for modifiers
 | |
| 	if modifiers&2 != 0 { // Ctrl
 | |
| 		err = proto.InputDispatchKeyEvent{
 | |
| 			Type:      proto.InputDispatchKeyEventTypeKeyDown,
 | |
| 			Key:       "Control",
 | |
| 			Code:      "ControlLeft",
 | |
| 			Modifiers: modifiers,
 | |
| 		}.Call(page)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("failed to send Ctrl key down: %v", err)
 | |
| 		}
 | |
| 	}
 | |
| 	if modifiers&1 != 0 { // Alt
 | |
| 		err = proto.InputDispatchKeyEvent{
 | |
| 			Type:      proto.InputDispatchKeyEventTypeKeyDown,
 | |
| 			Key:       "Alt",
 | |
| 			Code:      "AltLeft",
 | |
| 			Modifiers: modifiers,
 | |
| 		}.Call(page)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("failed to send Alt key down: %v", err)
 | |
| 		}
 | |
| 	}
 | |
| 	if modifiers&4 != 0 { // Shift
 | |
| 		err = proto.InputDispatchKeyEvent{
 | |
| 			Type:      proto.InputDispatchKeyEventTypeKeyDown,
 | |
| 			Key:       "Shift",
 | |
| 			Code:      "ShiftLeft",
 | |
| 			Modifiers: modifiers,
 | |
| 		}.Call(page)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("failed to send Shift key down: %v", err)
 | |
| 		}
 | |
| 	}
 | |
| 	if modifiers&8 != 0 { // Meta
 | |
| 		err = proto.InputDispatchKeyEvent{
 | |
| 			Type:      proto.InputDispatchKeyEventTypeKeyDown,
 | |
| 			Key:       "Meta",
 | |
| 			Code:      "MetaLeft",
 | |
| 			Modifiers: modifiers,
 | |
| 		}.Call(page)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("failed to send Meta key down: %v", err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Send main key down
 | |
| 	err = proto.InputDispatchKeyEvent{
 | |
| 		Type:      proto.InputDispatchKeyEventTypeKeyDown,
 | |
| 		Key:       mainKey,
 | |
| 		Code:      keyCode,
 | |
| 		Modifiers: modifiers,
 | |
| 	}.Call(page)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to send main key down: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// Send main key up
 | |
| 	err = proto.InputDispatchKeyEvent{
 | |
| 		Type:      proto.InputDispatchKeyEventTypeKeyUp,
 | |
| 		Key:       mainKey,
 | |
| 		Code:      keyCode,
 | |
| 		Modifiers: modifiers,
 | |
| 	}.Call(page)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to send main key up: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// Send key up events for modifiers (in reverse order)
 | |
| 	if modifiers&8 != 0 { // Meta
 | |
| 		err = proto.InputDispatchKeyEvent{
 | |
| 			Type:      proto.InputDispatchKeyEventTypeKeyUp,
 | |
| 			Key:       "Meta",
 | |
| 			Code:      "MetaLeft",
 | |
| 			Modifiers: modifiers,
 | |
| 		}.Call(page)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("failed to send Meta key up: %v", err)
 | |
| 		}
 | |
| 	}
 | |
| 	if modifiers&4 != 0 { // Shift
 | |
| 		err = proto.InputDispatchKeyEvent{
 | |
| 			Type:      proto.InputDispatchKeyEventTypeKeyUp,
 | |
| 			Key:       "Shift",
 | |
| 			Code:      "ShiftLeft",
 | |
| 			Modifiers: modifiers,
 | |
| 		}.Call(page)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("failed to send Shift key up: %v", err)
 | |
| 		}
 | |
| 	}
 | |
| 	if modifiers&1 != 0 { // Alt
 | |
| 		err = proto.InputDispatchKeyEvent{
 | |
| 			Type:      proto.InputDispatchKeyEventTypeKeyUp,
 | |
| 			Key:       "Alt",
 | |
| 			Code:      "AltLeft",
 | |
| 			Modifiers: modifiers,
 | |
| 		}.Call(page)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("failed to send Alt key up: %v", err)
 | |
| 		}
 | |
| 	}
 | |
| 	if modifiers&2 != 0 { // Ctrl
 | |
| 		err = proto.InputDispatchKeyEvent{
 | |
| 			Type:      proto.InputDispatchKeyEventTypeKeyUp,
 | |
| 			Key:       "Control",
 | |
| 			Code:      "ControlLeft",
 | |
| 			Modifiers: modifiers,
 | |
| 		}.Call(page)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("failed to send Ctrl key up: %v", err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	d.debugLog("Successfully sent key combination")
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // getKeyCode converts a key name to its corresponding key code
 | |
| func (d *Daemon) getKeyCode(key string) (string, error) {
 | |
| 	// Map common key names to their codes
 | |
| 	keyMap := map[string]string{
 | |
| 		// Letters
 | |
| 		"a": "KeyA", "b": "KeyB", "c": "KeyC", "d": "KeyD", "e": "KeyE", "f": "KeyF",
 | |
| 		"g": "KeyG", "h": "KeyH", "i": "KeyI", "j": "KeyJ", "k": "KeyK", "l": "KeyL",
 | |
| 		"m": "KeyM", "n": "KeyN", "o": "KeyO", "p": "KeyP", "q": "KeyQ", "r": "KeyR",
 | |
| 		"s": "KeyS", "t": "KeyT", "u": "KeyU", "v": "KeyV", "w": "KeyW", "x": "KeyX",
 | |
| 		"y": "KeyY", "z": "KeyZ",
 | |
| 
 | |
| 		// Numbers
 | |
| 		"0": "Digit0", "1": "Digit1", "2": "Digit2", "3": "Digit3", "4": "Digit4",
 | |
| 		"5": "Digit5", "6": "Digit6", "7": "Digit7", "8": "Digit8", "9": "Digit9",
 | |
| 
 | |
| 		// Function keys
 | |
| 		"F1": "F1", "F2": "F2", "F3": "F3", "F4": "F4", "F5": "F5", "F6": "F6",
 | |
| 		"F7": "F7", "F8": "F8", "F9": "F9", "F10": "F10", "F11": "F11", "F12": "F12",
 | |
| 
 | |
| 		// Special keys
 | |
| 		"Enter": "Enter", "Return": "Enter",
 | |
| 		"Escape": "Escape", "Esc": "Escape",
 | |
| 		"Tab":   "Tab",
 | |
| 		"Space": "Space", " ": "Space",
 | |
| 		"Backspace": "Backspace",
 | |
| 		"Delete":    "Delete", "Del": "Delete",
 | |
| 		"Insert": "Insert", "Ins": "Insert",
 | |
| 		"Home":   "Home",
 | |
| 		"End":    "End",
 | |
| 		"PageUp": "PageUp", "PgUp": "PageUp",
 | |
| 		"PageDown": "PageDown", "PgDn": "PageDown",
 | |
| 
 | |
| 		// Arrow keys
 | |
| 		"ArrowUp": "ArrowUp", "Up": "ArrowUp",
 | |
| 		"ArrowDown": "ArrowDown", "Down": "ArrowDown",
 | |
| 		"ArrowLeft": "ArrowLeft", "Left": "ArrowLeft",
 | |
| 		"ArrowRight": "ArrowRight", "Right": "ArrowRight",
 | |
| 
 | |
| 		// Punctuation
 | |
| 		";": "Semicolon", ":": "Semicolon",
 | |
| 		"=": "Equal", "+": "Equal",
 | |
| 		",": "Comma", "<": "Comma",
 | |
| 		"-": "Minus", "_": "Minus",
 | |
| 		".": "Period", ">": "Period",
 | |
| 		"/": "Slash", "?": "Slash",
 | |
| 		"`": "Backquote", "~": "Backquote",
 | |
| 		"[": "BracketLeft", "{": "BracketLeft",
 | |
| 		"\\": "Backslash", "|": "Backslash",
 | |
| 		"]": "BracketRight", "}": "BracketRight",
 | |
| 		"'": "Quote", "\"": "Quote",
 | |
| 	}
 | |
| 
 | |
| 	// Convert to lowercase for lookup
 | |
| 	lowerKey := strings.ToLower(key)
 | |
| 	if code, exists := keyMap[lowerKey]; exists {
 | |
| 		return code, nil
 | |
| 	}
 | |
| 
 | |
| 	// If not found in map, try the key as-is (might be a valid code already)
 | |
| 	return key, nil
 | |
| }
 | |
| 
 | |
| // specialKey sends a special key (e.g., "Enter", "Escape", "Tab", "F1", "ArrowUp")
 | |
| func (d *Daemon) specialKey(tabID, key string, timeout int) error {
 | |
| 	d.debugLog("Sending special key: %s", key)
 | |
| 
 | |
| 	// Create context with timeout
 | |
| 	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
 | |
| 	defer cancel()
 | |
| 
 | |
| 	// Execute in goroutine with timeout
 | |
| 	done := make(chan error, 1)
 | |
| 	go func() {
 | |
| 		done <- d.performSpecialKey(tabID, key)
 | |
| 	}()
 | |
| 
 | |
| 	select {
 | |
| 	case err := <-done:
 | |
| 		return err
 | |
| 	case <-ctx.Done():
 | |
| 		return fmt.Errorf("special key operation timed out after %d seconds", timeout)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // performSpecialKey performs the actual special key operation
 | |
| func (d *Daemon) performSpecialKey(tabID, key string) error {
 | |
| 	page, err := d.getTab(tabID)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	// Convert key name to key code
 | |
| 	keyCode, err := d.getKeyCode(key)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to get key code for '%s': %v", key, err)
 | |
| 	}
 | |
| 
 | |
| 	// Send key down
 | |
| 	err = proto.InputDispatchKeyEvent{
 | |
| 		Type: proto.InputDispatchKeyEventTypeKeyDown,
 | |
| 		Key:  key,
 | |
| 		Code: keyCode,
 | |
| 	}.Call(page)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to send key down: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// Send key up
 | |
| 	err = proto.InputDispatchKeyEvent{
 | |
| 		Type: proto.InputDispatchKeyEventTypeKeyUp,
 | |
| 		Key:  key,
 | |
| 		Code: keyCode,
 | |
| 	}.Call(page)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to send key up: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	d.debugLog("Successfully sent special key")
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // modifierClick performs a click with modifier keys (e.g., Ctrl+click, Shift+click)
 | |
| func (d *Daemon) modifierClick(tabID, selector, modifiers string, timeout int) error {
 | |
| 	d.debugLog("Performing modifier click on element: %s with modifiers: %s", selector, modifiers)
 | |
| 
 | |
| 	// Create context with timeout
 | |
| 	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
 | |
| 	defer cancel()
 | |
| 
 | |
| 	// Execute in goroutine with timeout
 | |
| 	done := make(chan error, 1)
 | |
| 	go func() {
 | |
| 		done <- d.performModifierClick(tabID, selector, modifiers)
 | |
| 	}()
 | |
| 
 | |
| 	select {
 | |
| 	case err := <-done:
 | |
| 		return err
 | |
| 	case <-ctx.Done():
 | |
| 		return fmt.Errorf("modifier click operation timed out after %d seconds", timeout)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // performModifierClick performs the actual modifier click operation
 | |
| func (d *Daemon) performModifierClick(tabID, selector, modifiers string) error {
 | |
| 	page, err := d.getTab(tabID)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	// Find the element
 | |
| 	element, err := page.Element(selector)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to find element with selector '%s': %v", selector, err)
 | |
| 	}
 | |
| 
 | |
| 	// Get element position
 | |
| 	box, err := element.Shape()
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to get element shape: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	if len(box.Quads) == 0 {
 | |
| 		return fmt.Errorf("element has no quads")
 | |
| 	}
 | |
| 
 | |
| 	// Calculate center point
 | |
| 	quad := box.Quads[0]
 | |
| 	centerX := (quad[0] + quad[2] + quad[4] + quad[6]) / 4
 | |
| 	centerY := (quad[1] + quad[3] + quad[5] + quad[7]) / 4
 | |
| 
 | |
| 	// Parse modifiers
 | |
| 	modifierBits := 0
 | |
| 	modifierParts := strings.Split(modifiers, "+")
 | |
| 	for _, mod := range modifierParts {
 | |
| 		mod = strings.TrimSpace(strings.ToLower(mod))
 | |
| 		switch mod {
 | |
| 		case "ctrl", "control":
 | |
| 			modifierBits |= 2 // ControlLeft
 | |
| 		case "alt":
 | |
| 			modifierBits |= 1 // AltLeft
 | |
| 		case "shift":
 | |
| 			modifierBits |= 4 // ShiftLeft
 | |
| 		case "meta", "cmd", "command":
 | |
| 			modifierBits |= 8 // MetaLeft
 | |
| 		default:
 | |
| 			return fmt.Errorf("unknown modifier: %s", mod)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Send modifier key down events
 | |
| 	if modifierBits&2 != 0 { // Ctrl
 | |
| 		err = proto.InputDispatchKeyEvent{
 | |
| 			Type:      proto.InputDispatchKeyEventTypeKeyDown,
 | |
| 			Key:       "Control",
 | |
| 			Code:      "ControlLeft",
 | |
| 			Modifiers: modifierBits,
 | |
| 		}.Call(page)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("failed to send Ctrl key down: %v", err)
 | |
| 		}
 | |
| 	}
 | |
| 	if modifierBits&1 != 0 { // Alt
 | |
| 		err = proto.InputDispatchKeyEvent{
 | |
| 			Type:      proto.InputDispatchKeyEventTypeKeyDown,
 | |
| 			Key:       "Alt",
 | |
| 			Code:      "AltLeft",
 | |
| 			Modifiers: modifierBits,
 | |
| 		}.Call(page)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("failed to send Alt key down: %v", err)
 | |
| 		}
 | |
| 	}
 | |
| 	if modifierBits&4 != 0 { // Shift
 | |
| 		err = proto.InputDispatchKeyEvent{
 | |
| 			Type:      proto.InputDispatchKeyEventTypeKeyDown,
 | |
| 			Key:       "Shift",
 | |
| 			Code:      "ShiftLeft",
 | |
| 			Modifiers: modifierBits,
 | |
| 		}.Call(page)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("failed to send Shift key down: %v", err)
 | |
| 		}
 | |
| 	}
 | |
| 	if modifierBits&8 != 0 { // Meta
 | |
| 		err = proto.InputDispatchKeyEvent{
 | |
| 			Type:      proto.InputDispatchKeyEventTypeKeyDown,
 | |
| 			Key:       "Meta",
 | |
| 			Code:      "MetaLeft",
 | |
| 			Modifiers: modifierBits,
 | |
| 		}.Call(page)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("failed to send Meta key down: %v", err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Perform click with modifiers
 | |
| 	err = proto.InputDispatchMouseEvent{
 | |
| 		Type:       proto.InputDispatchMouseEventTypeMousePressed,
 | |
| 		X:          centerX,
 | |
| 		Y:          centerY,
 | |
| 		Button:     proto.InputMouseButtonLeft,
 | |
| 		ClickCount: 1,
 | |
| 		Modifiers:  modifierBits,
 | |
| 	}.Call(page)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to press mouse button with modifiers: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// Release mouse button
 | |
| 	err = proto.InputDispatchMouseEvent{
 | |
| 		Type:       proto.InputDispatchMouseEventTypeMouseReleased,
 | |
| 		X:          centerX,
 | |
| 		Y:          centerY,
 | |
| 		Button:     proto.InputMouseButtonLeft,
 | |
| 		ClickCount: 1,
 | |
| 		Modifiers:  modifierBits,
 | |
| 	}.Call(page)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to release mouse button with modifiers: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// Send modifier key up events (in reverse order)
 | |
| 	if modifierBits&8 != 0 { // Meta
 | |
| 		err = proto.InputDispatchKeyEvent{
 | |
| 			Type:      proto.InputDispatchKeyEventTypeKeyUp,
 | |
| 			Key:       "Meta",
 | |
| 			Code:      "MetaLeft",
 | |
| 			Modifiers: modifierBits,
 | |
| 		}.Call(page)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("failed to send Meta key up: %v", err)
 | |
| 		}
 | |
| 	}
 | |
| 	if modifierBits&4 != 0 { // Shift
 | |
| 		err = proto.InputDispatchKeyEvent{
 | |
| 			Type:      proto.InputDispatchKeyEventTypeKeyUp,
 | |
| 			Key:       "Shift",
 | |
| 			Code:      "ShiftLeft",
 | |
| 			Modifiers: modifierBits,
 | |
| 		}.Call(page)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("failed to send Shift key up: %v", err)
 | |
| 		}
 | |
| 	}
 | |
| 	if modifierBits&1 != 0 { // Alt
 | |
| 		err = proto.InputDispatchKeyEvent{
 | |
| 			Type:      proto.InputDispatchKeyEventTypeKeyUp,
 | |
| 			Key:       "Alt",
 | |
| 			Code:      "AltLeft",
 | |
| 			Modifiers: modifierBits,
 | |
| 		}.Call(page)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("failed to send Alt key up: %v", err)
 | |
| 		}
 | |
| 	}
 | |
| 	if modifierBits&2 != 0 { // Ctrl
 | |
| 		err = proto.InputDispatchKeyEvent{
 | |
| 			Type:      proto.InputDispatchKeyEventTypeKeyUp,
 | |
| 			Key:       "Control",
 | |
| 			Code:      "ControlLeft",
 | |
| 			Modifiers: modifierBits,
 | |
| 		}.Call(page)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("failed to send Ctrl key up: %v", err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	d.debugLog("Successfully performed modifier click")
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // Placeholder implementations for remaining methods
 | |
| // These will be fully implemented in subsequent updates
 | |
| 
 | |
| func (d *Daemon) touchTap(tabID string, x, y int, timeout int) error {
 | |
| 	return fmt.Errorf("touch-tap not yet implemented")
 | |
| }
 | |
| 
 | |
| func (d *Daemon) touchLongPress(tabID string, x, y, duration int, timeout int) error {
 | |
| 	return fmt.Errorf("touch-long-press not yet implemented")
 | |
| }
 | |
| 
 | |
| func (d *Daemon) touchSwipe(tabID string, startX, startY, endX, endY int, timeout int) error {
 | |
| 	return fmt.Errorf("touch-swipe not yet implemented")
 | |
| }
 | |
| 
 | |
| func (d *Daemon) pinchZoom(tabID string, centerX, centerY int, scale float64, timeout int) error {
 | |
| 	return fmt.Errorf("pinch-zoom not yet implemented")
 | |
| }
 | |
| 
 | |
| func (d *Daemon) scrollElement(tabID, selector string, deltaX, deltaY int, timeout int) error {
 | |
| 	return fmt.Errorf("scroll-element not yet implemented")
 | |
| }
 | |
| 
 | |
| func (d *Daemon) scrollToCoordinates(tabID string, x, y int, timeout int) error {
 | |
| 	return fmt.Errorf("scroll-to-coordinates not yet implemented")
 | |
| }
 | |
| 
 | |
| func (d *Daemon) selectText(tabID, selector string, startIndex, endIndex int, timeout int) error {
 | |
| 	return fmt.Errorf("select-text not yet implemented")
 | |
| }
 | |
| 
 | |
| func (d *Daemon) selectAllText(tabID, selector string, timeout int) error {
 | |
| 	return fmt.Errorf("select-all-text not yet implemented")
 | |
| }
 | |
| 
 | |
| // AxeResults represents the results from running axe-core accessibility tests
 | |
| type AxeResults struct {
 | |
| 	Violations   []AxeViolation    `json:"violations"`
 | |
| 	Passes       []AxePass         `json:"passes"`
 | |
| 	Incomplete   []AxeIncomplete   `json:"incomplete"`
 | |
| 	Inapplicable []AxeInapplicable `json:"inapplicable"`
 | |
| 	TestEngine   AxeTestEngine     `json:"testEngine"`
 | |
| 	TestRunner   AxeTestRunner     `json:"testRunner"`
 | |
| 	Timestamp    string            `json:"timestamp"`
 | |
| 	URL          string            `json:"url"`
 | |
| }
 | |
| 
 | |
| // AxeViolation represents an accessibility violation found by axe-core
 | |
| type AxeViolation struct {
 | |
| 	ID          string    `json:"id"`
 | |
| 	Impact      string    `json:"impact"`
 | |
| 	Tags        []string  `json:"tags"`
 | |
| 	Description string    `json:"description"`
 | |
| 	Help        string    `json:"help"`
 | |
| 	HelpURL     string    `json:"helpUrl"`
 | |
| 	Nodes       []AxeNode `json:"nodes"`
 | |
| }
 | |
| 
 | |
| // AxePass represents an accessibility check that passed
 | |
| type AxePass struct {
 | |
| 	ID          string    `json:"id"`
 | |
| 	Impact      string    `json:"impact"`
 | |
| 	Tags        []string  `json:"tags"`
 | |
| 	Description string    `json:"description"`
 | |
| 	Help        string    `json:"help"`
 | |
| 	HelpURL     string    `json:"helpUrl"`
 | |
| 	Nodes       []AxeNode `json:"nodes"`
 | |
| }
 | |
| 
 | |
| // AxeIncomplete represents an accessibility check that needs manual review
 | |
| type AxeIncomplete struct {
 | |
| 	ID          string    `json:"id"`
 | |
| 	Impact      string    `json:"impact"`
 | |
| 	Tags        []string  `json:"tags"`
 | |
| 	Description string    `json:"description"`
 | |
| 	Help        string    `json:"help"`
 | |
| 	HelpURL     string    `json:"helpUrl"`
 | |
| 	Nodes       []AxeNode `json:"nodes"`
 | |
| }
 | |
| 
 | |
| // AxeInapplicable represents an accessibility check that doesn't apply to this page
 | |
| type AxeInapplicable struct {
 | |
| 	ID          string   `json:"id"`
 | |
| 	Impact      string   `json:"impact"`
 | |
| 	Tags        []string `json:"tags"`
 | |
| 	Description string   `json:"description"`
 | |
| 	Help        string   `json:"help"`
 | |
| 	HelpURL     string   `json:"helpUrl"`
 | |
| }
 | |
| 
 | |
| // AxeNode represents a specific DOM node with accessibility issues
 | |
| type AxeNode struct {
 | |
| 	HTML   string           `json:"html"`
 | |
| 	Impact string           `json:"impact"`
 | |
| 	Target []string         `json:"target"`
 | |
| 	Any    []AxeCheckResult `json:"any"`
 | |
| 	All    []AxeCheckResult `json:"all"`
 | |
| 	None   []AxeCheckResult `json:"none"`
 | |
| }
 | |
| 
 | |
| // AxeCheckResult represents the result of a specific accessibility check
 | |
| type AxeCheckResult struct {
 | |
| 	ID      string          `json:"id"`
 | |
| 	Impact  string          `json:"impact"`
 | |
| 	Message string          `json:"message"`
 | |
| 	Data    json.RawMessage `json:"data"` // Can be string or object, use RawMessage
 | |
| }
 | |
| 
 | |
| // AxeTestEngine represents the axe-core test engine information
 | |
| type AxeTestEngine struct {
 | |
| 	Name    string `json:"name"`
 | |
| 	Version string `json:"version"`
 | |
| }
 | |
| 
 | |
| // AxeTestRunner represents the test runner information
 | |
| type AxeTestRunner struct {
 | |
| 	Name string `json:"name"`
 | |
| }
 | |
| 
 | |
| // injectLibrary injects a JavaScript library from URL or known library name
 | |
| func (d *Daemon) injectLibrary(tabID string, library string, timeout int) error {
 | |
| 	d.debugLog("Injecting library for tab: %s, library: %s", tabID, library)
 | |
| 
 | |
| 	page, err := d.getTab(tabID)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to get page: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// Map of known libraries to their CDN URLs
 | |
| 	knownLibraries := map[string]string{
 | |
| 		"axe":        "https://cdn.jsdelivr.net/npm/axe-core@4.8.0/axe.min.js",
 | |
| 		"axe-core":   "https://cdn.jsdelivr.net/npm/axe-core@4.8.0/axe.min.js",
 | |
| 		"jquery":     "https://code.jquery.com/jquery-3.7.1.min.js",
 | |
| 		"lodash":     "https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js",
 | |
| 		"moment":     "https://cdn.jsdelivr.net/npm/moment@2.29.4/moment.min.js",
 | |
| 		"underscore": "https://cdn.jsdelivr.net/npm/underscore@1.13.6/underscore-min.js",
 | |
| 	}
 | |
| 
 | |
| 	// Determine the URL to inject
 | |
| 	var libraryURL string
 | |
| 	if strings.HasPrefix(library, "http://") || strings.HasPrefix(library, "https://") {
 | |
| 		// Direct URL provided
 | |
| 		libraryURL = library
 | |
| 	} else {
 | |
| 		// Check if it's a known library
 | |
| 		if url, ok := knownLibraries[strings.ToLower(library)]; ok {
 | |
| 			libraryURL = url
 | |
| 		} else {
 | |
| 			return fmt.Errorf("unknown library '%s' and not a valid URL", library)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// JavaScript code to inject the library
 | |
| 	injectCode := fmt.Sprintf(`() => {
 | |
| 		return new Promise((resolve, reject) => {
 | |
| 			// Check if script is already loaded
 | |
| 			const existingScript = document.querySelector('script[src="%s"]');
 | |
| 			if (existingScript) {
 | |
| 				resolve(true);
 | |
| 				return;
 | |
| 			}
 | |
| 
 | |
| 			const script = document.createElement('script');
 | |
| 			script.src = '%s';
 | |
| 			script.onload = () => resolve(true);
 | |
| 			script.onerror = () => reject(new Error('Failed to load library from %s'));
 | |
| 			document.head.appendChild(script);
 | |
| 		});
 | |
| 	}`, libraryURL, libraryURL, libraryURL)
 | |
| 
 | |
| 	// Execute with timeout
 | |
| 	if timeout > 0 {
 | |
| 		ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
 | |
| 		defer cancel()
 | |
| 
 | |
| 		done := make(chan error, 1)
 | |
| 		go func() {
 | |
| 			_, err := page.Eval(injectCode)
 | |
| 			done <- err
 | |
| 		}()
 | |
| 
 | |
| 		select {
 | |
| 		case err := <-done:
 | |
| 			if err != nil {
 | |
| 				return fmt.Errorf("failed to inject library: %w", err)
 | |
| 			}
 | |
| 		case <-ctx.Done():
 | |
| 			return fmt.Errorf("library injection timed out after %d seconds", timeout)
 | |
| 		}
 | |
| 	} else {
 | |
| 		_, err = page.Eval(injectCode)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("failed to inject library: %w", err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	d.debugLog("Successfully injected library: %s", library)
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // injectAxeCore injects the axe-core library into the page
 | |
| func (d *Daemon) injectAxeCore(tabID string, axeVersion string, timeout int) error {
 | |
| 	d.debugLog("Injecting axe-core library into tab: %s (version: %s)", tabID, axeVersion)
 | |
| 
 | |
| 	page, err := d.getTab(tabID)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to get page: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// Default to latest stable version if not specified
 | |
| 	if axeVersion == "" {
 | |
| 		axeVersion = "4.8.0"
 | |
| 	}
 | |
| 
 | |
| 	// Check if axe is already loaded
 | |
| 	checkCode := `() => typeof axe !== 'undefined'`
 | |
| 	checkResult, err := page.Eval(checkCode)
 | |
| 	if err == nil && checkResult.Value.Bool() {
 | |
| 		d.debugLog("axe-core already loaded in tab: %s", tabID)
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	// Inject axe-core from CDN
 | |
| 	injectCode := fmt.Sprintf(`() => {
 | |
| 		return new Promise((resolve, reject) => {
 | |
| 			const script = document.createElement('script');
 | |
| 			script.src = 'https://cdn.jsdelivr.net/npm/axe-core@%s/axe.min.js';
 | |
| 			script.onload = () => resolve(true);
 | |
| 			script.onerror = () => reject(new Error('Failed to load axe-core'));
 | |
| 			document.head.appendChild(script);
 | |
| 		});
 | |
| 	}`, axeVersion)
 | |
| 
 | |
| 	// Execute injection with timeout
 | |
| 	if timeout > 0 {
 | |
| 		ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
 | |
| 		defer cancel()
 | |
| 
 | |
| 		done := make(chan error, 1)
 | |
| 		go func() {
 | |
| 			_, err := page.Eval(injectCode)
 | |
| 			done <- err
 | |
| 		}()
 | |
| 
 | |
| 		select {
 | |
| 		case err := <-done:
 | |
| 			if err != nil {
 | |
| 				return fmt.Errorf("failed to inject axe-core: %w", err)
 | |
| 			}
 | |
| 		case <-ctx.Done():
 | |
| 			return fmt.Errorf("axe-core injection timed out after %d seconds", timeout)
 | |
| 		}
 | |
| 	} else {
 | |
| 		_, err := page.Eval(injectCode)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("failed to inject axe-core: %w", err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	d.debugLog("Successfully injected axe-core into tab: %s", tabID)
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // runAxeCore runs axe-core accessibility tests on the page
 | |
| func (d *Daemon) runAxeCore(tabID string, options map[string]interface{}, timeout int) (*AxeResults, error) {
 | |
| 	d.debugLog("Running axe-core tests for tab: %s", tabID)
 | |
| 
 | |
| 	page, err := d.getTab(tabID)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to get page: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// Check if axe is loaded
 | |
| 	checkCode := `() => typeof axe !== 'undefined'`
 | |
| 	checkResult, err := page.Eval(checkCode)
 | |
| 	if err != nil || !checkResult.Value.Bool() {
 | |
| 		return nil, fmt.Errorf("axe-core is not loaded - call inject-axe first")
 | |
| 	}
 | |
| 
 | |
| 	// Build axe.run() options
 | |
| 	optionsJSON := "{}"
 | |
| 	if options != nil && len(options) > 0 {
 | |
| 		optionsBytes, err := json.Marshal(options)
 | |
| 		if err != nil {
 | |
| 			return nil, fmt.Errorf("failed to marshal options: %w", err)
 | |
| 		}
 | |
| 		optionsJSON = string(optionsBytes)
 | |
| 	}
 | |
| 
 | |
| 	// Run axe tests - axe.run() returns a Promise, so we need to await it and stringify
 | |
| 	runCode := fmt.Sprintf(`async () => {
 | |
| 		const results = await axe.run(%s);
 | |
| 		return JSON.stringify(results);
 | |
| 	}`, optionsJSON)
 | |
| 
 | |
| 	var jsResult *proto.RuntimeRemoteObject
 | |
| 	if timeout > 0 {
 | |
| 		ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
 | |
| 		defer cancel()
 | |
| 
 | |
| 		done := make(chan struct {
 | |
| 			result *proto.RuntimeRemoteObject
 | |
| 			err    error
 | |
| 		}, 1)
 | |
| 
 | |
| 		go func() {
 | |
| 			result, err := page.Eval(runCode)
 | |
| 			done <- struct {
 | |
| 				result *proto.RuntimeRemoteObject
 | |
| 				err    error
 | |
| 			}{result, err}
 | |
| 		}()
 | |
| 
 | |
| 		select {
 | |
| 		case res := <-done:
 | |
| 			if res.err != nil {
 | |
| 				return nil, fmt.Errorf("failed to run axe-core: %w", res.err)
 | |
| 			}
 | |
| 			jsResult = res.result
 | |
| 		case <-ctx.Done():
 | |
| 			return nil, fmt.Errorf("axe-core execution timed out after %d seconds", timeout)
 | |
| 		}
 | |
| 	} else {
 | |
| 		jsResult, err = page.Eval(runCode)
 | |
| 		if err != nil {
 | |
| 			return nil, fmt.Errorf("failed to run axe-core: %w", err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Parse the results
 | |
| 	resultsJSON := jsResult.Value.Str()
 | |
| 	var results AxeResults
 | |
| 	err = json.Unmarshal([]byte(resultsJSON), &results)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to parse axe-core results: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	d.debugLog("Successfully ran axe-core tests for tab: %s (found %d violations)", tabID, len(results.Violations))
 | |
| 	return &results, nil
 | |
| }
 | |
| 
 | |
| // ContrastCheckResult represents the result of contrast checking for text elements
 | |
| type ContrastCheckResult struct {
 | |
| 	TotalElements int                    `json:"total_elements"`
 | |
| 	PassedAA      int                    `json:"passed_aa"`
 | |
| 	PassedAAA     int                    `json:"passed_aaa"`
 | |
| 	FailedAA      int                    `json:"failed_aa"`
 | |
| 	FailedAAA     int                    `json:"failed_aaa"`
 | |
| 	UnableToCheck int                    `json:"unable_to_check"`
 | |
| 	Elements      []ContrastCheckElement `json:"elements"`
 | |
| }
 | |
| 
 | |
| // ContrastCheckElement represents a single element's contrast check
 | |
| type ContrastCheckElement struct {
 | |
| 	Selector        string  `json:"selector"`
 | |
| 	Text            string  `json:"text"`
 | |
| 	ForegroundColor string  `json:"foreground_color"`
 | |
| 	BackgroundColor string  `json:"background_color"`
 | |
| 	ContrastRatio   float64 `json:"contrast_ratio"`
 | |
| 	FontSize        string  `json:"font_size"`
 | |
| 	FontWeight      string  `json:"font_weight"`
 | |
| 	IsLargeText     bool    `json:"is_large_text"`
 | |
| 	PassesAA        bool    `json:"passes_aa"`
 | |
| 	PassesAAA       bool    `json:"passes_aaa"`
 | |
| 	RequiredAA      float64 `json:"required_aa"`
 | |
| 	RequiredAAA     float64 `json:"required_aaa"`
 | |
| 	Error           string  `json:"error,omitempty"`
 | |
| }
 | |
| 
 | |
| // checkContrast checks color contrast for text elements on the page
 | |
| func (d *Daemon) checkContrast(tabID string, selector string, timeout int) (*ContrastCheckResult, error) {
 | |
| 	d.debugLog("Checking contrast for tab: %s, selector: %s", tabID, selector)
 | |
| 
 | |
| 	page, err := d.getTab(tabID)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to get page: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// Default selector to check all text elements
 | |
| 	if selector == "" {
 | |
| 		selector = "p, h1, h2, h3, h4, h5, h6, a, button, span, div, li, td, th, label, input, textarea"
 | |
| 	}
 | |
| 
 | |
| 	// JavaScript code to check contrast for all matching elements
 | |
| 	jsCode := fmt.Sprintf(`() => {
 | |
| 		// Helper function to parse RGB color
 | |
| 		function parseColor(colorStr) {
 | |
| 			const rgb = colorStr.match(/\d+/g);
 | |
| 			if (!rgb || rgb.length < 3) return null;
 | |
| 			return {
 | |
| 				r: parseInt(rgb[0]),
 | |
| 				g: parseInt(rgb[1]),
 | |
| 				b: parseInt(rgb[2]),
 | |
| 				a: rgb.length > 3 ? parseFloat(rgb[3]) : 1
 | |
| 			};
 | |
| 		}
 | |
| 
 | |
| 		// Helper function to calculate relative luminance
 | |
| 		function getLuminance(r, g, b) {
 | |
| 			const rsRGB = r / 255;
 | |
| 			const gsRGB = g / 255;
 | |
| 			const bsRGB = b / 255;
 | |
| 
 | |
| 			const r2 = rsRGB <= 0.03928 ? rsRGB / 12.92 : Math.pow((rsRGB + 0.055) / 1.055, 2.4);
 | |
| 			const g2 = gsRGB <= 0.03928 ? gsRGB / 12.92 : Math.pow((gsRGB + 0.055) / 1.055, 2.4);
 | |
| 			const b2 = bsRGB <= 0.03928 ? bsRGB / 12.92 : Math.pow((bsRGB + 0.055) / 1.055, 2.4);
 | |
| 
 | |
| 			return 0.2126 * r2 + 0.7152 * g2 + 0.0722 * b2;
 | |
| 		}
 | |
| 
 | |
| 		// Helper function to calculate contrast ratio
 | |
| 		function getContrastRatio(fg, bg) {
 | |
| 			const l1 = getLuminance(fg.r, fg.g, fg.b);
 | |
| 			const l2 = getLuminance(bg.r, bg.g, bg.b);
 | |
| 			const lighter = Math.max(l1, l2);
 | |
| 			const darker = Math.min(l1, l2);
 | |
| 			return (lighter + 0.05) / (darker + 0.05);
 | |
| 		}
 | |
| 
 | |
| 		// Helper function to get effective background color
 | |
| 		function getEffectiveBackground(element) {
 | |
| 			let current = element;
 | |
| 			while (current && current !== document.body.parentElement) {
 | |
| 				const style = window.getComputedStyle(current);
 | |
| 				const bgColor = style.backgroundColor;
 | |
| 				const parsed = parseColor(bgColor);
 | |
| 
 | |
| 				if (parsed && parsed.a > 0) {
 | |
| 					// Check if it's not transparent
 | |
| 					if (!(parsed.r === 0 && parsed.g === 0 && parsed.b === 0 && parsed.a === 0)) {
 | |
| 						return bgColor;
 | |
| 					}
 | |
| 				}
 | |
| 
 | |
| 				current = current.parentElement;
 | |
| 			}
 | |
| 			return 'rgb(255, 255, 255)'; // Default to white
 | |
| 		}
 | |
| 
 | |
| 		// Helper function to check if text is large
 | |
| 		function isLargeText(fontSize, fontWeight) {
 | |
| 			const size = parseFloat(fontSize);
 | |
| 			const weight = parseInt(fontWeight) || 400;
 | |
| 
 | |
| 			// 18pt (24px) or larger, or 14pt (18.66px) bold or larger
 | |
| 			return size >= 24 || (size >= 18.66 && weight >= 700);
 | |
| 		}
 | |
| 
 | |
| 		// Get all matching elements
 | |
| 		const elements = document.querySelectorAll('%s');
 | |
| 		const results = [];
 | |
| 
 | |
| 		elements.forEach((element, index) => {
 | |
| 			try {
 | |
| 				// Skip if element has no text content
 | |
| 				const text = element.textContent.trim();
 | |
| 				if (!text || text.length === 0) return;
 | |
| 
 | |
| 				// Get computed styles
 | |
| 				const style = window.getComputedStyle(element);
 | |
| 				const fgColor = style.color;
 | |
| 				const bgColor = getEffectiveBackground(element);
 | |
| 				const fontSize = style.fontSize;
 | |
| 				const fontWeight = style.fontWeight;
 | |
| 
 | |
| 				// Parse colors
 | |
| 				const fg = parseColor(fgColor);
 | |
| 				const bg = parseColor(bgColor);
 | |
| 
 | |
| 				if (!fg || !bg) {
 | |
| 					results.push({
 | |
| 						selector: '%s:nth-of-type(' + (index + 1) + ')',
 | |
| 						text: text.substring(0, 100),
 | |
| 						error: 'Unable to parse colors'
 | |
| 					});
 | |
| 					return;
 | |
| 				}
 | |
| 
 | |
| 				// Calculate contrast ratio
 | |
| 				const ratio = getContrastRatio(fg, bg);
 | |
| 				const large = isLargeText(fontSize, fontWeight);
 | |
| 
 | |
| 				// WCAG requirements
 | |
| 				const requiredAA = large ? 3.0 : 4.5;
 | |
| 				const requiredAAA = large ? 4.5 : 7.0;
 | |
| 
 | |
| 				results.push({
 | |
| 					selector: '%s:nth-of-type(' + (index + 1) + ')',
 | |
| 					text: text.substring(0, 100),
 | |
| 					foreground_color: fgColor,
 | |
| 					background_color: bgColor,
 | |
| 					contrast_ratio: Math.round(ratio * 100) / 100,
 | |
| 					font_size: fontSize,
 | |
| 					font_weight: fontWeight,
 | |
| 					is_large_text: large,
 | |
| 					passes_aa: ratio >= requiredAA,
 | |
| 					passes_aaa: ratio >= requiredAAA,
 | |
| 					required_aa: requiredAA,
 | |
| 					required_aaa: requiredAAA
 | |
| 				});
 | |
| 			} catch (e) {
 | |
| 				results.push({
 | |
| 					selector: '%s:nth-of-type(' + (index + 1) + ')',
 | |
| 					text: element.textContent.trim().substring(0, 100),
 | |
| 					error: e.message
 | |
| 				});
 | |
| 			}
 | |
| 		});
 | |
| 
 | |
| 		return JSON.stringify(results);
 | |
| 	}`, selector, selector, selector, selector)
 | |
| 
 | |
| 	var jsResult *proto.RuntimeRemoteObject
 | |
| 	if timeout > 0 {
 | |
| 		ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
 | |
| 		defer cancel()
 | |
| 
 | |
| 		done := make(chan struct {
 | |
| 			result *proto.RuntimeRemoteObject
 | |
| 			err    error
 | |
| 		}, 1)
 | |
| 
 | |
| 		go func() {
 | |
| 			result, err := page.Eval(jsCode)
 | |
| 			done <- struct {
 | |
| 				result *proto.RuntimeRemoteObject
 | |
| 				err    error
 | |
| 			}{result, err}
 | |
| 		}()
 | |
| 
 | |
| 		select {
 | |
| 		case res := <-done:
 | |
| 			if res.err != nil {
 | |
| 				return nil, fmt.Errorf("failed to check contrast: %w", res.err)
 | |
| 			}
 | |
| 			jsResult = res.result
 | |
| 		case <-ctx.Done():
 | |
| 			return nil, fmt.Errorf("contrast check timed out after %d seconds", timeout)
 | |
| 		}
 | |
| 	} else {
 | |
| 		jsResult, err = page.Eval(jsCode)
 | |
| 		if err != nil {
 | |
| 			return nil, fmt.Errorf("failed to check contrast: %w", err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Parse the results
 | |
| 	resultsJSON := jsResult.Value.Str()
 | |
| 	var elements []ContrastCheckElement
 | |
| 	err = json.Unmarshal([]byte(resultsJSON), &elements)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to parse contrast results: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	// Calculate summary statistics
 | |
| 	result := &ContrastCheckResult{
 | |
| 		TotalElements: len(elements),
 | |
| 		Elements:      elements,
 | |
| 	}
 | |
| 
 | |
| 	for _, elem := range elements {
 | |
| 		if elem.Error != "" {
 | |
| 			result.UnableToCheck++
 | |
| 		} else {
 | |
| 			if elem.PassesAA {
 | |
| 				result.PassedAA++
 | |
| 			} else {
 | |
| 				result.FailedAA++
 | |
| 			}
 | |
| 			if elem.PassesAAA {
 | |
| 				result.PassedAAA++
 | |
| 			} else {
 | |
| 				result.FailedAAA++
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	d.debugLog("Successfully checked contrast for tab: %s (checked %d elements)", tabID, len(elements))
 | |
| 	return result, nil
 | |
| }
 | |
| 
 | |
| // KeyboardTestResult represents the result of keyboard navigation testing
 | |
| type KeyboardTestResult struct {
 | |
| 	TotalInteractive int                   `json:"total_interactive"`
 | |
| 	Focusable        int                   `json:"focusable"`
 | |
| 	NotFocusable     int                   `json:"not_focusable"`
 | |
| 	NoFocusIndicator int                   `json:"no_focus_indicator"`
 | |
| 	KeyboardTraps    int                   `json:"keyboard_traps"`
 | |
| 	TabOrder         []KeyboardTestElement `json:"tab_order"`
 | |
| 	Issues           []KeyboardTestIssue   `json:"issues"`
 | |
| }
 | |
| 
 | |
| // KeyboardTestElement represents an interactive element in tab order
 | |
| type KeyboardTestElement struct {
 | |
| 	Index         int    `json:"index"`
 | |
| 	Selector      string `json:"selector"`
 | |
| 	TagName       string `json:"tag_name"`
 | |
| 	Role          string `json:"role"`
 | |
| 	Text          string `json:"text"`
 | |
| 	TabIndex      int    `json:"tab_index"`
 | |
| 	HasFocusStyle bool   `json:"has_focus_style"`
 | |
| 	IsVisible     bool   `json:"is_visible"`
 | |
| }
 | |
| 
 | |
| // KeyboardTestIssue represents a keyboard accessibility issue
 | |
| type KeyboardTestIssue struct {
 | |
| 	Type        string `json:"type"`
 | |
| 	Severity    string `json:"severity"`
 | |
| 	Element     string `json:"element"`
 | |
| 	Description string `json:"description"`
 | |
| }
 | |
| 
 | |
| // testKeyboardNavigation tests keyboard navigation and accessibility
 | |
| func (d *Daemon) testKeyboardNavigation(tabID string, timeout int) (*KeyboardTestResult, error) {
 | |
| 	d.debugLog("Testing keyboard navigation for tab: %s", tabID)
 | |
| 
 | |
| 	page, err := d.getTab(tabID)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to get page: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// JavaScript code to test keyboard navigation
 | |
| 	jsCode := `() => {
 | |
| 		const results = {
 | |
| 			total_interactive: 0,
 | |
| 			focusable: 0,
 | |
| 			not_focusable: 0,
 | |
| 			no_focus_indicator: 0,
 | |
| 			keyboard_traps: 0,
 | |
| 			tab_order: [],
 | |
| 			issues: []
 | |
| 		};
 | |
| 
 | |
| 		// Helper to check if element is visible
 | |
| 		function isVisible(element) {
 | |
| 			const style = window.getComputedStyle(element);
 | |
| 			return style.display !== 'none' &&
 | |
| 				   style.visibility !== 'hidden' &&
 | |
| 				   style.opacity !== '0' &&
 | |
| 				   element.offsetWidth > 0 &&
 | |
| 				   element.offsetHeight > 0;
 | |
| 		}
 | |
| 
 | |
| 		// Helper to get element selector
 | |
| 		function getSelector(element) {
 | |
| 			if (element.id) return '#' + element.id;
 | |
| 			if (element.className && typeof element.className === 'string') {
 | |
| 				const classes = element.className.trim().split(/\s+/).slice(0, 2).join('.');
 | |
| 				if (classes) return element.tagName.toLowerCase() + '.' + classes;
 | |
| 			}
 | |
| 			return element.tagName.toLowerCase();
 | |
| 		}
 | |
| 
 | |
| 		// Helper to check focus indicator
 | |
| 		function hasFocusIndicator(element) {
 | |
| 			element.focus();
 | |
| 			const focusedStyle = window.getComputedStyle(element);
 | |
| 			element.blur();
 | |
| 			const blurredStyle = window.getComputedStyle(element);
 | |
| 
 | |
| 			// Check for outline changes
 | |
| 			if (focusedStyle.outline !== blurredStyle.outline &&
 | |
| 				focusedStyle.outline !== 'none' &&
 | |
| 				focusedStyle.outlineWidth !== '0px') {
 | |
| 				return true;
 | |
| 			}
 | |
| 
 | |
| 			// Check for border changes
 | |
| 			if (focusedStyle.border !== blurredStyle.border) {
 | |
| 				return true;
 | |
| 			}
 | |
| 
 | |
| 			// Check for background changes
 | |
| 			if (focusedStyle.backgroundColor !== blurredStyle.backgroundColor) {
 | |
| 				return true;
 | |
| 			}
 | |
| 
 | |
| 			// Check for box-shadow changes
 | |
| 			if (focusedStyle.boxShadow !== blurredStyle.boxShadow &&
 | |
| 				focusedStyle.boxShadow !== 'none') {
 | |
| 				return true;
 | |
| 			}
 | |
| 
 | |
| 			return false;
 | |
| 		}
 | |
| 
 | |
| 		// Get all interactive elements
 | |
| 		const interactiveSelectors = [
 | |
| 			'a[href]',
 | |
| 			'button',
 | |
| 			'input:not([type="hidden"])',
 | |
| 			'select',
 | |
| 			'textarea',
 | |
| 			'[tabindex]:not([tabindex="-1"])',
 | |
| 			'[role="button"]',
 | |
| 			'[role="link"]',
 | |
| 			'[role="checkbox"]',
 | |
| 			'[role="radio"]',
 | |
| 			'[role="tab"]',
 | |
| 			'[role="menuitem"]'
 | |
| 		];
 | |
| 
 | |
| 		const allInteractive = document.querySelectorAll(interactiveSelectors.join(','));
 | |
| 		results.total_interactive = allInteractive.length;
 | |
| 
 | |
| 		// Test each interactive element
 | |
| 		allInteractive.forEach((element, index) => {
 | |
| 			const visible = isVisible(element);
 | |
| 			const selector = getSelector(element);
 | |
| 			const tagName = element.tagName.toLowerCase();
 | |
| 			const role = element.getAttribute('role') || '';
 | |
| 			const text = element.textContent.trim().substring(0, 50);
 | |
| 			const tabIndex = element.tabIndex;
 | |
| 
 | |
| 			// Check if element is focusable
 | |
| 			let isFocusable = false;
 | |
| 			try {
 | |
| 				element.focus();
 | |
| 				isFocusable = document.activeElement === element;
 | |
| 				element.blur();
 | |
| 			} catch (e) {
 | |
| 				// Element not focusable
 | |
| 			}
 | |
| 
 | |
| 			if (visible) {
 | |
| 				if (isFocusable) {
 | |
| 					results.focusable++;
 | |
| 
 | |
| 					// Check for focus indicator
 | |
| 					const hasFocus = hasFocusIndicator(element);
 | |
| 					if (!hasFocus) {
 | |
| 						results.no_focus_indicator++;
 | |
| 						results.issues.push({
 | |
| 							type: 'no_focus_indicator',
 | |
| 							severity: 'high',
 | |
| 							element: selector,
 | |
| 							description: 'Interactive element lacks visible focus indicator'
 | |
| 						});
 | |
| 					}
 | |
| 
 | |
| 					// Add to tab order
 | |
| 					results.tab_order.push({
 | |
| 						index: results.tab_order.length,
 | |
| 						selector: selector,
 | |
| 						tag_name: tagName,
 | |
| 						role: role,
 | |
| 						text: text,
 | |
| 						tab_index: tabIndex,
 | |
| 						has_focus_style: hasFocus,
 | |
| 						is_visible: visible
 | |
| 					});
 | |
| 				} else {
 | |
| 					results.not_focusable++;
 | |
| 					results.issues.push({
 | |
| 						type: 'not_focusable',
 | |
| 						severity: 'high',
 | |
| 						element: selector,
 | |
| 						description: 'Interactive element is not keyboard focusable'
 | |
| 					});
 | |
| 				}
 | |
| 			}
 | |
| 		});
 | |
| 
 | |
| 		// Test for keyboard traps by simulating tab navigation
 | |
| 		const focusableElements = Array.from(allInteractive).filter(el => {
 | |
| 			try {
 | |
| 				el.focus();
 | |
| 				const focused = document.activeElement === el;
 | |
| 				el.blur();
 | |
| 				return focused && isVisible(el);
 | |
| 			} catch (e) {
 | |
| 				return false;
 | |
| 			}
 | |
| 		});
 | |
| 
 | |
| 		// Simple keyboard trap detection
 | |
| 		if (focusableElements.length > 0) {
 | |
| 			const firstElement = focusableElements[0];
 | |
| 			const lastElement = focusableElements[focusableElements.length - 1];
 | |
| 
 | |
| 			// Focus first element
 | |
| 			firstElement.focus();
 | |
| 
 | |
| 			// Simulate Shift+Tab from first element
 | |
| 			// If focus doesn't move to last element or body, might be a trap
 | |
| 			// Note: This is a simplified check, real trap detection is complex
 | |
| 		}
 | |
| 
 | |
| 		return JSON.stringify(results);
 | |
| 	}`
 | |
| 
 | |
| 	var jsResult *proto.RuntimeRemoteObject
 | |
| 	if timeout > 0 {
 | |
| 		ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
 | |
| 		defer cancel()
 | |
| 
 | |
| 		done := make(chan struct {
 | |
| 			result *proto.RuntimeRemoteObject
 | |
| 			err    error
 | |
| 		}, 1)
 | |
| 
 | |
| 		go func() {
 | |
| 			result, err := page.Eval(jsCode)
 | |
| 			done <- struct {
 | |
| 				result *proto.RuntimeRemoteObject
 | |
| 				err    error
 | |
| 			}{result, err}
 | |
| 		}()
 | |
| 
 | |
| 		select {
 | |
| 		case res := <-done:
 | |
| 			if res.err != nil {
 | |
| 				return nil, fmt.Errorf("failed to test keyboard navigation: %w", res.err)
 | |
| 			}
 | |
| 			jsResult = res.result
 | |
| 		case <-ctx.Done():
 | |
| 			return nil, fmt.Errorf("keyboard navigation test timed out after %d seconds", timeout)
 | |
| 		}
 | |
| 	} else {
 | |
| 		jsResult, err = page.Eval(jsCode)
 | |
| 		if err != nil {
 | |
| 			return nil, fmt.Errorf("failed to test keyboard navigation: %w", err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Parse the results
 | |
| 	resultsJSON := jsResult.Value.Str()
 | |
| 	var result KeyboardTestResult
 | |
| 	err = json.Unmarshal([]byte(resultsJSON), &result)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to parse keyboard test results: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	d.debugLog("Successfully tested keyboard navigation for tab: %s (found %d issues)", tabID, len(result.Issues))
 | |
| 	return &result, nil
 | |
| }
 | |
| 
 | |
| // ZoomTestResult represents the result of zoom level testing
 | |
| type ZoomTestResult struct {
 | |
| 	ZoomLevels []ZoomLevelTest `json:"zoom_levels"`
 | |
| 	Issues     []ZoomTestIssue `json:"issues"`
 | |
| }
 | |
| 
 | |
| // ZoomLevelTest represents testing at a specific zoom level
 | |
| type ZoomLevelTest struct {
 | |
| 	ZoomLevel           float64 `json:"zoom_level"`
 | |
| 	ViewportWidth       int     `json:"viewport_width"`
 | |
| 	ViewportHeight      int     `json:"viewport_height"`
 | |
| 	HasHorizontalScroll bool    `json:"has_horizontal_scroll"`
 | |
| 	ContentWidth        int     `json:"content_width"`
 | |
| 	ContentHeight       int     `json:"content_height"`
 | |
| 	VisibleElements     int     `json:"visible_elements"`
 | |
| 	OverflowingElements int     `json:"overflowing_elements"`
 | |
| 	TextReadable        bool    `json:"text_readable"`
 | |
| }
 | |
| 
 | |
| // ZoomTestIssue represents an issue found during zoom testing
 | |
| type ZoomTestIssue struct {
 | |
| 	ZoomLevel   float64 `json:"zoom_level"`
 | |
| 	Type        string  `json:"type"`
 | |
| 	Severity    string  `json:"severity"`
 | |
| 	Description string  `json:"description"`
 | |
| 	Element     string  `json:"element,omitempty"`
 | |
| }
 | |
| 
 | |
| // testZoom tests page at different zoom levels
 | |
| func (d *Daemon) testZoom(tabID string, zoomLevels []float64, timeout int) (*ZoomTestResult, error) {
 | |
| 	d.debugLog("Testing zoom levels for tab: %s", tabID)
 | |
| 
 | |
| 	page, err := d.getTab(tabID)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to get page: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// Default zoom levels if none provided
 | |
| 	if len(zoomLevels) == 0 {
 | |
| 		zoomLevels = []float64{1.0, 2.0, 4.0}
 | |
| 	}
 | |
| 
 | |
| 	result := &ZoomTestResult{
 | |
| 		ZoomLevels: make([]ZoomLevelTest, 0, len(zoomLevels)),
 | |
| 		Issues:     make([]ZoomTestIssue, 0),
 | |
| 	}
 | |
| 
 | |
| 	// Get original viewport size
 | |
| 	originalViewport, err := page.Eval(`() => {
 | |
| 		return JSON.stringify({
 | |
| 			width: window.innerWidth,
 | |
| 			height: window.innerHeight
 | |
| 		});
 | |
| 	}`)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to get viewport size: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	var viewportData struct {
 | |
| 		Width  int `json:"width"`
 | |
| 		Height int `json:"height"`
 | |
| 	}
 | |
| 	err = json.Unmarshal([]byte(originalViewport.Value.Str()), &viewportData)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to parse viewport data: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	// Test each zoom level
 | |
| 	for _, zoom := range zoomLevels {
 | |
| 		d.debugLog("Testing zoom level: %.1f", zoom)
 | |
| 
 | |
| 		// Set zoom level using Emulation domain
 | |
| 		err = page.SetViewport(&proto.EmulationSetDeviceMetricsOverride{
 | |
| 			Width:             viewportData.Width,
 | |
| 			Height:            viewportData.Height,
 | |
| 			DeviceScaleFactor: zoom,
 | |
| 			Mobile:            false,
 | |
| 		})
 | |
| 		if err != nil {
 | |
| 			d.debugLog("Failed to set zoom level %.1f: %v", zoom, err)
 | |
| 			result.Issues = append(result.Issues, ZoomTestIssue{
 | |
| 				ZoomLevel:   zoom,
 | |
| 				Type:        "zoom_error",
 | |
| 				Severity:    "high",
 | |
| 				Description: fmt.Sprintf("Failed to set zoom level: %v", err),
 | |
| 			})
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		// Wait a moment for reflow
 | |
| 		time.Sleep(500 * time.Millisecond)
 | |
| 
 | |
| 		// JavaScript to analyze page at this zoom level
 | |
| 		jsCode := `() => {
 | |
| 			const body = document.body;
 | |
| 			const html = document.documentElement;
 | |
| 
 | |
| 			// Get content dimensions
 | |
| 			const contentWidth = Math.max(
 | |
| 				body.scrollWidth, body.offsetWidth,
 | |
| 				html.clientWidth, html.scrollWidth, html.offsetWidth
 | |
| 			);
 | |
| 			const contentHeight = Math.max(
 | |
| 				body.scrollHeight, body.offsetHeight,
 | |
| 				html.clientHeight, html.scrollHeight, html.offsetHeight
 | |
| 			);
 | |
| 
 | |
| 			// Check for horizontal scroll
 | |
| 			const hasHorizontalScroll = contentWidth > window.innerWidth;
 | |
| 
 | |
| 			// Count visible elements
 | |
| 			const allElements = document.querySelectorAll('*');
 | |
| 			let visibleCount = 0;
 | |
| 			let overflowingCount = 0;
 | |
| 
 | |
| 			allElements.forEach(el => {
 | |
| 				const style = window.getComputedStyle(el);
 | |
| 				const rect = el.getBoundingClientRect();
 | |
| 
 | |
| 				if (style.display !== 'none' && style.visibility !== 'hidden' &&
 | |
| 					rect.width > 0 && rect.height > 0) {
 | |
| 					visibleCount++;
 | |
| 
 | |
| 					// Check if element overflows viewport
 | |
| 					if (rect.right > window.innerWidth || rect.left < 0) {
 | |
| 						overflowingCount++;
 | |
| 					}
 | |
| 				}
 | |
| 			});
 | |
| 
 | |
| 			// Check text readability (minimum font size)
 | |
| 			const textElements = document.querySelectorAll('p, span, div, a, button, li, td, th, label');
 | |
| 			let minFontSize = Infinity;
 | |
| 			textElements.forEach(el => {
 | |
| 				const style = window.getComputedStyle(el);
 | |
| 				const fontSize = parseFloat(style.fontSize);
 | |
| 				if (fontSize > 0 && fontSize < minFontSize) {
 | |
| 					minFontSize = fontSize;
 | |
| 				}
 | |
| 			});
 | |
| 
 | |
| 			// Text is readable if minimum font size is at least 9px (WCAG recommendation)
 | |
| 			const textReadable = minFontSize >= 9;
 | |
| 
 | |
| 			return {
 | |
| 				viewport_width: window.innerWidth,
 | |
| 				viewport_height: window.innerHeight,
 | |
| 				has_horizontal_scroll: hasHorizontalScroll,
 | |
| 				content_width: contentWidth,
 | |
| 				content_height: contentHeight,
 | |
| 				visible_elements: visibleCount,
 | |
| 				overflowing_elements: overflowingCount,
 | |
| 				text_readable: textReadable,
 | |
| 				min_font_size: minFontSize
 | |
| 			};
 | |
| 			return JSON.stringify(result);
 | |
| 		}`
 | |
| 
 | |
| 		var jsResult *proto.RuntimeRemoteObject
 | |
| 		if timeout > 0 {
 | |
| 			ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
 | |
| 			defer cancel()
 | |
| 
 | |
| 			done := make(chan struct {
 | |
| 				result *proto.RuntimeRemoteObject
 | |
| 				err    error
 | |
| 			}, 1)
 | |
| 
 | |
| 			go func() {
 | |
| 				res, err := page.Eval(jsCode)
 | |
| 				done <- struct {
 | |
| 					result *proto.RuntimeRemoteObject
 | |
| 					err    error
 | |
| 				}{res, err}
 | |
| 			}()
 | |
| 
 | |
| 			select {
 | |
| 			case res := <-done:
 | |
| 				if res.err != nil {
 | |
| 					result.Issues = append(result.Issues, ZoomTestIssue{
 | |
| 						ZoomLevel:   zoom,
 | |
| 						Type:        "evaluation_error",
 | |
| 						Severity:    "high",
 | |
| 						Description: fmt.Sprintf("Failed to evaluate page: %v", res.err),
 | |
| 					})
 | |
| 					continue
 | |
| 				}
 | |
| 				jsResult = res.result
 | |
| 			case <-ctx.Done():
 | |
| 				result.Issues = append(result.Issues, ZoomTestIssue{
 | |
| 					ZoomLevel:   zoom,
 | |
| 					Type:        "timeout",
 | |
| 					Severity:    "high",
 | |
| 					Description: fmt.Sprintf("Evaluation timed out after %d seconds", timeout),
 | |
| 				})
 | |
| 				continue
 | |
| 			}
 | |
| 		} else {
 | |
| 			jsResult, err = page.Eval(jsCode)
 | |
| 			if err != nil {
 | |
| 				result.Issues = append(result.Issues, ZoomTestIssue{
 | |
| 					ZoomLevel:   zoom,
 | |
| 					Type:        "evaluation_error",
 | |
| 					Severity:    "high",
 | |
| 					Description: fmt.Sprintf("Failed to evaluate page: %v", err),
 | |
| 				})
 | |
| 				continue
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		// Parse the results
 | |
| 		var zoomTest ZoomLevelTest
 | |
| 		err = json.Unmarshal([]byte(jsResult.Value.Str()), &zoomTest)
 | |
| 		if err != nil {
 | |
| 			result.Issues = append(result.Issues, ZoomTestIssue{
 | |
| 				ZoomLevel:   zoom,
 | |
| 				Type:        "parse_error",
 | |
| 				Severity:    "high",
 | |
| 				Description: fmt.Sprintf("Failed to parse results: %v", err),
 | |
| 			})
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		zoomTest.ZoomLevel = zoom
 | |
| 		result.ZoomLevels = append(result.ZoomLevels, zoomTest)
 | |
| 
 | |
| 		// Check for issues
 | |
| 		if zoomTest.HasHorizontalScroll {
 | |
| 			result.Issues = append(result.Issues, ZoomTestIssue{
 | |
| 				ZoomLevel:   zoom,
 | |
| 				Type:        "horizontal_scroll",
 | |
| 				Severity:    "medium",
 | |
| 				Description: "Page has horizontal scrollbar (WCAG 1.4.10 violation)",
 | |
| 			})
 | |
| 		}
 | |
| 
 | |
| 		if zoomTest.OverflowingElements > 0 {
 | |
| 			result.Issues = append(result.Issues, ZoomTestIssue{
 | |
| 				ZoomLevel:   zoom,
 | |
| 				Type:        "overflowing_content",
 | |
| 				Severity:    "medium",
 | |
| 				Description: fmt.Sprintf("%d elements overflow viewport", zoomTest.OverflowingElements),
 | |
| 			})
 | |
| 		}
 | |
| 
 | |
| 		if !zoomTest.TextReadable {
 | |
| 			result.Issues = append(result.Issues, ZoomTestIssue{
 | |
| 				ZoomLevel:   zoom,
 | |
| 				Type:        "text_too_small",
 | |
| 				Severity:    "high",
 | |
| 				Description: "Text size too small for readability",
 | |
| 			})
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Reset viewport to original
 | |
| 	err = page.SetViewport(&proto.EmulationSetDeviceMetricsOverride{
 | |
| 		Width:             viewportData.Width,
 | |
| 		Height:            viewportData.Height,
 | |
| 		DeviceScaleFactor: 1.0,
 | |
| 		Mobile:            false,
 | |
| 	})
 | |
| 	if err != nil {
 | |
| 		d.debugLog("Warning: Failed to reset viewport: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	d.debugLog("Successfully tested zoom levels for tab: %s (found %d issues)", tabID, len(result.Issues))
 | |
| 	return result, nil
 | |
| }
 | |
| 
 | |
| // ReflowTestResult represents the result of reflow/responsive testing
 | |
| type ReflowTestResult struct {
 | |
| 	Breakpoints []ReflowBreakpoint `json:"breakpoints"`
 | |
| 	Issues      []ReflowTestIssue  `json:"issues"`
 | |
| }
 | |
| 
 | |
| // ReflowBreakpoint represents testing at a specific viewport width
 | |
| type ReflowBreakpoint struct {
 | |
| 	Width               int  `json:"width"`
 | |
| 	Height              int  `json:"height"`
 | |
| 	HasHorizontalScroll bool `json:"has_horizontal_scroll"`
 | |
| 	ContentWidth        int  `json:"content_width"`
 | |
| 	ContentHeight       int  `json:"content_height"`
 | |
| 	VisibleElements     int  `json:"visible_elements"`
 | |
| 	OverflowingElements int  `json:"overflowing_elements"`
 | |
| 	ResponsiveLayout    bool `json:"responsive_layout"`
 | |
| }
 | |
| 
 | |
| // ReflowTestIssue represents an issue found during reflow testing
 | |
| type ReflowTestIssue struct {
 | |
| 	Width       int    `json:"width"`
 | |
| 	Type        string `json:"type"`
 | |
| 	Severity    string `json:"severity"`
 | |
| 	Description string `json:"description"`
 | |
| 	Element     string `json:"element,omitempty"`
 | |
| }
 | |
| 
 | |
| // testReflow tests page at different viewport widths for responsive design
 | |
| func (d *Daemon) testReflow(tabID string, widths []int, timeout int) (*ReflowTestResult, error) {
 | |
| 	d.debugLog("Testing reflow at different widths for tab: %s", tabID)
 | |
| 
 | |
| 	page, err := d.getTab(tabID)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to get page: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// Default widths if none provided (WCAG 1.4.10 breakpoints)
 | |
| 	if len(widths) == 0 {
 | |
| 		widths = []int{320, 1280}
 | |
| 	}
 | |
| 
 | |
| 	result := &ReflowTestResult{
 | |
| 		Breakpoints: make([]ReflowBreakpoint, 0, len(widths)),
 | |
| 		Issues:      make([]ReflowTestIssue, 0),
 | |
| 	}
 | |
| 
 | |
| 	// Get original viewport size
 | |
| 	originalViewport, err := page.Eval(`() => {
 | |
| 		return JSON.stringify({
 | |
| 			width: window.innerWidth,
 | |
| 			height: window.innerHeight
 | |
| 		});
 | |
| 	}`)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to get viewport size: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	var viewportData struct {
 | |
| 		Width  int `json:"width"`
 | |
| 		Height int `json:"height"`
 | |
| 	}
 | |
| 	err = json.Unmarshal([]byte(originalViewport.Value.Str()), &viewportData)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to parse viewport data: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	// Test each width
 | |
| 	for _, width := range widths {
 | |
| 		d.debugLog("Testing width: %dpx", width)
 | |
| 
 | |
| 		// Set viewport width
 | |
| 		err = page.SetViewport(&proto.EmulationSetDeviceMetricsOverride{
 | |
| 			Width:             width,
 | |
| 			Height:            viewportData.Height,
 | |
| 			DeviceScaleFactor: 1.0,
 | |
| 			Mobile:            width <= 768, // Consider mobile for small widths
 | |
| 		})
 | |
| 		if err != nil {
 | |
| 			d.debugLog("Failed to set width %d: %v", width, err)
 | |
| 			result.Issues = append(result.Issues, ReflowTestIssue{
 | |
| 				Width:       width,
 | |
| 				Type:        "viewport_error",
 | |
| 				Severity:    "high",
 | |
| 				Description: fmt.Sprintf("Failed to set viewport width: %v", err),
 | |
| 			})
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		// Wait for reflow
 | |
| 		time.Sleep(500 * time.Millisecond)
 | |
| 
 | |
| 		// JavaScript to analyze page at this width
 | |
| 		jsCode := `() => {
 | |
| 			const body = document.body;
 | |
| 			const html = document.documentElement;
 | |
| 
 | |
| 			// Get content dimensions
 | |
| 			const contentWidth = Math.max(
 | |
| 				body.scrollWidth, body.offsetWidth,
 | |
| 				html.clientWidth, html.scrollWidth, html.offsetWidth
 | |
| 			);
 | |
| 			const contentHeight = Math.max(
 | |
| 				body.scrollHeight, body.offsetHeight,
 | |
| 				html.clientHeight, html.scrollHeight, html.offsetHeight
 | |
| 			);
 | |
| 
 | |
| 			// Check for horizontal scroll
 | |
| 			const hasHorizontalScroll = contentWidth > window.innerWidth;
 | |
| 
 | |
| 			// Count visible and overflowing elements
 | |
| 			const allElements = document.querySelectorAll('*');
 | |
| 			let visibleCount = 0;
 | |
| 			let overflowingCount = 0;
 | |
| 
 | |
| 			allElements.forEach(el => {
 | |
| 				const style = window.getComputedStyle(el);
 | |
| 				const rect = el.getBoundingClientRect();
 | |
| 
 | |
| 				if (style.display !== 'none' && style.visibility !== 'hidden' &&
 | |
| 					rect.width > 0 && rect.height > 0) {
 | |
| 					visibleCount++;
 | |
| 
 | |
| 					// Check if element overflows viewport
 | |
| 					if (rect.right > window.innerWidth + 5 || rect.left < -5) {
 | |
| 						overflowingCount++;
 | |
| 					}
 | |
| 				}
 | |
| 			});
 | |
| 
 | |
| 			// Check if layout appears responsive
 | |
| 			// (content width should not significantly exceed viewport width)
 | |
| 			const responsiveLayout = contentWidth <= window.innerWidth + 20;
 | |
| 
 | |
| 			return JSON.stringify({
 | |
| 				width: window.innerWidth,
 | |
| 				height: window.innerHeight,
 | |
| 				has_horizontal_scroll: hasHorizontalScroll,
 | |
| 				content_width: contentWidth,
 | |
| 				content_height: contentHeight,
 | |
| 				visible_elements: visibleCount,
 | |
| 				overflowing_elements: overflowingCount,
 | |
| 				responsive_layout: responsiveLayout
 | |
| 			});
 | |
| 		}`
 | |
| 
 | |
| 		var jsResult *proto.RuntimeRemoteObject
 | |
| 		if timeout > 0 {
 | |
| 			ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
 | |
| 			defer cancel()
 | |
| 
 | |
| 			done := make(chan struct {
 | |
| 				result *proto.RuntimeRemoteObject
 | |
| 				err    error
 | |
| 			}, 1)
 | |
| 
 | |
| 			go func() {
 | |
| 				res, err := page.Eval(jsCode)
 | |
| 				done <- struct {
 | |
| 					result *proto.RuntimeRemoteObject
 | |
| 					err    error
 | |
| 				}{res, err}
 | |
| 			}()
 | |
| 
 | |
| 			select {
 | |
| 			case res := <-done:
 | |
| 				if res.err != nil {
 | |
| 					result.Issues = append(result.Issues, ReflowTestIssue{
 | |
| 						Width:       width,
 | |
| 						Type:        "evaluation_error",
 | |
| 						Severity:    "high",
 | |
| 						Description: fmt.Sprintf("Failed to evaluate page: %v", res.err),
 | |
| 					})
 | |
| 					continue
 | |
| 				}
 | |
| 				jsResult = res.result
 | |
| 			case <-ctx.Done():
 | |
| 				result.Issues = append(result.Issues, ReflowTestIssue{
 | |
| 					Width:       width,
 | |
| 					Type:        "timeout",
 | |
| 					Severity:    "high",
 | |
| 					Description: fmt.Sprintf("Evaluation timed out after %d seconds", timeout),
 | |
| 				})
 | |
| 				continue
 | |
| 			}
 | |
| 		} else {
 | |
| 			jsResult, err = page.Eval(jsCode)
 | |
| 			if err != nil {
 | |
| 				result.Issues = append(result.Issues, ReflowTestIssue{
 | |
| 					Width:       width,
 | |
| 					Type:        "evaluation_error",
 | |
| 					Severity:    "high",
 | |
| 					Description: fmt.Sprintf("Failed to evaluate page: %v", err),
 | |
| 				})
 | |
| 				continue
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		// Parse the results
 | |
| 		var breakpoint ReflowBreakpoint
 | |
| 		err = json.Unmarshal([]byte(jsResult.Value.Str()), &breakpoint)
 | |
| 		if err != nil {
 | |
| 			result.Issues = append(result.Issues, ReflowTestIssue{
 | |
| 				Width:       width,
 | |
| 				Type:        "parse_error",
 | |
| 				Severity:    "high",
 | |
| 				Description: fmt.Sprintf("Failed to parse results: %v", err),
 | |
| 			})
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		result.Breakpoints = append(result.Breakpoints, breakpoint)
 | |
| 
 | |
| 		// Check for issues
 | |
| 		if breakpoint.HasHorizontalScroll {
 | |
| 			result.Issues = append(result.Issues, ReflowTestIssue{
 | |
| 				Width:       width,
 | |
| 				Type:        "horizontal_scroll",
 | |
| 				Severity:    "high",
 | |
| 				Description: "Page requires horizontal scrolling (WCAG 1.4.10 violation)",
 | |
| 			})
 | |
| 		}
 | |
| 
 | |
| 		if !breakpoint.ResponsiveLayout {
 | |
| 			result.Issues = append(result.Issues, ReflowTestIssue{
 | |
| 				Width:       width,
 | |
| 				Type:        "non_responsive",
 | |
| 				Severity:    "high",
 | |
| 				Description: fmt.Sprintf("Content width (%dpx) exceeds viewport width (%dpx)", breakpoint.ContentWidth, breakpoint.Width),
 | |
| 			})
 | |
| 		}
 | |
| 
 | |
| 		if breakpoint.OverflowingElements > 0 {
 | |
| 			result.Issues = append(result.Issues, ReflowTestIssue{
 | |
| 				Width:       width,
 | |
| 				Type:        "overflowing_content",
 | |
| 				Severity:    "medium",
 | |
| 				Description: fmt.Sprintf("%d elements overflow viewport", breakpoint.OverflowingElements),
 | |
| 			})
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Reset viewport to original
 | |
| 	err = page.SetViewport(&proto.EmulationSetDeviceMetricsOverride{
 | |
| 		Width:             viewportData.Width,
 | |
| 		Height:            viewportData.Height,
 | |
| 		DeviceScaleFactor: 1.0,
 | |
| 		Mobile:            false,
 | |
| 	})
 | |
| 	if err != nil {
 | |
| 		d.debugLog("Warning: Failed to reset viewport: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	d.debugLog("Successfully tested reflow for tab: %s (found %d issues)", tabID, len(result.Issues))
 | |
| 	return result, nil
 | |
| }
 |