accessibility
This commit is contained in:
		
							
								
								
									
										443
									
								
								daemon/daemon.go
									
									
									
									
									
								
							
							
						
						
									
										443
									
								
								daemon/daemon.go
									
									
									
									
									
								
							| @@ -957,6 +957,84 @@ func (d *Daemon) handleCommand(w http.ResponseWriter, r *http.Request) { | ||||
| 			response = Response{Success: true, Data: result} | ||||
| 		} | ||||
|  | ||||
| 	// Accessibility tree commands | ||||
| 	case "get-accessibility-tree": | ||||
| 		tabID := cmd.Params["tab"] | ||||
| 		depth := cmd.Params["depth"] | ||||
| 		timeoutStr := cmd.Params["timeout"] | ||||
|  | ||||
| 		// Parse timeout (default to 5 seconds if not specified) | ||||
| 		timeout := 5 | ||||
| 		if timeoutStr != "" { | ||||
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 { | ||||
| 				timeout = parsedTimeout | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// Parse depth (optional) | ||||
| 		var depthInt *int | ||||
| 		if depth != "" { | ||||
| 			if parsedDepth, err := strconv.Atoi(depth); err == nil && parsedDepth >= 0 { | ||||
| 				depthInt = &parsedDepth | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		result, err := d.getAccessibilityTree(tabID, depthInt, timeout) | ||||
| 		if err != nil { | ||||
| 			response = Response{Success: false, Error: err.Error()} | ||||
| 		} else { | ||||
| 			response = Response{Success: true, Data: result} | ||||
| 		} | ||||
|  | ||||
| 	case "get-partial-accessibility-tree": | ||||
| 		tabID := cmd.Params["tab"] | ||||
| 		selector := cmd.Params["selector"] | ||||
| 		fetchRelatives := cmd.Params["fetch-relatives"] // "true" or "false" | ||||
| 		timeoutStr := cmd.Params["timeout"] | ||||
|  | ||||
| 		// Parse timeout (default to 5 seconds if not specified) | ||||
| 		timeout := 5 | ||||
| 		if timeoutStr != "" { | ||||
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 { | ||||
| 				timeout = parsedTimeout | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// Parse fetchRelatives (default to true) | ||||
| 		fetchRel := true | ||||
| 		if fetchRelatives == "false" { | ||||
| 			fetchRel = false | ||||
| 		} | ||||
|  | ||||
| 		result, err := d.getPartialAccessibilityTree(tabID, selector, fetchRel, timeout) | ||||
| 		if err != nil { | ||||
| 			response = Response{Success: false, Error: err.Error()} | ||||
| 		} else { | ||||
| 			response = Response{Success: true, Data: result} | ||||
| 		} | ||||
|  | ||||
| 	case "query-accessibility-tree": | ||||
| 		tabID := cmd.Params["tab"] | ||||
| 		selector := cmd.Params["selector"] | ||||
| 		accessibleName := cmd.Params["accessible-name"] | ||||
| 		role := cmd.Params["role"] | ||||
| 		timeoutStr := cmd.Params["timeout"] | ||||
|  | ||||
| 		// Parse timeout (default to 5 seconds if not specified) | ||||
| 		timeout := 5 | ||||
| 		if timeoutStr != "" { | ||||
| 			if parsedTimeout, err := strconv.Atoi(timeoutStr); err == nil && parsedTimeout > 0 { | ||||
| 				timeout = parsedTimeout | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		result, err := d.queryAccessibilityTree(tabID, selector, accessibleName, role, timeout) | ||||
| 		if err != nil { | ||||
| 			response = Response{Success: false, Error: err.Error()} | ||||
| 		} else { | ||||
| 			response = Response{Success: true, Data: result} | ||||
| 		} | ||||
|  | ||||
| 	default: | ||||
| 		d.debugLog("Unknown action: %s", cmd.Action) | ||||
| 		response = Response{Success: false, Error: "Unknown action"} | ||||
| @@ -4828,3 +4906,368 @@ func (d *Daemon) getFileInfo(filePath string, result *FileManagementResult) (*Fi | ||||
| 	d.debugLog("Retrieved info for file: %s", filePath) | ||||
| 	return result, nil | ||||
| } | ||||
|  | ||||
| // Accessibility tree data structures | ||||
|  | ||||
| // AXNode represents a node in the accessibility tree | ||||
| type AXNode struct { | ||||
| 	NodeID           string       `json:"nodeId"` | ||||
| 	Ignored          bool         `json:"ignored"` | ||||
| 	IgnoredReasons   []AXProperty `json:"ignoredReasons,omitempty"` | ||||
| 	Role             *AXValue     `json:"role,omitempty"` | ||||
| 	ChromeRole       *AXValue     `json:"chromeRole,omitempty"` | ||||
| 	Name             *AXValue     `json:"name,omitempty"` | ||||
| 	Description      *AXValue     `json:"description,omitempty"` | ||||
| 	Value            *AXValue     `json:"value,omitempty"` | ||||
| 	Properties       []AXProperty `json:"properties,omitempty"` | ||||
| 	ParentID         string       `json:"parentId,omitempty"` | ||||
| 	ChildIDs         []string     `json:"childIds,omitempty"` | ||||
| 	BackendDOMNodeID int          `json:"backendDOMNodeId,omitempty"` | ||||
| 	FrameID          string       `json:"frameId,omitempty"` | ||||
| } | ||||
|  | ||||
| // AXProperty represents a property of an accessibility node | ||||
| type AXProperty struct { | ||||
| 	Name  string   `json:"name"` | ||||
| 	Value *AXValue `json:"value"` | ||||
| } | ||||
|  | ||||
| // AXValue represents a computed accessibility value | ||||
| type AXValue struct { | ||||
| 	Type         string          `json:"type"` | ||||
| 	Value        interface{}     `json:"value,omitempty"` | ||||
| 	RelatedNodes []AXRelatedNode `json:"relatedNodes,omitempty"` | ||||
| 	Sources      []AXValueSource `json:"sources,omitempty"` | ||||
| } | ||||
|  | ||||
| // AXRelatedNode represents a related node in the accessibility tree | ||||
| type AXRelatedNode struct { | ||||
| 	BackendDOMNodeID int    `json:"backendDOMNodeId"` | ||||
| 	IDRef            string `json:"idref,omitempty"` | ||||
| 	Text             string `json:"text,omitempty"` | ||||
| } | ||||
|  | ||||
| // AXValueSource represents a source for a computed accessibility value | ||||
| type AXValueSource struct { | ||||
| 	Type              string   `json:"type"` | ||||
| 	Value             *AXValue `json:"value,omitempty"` | ||||
| 	Attribute         string   `json:"attribute,omitempty"` | ||||
| 	AttributeValue    *AXValue `json:"attributeValue,omitempty"` | ||||
| 	Superseded        bool     `json:"superseded,omitempty"` | ||||
| 	NativeSource      string   `json:"nativeSource,omitempty"` | ||||
| 	NativeSourceValue *AXValue `json:"nativeSourceValue,omitempty"` | ||||
| 	Invalid           bool     `json:"invalid,omitempty"` | ||||
| 	InvalidReason     string   `json:"invalidReason,omitempty"` | ||||
| } | ||||
|  | ||||
| // AccessibilityTreeResult represents the result of accessibility tree operations | ||||
| type AccessibilityTreeResult struct { | ||||
| 	Nodes []AXNode `json:"nodes"` | ||||
| } | ||||
|  | ||||
| // AccessibilityQueryResult represents the result of accessibility queries | ||||
| type AccessibilityQueryResult struct { | ||||
| 	Nodes []AXNode `json:"nodes"` | ||||
| } | ||||
|  | ||||
| // getAccessibilityTree retrieves the full accessibility tree for a tab | ||||
| func (d *Daemon) getAccessibilityTree(tabID string, depth *int, timeout int) (*AccessibilityTreeResult, error) { | ||||
| 	d.debugLog("Getting accessibility tree for tab: %s with depth: %v, timeout: %d", tabID, depth, timeout) | ||||
|  | ||||
| 	// Use current tab if not specified | ||||
| 	if tabID == "" { | ||||
| 		tabID = d.currentTab | ||||
| 	} | ||||
|  | ||||
| 	if tabID == "" { | ||||
| 		return nil, fmt.Errorf("no tab specified and no current tab available") | ||||
| 	} | ||||
|  | ||||
| 	page, err := d.getTab(tabID) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to get page: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	// Enable accessibility domain | ||||
| 	err = proto.AccessibilityEnable{}.Call(page) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to enable accessibility domain: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	// Build the request parameters | ||||
| 	params := proto.AccessibilityGetFullAXTree{} | ||||
| 	if depth != nil { | ||||
| 		params.Depth = depth | ||||
| 	} | ||||
|  | ||||
| 	// Call the Chrome DevTools Protocol Accessibility.getFullAXTree method | ||||
| 	result, err := proto.AccessibilityGetFullAXTree{}.Call(page) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to get accessibility tree: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	// Parse the result | ||||
| 	var axResult AccessibilityTreeResult | ||||
| 	for _, node := range result.Nodes { | ||||
| 		axNode := d.convertProtoAXNode(node) | ||||
| 		axResult.Nodes = append(axResult.Nodes, axNode) | ||||
| 	} | ||||
|  | ||||
| 	d.debugLog("Successfully retrieved accessibility tree with %d nodes for tab: %s", len(axResult.Nodes), tabID) | ||||
| 	return &axResult, nil | ||||
| } | ||||
|  | ||||
| // convertProtoAXNode converts a proto.AccessibilityAXNode to our AXNode struct | ||||
| func (d *Daemon) convertProtoAXNode(protoNode *proto.AccessibilityAXNode) AXNode { | ||||
| 	node := AXNode{ | ||||
| 		NodeID:           string(protoNode.NodeID), | ||||
| 		Ignored:          protoNode.Ignored, | ||||
| 		BackendDOMNodeID: int(protoNode.BackendDOMNodeID), | ||||
| 	} | ||||
|  | ||||
| 	// Convert role | ||||
| 	if protoNode.Role != nil { | ||||
| 		node.Role = d.convertProtoAXValue(protoNode.Role) | ||||
| 	} | ||||
|  | ||||
| 	// Convert chrome role | ||||
| 	if protoNode.ChromeRole != nil { | ||||
| 		node.ChromeRole = d.convertProtoAXValue(protoNode.ChromeRole) | ||||
| 	} | ||||
|  | ||||
| 	// Convert name | ||||
| 	if protoNode.Name != nil { | ||||
| 		node.Name = d.convertProtoAXValue(protoNode.Name) | ||||
| 	} | ||||
|  | ||||
| 	// Convert description | ||||
| 	if protoNode.Description != nil { | ||||
| 		node.Description = d.convertProtoAXValue(protoNode.Description) | ||||
| 	} | ||||
|  | ||||
| 	// Convert value | ||||
| 	if protoNode.Value != nil { | ||||
| 		node.Value = d.convertProtoAXValue(protoNode.Value) | ||||
| 	} | ||||
|  | ||||
| 	// Convert properties | ||||
| 	for _, prop := range protoNode.Properties { | ||||
| 		node.Properties = append(node.Properties, AXProperty{ | ||||
| 			Name:  string(prop.Name), | ||||
| 			Value: d.convertProtoAXValue(prop.Value), | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	// Convert ignored reasons | ||||
| 	for _, reason := range protoNode.IgnoredReasons { | ||||
| 		node.IgnoredReasons = append(node.IgnoredReasons, AXProperty{ | ||||
| 			Name:  string(reason.Name), | ||||
| 			Value: d.convertProtoAXValue(reason.Value), | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	// Convert parent and child IDs | ||||
| 	if protoNode.ParentID != "" { | ||||
| 		node.ParentID = string(protoNode.ParentID) | ||||
| 	} | ||||
|  | ||||
| 	for _, childID := range protoNode.ChildIDs { | ||||
| 		node.ChildIDs = append(node.ChildIDs, string(childID)) | ||||
| 	} | ||||
|  | ||||
| 	if protoNode.FrameID != "" { | ||||
| 		node.FrameID = string(protoNode.FrameID) | ||||
| 	} | ||||
|  | ||||
| 	return node | ||||
| } | ||||
|  | ||||
| // convertProtoAXValue converts a proto.AccessibilityAXValue to our AXValue struct | ||||
| func (d *Daemon) convertProtoAXValue(protoValue *proto.AccessibilityAXValue) *AXValue { | ||||
| 	if protoValue == nil { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	value := &AXValue{ | ||||
| 		Type:  string(protoValue.Type), | ||||
| 		Value: protoValue.Value, | ||||
| 	} | ||||
|  | ||||
| 	// Convert related nodes | ||||
| 	for _, relatedNode := range protoValue.RelatedNodes { | ||||
| 		value.RelatedNodes = append(value.RelatedNodes, AXRelatedNode{ | ||||
| 			BackendDOMNodeID: int(relatedNode.BackendDOMNodeID), | ||||
| 			IDRef:            relatedNode.Idref, | ||||
| 			Text:             relatedNode.Text, | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	// Convert sources | ||||
| 	for _, source := range protoValue.Sources { | ||||
| 		axSource := AXValueSource{ | ||||
| 			Type:          string(source.Type), | ||||
| 			Superseded:    source.Superseded, | ||||
| 			Invalid:       source.Invalid, | ||||
| 			InvalidReason: source.InvalidReason, | ||||
| 		} | ||||
|  | ||||
| 		if source.Value != nil { | ||||
| 			axSource.Value = d.convertProtoAXValue(source.Value) | ||||
| 		} | ||||
|  | ||||
| 		if source.Attribute != "" { | ||||
| 			axSource.Attribute = source.Attribute | ||||
| 		} | ||||
|  | ||||
| 		if source.AttributeValue != nil { | ||||
| 			axSource.AttributeValue = d.convertProtoAXValue(source.AttributeValue) | ||||
| 		} | ||||
|  | ||||
| 		if source.NativeSource != "" { | ||||
| 			axSource.NativeSource = string(source.NativeSource) | ||||
| 		} | ||||
|  | ||||
| 		if source.NativeSourceValue != nil { | ||||
| 			axSource.NativeSourceValue = d.convertProtoAXValue(source.NativeSourceValue) | ||||
| 		} | ||||
|  | ||||
| 		value.Sources = append(value.Sources, axSource) | ||||
| 	} | ||||
|  | ||||
| 	return value | ||||
| } | ||||
|  | ||||
| // getPartialAccessibilityTree retrieves a partial accessibility tree for a specific element | ||||
| func (d *Daemon) getPartialAccessibilityTree(tabID, selector string, fetchRelatives bool, timeout int) (*AccessibilityTreeResult, error) { | ||||
| 	d.debugLog("Getting partial accessibility tree for tab: %s, selector: %s, fetchRelatives: %v, timeout: %d", tabID, selector, fetchRelatives, timeout) | ||||
|  | ||||
| 	// Use current tab if not specified | ||||
| 	if tabID == "" { | ||||
| 		tabID = d.currentTab | ||||
| 	} | ||||
|  | ||||
| 	if tabID == "" { | ||||
| 		return nil, fmt.Errorf("no tab specified and no current tab available") | ||||
| 	} | ||||
|  | ||||
| 	page, err := d.getTab(tabID) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to get page: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	// Enable accessibility domain | ||||
| 	err = proto.AccessibilityEnable{}.Call(page) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to enable accessibility domain: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	// Find the DOM node first | ||||
| 	var element *rod.Element | ||||
| 	if timeout > 0 { | ||||
| 		element, err = page.Timeout(time.Duration(timeout) * time.Second).Element(selector) | ||||
| 	} else { | ||||
| 		element, err = page.Element(selector) | ||||
| 	} | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to find element: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	// Get the backend node ID | ||||
| 	nodeInfo, err := element.Describe(1, false) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to describe element: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	// Call the Chrome DevTools Protocol Accessibility.getPartialAXTree method | ||||
| 	result, err := proto.AccessibilityGetPartialAXTree{ | ||||
| 		BackendNodeID:  nodeInfo.BackendNodeID, | ||||
| 		FetchRelatives: fetchRelatives, | ||||
| 	}.Call(page) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to get partial accessibility tree: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	// Parse the result | ||||
| 	var axResult AccessibilityTreeResult | ||||
| 	for _, node := range result.Nodes { | ||||
| 		axNode := d.convertProtoAXNode(node) | ||||
| 		axResult.Nodes = append(axResult.Nodes, axNode) | ||||
| 	} | ||||
|  | ||||
| 	d.debugLog("Successfully retrieved partial accessibility tree with %d nodes for tab: %s", len(axResult.Nodes), tabID) | ||||
| 	return &axResult, nil | ||||
| } | ||||
|  | ||||
| // queryAccessibilityTree queries the accessibility tree for nodes matching specific criteria | ||||
| func (d *Daemon) queryAccessibilityTree(tabID, selector, accessibleName, role string, timeout int) (*AccessibilityQueryResult, error) { | ||||
| 	d.debugLog("Querying accessibility tree for tab: %s, selector: %s, name: %s, role: %s, timeout: %d", tabID, selector, accessibleName, role, timeout) | ||||
|  | ||||
| 	// Use current tab if not specified | ||||
| 	if tabID == "" { | ||||
| 		tabID = d.currentTab | ||||
| 	} | ||||
|  | ||||
| 	if tabID == "" { | ||||
| 		return nil, fmt.Errorf("no tab specified and no current tab available") | ||||
| 	} | ||||
|  | ||||
| 	page, err := d.getTab(tabID) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to get page: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	// Enable accessibility domain | ||||
| 	err = proto.AccessibilityEnable{}.Call(page) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to enable accessibility domain: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	// Find the DOM node first if selector is provided | ||||
| 	var backendNodeID *proto.DOMBackendNodeID | ||||
| 	if selector != "" { | ||||
| 		var element *rod.Element | ||||
| 		if timeout > 0 { | ||||
| 			element, err = page.Timeout(time.Duration(timeout) * time.Second).Element(selector) | ||||
| 		} else { | ||||
| 			element, err = page.Element(selector) | ||||
| 		} | ||||
| 		if err != nil { | ||||
| 			return nil, fmt.Errorf("failed to find element: %w", err) | ||||
| 		} | ||||
|  | ||||
| 		// Get the backend node ID | ||||
| 		nodeInfo, err := element.Describe(1, false) | ||||
| 		if err != nil { | ||||
| 			return nil, fmt.Errorf("failed to describe element: %w", err) | ||||
| 		} | ||||
| 		backendNodeID = &nodeInfo.BackendNodeID | ||||
| 	} | ||||
|  | ||||
| 	// Build query parameters | ||||
| 	queryParams := proto.AccessibilityQueryAXTree{} | ||||
| 	if backendNodeID != nil { | ||||
| 		queryParams.BackendNodeID = *backendNodeID | ||||
| 	} | ||||
| 	if accessibleName != "" { | ||||
| 		queryParams.AccessibleName = accessibleName | ||||
| 	} | ||||
| 	if role != "" { | ||||
| 		queryParams.Role = role | ||||
| 	} | ||||
|  | ||||
| 	// Call the Chrome DevTools Protocol Accessibility.queryAXTree method | ||||
| 	result, err := queryParams.Call(page) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to query accessibility tree: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	// Parse the result | ||||
| 	var axResult AccessibilityQueryResult | ||||
| 	for _, node := range result.Nodes { | ||||
| 		axNode := d.convertProtoAXNode(node) | ||||
| 		axResult.Nodes = append(axResult.Nodes, axNode) | ||||
| 	} | ||||
|  | ||||
| 	d.debugLog("Successfully queried accessibility tree with %d matching nodes for tab: %s", len(axResult.Nodes), tabID) | ||||
| 	return &axResult, nil | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user