cremote/client/client.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
}