4571 lines
125 KiB
Go
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
|
|
}
|