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