accessibility

This commit is contained in:
Josh at WLTechBlog
2025-08-29 12:11:54 -05:00
parent 6bad614f9e
commit 7f4d8b8e84
12 changed files with 2708 additions and 1577 deletions

View File

@@ -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
}