diff --git a/internal/hierarchy/hierarchy.go b/internal/hierarchy/hierarchy.go index fbadd25..60f5317 100644 --- a/internal/hierarchy/hierarchy.go +++ b/internal/hierarchy/hierarchy.go @@ -183,7 +183,8 @@ func match(element *Element, kind, value string) bool { case "text": return element.Text == value case "desc": - return element.Description == value + // Exact match, or iOS merged label "desc, child text". + return element.Description == value || strings.HasPrefix(element.Description, value+", ") case "descPrefix": return strings.HasPrefix(element.Description, value) default: @@ -191,21 +192,34 @@ func match(element *Element, kind, value string) bool { } } -// boundsPattern matches "[l,t,r,b]" (4-value Maestro format). +// boundsPattern matches "[l,t,r,b]" (4-value Android/Maestro format). var boundsPattern = regexp.MustCompile(`^\[(-?\d+),(-?\d+),(-?\d+),(-?\d+)\]$`) +// boundsPatternTwo matches "[x1,y1][x2,y2]" (iOS XCUITest format). +var boundsPatternTwo = regexp.MustCompile(`^\[(-?\d+),(-?\d+)\]\[(-?\d+),(-?\d+)\]$`) + func parseBounds(text string) (Bounds, error) { - m := boundsPattern.FindStringSubmatch(text) - if m == nil { - return Bounds{}, fmt.Errorf("bounds %q: not in [L,T,R,B] form", text) - } - coords := make([]int, 4) - for i := range 4 { - v, err := strconv.Atoi(m[i+1]) - if err != nil { - return Bounds{}, err + if m := boundsPattern.FindStringSubmatch(text); m != nil { + coords := make([]int, 4) + for i := range 4 { + v, err := strconv.Atoi(m[i+1]) + if err != nil { + return Bounds{}, err + } + coords[i] = v } - coords[i] = v + return Bounds{Left: coords[0], Top: coords[1], Right: coords[2], Bottom: coords[3]}, nil } - return Bounds{Left: coords[0], Top: coords[1], Right: coords[2], Bottom: coords[3]}, nil + if m := boundsPatternTwo.FindStringSubmatch(text); m != nil { + coords := make([]int, 4) + for i := range 4 { + v, err := strconv.Atoi(m[i+1]) + if err != nil { + return Bounds{}, err + } + coords[i] = v + } + return Bounds{Left: coords[0], Top: coords[1], Right: coords[2], Bottom: coords[3]}, nil + } + return Bounds{}, fmt.Errorf("bounds %q: not in [L,T,R,B] or [x1,y1][x2,y2] form", text) } diff --git a/internal/hierarchy/hierarchy_test.go b/internal/hierarchy/hierarchy_test.go index fa1c595..efd6284 100644 --- a/internal/hierarchy/hierarchy_test.go +++ b/internal/hierarchy/hierarchy_test.go @@ -164,3 +164,38 @@ func TestAccessibilityTextFallback(t *testing.T) { t.Fatal("expected match via accessibilityText fallback") } } + +func TestIOSMergedLabel(t *testing.T) { + // iOS merges contentDescription with child text: "add_account_button, + Add account" + input := `{ + "attributes": {"accessibilityText": "add_account_button, + Add account", "bounds": "[20,777][382,825]"}, + "children": [] + }` + tree, _ := Parse(input) + el := tree.Find("desc:add_account_button") + if el == nil { + t.Fatal("expected desc: to match iOS merged label") + } + if el.Bounds.Left != 20 || el.Bounds.Top != 777 || el.Bounds.Right != 382 || el.Bounds.Bottom != 825 { + t.Errorf("unexpected bounds: %+v", el.Bounds) + } +} + +func TestIOSBoundsFormat(t *testing.T) { + input := `{ + "attributes": {"accessibilityText": "account_card:abc123, Tim, $100", "bounds": "[20,130][382,202]"}, + "children": [] + }` + tree, _ := Parse(input) + el := tree.Find("descPrefix:account_card:") + if el == nil { + t.Fatal("expected descPrefix to match iOS account card") + } + if el.Bounds.Left != 20 || el.Bounds.Top != 130 || el.Bounds.Right != 382 || el.Bounds.Bottom != 202 { + t.Errorf("unexpected bounds: %+v", el.Bounds) + } + cx, cy := el.Bounds.Center() + if cx != 201 || cy != 166 { + t.Errorf("unexpected center: (%d, %d)", cx, cy) + } +}