2456 lines
65 KiB
Go
2456 lines
65 KiB
Go
package client
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"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
|
|
}
|