mirror of
https://github.com/CharlesWiltgen/Axiom.git
synced 2026-09-20 19:58:20 +08:00
feat(xcui): add a11y-tree parse types, find/walk, and parser fuzz target
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
package main
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
// Frame is the numeric rect AXe emits alongside the string AXFrame.
|
||||
type Frame struct {
|
||||
X float64 `json:"x"`
|
||||
Y float64 `json:"y"`
|
||||
Width float64 `json:"width"`
|
||||
Height float64 `json:"height"`
|
||||
}
|
||||
|
||||
// AXElement mirrors one node of `axe describe-ui` output. Fields that AXe
|
||||
// emits as JSON null are pointers so absence is distinguishable from "".
|
||||
type AXElement struct {
|
||||
AXUniqueID *string `json:"AXUniqueId"`
|
||||
AXLabel *string `json:"AXLabel"`
|
||||
AXValue *string `json:"AXValue"`
|
||||
Title *string `json:"title"`
|
||||
Help *string `json:"help"`
|
||||
Subrole *string `json:"subrole"`
|
||||
Role string `json:"role"`
|
||||
RoleDescription string `json:"role_description"`
|
||||
Type string `json:"type"`
|
||||
AXFrame string `json:"AXFrame"`
|
||||
Frame Frame `json:"frame"`
|
||||
Enabled bool `json:"enabled"`
|
||||
PID int `json:"pid"`
|
||||
Children []AXElement `json:"children"`
|
||||
}
|
||||
|
||||
func parseDescribeUI(data []byte) ([]AXElement, error) {
|
||||
var roots []AXElement
|
||||
if err := json.Unmarshal(data, &roots); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return roots, nil
|
||||
}
|
||||
|
||||
// walk visits every element depth-first, roots first.
|
||||
func walk(roots []AXElement, visit func(AXElement)) {
|
||||
for _, el := range roots {
|
||||
visit(el)
|
||||
walk(el.Children, visit)
|
||||
}
|
||||
}
|
||||
|
||||
// findByID returns every element whose AXUniqueId equals id. More than one
|
||||
// match means the identifier isn't unique (the --single assertion catches it).
|
||||
func findByID(roots []AXElement, id string) []AXElement {
|
||||
var out []AXElement
|
||||
walk(roots, func(el AXElement) {
|
||||
if el.AXUniqueID != nil && *el.AXUniqueID == id {
|
||||
out = append(out, el)
|
||||
}
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
func deref(s *string) string {
|
||||
if s == nil {
|
||||
return ""
|
||||
}
|
||||
return *s
|
||||
}
|
||||
@@ -7,3 +7,60 @@ func TestVersionConstSet(t *testing.T) {
|
||||
t.Fatal("version const must be set")
|
||||
}
|
||||
}
|
||||
|
||||
const sampleTree = `[
|
||||
{
|
||||
"AXUniqueId": null, "AXLabel": "App", "AXValue": null,
|
||||
"role": "AXApplication", "type": "Application", "enabled": true,
|
||||
"frame": {"x":0,"y":0,"width":402,"height":874},
|
||||
"children": [
|
||||
{
|
||||
"AXUniqueId": "artist.hero", "AXLabel": "Artwork for The Chemical Brothers",
|
||||
"AXValue": null, "role": "AXImage", "type": "Image", "enabled": true,
|
||||
"frame": {"x":0,"y":0,"width":402,"height":402}, "children": []
|
||||
},
|
||||
{
|
||||
"AXUniqueId": "play.all", "AXLabel": "Play all", "AXValue": null,
|
||||
"role": "AXButton", "type": "Button", "enabled": true,
|
||||
"frame": {"x":16,"y":420,"width":120,"height":44}, "children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]`
|
||||
|
||||
func TestParseDescribeUI(t *testing.T) {
|
||||
roots, err := parseDescribeUI([]byte(sampleTree))
|
||||
if err != nil {
|
||||
t.Fatalf("parse error: %v", err)
|
||||
}
|
||||
if len(roots) != 1 || len(roots[0].Children) != 2 {
|
||||
t.Fatalf("got %d roots / %d children, want 1 / 2", len(roots), len(roots[0].Children))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindByID(t *testing.T) {
|
||||
roots, _ := parseDescribeUI([]byte(sampleTree))
|
||||
matches := findByID(roots, "artist.hero")
|
||||
if len(matches) != 1 {
|
||||
t.Fatalf("got %d matches, want 1", len(matches))
|
||||
}
|
||||
if got := deref(matches[0].AXLabel); got != "Artwork for The Chemical Brothers" {
|
||||
t.Errorf("label = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindByIDAbsent(t *testing.T) {
|
||||
roots, _ := parseDescribeUI([]byte(sampleTree))
|
||||
if matches := findByID(roots, "nope"); len(matches) != 0 {
|
||||
t.Errorf("got %d matches, want 0", len(matches))
|
||||
}
|
||||
}
|
||||
|
||||
func FuzzParseDescribeUI(f *testing.F) {
|
||||
f.Add([]byte(sampleTree))
|
||||
f.Add([]byte(`[]`))
|
||||
f.Add([]byte(`[{"AXUniqueId":null,"children":[]}]`))
|
||||
f.Fuzz(func(t *testing.T, data []byte) {
|
||||
_, _ = parseDescribeUI(data) // must not panic
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user