From ae629d9d7dc76402a7286efc0836a1171e665f89 Mon Sep 17 00:00:00 2001 From: PJ Date: Wed, 19 Aug 2026 10:06:56 +0530 Subject: [PATCH] fix(hierarchy): reach the accessible label through every name for it label and accessibilityLabel aliased onto accessibilityText alone, which only the ios sidecar writes, and alias expansion is ONE level: the hop from accessibilityText to content-desc was never taken, so both keys matched nothing on android and on the chrome dump, which write the fact under content-desc. ariaLabel and contentDescription aliased onto nothing at all and matched nothing anywhere. The web runtime resolves all four against the live DOM, so a selector naming a field this way found it on one host and no element at all on the other. The keys are accepted, so no unknown-key error fires, and a property over the element that was never found passes having checked nothing. Each name lists both keys rather than chaining through accessibilityText: transitive expansion would silently widen every existing key at once. --- internal/hierarchy/hierarchy.go | 11 +++-- internal/hierarchy/hierarchy_test.go | 61 ++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/internal/hierarchy/hierarchy.go b/internal/hierarchy/hierarchy.go index ed41ed4..106738c 100644 --- a/internal/hierarchy/hierarchy.go +++ b/internal/hierarchy/hierarchy.go @@ -259,9 +259,14 @@ type AttrFilter struct { var attributeAliases = map[string][]string{ // Android XML legacy name; web driver uses content-desc; the sidecar normalises to accessibilityText "content-desc": {"accessibilityText"}, - // iOS AXElement / UIKit names - "label": {"accessibilityText"}, - "accessibilityLabel": {"accessibilityText"}, + // Every other name for the accessible label. Alias expansion is one level, + // so each name lists both keys a producer writes the fact under rather than + // hopping through accessibilityText: android and the chrome dump write + // content-desc, the ios sidecar writes accessibilityText. + "label": {"accessibilityText", "content-desc"}, + "accessibilityLabel": {"accessibilityText", "content-desc"}, + "ariaLabel": {"accessibilityText", "content-desc"}, + "contentDescription": {"accessibilityText", "content-desc"}, // accessibilityText is the canonical key; also check content-desc for Android/web "accessibilityText": {"content-desc"}, // resource-id canonical key; also check identifier (iOS AXElement raw field) diff --git a/internal/hierarchy/hierarchy_test.go b/internal/hierarchy/hierarchy_test.go index 89809a4..e9bd9b1 100644 --- a/internal/hierarchy/hierarchy_test.go +++ b/internal/hierarchy/hierarchy_test.go @@ -552,6 +552,24 @@ const classAttrDump = `{ ] }` +const webAttrDump = `{ + "attributes": {"resource-id": "page", "tag": "html", "bounds": "[0,0,1280,720]"}, + "children": [ + { + "attributes": {"resource-id": "login_email", "content-desc": "login_email", "tag": "input", "bounds": "[0,0,300,40]"}, + "children": [], + "editable": true, + "enabled": true + }, + { + "attributes": {"resource-id": "customer_row_a1", "data-testid": "customer-row", "tag": "div", "bounds": "[0,40,300,80]"}, + "children": [], + "clickable": true, + "enabled": true + } + ] +}` + func TestRawResourceIDSubstringMatch(t *testing.T) { tree, _ := Parse(androidAttrDump) el := tree.Find("resource-id:row1") @@ -592,6 +610,49 @@ func TestClassNameAliasMatchesClass(t *testing.T) { } } +// One fact, four names, and only two of them reached it. Android and the chrome +// dump write the accessible name under content-desc; ios writes it under +// accessibilityText. label and accessibilityLabel aliased onto accessibilityText +// alone, and alias expansion is ONE level, so the hop from there to content-desc +// was never taken: both keys matched nothing on the two platforms that write +// content-desc. ariaLabel and contentDescription aliased onto nothing at all and +// matched nothing anywhere. The web runtime resolves all four against the live +// DOM, so a selector naming a field this way found it on one host and no element +// at all on the other, with no unknown-key error to say so. +func TestAccessibilityLabelAliasesReachContentDesc(t *testing.T) { + tree, _ := Parse(webAttrDump) + for _, key := range []string{"label", "accessibilityLabel", "ariaLabel", "contentDescription"} { + element := tree.Find(key + ":login_email") + if element == nil { + t.Fatalf("expected %s: to match the content-desc attribute via alias", key) + } + if element.ResourceID != "login_email" { + t.Fatalf("%s: matched %q, want login_email", key, element.ResourceID) + } + object := tree.FindBySelector(Selector{Filters: []AttrFilter{ + {Attr: key, Value: "login_email"}, + }}) + if object == nil { + t.Fatalf("expected the object form of %s to match content-desc via alias", key) + } + if object.ResourceID != element.ResourceID { + t.Fatalf("the object form of %s matched %q, want %q", + key, object.ResourceID, element.ResourceID) + } + } +} + +// The iOS sidecar writes the same fact under accessibilityText, which the two +// names already reached and have to keep reaching. +func TestAccessibilityLabelAliasesStillReachAccessibilityText(t *testing.T) { + tree, _ := Parse(iosAttrDump) + for _, key := range []string{"label", "accessibilityLabel", "ariaLabel", "contentDescription"} { + if tree.Find(key+":Close") == nil { + t.Fatalf("expected %s: to match the accessibilityText attribute via alias", key) + } + } +} + func TestContentDescAliasOnIOS(t *testing.T) { tree, _ := Parse(iosAttrDump) el := tree.Find("content-desc:Close")