From 5f66e5fb3e0fbd9e27a1868749d581413bb531a4 Mon Sep 17 00:00:00 2001 From: PJ Date: Wed, 19 Aug 2026 09:55:22 +0530 Subject: [PATCH] fix(hierarchy): reach the class attribute through className className is an accepted selector key that no producer writes: android reports the view class, ios the element type and the chrome dump el.className, all of them under `class`. With no alias onto that key the selector matched NOTHING here on every platform while the web runtime resolved it against the live DOM, so {className: "status"} named the row and the badge on one host and no element at all on the other. The failure is silent: the key is accepted, so no unknown-key error fires, and a property over the element that was never found passes having checked nothing. --- internal/hierarchy/hierarchy.go | 2 ++ internal/hierarchy/hierarchy_test.go | 36 ++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/internal/hierarchy/hierarchy.go b/internal/hierarchy/hierarchy.go index 66e1b01..ed41ed4 100644 --- a/internal/hierarchy/hierarchy.go +++ b/internal/hierarchy/hierarchy.go @@ -275,6 +275,8 @@ var attributeAliases = map[string][]string{ "placeholderValue": {"hintText"}, // iOS AXElement raw name for class "elementType": {"class"}, + // DOM property name for class; every producer writes the attribute as class + "className": {"class"}, } // selectorKeys is every key an object selector may use. It is the union of the diff --git a/internal/hierarchy/hierarchy_test.go b/internal/hierarchy/hierarchy_test.go index b8cbb4f..89809a4 100644 --- a/internal/hierarchy/hierarchy_test.go +++ b/internal/hierarchy/hierarchy_test.go @@ -540,6 +540,18 @@ const iosAttrDump = `{ ] }` +const classAttrDump = `{ + "attributes": {"resource-id": "com.app:id/list", "class": "android.widget.FrameLayout", "bounds": "[0,0,1080,2340]"}, + "children": [ + { + "attributes": {"resource-id": "com.app:id/row1", "class": "android.widget.Button", "bounds": "[0,0,1080,200]"}, + "children": [], + "clickable": true, + "enabled": true + } + ] +}` + func TestRawResourceIDSubstringMatch(t *testing.T) { tree, _ := Parse(androidAttrDump) el := tree.Find("resource-id:row1") @@ -556,6 +568,30 @@ func TestLabelAliasMatchesAccessibilityText(t *testing.T) { } } +// className is a spec key no producer writes: android reports the view class, +// ios the element type and the chrome dump el.className, all under `class`. The +// key matched nothing at all here while the web runtime resolved it against the +// live DOM, and the key being accepted meant no unknown-key error said so. +func TestClassNameAliasMatchesClass(t *testing.T) { + tree, _ := Parse(classAttrDump) + el := tree.Find("className:Button") + if el == nil { + t.Fatal("expected className: to match the class attribute via alias") + } + if el.ResourceID != "com.app:id/row1" { + t.Fatalf("got %q, want row1", el.ResourceID) + } + object := tree.FindBySelector(Selector{Filters: []AttrFilter{ + {Attr: "className", Value: "Button"}, + }}) + if object == nil { + t.Fatal("expected the object form to match the class attribute via alias") + } + if object.ResourceID != el.ResourceID { + t.Fatalf("the object form matched %q, want %q", object.ResourceID, el.ResourceID) + } +} + func TestContentDescAliasOnIOS(t *testing.T) { tree, _ := Parse(iosAttrDump) el := tree.Find("content-desc:Close")