Files
cremote/client/client.go
Josh at WLTechBlog ccd8c77a3e remove sensory tools
2025-10-07 11:47:47 -05:00

4571 lines
125 KiB
Go

package client
import (
"bytes"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
"strconv"
"strings"
"time"
)
// Client is the client for communicating with the daemon
type Client struct {
serverURL string
httpClient *http.Client
}
// 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"`
}
// NewClient creates a new client
func NewClient(host string, port int) *Client {
return &Client{
serverURL: fmt.Sprintf("http://%s:%d", host, port),
httpClient: &http.Client{
Timeout: 60 * time.Second, // 60 second timeout for long operations
},
}
}
// CheckStatus checks if the daemon is running
func (c *Client) CheckStatus() (bool, error) {
resp, err := c.httpClient.Get(c.serverURL + "/status")
if err != nil {
return false, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
var response Response
err = json.NewDecoder(resp.Body).Decode(&response)
if err != nil {
return false, err
}
return response.Success, nil
}
// GetVersion gets the daemon version
func (c *Client) GetVersion() (string, error) {
response, err := c.SendCommand("version", map[string]string{})
if err != nil {
return "", err
}
if !response.Success {
return "", fmt.Errorf("failed to get version: %s", response.Error)
}
// The version should be in the Data field as a string
if version, ok := response.Data.(string); ok {
return version, nil
}
return "", fmt.Errorf("unexpected version response format")
}
// TabInfo contains information about a tab
type TabInfo struct {
ID string `json:"id"`
URL string `json:"url"`
IsCurrent bool `json:"is_current"`
HistoryIndex int `json:"history_index"` // Position in tab history (higher = more recent)
}
// ListTabs returns a list of all open tabs
func (c *Client) ListTabs() ([]TabInfo, error) {
resp, err := http.Get(c.serverURL + "/status")
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
var response Response
err = json.NewDecoder(resp.Body).Decode(&response)
if err != nil {
return nil, fmt.Errorf("failed to parse response: %w", err)
}
if !response.Success {
return nil, fmt.Errorf("daemon returned error: %s", response.Error)
}
// Extract the data
data, ok := response.Data.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("unexpected response format")
}
// Get the tabs
tabsData, ok := data["tabs"].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("unexpected tabs format")
}
// Get the current tab
currentTab, _ := data["current_tab"].(string)
// Get the tab history
tabHistoryData, ok := data["tab_history"].([]interface{})
if !ok {
tabHistoryData = []interface{}{}
}
// Create a map of tab history indices
tabHistoryIndices := make(map[string]int)
for i, idInterface := range tabHistoryData {
id, ok := idInterface.(string)
if ok {
tabHistoryIndices[id] = i
}
}
// Convert to TabInfo
tabs := make([]TabInfo, 0, len(tabsData))
for id, urlInterface := range tabsData {
url, ok := urlInterface.(string)
if !ok {
url = "<unknown>"
}
// Get the history index (default to -1 if not in history)
historyIndex, inHistory := tabHistoryIndices[id]
if !inHistory {
historyIndex = -1
}
tabs = append(tabs, TabInfo{
ID: id,
URL: url,
IsCurrent: id == currentTab,
HistoryIndex: historyIndex,
})
}
return tabs, nil
}
// SendCommand sends a command to the daemon
func (c *Client) SendCommand(action string, params map[string]string) (*Response, error) {
cmd := Command{
Action: action,
Params: params,
}
jsonData, err := json.Marshal(cmd)
if err != nil {
return nil, err
}
resp, err := c.httpClient.Post(c.serverURL+"/command", "application/json", bytes.NewBuffer(jsonData))
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, body)
}
var response Response
err = json.NewDecoder(resp.Body).Decode(&response)
if err != nil {
return nil, err
}
return &response, nil
}
// OpenTab opens a new tab
// timeout is in seconds, 0 means no timeout
func (c *Client) OpenTab(timeout int) (string, error) {
params := map[string]string{}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("open-tab", params)
if err != nil {
return "", err
}
if !resp.Success {
return "", fmt.Errorf("failed to open tab: %s", resp.Error)
}
tabID, ok := resp.Data.(string)
if !ok {
return "", fmt.Errorf("unexpected response data type")
}
return tabID, nil
}
// LoadURL loads a URL in a tab
// If tabID is empty, the current tab will be used
// timeout is in seconds, 0 means no timeout
func (c *Client) LoadURL(tabID, url string, timeout int) error {
params := map[string]string{
"url": url,
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("load-url", params)
if err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("failed to load URL: %s", resp.Error)
}
return nil
}
// FillFormField fills a form field with a value
// If tabID is empty, the current tab will be used
// timeout is in seconds, 0 means no timeout
func (c *Client) FillFormField(tabID, selector, value string, timeout int) error {
params := map[string]string{
"selector": selector,
"value": value,
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("fill-form", params)
if err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("failed to fill form field: %s", resp.Error)
}
return nil
}
// UploadFile uploads a file to a file input
// If tabID is empty, the current tab will be used
// timeout is in seconds, 0 means no timeout
func (c *Client) UploadFile(tabID, selector, filePath string, timeout int) error {
params := map[string]string{
"selector": selector,
"file": filePath,
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("upload-file", params)
if err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("failed to upload file: %s", resp.Error)
}
return nil
}
// SelectElement selects an option in a select dropdown
// If tabID is empty, the current tab will be used
// timeout is in seconds, 0 means no timeout
func (c *Client) SelectElement(tabID, selector, value string, timeout int) error {
params := map[string]string{
"selector": selector,
"value": value,
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("select-element", params)
if err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("failed to select element: %s", resp.Error)
}
return nil
}
// SubmitForm submits a form
// If tabID is empty, the current tab will be used
// timeout is in seconds, 0 means no timeout
func (c *Client) SubmitForm(tabID, selector string, timeout int) error {
params := map[string]string{
"selector": selector,
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("submit-form", params)
if err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("failed to submit form: %s", resp.Error)
}
return nil
}
// GetPageSource gets the source code of a page
// If tabID is empty, the current tab will be used
// timeout is in seconds, 0 means no timeout
func (c *Client) GetPageSource(tabID string, timeout int) (string, error) {
params := map[string]string{}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("get-source", params)
if err != nil {
return "", err
}
if !resp.Success {
return "", fmt.Errorf("failed to get page source: %s", resp.Error)
}
source, ok := resp.Data.(string)
if !ok {
return "", fmt.Errorf("unexpected response data type")
}
return source, nil
}
// GetElementHTML gets the HTML of an element
// If tabID is empty, the current tab will be used
// timeout is in seconds, 0 means no timeout
func (c *Client) GetElementHTML(tabID, selector string, timeout int) (string, error) {
params := map[string]string{
"selector": selector,
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("get-element", params)
if err != nil {
return "", err
}
if !resp.Success {
return "", fmt.Errorf("failed to get element HTML: %s", resp.Error)
}
html, ok := resp.Data.(string)
if !ok {
return "", fmt.Errorf("unexpected response data type")
}
return html, nil
}
// CloseTab closes a tab
// If tabID is empty, the current tab will be used
// timeout is in seconds, 0 means no timeout
func (c *Client) CloseTab(tabID string, timeout int) error {
params := map[string]string{}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("close-tab", params)
if err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("failed to close tab: %s", resp.Error)
}
return nil
}
// WaitNavigation waits for a navigation event
// If tabID is empty, the current tab will be used
func (c *Client) WaitNavigation(tabID string, timeout int) error {
params := map[string]string{
"timeout": fmt.Sprintf("%d", timeout),
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
resp, err := c.SendCommand("wait-navigation", params)
if err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("failed to wait for navigation: %s", resp.Error)
}
return nil
}
// EvalJS executes JavaScript code in a tab and returns the result
// If tabID is empty, the current tab will be used
// timeout is in seconds, 0 means no timeout
func (c *Client) EvalJS(tabID, jsCode string, timeout int) (string, error) {
params := map[string]string{
"code": jsCode,
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("eval-js", params)
if err != nil {
return "", err
}
if !resp.Success {
return "", fmt.Errorf("failed to execute JavaScript: %s", resp.Error)
}
// Convert result to string if it exists
if resp.Data != nil {
if result, ok := resp.Data.(string); ok {
return result, nil
}
// If it's not a string, convert it to string representation
return fmt.Sprintf("%v", resp.Data), nil
}
return "", nil
}
// TakeScreenshot takes a screenshot of a tab and saves it to a file
// If tabID is empty, the current tab will be used
// timeout is in seconds, 0 means no timeout
func (c *Client) TakeScreenshot(tabID, outputPath string, fullPage bool, timeout int) error {
params := map[string]string{
"output": outputPath,
"full-page": strconv.FormatBool(fullPage),
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("screenshot", params)
if err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("failed to take screenshot: %s", resp.Error)
}
return nil
}
// SwitchToIframe switches the context to an iframe for subsequent commands
// If tabID is empty, the current tab will be used
// timeout is in seconds, 0 means no timeout
func (c *Client) SwitchToIframe(tabID, selector string, timeout int) error {
params := map[string]string{
"selector": selector,
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("switch-iframe", params)
if err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("failed to switch to iframe: %s", resp.Error)
}
return nil
}
// SwitchToMain switches the context back to the main page
// If tabID is empty, the current tab will be used
// timeout is in seconds, 0 means no timeout
func (c *Client) SwitchToMain(tabID string, timeout int) error {
params := map[string]string{}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("switch-main", params)
if err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("failed to switch to main context: %s", resp.Error)
}
return nil
}
// ClickElement clicks on an element
// If tabID is empty, the current tab will be used
// timeout is in seconds, 0 means no timeout
func (c *Client) ClickElement(tabID, selector string, timeout int) error {
params := map[string]string{
"selector": selector,
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("click-element", params)
if err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("failed to click element: %s", resp.Error)
}
return nil
}
// UploadFileToContainer uploads a file from the client to the container
// localPath: path to the file on the client machine
// containerPath: optional path where to store the file in the container (defaults to /tmp/filename)
func (c *Client) UploadFileToContainer(localPath, containerPath string) (string, error) {
// Open the local file
file, err := os.Open(localPath)
if err != nil {
return "", fmt.Errorf("failed to open local file: %w", err)
}
defer file.Close()
// Get file info
fileInfo, err := file.Stat()
if err != nil {
return "", fmt.Errorf("failed to get file info: %w", err)
}
// Create multipart form
var body bytes.Buffer
writer := multipart.NewWriter(&body)
// Add the file field
fileWriter, err := writer.CreateFormFile("file", fileInfo.Name())
if err != nil {
return "", fmt.Errorf("failed to create form file: %w", err)
}
_, err = io.Copy(fileWriter, file)
if err != nil {
return "", fmt.Errorf("failed to copy file data: %w", err)
}
// Add the path field if specified
if containerPath != "" {
err = writer.WriteField("path", containerPath)
if err != nil {
return "", fmt.Errorf("failed to write path field: %w", err)
}
}
err = writer.Close()
if err != nil {
return "", fmt.Errorf("failed to close multipart writer: %w", err)
}
// Send the request
req, err := http.NewRequest("POST", c.serverURL+"/upload", &body)
if err != nil {
return "", fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", writer.FormDataContentType())
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return "", fmt.Errorf("upload failed with status %d: %s", resp.StatusCode, body)
}
// Parse the response
var response Response
err = json.NewDecoder(resp.Body).Decode(&response)
if err != nil {
return "", fmt.Errorf("failed to decode response: %w", err)
}
if !response.Success {
return "", fmt.Errorf("upload failed: %s", response.Error)
}
// Extract the target path from response
data, ok := response.Data.(map[string]interface{})
if !ok {
return "", fmt.Errorf("invalid response data format")
}
targetPath, ok := data["target_path"].(string)
if !ok {
return "", fmt.Errorf("target_path not found in response")
}
return targetPath, nil
}
// DownloadFileFromContainer downloads a file from the container to the client
// containerPath: path to the file in the container
// localPath: path where to save the file on the client machine
func (c *Client) DownloadFileFromContainer(containerPath, localPath string) error {
// Create the request
url := fmt.Sprintf("%s/download?path=%s", c.serverURL, containerPath)
resp, err := http.Get(url)
if err != nil {
return fmt.Errorf("failed to send download request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("download failed with status %d: %s", resp.StatusCode, body)
}
// Create the local file
localFile, err := os.Create(localPath)
if err != nil {
return fmt.Errorf("failed to create local file: %w", err)
}
defer localFile.Close()
// Copy the response body to the local file
_, err = io.Copy(localFile, resp.Body)
if err != nil {
return fmt.Errorf("failed to save file: %w", err)
}
return nil
}
// GetConsoleLogs retrieves console logs from a tab
// If tabID is empty, the current tab will be used
// If clear is true, the logs will be cleared after retrieval
func (c *Client) GetConsoleLogs(tabID string, clear bool) ([]map[string]interface{}, error) {
params := map[string]string{}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add clear flag if specified
if clear {
params["clear"] = "true"
}
resp, err := c.SendCommand("console-logs", params)
if err != nil {
return nil, err
}
if !resp.Success {
return nil, fmt.Errorf("failed to get console logs: %s", resp.Error)
}
// Convert response data to slice of console logs
logs, ok := resp.Data.([]interface{})
if !ok {
return nil, fmt.Errorf("unexpected response data type")
}
result := make([]map[string]interface{}, len(logs))
for i, log := range logs {
logMap, ok := log.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("unexpected log entry format")
}
result[i] = logMap
}
return result, nil
}
// ExecuteConsoleCommand executes a command in the browser console
// If tabID is empty, the current tab will be used
// timeout is in seconds, 0 means no timeout
func (c *Client) ExecuteConsoleCommand(tabID, command string, timeout int) (string, error) {
params := map[string]string{
"command": command,
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("console-command", params)
if err != nil {
return "", err
}
if !resp.Success {
return "", fmt.Errorf("failed to execute console command: %s", resp.Error)
}
result, ok := resp.Data.(string)
if !ok {
return "", fmt.Errorf("unexpected response data type")
}
return result, nil
}
// 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
}
// 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"`
}
// 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"`
}
// 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"`
}
// 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"`
}
// 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"`
}
// CheckElement checks various states of an element
// checkType can be: "exists", "visible", "enabled", "focused", "selected", "all"
// timeout is in seconds, 0 means no timeout
func (c *Client) CheckElement(tabID, selector, checkType string, timeout int) (*ElementCheckResult, error) {
params := map[string]string{
"selector": selector,
"type": checkType,
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("check-element", params)
if err != nil {
return nil, err
}
if !resp.Success {
return nil, fmt.Errorf("failed to check element: %s", resp.Error)
}
// Parse the response data
data, ok := resp.Data.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("unexpected response data type")
}
result := &ElementCheckResult{}
if exists, ok := data["exists"].(bool); ok {
result.Exists = exists
}
if visible, ok := data["visible"].(bool); ok {
result.Visible = visible
}
if enabled, ok := data["enabled"].(bool); ok {
result.Enabled = enabled
}
if focused, ok := data["focused"].(bool); ok {
result.Focused = focused
}
if selected, ok := data["selected"].(bool); ok {
result.Selected = selected
}
if count, ok := data["count"].(float64); ok {
result.Count = int(count)
}
return result, nil
}
// GetElementAttributes gets attributes, properties, and computed styles of an element
// attributes can be a comma-separated list of attribute names or "all" for common attributes
// Use prefixes: "style_" for computed styles, "prop_" for JavaScript properties
// timeout is in seconds, 0 means no timeout
func (c *Client) GetElementAttributes(tabID, selector, attributes string, timeout int) (map[string]interface{}, error) {
params := map[string]string{
"selector": selector,
"attributes": attributes,
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("get-element-attributes", params)
if err != nil {
return nil, err
}
if !resp.Success {
return nil, fmt.Errorf("failed to get element attributes: %s", resp.Error)
}
// Parse the response data
result, ok := resp.Data.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("unexpected response data type")
}
return result, nil
}
// CountElements counts the number of elements matching a selector
// timeout is in seconds, 0 means no timeout
func (c *Client) CountElements(tabID, selector string, timeout int) (int, error) {
params := map[string]string{
"selector": selector,
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("count-elements", params)
if err != nil {
return 0, err
}
if !resp.Success {
return 0, fmt.Errorf("failed to count elements: %s", resp.Error)
}
// Parse the response data
count, ok := resp.Data.(float64)
if !ok {
return 0, fmt.Errorf("unexpected response data type")
}
return int(count), nil
}
// ExtractMultiple extracts data from multiple selectors in a single call
// selectors should be a map[string]string where keys are labels and values are CSS selectors
// timeout is in seconds, 0 means no timeout
func (c *Client) ExtractMultiple(tabID string, selectors map[string]string, timeout int) (*MultipleExtractionResult, error) {
// Convert selectors map to JSON
selectorsJSON, err := json.Marshal(selectors)
if err != nil {
return nil, fmt.Errorf("failed to marshal selectors: %w", err)
}
params := map[string]string{
"selectors": string(selectorsJSON),
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("extract-multiple", params)
if err != nil {
return nil, err
}
if !resp.Success {
return nil, fmt.Errorf("failed to extract multiple: %s", resp.Error)
}
// Parse the response data
data, ok := resp.Data.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("unexpected response data type")
}
result := &MultipleExtractionResult{
Results: make(map[string]interface{}),
Errors: make(map[string]string),
}
if results, ok := data["results"].(map[string]interface{}); ok {
result.Results = results
}
if errors, ok := data["errors"].(map[string]interface{}); ok {
for key, value := range errors {
if errorStr, ok := value.(string); ok {
result.Errors[key] = errorStr
}
}
}
return result, nil
}
// ExtractLinks extracts all links from the page with optional filtering
// containerSelector: optional CSS selector to limit search to a container (empty for entire page)
// hrefPattern: optional regex pattern to filter links by href (empty for no filtering)
// textPattern: optional regex pattern to filter links by text content (empty for no filtering)
// timeout is in seconds, 0 means no timeout
func (c *Client) ExtractLinks(tabID, containerSelector, hrefPattern, textPattern string, timeout int) (*LinksExtractionResult, error) {
params := map[string]string{}
// Add optional parameters
if containerSelector != "" {
params["selector"] = containerSelector
}
if hrefPattern != "" {
params["href-pattern"] = hrefPattern
}
if textPattern != "" {
params["text-pattern"] = textPattern
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("extract-links", params)
if err != nil {
return nil, err
}
if !resp.Success {
return nil, fmt.Errorf("failed to extract links: %s", resp.Error)
}
// Parse the response data
data, ok := resp.Data.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("unexpected response data type")
}
result := &LinksExtractionResult{
Links: make([]LinkInfo, 0),
Count: 0,
}
if count, ok := data["count"].(float64); ok {
result.Count = int(count)
}
if linksData, ok := data["links"].([]interface{}); ok {
for _, linkInterface := range linksData {
if linkMap, ok := linkInterface.(map[string]interface{}); ok {
linkInfo := LinkInfo{}
if href, ok := linkMap["href"].(string); ok {
linkInfo.Href = href
}
if text, ok := linkMap["text"].(string); ok {
linkInfo.Text = text
}
if title, ok := linkMap["title"].(string); ok {
linkInfo.Title = title
}
if target, ok := linkMap["target"].(string); ok {
linkInfo.Target = target
}
result.Links = append(result.Links, linkInfo)
}
}
}
return result, nil
}
// ExtractTable extracts table data as structured JSON
// selector: CSS selector for the table element
// includeHeaders: whether to extract and use headers for structured data
// timeout is in seconds, 0 means no timeout
func (c *Client) ExtractTable(tabID, selector string, includeHeaders bool, timeout int) (*TableExtractionResult, error) {
params := map[string]string{
"selector": selector,
"include-headers": strconv.FormatBool(includeHeaders),
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("extract-table", params)
if err != nil {
return nil, err
}
if !resp.Success {
return nil, fmt.Errorf("failed to extract table: %s", resp.Error)
}
// Parse the response data
data, ok := resp.Data.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("unexpected response data type")
}
result := &TableExtractionResult{
Rows: make([][]string, 0),
}
if count, ok := data["count"].(float64); ok {
result.Count = int(count)
}
// Parse headers if present
if headersData, ok := data["headers"].([]interface{}); ok {
headers := make([]string, 0)
for _, headerInterface := range headersData {
if header, ok := headerInterface.(string); ok {
headers = append(headers, header)
}
}
result.Headers = headers
}
// Parse rows
if rowsData, ok := data["rows"].([]interface{}); ok {
for _, rowInterface := range rowsData {
if rowArray, ok := rowInterface.([]interface{}); ok {
row := make([]string, 0)
for _, cellInterface := range rowArray {
if cell, ok := cellInterface.(string); ok {
row = append(row, cell)
}
}
result.Rows = append(result.Rows, row)
}
}
}
// Parse structured data if present
if dataArray, ok := data["data"].([]interface{}); ok {
structuredData := make([]map[string]string, 0)
for _, dataInterface := range dataArray {
if dataMap, ok := dataInterface.(map[string]interface{}); ok {
rowMap := make(map[string]string)
for key, value := range dataMap {
if valueStr, ok := value.(string); ok {
rowMap[key] = valueStr
}
}
structuredData = append(structuredData, rowMap)
}
}
result.Data = structuredData
}
return result, nil
}
// ExtractText extracts text content with optional pattern matching
// selector: CSS selector for elements to extract text from
// pattern: optional regex pattern to match within the extracted text (empty for no pattern matching)
// extractType: type of text extraction - "text", "innerText", "textContent" (default: "textContent")
// timeout is in seconds, 0 means no timeout
func (c *Client) ExtractText(tabID, selector, pattern, extractType string, timeout int) (*TextExtractionResult, error) {
params := map[string]string{
"selector": selector,
}
// Add optional parameters
if pattern != "" {
params["pattern"] = pattern
}
if extractType != "" {
params["type"] = extractType
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("extract-text", params)
if err != nil {
return nil, err
}
if !resp.Success {
return nil, fmt.Errorf("failed to extract text: %s", resp.Error)
}
// Parse the response data
data, ok := resp.Data.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("unexpected response data type")
}
result := &TextExtractionResult{}
if text, ok := data["text"].(string); ok {
result.Text = text
}
if count, ok := data["count"].(float64); ok {
result.Count = int(count)
}
// Parse matches if present
if matchesData, ok := data["matches"].([]interface{}); ok {
matches := make([]string, 0)
for _, matchInterface := range matchesData {
if match, ok := matchInterface.(string); ok {
matches = append(matches, match)
}
}
result.Matches = matches
}
return result, nil
}
// AnalyzeForm analyzes a form and returns detailed information about its fields
// selector: CSS selector for the form element
// timeout is in seconds, 0 means no timeout
func (c *Client) AnalyzeForm(tabID, selector string, timeout int) (*FormAnalysisResult, error) {
params := map[string]string{
"selector": selector,
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("analyze-form", params)
if err != nil {
return nil, err
}
if !resp.Success {
return nil, fmt.Errorf("failed to analyze form: %s", resp.Error)
}
// Parse the response data
data, ok := resp.Data.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("unexpected response data type")
}
result := &FormAnalysisResult{
Fields: make([]FormField, 0),
}
if action, ok := data["action"].(string); ok {
result.Action = action
}
if method, ok := data["method"].(string); ok {
result.Method = method
}
if fieldCount, ok := data["field_count"].(float64); ok {
result.FieldCount = int(fieldCount)
}
if canSubmit, ok := data["can_submit"].(bool); ok {
result.CanSubmit = canSubmit
}
if submitText, ok := data["submit_text"].(string); ok {
result.SubmitText = submitText
}
// Parse fields
if fieldsData, ok := data["fields"].([]interface{}); ok {
for _, fieldInterface := range fieldsData {
if fieldMap, ok := fieldInterface.(map[string]interface{}); ok {
field := FormField{}
if name, ok := fieldMap["name"].(string); ok {
field.Name = name
}
if fieldType, ok := fieldMap["type"].(string); ok {
field.Type = fieldType
}
if value, ok := fieldMap["value"].(string); ok {
field.Value = value
}
if placeholder, ok := fieldMap["placeholder"].(string); ok {
field.Placeholder = placeholder
}
if required, ok := fieldMap["required"].(bool); ok {
field.Required = required
}
if disabled, ok := fieldMap["disabled"].(bool); ok {
field.Disabled = disabled
}
if readonly, ok := fieldMap["readonly"].(bool); ok {
field.ReadOnly = readonly
}
if selector, ok := fieldMap["selector"].(string); ok {
field.Selector = selector
}
if label, ok := fieldMap["label"].(string); ok {
field.Label = label
}
// Parse options if present
if optionsData, ok := fieldMap["options"].([]interface{}); ok {
options := make([]FormFieldOption, 0)
for _, optionInterface := range optionsData {
if optionMap, ok := optionInterface.(map[string]interface{}); ok {
option := FormFieldOption{}
if value, ok := optionMap["value"].(string); ok {
option.Value = value
}
if text, ok := optionMap["text"].(string); ok {
option.Text = text
}
if selected, ok := optionMap["selected"].(bool); ok {
option.Selected = selected
}
options = append(options, option)
}
}
field.Options = options
}
result.Fields = append(result.Fields, field)
}
}
}
return result, nil
}
// InteractMultiple performs multiple interactions in sequence
// interactions: slice of InteractionItem specifying what actions to perform
// timeout is in seconds, 0 means no timeout
func (c *Client) InteractMultiple(tabID string, interactions []InteractionItem, timeout int) (*MultipleInteractionResult, error) {
// Convert interactions to JSON
interactionsJSON, err := json.Marshal(interactions)
if err != nil {
return nil, fmt.Errorf("failed to marshal interactions: %w", err)
}
params := map[string]string{
"interactions": string(interactionsJSON),
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("interact-multiple", params)
if err != nil {
return nil, err
}
if !resp.Success {
return nil, fmt.Errorf("failed to perform multiple interactions: %s", resp.Error)
}
// Parse the response data
data, ok := resp.Data.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("unexpected response data type")
}
result := &MultipleInteractionResult{
Results: make([]InteractionResult, 0),
}
if successCount, ok := data["success_count"].(float64); ok {
result.SuccessCount = int(successCount)
}
if errorCount, ok := data["error_count"].(float64); ok {
result.ErrorCount = int(errorCount)
}
if totalCount, ok := data["total_count"].(float64); ok {
result.TotalCount = int(totalCount)
}
// Parse results
if resultsData, ok := data["results"].([]interface{}); ok {
for _, resultInterface := range resultsData {
if resultMap, ok := resultInterface.(map[string]interface{}); ok {
interactionResult := InteractionResult{}
if selector, ok := resultMap["selector"].(string); ok {
interactionResult.Selector = selector
}
if action, ok := resultMap["action"].(string); ok {
interactionResult.Action = action
}
if success, ok := resultMap["success"].(bool); ok {
interactionResult.Success = success
}
if errorMsg, ok := resultMap["error"].(string); ok {
interactionResult.Error = errorMsg
}
result.Results = append(result.Results, interactionResult)
}
}
}
return result, nil
}
// FillFormBulk fills multiple form fields in a single operation
// formSelector: CSS selector for the form element (optional, can be empty to search entire page)
// fields: map of field names/selectors to values
// timeout is in seconds, 0 means no timeout
func (c *Client) FillFormBulk(tabID, formSelector string, fields map[string]string, timeout int) (*FormBulkFillResult, error) {
// Convert fields to JSON
fieldsJSON, err := json.Marshal(fields)
if err != nil {
return nil, fmt.Errorf("failed to marshal fields: %w", err)
}
params := map[string]string{
"fields": string(fieldsJSON),
}
// Add form selector if provided
if formSelector != "" {
params["form-selector"] = formSelector
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("fill-form-bulk", params)
if err != nil {
return nil, err
}
if !resp.Success {
return nil, fmt.Errorf("failed to fill form bulk: %s", resp.Error)
}
// Parse the response data
data, ok := resp.Data.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("unexpected response data type")
}
result := &FormBulkFillResult{
FilledFields: make([]InteractionResult, 0),
}
if successCount, ok := data["success_count"].(float64); ok {
result.SuccessCount = int(successCount)
}
if errorCount, ok := data["error_count"].(float64); ok {
result.ErrorCount = int(errorCount)
}
if totalCount, ok := data["total_count"].(float64); ok {
result.TotalCount = int(totalCount)
}
// Parse filled fields
if fieldsData, ok := data["filled_fields"].([]interface{}); ok {
for _, fieldInterface := range fieldsData {
if fieldMap, ok := fieldInterface.(map[string]interface{}); ok {
fieldResult := InteractionResult{}
if selector, ok := fieldMap["selector"].(string); ok {
fieldResult.Selector = selector
}
if action, ok := fieldMap["action"].(string); ok {
fieldResult.Action = action
}
if success, ok := fieldMap["success"].(bool); ok {
fieldResult.Success = success
}
if errorMsg, ok := fieldMap["error"].(string); ok {
fieldResult.Error = errorMsg
}
result.FilledFields = append(result.FilledFields, fieldResult)
}
}
}
return result, nil
}
// GetPageInfo retrieves comprehensive page metadata and state information
func (c *Client) GetPageInfo(tabID string, timeout int) (*PageInfo, error) {
params := map[string]string{}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("get-page-info", params)
if err != nil {
return nil, err
}
if !resp.Success {
return nil, fmt.Errorf("failed to get page info: %s", resp.Error)
}
// Parse the response data
data, ok := resp.Data.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("unexpected response data type")
}
result := &PageInfo{}
if title, ok := data["title"].(string); ok {
result.Title = title
}
if url, ok := data["url"].(string); ok {
result.URL = url
}
if loadingState, ok := data["loading_state"].(string); ok {
result.LoadingState = loadingState
}
if readyState, ok := data["ready_state"].(string); ok {
result.ReadyState = readyState
}
if referrer, ok := data["referrer"].(string); ok {
result.Referrer = referrer
}
if domain, ok := data["domain"].(string); ok {
result.Domain = domain
}
if protocol, ok := data["protocol"].(string); ok {
result.Protocol = protocol
}
if charset, ok := data["charset"].(string); ok {
result.Charset = charset
}
if contentType, ok := data["content_type"].(string); ok {
result.ContentType = contentType
}
if lastModified, ok := data["last_modified"].(string); ok {
result.LastModified = lastModified
}
if cookieEnabled, ok := data["cookie_enabled"].(bool); ok {
result.CookieEnabled = cookieEnabled
}
if onlineStatus, ok := data["online_status"].(bool); ok {
result.OnlineStatus = onlineStatus
}
return result, nil
}
// GetViewportInfo retrieves viewport and scroll information
func (c *Client) GetViewportInfo(tabID string, timeout int) (*ViewportInfo, error) {
params := map[string]string{}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("get-viewport-info", params)
if err != nil {
return nil, err
}
if !resp.Success {
return nil, fmt.Errorf("failed to get viewport info: %s", resp.Error)
}
// Parse the response data
data, ok := resp.Data.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("unexpected response data type")
}
result := &ViewportInfo{}
if width, ok := data["width"].(float64); ok {
result.Width = int(width)
}
if height, ok := data["height"].(float64); ok {
result.Height = int(height)
}
if scrollX, ok := data["scroll_x"].(float64); ok {
result.ScrollX = int(scrollX)
}
if scrollY, ok := data["scroll_y"].(float64); ok {
result.ScrollY = int(scrollY)
}
if scrollWidth, ok := data["scroll_width"].(float64); ok {
result.ScrollWidth = int(scrollWidth)
}
if scrollHeight, ok := data["scroll_height"].(float64); ok {
result.ScrollHeight = int(scrollHeight)
}
if clientWidth, ok := data["client_width"].(float64); ok {
result.ClientWidth = int(clientWidth)
}
if clientHeight, ok := data["client_height"].(float64); ok {
result.ClientHeight = int(clientHeight)
}
if devicePixelRatio, ok := data["device_pixel_ratio"].(float64); ok {
result.DevicePixelRatio = devicePixelRatio
}
if orientation, ok := data["orientation"].(string); ok {
result.Orientation = orientation
}
return result, nil
}
// GetPerformance retrieves page performance metrics
func (c *Client) GetPerformance(tabID string, timeout int) (*PerformanceMetrics, error) {
params := map[string]string{}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("get-performance", params)
if err != nil {
return nil, err
}
if !resp.Success {
return nil, fmt.Errorf("failed to get performance metrics: %s", resp.Error)
}
// Parse the response data
data, ok := resp.Data.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("unexpected response data type")
}
result := &PerformanceMetrics{}
if navigationStart, ok := data["navigation_start"].(float64); ok {
result.NavigationStart = int64(navigationStart)
}
if loadEventEnd, ok := data["load_event_end"].(float64); ok {
result.LoadEventEnd = int64(loadEventEnd)
}
if domContentLoaded, ok := data["dom_content_loaded"].(float64); ok {
result.DOMContentLoaded = int64(domContentLoaded)
}
if firstPaint, ok := data["first_paint"].(float64); ok {
result.FirstPaint = int64(firstPaint)
}
if firstContentfulPaint, ok := data["first_contentful_paint"].(float64); ok {
result.FirstContentfulPaint = int64(firstContentfulPaint)
}
if loadTime, ok := data["load_time"].(float64); ok {
result.LoadTime = int64(loadTime)
}
if domLoadTime, ok := data["dom_load_time"].(float64); ok {
result.DOMLoadTime = int64(domLoadTime)
}
if resourceCount, ok := data["resource_count"].(float64); ok {
result.ResourceCount = int(resourceCount)
}
if jsHeapSizeLimit, ok := data["js_heap_size_limit"].(float64); ok {
result.JSHeapSizeLimit = int64(jsHeapSizeLimit)
}
if jsHeapSizeTotal, ok := data["js_heap_size_total"].(float64); ok {
result.JSHeapSizeTotal = int64(jsHeapSizeTotal)
}
if jsHeapSizeUsed, ok := data["js_heap_size_used"].(float64); ok {
result.JSHeapSizeUsed = int64(jsHeapSizeUsed)
}
return result, nil
}
// CheckContent verifies specific content types and loading states
// contentType can be: "images", "scripts", "styles", "forms", "links", "iframes", "errors"
func (c *Client) CheckContent(tabID string, contentType string, timeout int) (*ContentCheck, error) {
params := map[string]string{
"type": contentType,
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("check-content", params)
if err != nil {
return nil, err
}
if !resp.Success {
return nil, fmt.Errorf("failed to check content: %s", resp.Error)
}
// Parse the response data
data, ok := resp.Data.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("unexpected response data type")
}
result := &ContentCheck{}
if contentTypeResult, ok := data["type"].(string); ok {
result.Type = contentTypeResult
}
if imagesLoaded, ok := data["images_loaded"].(float64); ok {
result.ImagesLoaded = int(imagesLoaded)
}
if imagesTotal, ok := data["images_total"].(float64); ok {
result.ImagesTotal = int(imagesTotal)
}
if scriptsLoaded, ok := data["scripts_loaded"].(float64); ok {
result.ScriptsLoaded = int(scriptsLoaded)
}
if scriptsTotal, ok := data["scripts_total"].(float64); ok {
result.ScriptsTotal = int(scriptsTotal)
}
if stylesLoaded, ok := data["styles_loaded"].(float64); ok {
result.StylesLoaded = int(stylesLoaded)
}
if stylesTotal, ok := data["styles_total"].(float64); ok {
result.StylesTotal = int(stylesTotal)
}
if formsPresent, ok := data["forms_present"].(float64); ok {
result.FormsPresent = int(formsPresent)
}
if linksPresent, ok := data["links_present"].(float64); ok {
result.LinksPresent = int(linksPresent)
}
if iframesPresent, ok := data["iframes_present"].(float64); ok {
result.IframesPresent = int(iframesPresent)
}
if hasErrors, ok := data["has_errors"].(bool); ok {
result.HasErrors = hasErrors
}
if errorCount, ok := data["error_count"].(float64); ok {
result.ErrorCount = int(errorCount)
}
if errorMessages, ok := data["error_messages"].([]interface{}); ok {
for _, msg := range errorMessages {
if msgStr, ok := msg.(string); ok {
result.ErrorMessages = append(result.ErrorMessages, msgStr)
}
}
}
return result, nil
}
// ScreenshotElement takes a screenshot of a specific element
// If tabID is empty, the current tab will be used
// timeout is in seconds, 0 means no timeout
func (c *Client) ScreenshotElement(tabID, selector, outputPath string, timeout int) error {
params := map[string]string{
"selector": selector,
"output": outputPath,
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("screenshot-element", params)
if err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("failed to take element screenshot: %s", resp.Error)
}
return nil
}
// Accessibility tree data structures (matching daemon types)
// 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
// If tabID is empty, the current tab will be used
// depth limits the tree depth (optional, nil for full tree)
// timeout is in seconds, 0 means no timeout
func (c *Client) GetAccessibilityTree(tabID string, depth *int, timeout int) (*AccessibilityTreeResult, error) {
params := map[string]string{}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add depth if specified
if depth != nil {
params["depth"] = strconv.Itoa(*depth)
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("get-accessibility-tree", params)
if err != nil {
return nil, err
}
if !resp.Success {
return nil, fmt.Errorf("failed to get accessibility tree: %s", resp.Error)
}
// Parse the response data
var result AccessibilityTreeResult
dataBytes, err := json.Marshal(resp.Data)
if err != nil {
return nil, fmt.Errorf("failed to marshal response data: %w", err)
}
err = json.Unmarshal(dataBytes, &result)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal accessibility tree result: %w", err)
}
return &result, nil
}
// GetPartialAccessibilityTree retrieves a partial accessibility tree for a specific element
// If tabID is empty, the current tab will be used
// selector is the CSS selector for the element to get the tree for
// fetchRelatives determines whether to include ancestors, siblings, and children
// timeout is in seconds, 0 means no timeout
func (c *Client) GetPartialAccessibilityTree(tabID, selector string, fetchRelatives bool, timeout int) (*AccessibilityTreeResult, error) {
params := map[string]string{
"selector": selector,
"fetch-relatives": strconv.FormatBool(fetchRelatives),
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("get-partial-accessibility-tree", params)
if err != nil {
return nil, err
}
if !resp.Success {
return nil, fmt.Errorf("failed to get partial accessibility tree: %s", resp.Error)
}
// Parse the response data
var result AccessibilityTreeResult
dataBytes, err := json.Marshal(resp.Data)
if err != nil {
return nil, fmt.Errorf("failed to marshal response data: %w", err)
}
err = json.Unmarshal(dataBytes, &result)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal accessibility tree result: %w", err)
}
return &result, nil
}
// QueryAccessibilityTree queries the accessibility tree for nodes matching specific criteria
// If tabID is empty, the current tab will be used
// selector is optional CSS selector to limit the search scope
// accessibleName is optional accessible name to match
// role is optional role to match
// timeout is in seconds, 0 means no timeout
func (c *Client) QueryAccessibilityTree(tabID, selector, accessibleName, role string, timeout int) (*AccessibilityQueryResult, error) {
params := map[string]string{}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add optional parameters
if selector != "" {
params["selector"] = selector
}
if accessibleName != "" {
params["accessible-name"] = accessibleName
}
if role != "" {
params["role"] = role
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("query-accessibility-tree", params)
if err != nil {
return nil, err
}
if !resp.Success {
return nil, fmt.Errorf("failed to query accessibility tree: %s", resp.Error)
}
// Parse the response data
var result AccessibilityQueryResult
dataBytes, err := json.Marshal(resp.Data)
if err != nil {
return nil, fmt.Errorf("failed to marshal response data: %w", err)
}
err = json.Unmarshal(dataBytes, &result)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal accessibility query result: %w", err)
}
return &result, nil
}
// ScreenshotEnhanced takes a screenshot with metadata
// If tabID is empty, the current tab will be used
// timeout is in seconds, 0 means no timeout
func (c *Client) ScreenshotEnhanced(tabID, outputPath string, fullPage bool, timeout int) (*ScreenshotMetadata, error) {
params := map[string]string{
"output": outputPath,
"full-page": strconv.FormatBool(fullPage),
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("screenshot-enhanced", params)
if err != nil {
return nil, err
}
if !resp.Success {
return nil, fmt.Errorf("failed to take enhanced screenshot: %s", resp.Error)
}
// Parse the response data
var metadata ScreenshotMetadata
dataBytes, err := json.Marshal(resp.Data)
if err != nil {
return nil, fmt.Errorf("failed to marshal response data: %w", err)
}
err = json.Unmarshal(dataBytes, &metadata)
if err != nil {
return nil, fmt.Errorf("failed to parse screenshot metadata: %w", err)
}
return &metadata, nil
}
// BulkFiles performs bulk file operations (upload/download)
// operationType: "upload" or "download"
// operations: slice of FileOperation structs
// timeout is in seconds, 0 means no timeout (default 30s for bulk operations)
func (c *Client) BulkFiles(operationType string, operations []FileOperation, timeout int) (*BulkFileResult, error) {
// Convert operations to JSON
operationsJSON, err := json.Marshal(operations)
if err != nil {
return nil, fmt.Errorf("failed to marshal operations: %w", err)
}
params := map[string]string{
"operation": operationType,
"files": string(operationsJSON),
}
// Add timeout if specified (default to 30 seconds for bulk operations)
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("bulk-files", params)
if err != nil {
return nil, err
}
if !resp.Success {
return nil, fmt.Errorf("failed to perform bulk file operations: %s", resp.Error)
}
// Parse the response data
var result BulkFileResult
dataBytes, err := json.Marshal(resp.Data)
if err != nil {
return nil, fmt.Errorf("failed to marshal response data: %w", err)
}
err = json.Unmarshal(dataBytes, &result)
if err != nil {
return nil, fmt.Errorf("failed to parse bulk file result: %w", err)
}
return &result, nil
}
// ManageFiles performs file management operations
// operation: "cleanup", "list", or "info"
// pattern: file pattern for cleanup/list operations, or file path for info
// maxAge: max age in hours for cleanup operations (optional)
func (c *Client) ManageFiles(operation, pattern, maxAge string) (*FileManagementResult, error) {
params := map[string]string{
"operation": operation,
}
// Add optional parameters
if pattern != "" {
params["pattern"] = pattern
}
if maxAge != "" {
params["max-age"] = maxAge
}
resp, err := c.SendCommand("manage-files", params)
if err != nil {
return nil, err
}
if !resp.Success {
return nil, fmt.Errorf("failed to manage files: %s", resp.Error)
}
// Parse the response data
var result FileManagementResult
dataBytes, err := json.Marshal(resp.Data)
if err != nil {
return nil, fmt.Errorf("failed to marshal response data: %w", err)
}
err = json.Unmarshal(dataBytes, &result)
if err != nil {
return nil, fmt.Errorf("failed to parse file management result: %w", err)
}
return &result, nil
}
// DisableCache disables browser cache for a tab
// If tabID is empty, the current tab will be used
// timeout is in seconds, 0 means no timeout
func (c *Client) DisableCache(tabID string, timeout int) error {
params := map[string]string{}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("disable-cache", params)
if err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("failed to disable cache: %s", resp.Error)
}
return nil
}
// EnableCache enables browser cache for a tab
// If tabID is empty, the current tab will be used
// timeout is in seconds, 0 means no timeout
func (c *Client) EnableCache(tabID string, timeout int) error {
params := map[string]string{}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("enable-cache", params)
if err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("failed to enable cache: %s", resp.Error)
}
return nil
}
// ClearCache clears browser cache for a tab
// If tabID is empty, the current tab will be used
// timeout is in seconds, 0 means no timeout
func (c *Client) ClearCache(tabID string, timeout int) error {
params := map[string]string{}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("clear-cache", params)
if err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("failed to clear cache: %s", resp.Error)
}
return nil
}
// ClearAllSiteData clears all site data including cookies, storage, cache, etc. for a tab
// If tabID is empty, the current tab will be used
// timeout is in seconds, 0 means no timeout
func (c *Client) ClearAllSiteData(tabID string, timeout int) error {
params := map[string]string{}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("clear-all-site-data", params)
if err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("failed to clear all site data: %s", resp.Error)
}
return nil
}
// ClearCookies clears cookies for a tab
// If tabID is empty, the current tab will be used
// timeout is in seconds, 0 means no timeout
func (c *Client) ClearCookies(tabID string, timeout int) error {
params := map[string]string{}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("clear-cookies", params)
if err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("failed to clear cookies: %s", resp.Error)
}
return nil
}
// ClearStorage clears web storage (localStorage, sessionStorage, IndexedDB, etc.) for a tab
// If tabID is empty, the current tab will be used
// timeout is in seconds, 0 means no timeout
func (c *Client) ClearStorage(tabID string, timeout int) error {
params := map[string]string{}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("clear-storage", params)
if err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("failed to clear storage: %s", resp.Error)
}
return nil
}
// DragAndDrop performs a drag and drop operation from source to target element
// If tabID is empty, the current tab will be used
// timeout is in seconds, 0 means no timeout
func (c *Client) DragAndDrop(tabID, sourceSelector, targetSelector string, timeout int) error {
params := map[string]string{
"source": sourceSelector,
"target": targetSelector,
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("drag-and-drop", params)
if err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("failed to perform drag and drop: %s", resp.Error)
}
return nil
}
// DragAndDropToCoordinates performs a drag and drop operation from source element to specific coordinates
// If tabID is empty, the current tab will be used
// timeout is in seconds, 0 means no timeout
func (c *Client) DragAndDropToCoordinates(tabID, sourceSelector string, targetX, targetY int, timeout int) error {
params := map[string]string{
"source": sourceSelector,
"target-x": strconv.Itoa(targetX),
"target-y": strconv.Itoa(targetY),
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("drag-and-drop-coordinates", params)
if err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("failed to perform drag and drop to coordinates: %s", resp.Error)
}
return nil
}
// DragAndDropByOffset performs a drag and drop operation from source element by a relative offset
// If tabID is empty, the current tab will be used
// timeout is in seconds, 0 means no timeout
func (c *Client) DragAndDropByOffset(tabID, sourceSelector string, offsetX, offsetY int, timeout int) error {
params := map[string]string{
"source": sourceSelector,
"offset-x": strconv.Itoa(offsetX),
"offset-y": strconv.Itoa(offsetY),
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("drag-and-drop-offset", params)
if err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("failed to perform drag and drop by offset: %s", resp.Error)
}
return nil
}
// RightClick performs a right-click on an element
// If tabID is empty, the current tab will be used
// timeout is in seconds, 0 means no timeout
func (c *Client) RightClick(tabID, selector string, timeout int) error {
params := map[string]string{
"selector": selector,
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("right-click", params)
if err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("failed to right-click element: %s", resp.Error)
}
return nil
}
// DoubleClick performs a double-click on an element
// If tabID is empty, the current tab will be used
// timeout is in seconds, 0 means no timeout
func (c *Client) DoubleClick(tabID, selector string, timeout int) error {
params := map[string]string{
"selector": selector,
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("double-click", params)
if err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("failed to double-click element: %s", resp.Error)
}
return nil
}
// MiddleClick performs a middle-click on an element
// If tabID is empty, the current tab will be used
// timeout is in seconds, 0 means no timeout
func (c *Client) MiddleClick(tabID, selector string, timeout int) error {
params := map[string]string{
"selector": selector,
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("middle-click", params)
if err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("failed to middle-click element: %s", resp.Error)
}
return nil
}
// Hover moves the mouse over an element without clicking
// If tabID is empty, the current tab will be used
// timeout is in seconds, 0 means no timeout
func (c *Client) Hover(tabID, selector string, timeout int) error {
params := map[string]string{
"selector": selector,
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("hover", params)
if err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("failed to hover over element: %s", resp.Error)
}
return nil
}
// MouseMove moves the mouse to specific coordinates without clicking
// If tabID is empty, the current tab will be used
// timeout is in seconds, 0 means no timeout
func (c *Client) MouseMove(tabID string, x, y int, timeout int) error {
params := map[string]string{
"x": strconv.Itoa(x),
"y": strconv.Itoa(y),
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("mouse-move", params)
if err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("failed to move mouse: %s", resp.Error)
}
return nil
}
// ScrollWheel performs mouse wheel scrolling at specific coordinates
// If tabID is empty, the current tab will be used
// deltaX and deltaY specify scroll amounts (negative = up/left, positive = down/right)
// timeout is in seconds, 0 means no timeout
func (c *Client) ScrollWheel(tabID string, x, y, deltaX, deltaY int, timeout int) error {
params := map[string]string{
"x": strconv.Itoa(x),
"y": strconv.Itoa(y),
"delta-x": strconv.Itoa(deltaX),
"delta-y": strconv.Itoa(deltaY),
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("scroll-wheel", params)
if err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("failed to scroll with mouse wheel: %s", resp.Error)
}
return nil
}
// KeyCombination sends a key combination (e.g., "Ctrl+C", "Alt+Tab", "Shift+Enter")
// If tabID is empty, the current tab will be used
// timeout is in seconds, 0 means no timeout
func (c *Client) KeyCombination(tabID, keys string, timeout int) error {
params := map[string]string{
"keys": keys,
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("key-combination", params)
if err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("failed to send key combination: %s", resp.Error)
}
return nil
}
// SpecialKey sends a special key (e.g., "Enter", "Escape", "Tab", "F1", "ArrowUp")
// If tabID is empty, the current tab will be used
// timeout is in seconds, 0 means no timeout
func (c *Client) SpecialKey(tabID, key string, timeout int) error {
params := map[string]string{
"key": key,
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("special-key", params)
if err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("failed to send special key: %s", resp.Error)
}
return nil
}
// ModifierClick performs a click with modifier keys (e.g., Ctrl+click, Shift+click)
// If tabID is empty, the current tab will be used
// timeout is in seconds, 0 means no timeout
func (c *Client) ModifierClick(tabID, selector, modifiers string, timeout int) error {
params := map[string]string{
"selector": selector,
"modifiers": modifiers,
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("modifier-click", params)
if err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("failed to perform modifier click: %s", resp.Error)
}
return nil
}
// TouchTap performs a single finger tap at specific coordinates
// If tabID is empty, the current tab will be used
// timeout is in seconds, 0 means no timeout
func (c *Client) TouchTap(tabID string, x, y int, timeout int) error {
params := map[string]string{
"x": strconv.Itoa(x),
"y": strconv.Itoa(y),
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("touch-tap", params)
if err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("failed to perform touch tap: %s", resp.Error)
}
return nil
}
// TouchLongPress performs a long press at specific coordinates
// If tabID is empty, the current tab will be used
// duration is in milliseconds (default: 1000ms)
// timeout is in seconds, 0 means no timeout
func (c *Client) TouchLongPress(tabID string, x, y, duration int, timeout int) error {
params := map[string]string{
"x": strconv.Itoa(x),
"y": strconv.Itoa(y),
}
if duration > 0 {
params["duration"] = strconv.Itoa(duration)
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("touch-long-press", params)
if err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("failed to perform touch long press: %s", resp.Error)
}
return nil
}
// TouchSwipe performs a swipe gesture from start to end coordinates
// If tabID is empty, the current tab will be used
// timeout is in seconds, 0 means no timeout
func (c *Client) TouchSwipe(tabID string, startX, startY, endX, endY int, timeout int) error {
params := map[string]string{
"start-x": strconv.Itoa(startX),
"start-y": strconv.Itoa(startY),
"end-x": strconv.Itoa(endX),
"end-y": strconv.Itoa(endY),
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("touch-swipe", params)
if err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("failed to perform touch swipe: %s", resp.Error)
}
return nil
}
// PinchZoom performs a pinch-to-zoom gesture
// If tabID is empty, the current tab will be used
// scale > 1.0 zooms in, scale < 1.0 zooms out
// timeout is in seconds, 0 means no timeout
func (c *Client) PinchZoom(tabID string, centerX, centerY int, scale float64, timeout int) error {
params := map[string]string{
"center-x": strconv.Itoa(centerX),
"center-y": strconv.Itoa(centerY),
"scale": fmt.Sprintf("%.2f", scale),
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("pinch-zoom", params)
if err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("failed to perform pinch zoom: %s", resp.Error)
}
return nil
}
// ScrollElement scrolls a specific element by a given amount
// If tabID is empty, the current tab will be used
// timeout is in seconds, 0 means no timeout
func (c *Client) ScrollElement(tabID, selector string, deltaX, deltaY int, timeout int) error {
params := map[string]string{
"selector": selector,
"delta-x": strconv.Itoa(deltaX),
"delta-y": strconv.Itoa(deltaY),
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("scroll-element", params)
if err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("failed to scroll element: %s", resp.Error)
}
return nil
}
// ScrollToCoordinates scrolls the page to specific coordinates
// If tabID is empty, the current tab will be used
// timeout is in seconds, 0 means no timeout
func (c *Client) ScrollToCoordinates(tabID string, x, y int, timeout int) error {
params := map[string]string{
"x": strconv.Itoa(x),
"y": strconv.Itoa(y),
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("scroll-to-coordinates", params)
if err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("failed to scroll to coordinates: %s", resp.Error)
}
return nil
}
// SelectText selects text within an element by character range
// If tabID is empty, the current tab will be used
// timeout is in seconds, 0 means no timeout
func (c *Client) SelectText(tabID, selector string, startIndex, endIndex int, timeout int) error {
params := map[string]string{
"selector": selector,
"start": strconv.Itoa(startIndex),
"end": strconv.Itoa(endIndex),
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("select-text", params)
if err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("failed to select text: %s", resp.Error)
}
return nil
}
// SelectAllText selects all text within an element or the entire page
// If tabID is empty, the current tab will be used
// If selector is empty, selects all text on the page
// timeout is in seconds, 0 means no timeout
func (c *Client) SelectAllText(tabID, selector string, timeout int) error {
params := map[string]string{}
if selector != "" {
params["selector"] = selector
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("select-all-text", params)
if err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("failed to select all text: %s", resp.Error)
}
return nil
}
// 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"`
}
// InjectAxeCore injects the axe-core library into the page
// If tabID is empty, the current tab will be used
// axeVersion specifies the axe-core version (e.g., "4.8.0"), empty string uses default
// timeout is in seconds, 0 means no timeout
func (c *Client) InjectAxeCore(tabID, axeVersion string, timeout int) error {
params := map[string]string{}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Only include version if it's provided
if axeVersion != "" {
params["version"] = axeVersion
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("inject-axe", params)
if err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("failed to inject axe-core: %s", resp.Error)
}
return nil
}
// RunAxeCore runs axe-core accessibility tests on the page
// If tabID is empty, the current tab will be used
// options is a map of axe.run() options (can be nil for defaults)
// timeout is in seconds, 0 means no timeout
func (c *Client) RunAxeCore(tabID string, options map[string]interface{}, timeout int) (*AxeResults, error) {
params := map[string]string{}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Include options if provided
if options != nil && len(options) > 0 {
optionsBytes, err := json.Marshal(options)
if err != nil {
return nil, fmt.Errorf("failed to marshal options: %w", err)
}
params["options"] = string(optionsBytes)
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("run-axe", params)
if err != nil {
return nil, err
}
if !resp.Success {
return nil, fmt.Errorf("failed to run axe-core: %s", resp.Error)
}
// Parse the response data
var result AxeResults
dataBytes, err := json.Marshal(resp.Data)
if err != nil {
return nil, fmt.Errorf("failed to marshal response data: %w", err)
}
err = json.Unmarshal(dataBytes, &result)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal axe results: %w", err)
}
return &result, 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"`
}
// PageAccessibilityReport represents a comprehensive accessibility assessment of a single page
type PageAccessibilityReport struct {
URL string `json:"url"`
Timestamp string `json:"timestamp"`
ComplianceStatus string `json:"compliance_status"` // COMPLIANT, NON_COMPLIANT, PARTIAL
OverallScore int `json:"overall_score"` // 0-100
LegalRisk string `json:"legal_risk"` // LOW, MEDIUM, HIGH, CRITICAL
CriticalIssues []AccessibilityIssue `json:"critical_issues"`
SeriousIssues []AccessibilityIssue `json:"serious_issues"`
HighIssues []AccessibilityIssue `json:"high_issues"`
MediumIssues []AccessibilityIssue `json:"medium_issues"`
SummaryByWCAG map[string]WCAGSummary `json:"summary_by_wcag"`
ContrastSummary ContrastSummary `json:"contrast_summary"`
KeyboardSummary KeyboardSummary `json:"keyboard_summary"`
ARIASummary ARIASummary `json:"aria_summary"`
FormSummary *FormSummary `json:"form_summary,omitempty"`
Screenshots map[string]string `json:"screenshots,omitempty"`
EstimatedHours int `json:"estimated_remediation_hours"`
}
// AccessibilityIssue represents a single accessibility issue
type AccessibilityIssue struct {
WCAG string `json:"wcag"`
Title string `json:"title"`
Description string `json:"description"`
Impact string `json:"impact"`
Count int `json:"count"`
Examples []string `json:"examples,omitempty"`
Remediation string `json:"remediation"`
}
// WCAGSummary represents violations grouped by WCAG principle
type WCAGSummary struct {
Violations int `json:"violations"`
Severity string `json:"severity"`
}
// ContrastSummary represents a summary of contrast check results
type ContrastSummary struct {
TotalChecked int `json:"total_checked"`
Passed int `json:"passed"`
Failed int `json:"failed"`
PassRate string `json:"pass_rate"`
CriticalFailures []ContrastFailure `json:"critical_failures"`
FailurePatterns map[string]FailurePattern `json:"failure_patterns"`
}
// ContrastFailure represents a critical contrast failure
type ContrastFailure struct {
Selector string `json:"selector"`
Text string `json:"text"`
Ratio float64 `json:"ratio"`
Required float64 `json:"required"`
FgColor string `json:"fg_color"`
BgColor string `json:"bg_color"`
Fix string `json:"fix"`
}
// FailurePattern represents a pattern of similar failures
type FailurePattern struct {
Count int `json:"count"`
Ratio float64 `json:"ratio"`
Fix string `json:"fix"`
}
// KeyboardSummary represents a summary of keyboard navigation results
type KeyboardSummary struct {
TotalInteractive int `json:"total_interactive"`
Focusable int `json:"focusable"`
MissingFocusIndicator int `json:"missing_focus_indicator"`
KeyboardTraps int `json:"keyboard_traps"`
TabOrderIssues int `json:"tab_order_issues"`
Issues []KeyboardIssue `json:"issues"`
}
// KeyboardIssue represents a keyboard accessibility issue
type KeyboardIssue struct {
Type string `json:"type"`
Severity string `json:"severity"`
Count int `json:"count"`
Description string `json:"description"`
Fix string `json:"fix"`
Examples []string `json:"examples,omitempty"`
}
// ARIASummary represents a summary of ARIA validation results
type ARIASummary struct {
TotalViolations int `json:"total_violations"`
MissingNames int `json:"missing_names"`
InvalidAttributes int `json:"invalid_attributes"`
HiddenInteractive int `json:"hidden_interactive"`
Issues []ARIAIssue `json:"issues"`
}
// ARIAIssue represents an ARIA accessibility issue
type ARIAIssue struct {
Type string `json:"type"`
Severity string `json:"severity"`
Count int `json:"count"`
Description string `json:"description"`
Fix string `json:"fix"`
Examples []string `json:"examples,omitempty"`
}
// FormSummary represents a summary of form accessibility
type FormSummary struct {
FormsFound int `json:"forms_found"`
Forms []FormAudit `json:"forms"`
}
// FormAudit represents accessibility audit of a single form
type FormAudit struct {
ID string `json:"id"`
Fields int `json:"fields"`
Issues []FormIssue `json:"issues"`
ARIACompliance string `json:"aria_compliance"`
KeyboardAccessible bool `json:"keyboard_accessible"`
RequiredMarked bool `json:"required_fields_marked"`
}
// FormIssue represents a form accessibility issue
type FormIssue struct {
Type string `json:"type"`
Severity string `json:"severity"`
Count int `json:"count,omitempty"`
Description string `json:"description"`
Fix string `json:"fix"`
Ratio float64 `json:"ratio,omitempty"`
}
// CheckContrast checks color contrast for text elements on the page
// If tabID is empty, the current tab will be used
// selector is optional CSS selector for specific elements (defaults to all text elements)
// timeout is in seconds, 0 means no timeout
func (c *Client) CheckContrast(tabID, selector string, timeout int) (*ContrastCheckResult, error) {
params := map[string]string{}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Only include selector if it's provided
if selector != "" {
params["selector"] = selector
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("check-contrast", params)
if err != nil {
return nil, err
}
if !resp.Success {
return nil, fmt.Errorf("failed to check contrast: %s", resp.Error)
}
// Parse the response data
var result ContrastCheckResult
dataBytes, err := json.Marshal(resp.Data)
if err != nil {
return nil, fmt.Errorf("failed to marshal response data: %w", err)
}
err = json.Unmarshal(dataBytes, &result)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal contrast results: %w", err)
}
return &result, nil
}
// GradientContrastResult represents the result of gradient contrast checking
type GradientContrastResult struct {
Selector string `json:"selector"`
TextColor string `json:"text_color"`
DarkestBgColor string `json:"darkest_bg_color"`
LightestBgColor string `json:"lightest_bg_color"`
WorstContrast float64 `json:"worst_contrast"`
BestContrast float64 `json:"best_contrast"`
PassesAA bool `json:"passes_aa"`
PassesAAA bool `json:"passes_aaa"`
RequiredAA float64 `json:"required_aa"`
RequiredAAA float64 `json:"required_aaa"`
IsLargeText bool `json:"is_large_text"`
SamplePoints int `json:"sample_points"`
Error string `json:"error,omitempty"`
}
// CheckGradientContrast checks color contrast for text on gradient backgrounds using ImageMagick
// If tabID is empty, the current tab will be used
// selector is required CSS selector for element with gradient background
// timeout is in seconds, 0 means no timeout
func (c *Client) CheckGradientContrast(tabID, selector string, timeout int) (*GradientContrastResult, error) {
if selector == "" {
return nil, fmt.Errorf("selector parameter is required for gradient contrast check")
}
params := map[string]string{
"selector": selector,
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("check-gradient-contrast", params)
if err != nil {
return nil, err
}
if !resp.Success {
return nil, fmt.Errorf("failed to check gradient contrast: %s", resp.Error)
}
// Parse the response data
var result GradientContrastResult
dataBytes, err := json.Marshal(resp.Data)
if err != nil {
return nil, fmt.Errorf("failed to marshal response data: %w", err)
}
err = json.Unmarshal(dataBytes, &result)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal gradient contrast results: %w", err)
}
return &result, nil
}
// MediaValidationResult represents the result of time-based media validation
type MediaValidationResult struct {
Videos []MediaElement `json:"videos"`
Audios []MediaElement `json:"audios"`
EmbeddedPlayers []MediaElement `json:"embedded_players"`
TranscriptLinks []string `json:"transcript_links"`
TotalViolations int `json:"total_violations"`
CriticalViolations int `json:"critical_violations"`
Warnings int `json:"warnings"`
}
// MediaElement represents a video or audio element
type MediaElement struct {
Type string `json:"type"` // "video", "audio", "youtube", "vimeo"
Src string `json:"src"`
HasCaptions bool `json:"has_captions"`
HasDescriptions bool `json:"has_descriptions"`
HasControls bool `json:"has_controls"`
Autoplay bool `json:"autoplay"`
CaptionTracks []Track `json:"caption_tracks"`
DescriptionTracks []Track `json:"description_tracks"`
Violations []string `json:"violations"`
Warnings []string `json:"warnings"`
}
// Track represents a text track (captions, descriptions, etc.)
type Track struct {
Kind string `json:"kind"`
Src string `json:"src"`
Srclang string `json:"srclang"`
Label string `json:"label"`
Accessible bool `json:"accessible"`
}
// ValidateMedia checks for video/audio captions, descriptions, and transcripts
// If tabID is empty, the current tab will be used
// timeout is in seconds, 0 means no timeout
func (c *Client) ValidateMedia(tabID string, timeout int) (*MediaValidationResult, error) {
params := map[string]string{}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("validate-media", params)
if err != nil {
return nil, err
}
if !resp.Success {
return nil, fmt.Errorf("failed to validate media: %s", resp.Error)
}
// Parse the response data
var result MediaValidationResult
dataBytes, err := json.Marshal(resp.Data)
if err != nil {
return nil, fmt.Errorf("failed to marshal response data: %w", err)
}
err = json.Unmarshal(dataBytes, &result)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal media validation results: %w", err)
}
return &result, nil
}
// HoverFocusTestResult represents the result of hover/focus content testing
type HoverFocusTestResult struct {
TotalElements int `json:"total_elements"`
ElementsWithIssues int `json:"elements_with_issues"`
PassedElements int `json:"passed_elements"`
Issues []HoverFocusIssue `json:"issues"`
TestedElements []HoverFocusElement `json:"tested_elements"`
}
// HoverFocusElement represents an element that shows content on hover/focus
type HoverFocusElement struct {
Selector string `json:"selector"`
Type string `json:"type"` // "tooltip", "dropdown", "popover", "custom"
Dismissible bool `json:"dismissible"`
Hoverable bool `json:"hoverable"`
Persistent bool `json:"persistent"`
PassesWCAG bool `json:"passes_wcag"`
Violations []string `json:"violations"`
}
// HoverFocusIssue represents a specific issue with hover/focus content
type HoverFocusIssue struct {
Selector string `json:"selector"`
Type string `json:"type"` // "not_dismissible", "not_hoverable", "not_persistent"
Severity string `json:"severity"` // "critical", "serious", "moderate"
Description string `json:"description"`
WCAG string `json:"wcag"` // "1.4.13"
}
// TestHoverFocusContent tests WCAG 1.4.13 compliance for content on hover or focus
// If tabID is empty, the current tab will be used
// timeout is in seconds, 0 means no timeout
func (c *Client) TestHoverFocusContent(tabID string, timeout int) (*HoverFocusTestResult, error) {
params := map[string]string{}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("test-hover-focus", params)
if err != nil {
return nil, err
}
if !resp.Success {
return nil, fmt.Errorf("failed to test hover/focus content: %s", resp.Error)
}
// Parse the response data
var result HoverFocusTestResult
dataBytes, err := json.Marshal(resp.Data)
if err != nil {
return nil, fmt.Errorf("failed to marshal response data: %w", err)
}
err = json.Unmarshal(dataBytes, &result)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal hover/focus test results: %w", err)
}
return &result, nil
}
// TextInImagesResult represents the result of text-in-images detection
type TextInImagesResult struct {
TotalImages int `json:"total_images"`
ImagesWithText int `json:"images_with_text"`
ImagesWithoutText int `json:"images_without_text"`
Violations int `json:"violations"`
Warnings int `json:"warnings"`
Images []ImageTextAnalysis `json:"images"`
}
// ImageTextAnalysis represents OCR analysis of a single image
type ImageTextAnalysis struct {
Src string `json:"src"`
Alt string `json:"alt"`
HasAlt bool `json:"has_alt"`
DetectedText string `json:"detected_text"`
TextLength int `json:"text_length"`
Confidence float64 `json:"confidence"`
IsViolation bool `json:"is_violation"`
ViolationType string `json:"violation_type"` // "missing_alt", "insufficient_alt", "decorative_with_text"
Recommendation string `json:"recommendation"`
}
// DetectTextInImages uses Tesseract OCR to detect text in images
// If tabID is empty, the current tab will be used
// timeout is in seconds, 0 means no timeout
func (c *Client) DetectTextInImages(tabID string, timeout int) (*TextInImagesResult, error) {
params := map[string]string{}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("detect-text-in-images", params)
if err != nil {
return nil, err
}
if !resp.Success {
return nil, fmt.Errorf("failed to detect text in images: %s", resp.Error)
}
// Parse the response data
var result TextInImagesResult
dataBytes, err := json.Marshal(resp.Data)
if err != nil {
return nil, fmt.Errorf("failed to marshal response data: %w", err)
}
err = json.Unmarshal(dataBytes, &result)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal text-in-images results: %w", err)
}
return &result, nil
}
// CrossPageConsistencyResult represents the result of cross-page consistency checking
type CrossPageConsistencyResult struct {
PagesAnalyzed int `json:"pages_analyzed"`
ConsistencyIssues int `json:"consistency_issues"`
NavigationIssues int `json:"navigation_issues"`
StructureIssues int `json:"structure_issues"`
Pages []PageConsistencyAnalysis `json:"pages"`
CommonNavigation []string `json:"common_navigation"`
InconsistentPages []string `json:"inconsistent_pages"`
}
// PageConsistencyAnalysis represents consistency analysis of a single page
type PageConsistencyAnalysis struct {
URL string `json:"url"`
Title string `json:"title"`
HasHeader bool `json:"has_header"`
HasFooter bool `json:"has_footer"`
HasNavigation bool `json:"has_navigation"`
NavigationLinks []string `json:"navigation_links"`
MainLandmarks int `json:"main_landmarks"`
HeaderLandmarks int `json:"header_landmarks"`
FooterLandmarks int `json:"footer_landmarks"`
NavigationLandmarks int `json:"navigation_landmarks"`
Issues []string `json:"issues"`
}
// CheckCrossPageConsistency analyzes multiple pages for consistency
// If tabID is empty, the current tab will be used
// timeout is in seconds per page, 0 means no timeout
func (c *Client) CheckCrossPageConsistency(tabID string, urls []string, timeout int) (*CrossPageConsistencyResult, error) {
if len(urls) == 0 {
return nil, fmt.Errorf("no URLs provided for consistency check")
}
params := map[string]string{
"urls": strings.Join(urls, ","),
}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("check-cross-page-consistency", params)
if err != nil {
return nil, err
}
if !resp.Success {
return nil, fmt.Errorf("failed to check cross-page consistency: %s", resp.Error)
}
// Parse the response data
var result CrossPageConsistencyResult
dataBytes, err := json.Marshal(resp.Data)
if err != nil {
return nil, fmt.Errorf("failed to marshal response data: %w", err)
}
err = json.Unmarshal(dataBytes, &result)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal cross-page consistency results: %w", err)
}
return &result, nil
}
// AnimationFlashResult represents the result of animation/flash detection
type AnimationFlashResult struct {
TotalAnimations int `json:"total_animations"`
FlashingContent int `json:"flashing_content"`
RapidAnimations int `json:"rapid_animations"`
AutoplayAnimations int `json:"autoplay_animations"`
Violations int `json:"violations"`
Warnings int `json:"warnings"`
Elements []AnimationFlashElement `json:"elements"`
}
// AnimationFlashElement represents an animated or flashing element
type AnimationFlashElement struct {
TagName string `json:"tag_name"`
Selector string `json:"selector"`
AnimationType string `json:"animation_type"` // "css", "gif", "video", "canvas", "svg"
FlashRate float64 `json:"flash_rate"` // Flashes per second
Duration float64 `json:"duration"` // Animation duration in seconds
IsAutoplay bool `json:"is_autoplay"`
HasControls bool `json:"has_controls"`
CanPause bool `json:"can_pause"`
IsViolation bool `json:"is_violation"`
ViolationType string `json:"violation_type"`
Recommendation string `json:"recommendation"`
}
// DetectAnimationFlash detects animations and flashing content
// If tabID is empty, the current tab will be used
// timeout is in seconds, 0 means no timeout
func (c *Client) DetectAnimationFlash(tabID string, timeout int) (*AnimationFlashResult, error) {
params := map[string]string{}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("detect-animation-flash", params)
if err != nil {
return nil, err
}
if !resp.Success {
return nil, fmt.Errorf("failed to detect animation/flash: %s", resp.Error)
}
// Parse the response data
var result AnimationFlashResult
dataBytes, err := json.Marshal(resp.Data)
if err != nil {
return nil, fmt.Errorf("failed to marshal response data: %w", err)
}
err = json.Unmarshal(dataBytes, &result)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal animation/flash results: %w", err)
}
return &result, nil
}
// EnhancedAccessibilityResult represents enhanced accessibility tree analysis
type EnhancedAccessibilityResult struct {
TotalElements int `json:"total_elements"`
ElementsWithIssues int `json:"elements_with_issues"`
ARIAViolations int `json:"aria_violations"`
RoleViolations int `json:"role_violations"`
RelationshipIssues int `json:"relationship_issues"`
LandmarkIssues int `json:"landmark_issues"`
Elements []EnhancedAccessibilityElement `json:"elements"`
}
// EnhancedAccessibilityElement represents an element with accessibility analysis
type EnhancedAccessibilityElement struct {
TagName string `json:"tag_name"`
Selector string `json:"selector"`
Role string `json:"role"`
AriaLabel string `json:"aria_label"`
AriaDescribedBy string `json:"aria_described_by"`
AriaLabelledBy string `json:"aria_labelled_by"`
AriaRequired bool `json:"aria_required"`
AriaInvalid bool `json:"aria_invalid"`
AriaHidden bool `json:"aria_hidden"`
TabIndex int `json:"tab_index"`
IsInteractive bool `json:"is_interactive"`
HasAccessibleName bool `json:"has_accessible_name"`
Issues []string `json:"issues"`
Recommendations []string `json:"recommendations"`
}
// AnalyzeEnhancedAccessibility performs enhanced accessibility tree analysis
// If tabID is empty, the current tab will be used
// timeout is in seconds, 0 means no timeout
func (c *Client) AnalyzeEnhancedAccessibility(tabID string, timeout int) (*EnhancedAccessibilityResult, error) {
params := map[string]string{}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("analyze-enhanced-accessibility", params)
if err != nil {
return nil, err
}
if !resp.Success {
return nil, fmt.Errorf("failed to analyze enhanced accessibility: %s", resp.Error)
}
// Parse the response data
var result EnhancedAccessibilityResult
dataBytes, err := json.Marshal(resp.Data)
if err != nil {
return nil, fmt.Errorf("failed to marshal response data: %w", err)
}
err = json.Unmarshal(dataBytes, &result)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal enhanced accessibility results: %w", err)
}
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
// If tabID is empty, the current tab will be used
// timeout is in seconds, 0 means no timeout
func (c *Client) TestKeyboardNavigation(tabID string, timeout int) (*KeyboardTestResult, error) {
params := map[string]string{}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("test-keyboard", params)
if err != nil {
return nil, err
}
if !resp.Success {
return nil, fmt.Errorf("failed to test keyboard navigation: %s", resp.Error)
}
// Parse the response data
var result KeyboardTestResult
dataBytes, err := json.Marshal(resp.Data)
if err != nil {
return nil, fmt.Errorf("failed to marshal response data: %w", err)
}
err = json.Unmarshal(dataBytes, &result)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal keyboard test results: %w", err)
}
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
// If tabID is empty, the current tab will be used
// zoomLevels is an array of zoom levels to test (e.g., []float64{1.0, 2.0, 4.0})
// If empty, defaults to [1.0, 2.0, 4.0]
// timeout is in seconds per zoom level, 0 means no timeout
func (c *Client) TestZoom(tabID string, zoomLevels []float64, timeout int) (*ZoomTestResult, error) {
params := map[string]string{}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Include zoom levels if provided
if len(zoomLevels) > 0 {
levels := make([]string, len(zoomLevels))
for i, level := range zoomLevels {
levels[i] = strconv.FormatFloat(level, 'f', 1, 64)
}
params["zoom_levels"] = strings.Join(levels, ",")
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("test-zoom", params)
if err != nil {
return nil, err
}
if !resp.Success {
return nil, fmt.Errorf("failed to test zoom: %s", resp.Error)
}
// Parse the response data
var result ZoomTestResult
dataBytes, err := json.Marshal(resp.Data)
if err != nil {
return nil, fmt.Errorf("failed to marshal response data: %w", err)
}
err = json.Unmarshal(dataBytes, &result)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal zoom test results: %w", err)
}
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
// If tabID is empty, the current tab will be used
// widths is an array of viewport widths to test (e.g., []int{320, 1280})
// If empty, defaults to [320, 1280] (WCAG 1.4.10 breakpoints)
// timeout is in seconds per width, 0 means no timeout
func (c *Client) TestReflow(tabID string, widths []int, timeout int) (*ReflowTestResult, error) {
params := map[string]string{}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Include widths if provided
if len(widths) > 0 {
widthStrs := make([]string, len(widths))
for i, width := range widths {
widthStrs[i] = strconv.Itoa(width)
}
params["widths"] = strings.Join(widthStrs, ",")
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("test-reflow", params)
if err != nil {
return nil, err
}
if !resp.Success {
return nil, fmt.Errorf("failed to test reflow: %s", resp.Error)
}
// Parse the response data
var result ReflowTestResult
dataBytes, err := json.Marshal(resp.Data)
if err != nil {
return nil, fmt.Errorf("failed to marshal response data: %w", err)
}
err = json.Unmarshal(dataBytes, &result)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal reflow test results: %w", err)
}
return &result, nil
}
// GetPageAccessibilityReport performs a comprehensive accessibility assessment of a page
// and returns a summarized report with actionable findings
// If tabID is empty, the current tab will be used
// tests is an array of test types to run (e.g., ["wcag", "contrast", "keyboard", "forms"])
// If empty, runs all tests
// standard is the WCAG standard to test against (e.g., "WCAG21AA")
// includeScreenshots determines whether to capture screenshots of violations
// timeout is in seconds, 0 means no timeout
func (c *Client) GetPageAccessibilityReport(tabID string, tests []string, standard string, includeScreenshots bool, timeout int) (*PageAccessibilityReport, error) {
params := map[string]string{}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Include tests if provided
if len(tests) > 0 {
params["tests"] = strings.Join(tests, ",")
} else {
params["tests"] = "all"
}
// Include standard if provided
if standard != "" {
params["standard"] = standard
} else {
params["standard"] = "WCAG21AA"
}
// Include screenshot flag
if includeScreenshots {
params["include_screenshots"] = "true"
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("page-accessibility-report", params)
if err != nil {
return nil, err
}
if !resp.Success {
return nil, fmt.Errorf("failed to get page accessibility report: %s", resp.Error)
}
// Parse the response data
var result PageAccessibilityReport
dataBytes, err := json.Marshal(resp.Data)
if err != nil {
return nil, fmt.Errorf("failed to marshal response data: %w", err)
}
err = json.Unmarshal(dataBytes, &result)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal page accessibility report: %w", err)
}
return &result, nil
}
// ContrastAuditResult represents a smart contrast audit with prioritized failures
type ContrastAuditResult struct {
TotalChecked int `json:"total_checked"`
Passed int `json:"passed"`
Failed int `json:"failed"`
PassRate string `json:"pass_rate"`
CriticalFailures []ContrastFailure `json:"critical_failures"`
FailurePatterns map[string]FailurePattern `json:"failure_patterns"`
}
// GetContrastAudit performs a smart contrast check with prioritized failures
// If tabID is empty, the current tab will be used
// prioritySelectors is an array of CSS selectors to prioritize (e.g., ["button", "a", "nav"])
// threshold is the WCAG level to test against ("AA" or "AAA")
// timeout is in seconds, 0 means no timeout
func (c *Client) GetContrastAudit(tabID string, prioritySelectors []string, threshold string, timeout int) (*ContrastAuditResult, error) {
params := map[string]string{}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Include priority selectors if provided
if len(prioritySelectors) > 0 {
params["priority_selectors"] = strings.Join(prioritySelectors, ",")
}
// Include threshold if provided
if threshold != "" {
params["threshold"] = threshold
} else {
params["threshold"] = "AA"
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("contrast-audit", params)
if err != nil {
return nil, err
}
if !resp.Success {
return nil, fmt.Errorf("failed to get contrast audit: %s", resp.Error)
}
// Parse the response data
var result ContrastAuditResult
dataBytes, err := json.Marshal(resp.Data)
if err != nil {
return nil, fmt.Errorf("failed to marshal response data: %w", err)
}
err = json.Unmarshal(dataBytes, &result)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal contrast audit: %w", err)
}
return &result, nil
}
// KeyboardAuditResult represents a keyboard navigation audit
type KeyboardAuditResult struct {
Status string `json:"status"` // PASS, FAIL, PARTIAL
TotalInteractive int `json:"total_interactive"`
Focusable int `json:"focusable"`
Issues []KeyboardIssue `json:"issues"`
TabOrderIssues []string `json:"tab_order_issues"`
Recommendation string `json:"recommendation"`
}
// GetKeyboardAudit performs a keyboard navigation assessment
// If tabID is empty, the current tab will be used
// checkFocusIndicators determines whether to check for visible focus indicators
// checkTabOrder determines whether to check tab order
// checkKeyboardTraps determines whether to check for keyboard traps
// timeout is in seconds, 0 means no timeout
func (c *Client) GetKeyboardAudit(tabID string, checkFocusIndicators, checkTabOrder, checkKeyboardTraps bool, timeout int) (*KeyboardAuditResult, error) {
params := map[string]string{}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Include check flags
if checkFocusIndicators {
params["check_focus_indicators"] = "true"
}
if checkTabOrder {
params["check_tab_order"] = "true"
}
if checkKeyboardTraps {
params["check_keyboard_traps"] = "true"
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("keyboard-audit", params)
if err != nil {
return nil, err
}
if !resp.Success {
return nil, fmt.Errorf("failed to get keyboard audit: %s", resp.Error)
}
// Parse the response data
var result KeyboardAuditResult
dataBytes, err := json.Marshal(resp.Data)
if err != nil {
return nil, fmt.Errorf("failed to marshal response data: %w", err)
}
err = json.Unmarshal(dataBytes, &result)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal keyboard audit: %w", err)
}
return &result, nil
}
// GetFormAccessibilityAudit performs a comprehensive form accessibility check
// If tabID is empty, the current tab will be used
// formSelector is an optional CSS selector for a specific form (defaults to all forms)
// timeout is in seconds, 0 means no timeout
func (c *Client) GetFormAccessibilityAudit(tabID, formSelector string, timeout int) (*FormSummary, error) {
params := map[string]string{}
// Only include tab ID if it's provided
if tabID != "" {
params["tab"] = tabID
}
// Only include form selector if it's provided
if formSelector != "" {
params["form_selector"] = formSelector
}
// Add timeout if specified
if timeout > 0 {
params["timeout"] = strconv.Itoa(timeout)
}
resp, err := c.SendCommand("form-accessibility-audit", params)
if err != nil {
return nil, err
}
if !resp.Success {
return nil, fmt.Errorf("failed to get form accessibility audit: %s", resp.Error)
}
// Parse the response data
var result FormSummary
dataBytes, err := json.Marshal(resp.Data)
if err != nil {
return nil, fmt.Errorf("failed to marshal response data: %w", err)
}
err = json.Unmarshal(dataBytes, &result)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal form accessibility audit: %w", err)
}
return &result, nil
}