mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
fix(folio): stop abusing contentDescription as data carrier (#45)
* feat(hierarchy): full-attribute selector system
- Add Attributes map to Element (raw platform attrs + serialized booleans)
- Add Selector / AttrFilter types for multi-filter AND matching
- Add matchAttr with alias expansion and substring/boolean semantics
- Add matchSelector (AND of all filters)
- id: and desc: keep exact/suffix/prefix semantics for backward compat
- text: widens to substring via matchAttr
- default: case routes unknown kinds to matchAttr (NEW)
- Add Tree.FindNode / Tree.FindAllNodes returning *Node
- Add Node.Find / Node.FindAll for scoped subtree string search
- Add Node.FindBySelector / Node.FindAllBySelector for object AND search
- Add attributeAliases for cross-platform name expansion
* test(hierarchy): full-attribute selector coverage
- raw resource-id: substring match
- label:/content-desc: alias expansion to accessibilityText on iOS
- scrollable:true/false boolean exact match
- title: iOS-only attribute, graceful nil on Android
- text: substring widening
- Selector AND: both filters must match; single miss returns nil
- Node.Find scoped search: descendants only, not siblings
* feat(verifier): object-form selectors + attrs + element-level find
- ax.find/findAll accept string or {attr:value} JS objects
- Object form builds Selector with AND semantics
- Returned element objects expose attrs sub-object (raw platform attrs)
- Returned element objects expose .find() and .findAll() scoped to subtree
- Element-level .find/.findAll accept string or object selectors
* feat(spec): extend AccessibilityElement and AccessibilityTree types
- AccessibilityElement gains attrs, find(), findAll()
- find/findAll on both Tree and Element accept string | AttrSelector
- AttrSelector = Record<string, string> for object-form AND matching
* feat(folio): migrate to chained object-form selectors
- Replace path queries (desc:X > desc:Y) with chained API
- Screen root lookups use { accessibilityText: "ScreenName" }
- Element-scoped searches use find/findAll with string or object
- Keep string selectors for descPrefix: and desc:Back (shows both forms)
* fix(folio): use account_card:id desc, expose balance via text semantics
* fix(folio): embed accountId in LedgerScreen desc, expose values via text semantics
* fix(spec): replace desc-parsing with text-based parseDollarCents extraction
This commit is contained in:
7 files changed
+593
-107
No files matched your search
@@ -307,3 +307,172 @@ func TestIOSBoundsFormat(t *testing.T) {
|
||||
t.Errorf("unexpected center: (%d, %d)", cx, cy)
|
||||
}
|
||||
}
|
||||
|
||||
// --- full-attribute selector tests ---
|
||||
|
||||
const androidAttrDump = `{
|
||||
"attributes": {"resource-id": "com.app:id/list", "bounds": "[0,0,1080,2340]"},
|
||||
"children": [
|
||||
{
|
||||
"attributes": {"resource-id": "com.app:id/row1", "scrollable": "true", "bounds": "[0,0,1080,200]"},
|
||||
"children": [],
|
||||
"clickable": true,
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"attributes": {"resource-id": "com.app:id/row2", "scrollable": "false", "bounds": "[0,200,1080,400]"},
|
||||
"children": [],
|
||||
"clickable": false,
|
||||
"enabled": true
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
const iosAttrDump = `{
|
||||
"attributes": {"bounds": "[0,0,390,844]"},
|
||||
"children": [
|
||||
{
|
||||
"attributes": {"accessibilityText": "Close", "title": "Settings", "bounds": "[0,0,100,50]"},
|
||||
"children": [],
|
||||
"enabled": true
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
func TestRawResourceIDSubstringMatch(t *testing.T) {
|
||||
tree, _ := Parse(androidAttrDump)
|
||||
el := tree.Find("resource-id:row1")
|
||||
if el == nil {
|
||||
t.Fatal("expected resource-id: to match via substring")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLabelAliasMatchesAccessibilityText(t *testing.T) {
|
||||
tree, _ := Parse(iosAttrDump)
|
||||
el := tree.Find("label:Close")
|
||||
if el == nil {
|
||||
t.Fatal("expected label: to match accessibilityText via alias")
|
||||
}
|
||||
}
|
||||
|
||||
func TestContentDescAliasOnIOS(t *testing.T) {
|
||||
tree, _ := Parse(iosAttrDump)
|
||||
el := tree.Find("content-desc:Close")
|
||||
if el == nil {
|
||||
t.Fatal("expected content-desc: to match accessibilityText via alias on iOS")
|
||||
}
|
||||
}
|
||||
|
||||
func TestScrollableTrueMatches(t *testing.T) {
|
||||
tree, _ := Parse(androidAttrDump)
|
||||
el := tree.Find("scrollable:true")
|
||||
if el == nil {
|
||||
t.Fatal("expected scrollable:true to match")
|
||||
}
|
||||
if el.ResourceID != "com.app:id/row1" {
|
||||
t.Fatalf("got %q, want row1", el.ResourceID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestScrollableFalseMatchesSecondRow(t *testing.T) {
|
||||
tree, _ := Parse(androidAttrDump)
|
||||
el := tree.Find("scrollable:false")
|
||||
if el == nil {
|
||||
t.Fatal("expected scrollable:false to match row2")
|
||||
}
|
||||
if el.ResourceID != "com.app:id/row2" {
|
||||
t.Fatalf("got %q, want row2", el.ResourceID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTitleMatchesIOSElement(t *testing.T) {
|
||||
tree, _ := Parse(iosAttrDump)
|
||||
el := tree.Find("title:Settings")
|
||||
if el == nil {
|
||||
t.Fatal("expected title:Settings to match iOS element")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTitleReturnsNilForAndroid(t *testing.T) {
|
||||
tree, _ := Parse(androidAttrDump)
|
||||
el := tree.Find("title:Settings")
|
||||
if el != nil {
|
||||
t.Fatal("expected title:Settings to return nil for Android element (graceful ignore)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestScrollableGracefulIgnoreOnIOS(t *testing.T) {
|
||||
tree, _ := Parse(iosAttrDump)
|
||||
el := tree.Find("scrollable:true")
|
||||
if el != nil {
|
||||
t.Fatal("expected scrollable:true to return nil on iOS hierarchy (graceful ignore)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTextIsNowSubstring(t *testing.T) {
|
||||
tree, _ := Parse(sampleDump)
|
||||
el := tree.Find("text:Hel")
|
||||
if el == nil {
|
||||
t.Fatal("expected text: to match substring")
|
||||
}
|
||||
if el.Text != "Hello" {
|
||||
t.Fatalf("got %q, want Hello", el.Text)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiFilterSelectorAND(t *testing.T) {
|
||||
tree, _ := Parse(androidAttrDump)
|
||||
sel := Selector{Filters: []AttrFilter{
|
||||
{Attr: "scrollable", Value: "true"},
|
||||
{Attr: "resource-id", Value: "row1"},
|
||||
}}
|
||||
node := tree.Root.FindBySelector(sel)
|
||||
if node == nil {
|
||||
t.Fatal("expected AND selector to find row1 (scrollable=true AND resource-id contains row1)")
|
||||
}
|
||||
if node.Element.ResourceID != "com.app:id/row1" {
|
||||
t.Fatalf("got %q, want row1", node.Element.ResourceID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiFilterSelectorMissReturnsNil(t *testing.T) {
|
||||
tree, _ := Parse(androidAttrDump)
|
||||
sel := Selector{Filters: []AttrFilter{
|
||||
{Attr: "scrollable", Value: "true"},
|
||||
{Attr: "resource-id", Value: "row2"}, // row2 is not scrollable=true
|
||||
}}
|
||||
node := tree.Root.FindBySelector(sel)
|
||||
if node != nil {
|
||||
t.Fatal("expected AND selector to return nil when one filter misses")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodeFindScopedSearch(t *testing.T) {
|
||||
tree, _ := Parse(pathDump)
|
||||
// A2 has a child B2 with content-desc "label_b"
|
||||
// Find the A node, then search its subtree for label_b -- should find B (not B2)
|
||||
aNode := tree.FindNode("id:A")
|
||||
if aNode == nil {
|
||||
t.Fatal("expected to find A node")
|
||||
}
|
||||
result := aNode.Find("desc:label_b")
|
||||
if result == nil {
|
||||
t.Fatal("expected Node.Find to find label_b in A's subtree")
|
||||
}
|
||||
if result.Element.ResourceID != "B" {
|
||||
t.Fatalf("got %q, want B (not B2 from sibling A2)", result.Element.ResourceID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodeFindDoesNotReturnSiblings(t *testing.T) {
|
||||
tree, _ := Parse(pathDump)
|
||||
a2Node := tree.FindNode("id:A2")
|
||||
if a2Node == nil {
|
||||
t.Fatal("expected to find A2 node")
|
||||
}
|
||||
// B is under A, not A2 -- should not be found from A2's subtree
|
||||
result := a2Node.Find("id:B")
|
||||
if result != nil && result.Element.ResourceID == "B" {
|
||||
t.Fatal("Node.Find should not return nodes from sibling subtrees")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user