mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
feat(hierarchy): add FindBySelectorPath for chained object selectors
Each selector in the chain is matched within the descendants of the previous match. Returns the deepest match (or nil) for FindBySelectorPath and every deepest match for FindAllBySelectorPath.
This commit is contained in:
1 parent
8be26fbf8b
commit
6c6bc97f40
2 files changed
+142
No files matched your search
@@ -298,6 +298,22 @@ func (t *Tree) FindAllNodes(selector string) []*Node {
|
||||
return searchSubtree(t.Root, kind, value)
|
||||
}
|
||||
|
||||
// FindBySelectorPath walks the selector chain starting from the tree root.
|
||||
func (t *Tree) FindBySelectorPath(path []Selector) *Node {
|
||||
if t == nil || t.Root == nil {
|
||||
return nil
|
||||
}
|
||||
return t.Root.FindBySelectorPath(path)
|
||||
}
|
||||
|
||||
// FindAllBySelectorPath walks the selector chain starting from the tree root.
|
||||
func (t *Tree) FindAllBySelectorPath(path []Selector) []*Node {
|
||||
if t == nil || t.Root == nil {
|
||||
return nil
|
||||
}
|
||||
return t.Root.FindAllBySelectorPath(path)
|
||||
}
|
||||
|
||||
// Find returns the first Node in this node's subtree (descendants only) matching
|
||||
// the string selector. Path queries within the selector are not supported here.
|
||||
func (n *Node) Find(selector string) *Node {
|
||||
@@ -346,6 +362,45 @@ func (n *Node) FindAllBySelector(sel Selector) []*Node {
|
||||
return result
|
||||
}
|
||||
|
||||
// FindBySelectorPath walks a chain of selectors. The first selector is matched
|
||||
// against descendants of the receiver; each subsequent selector is matched
|
||||
// against descendants of the previous match. Returns the deepest match or nil.
|
||||
func (n *Node) FindBySelectorPath(path []Selector) *Node {
|
||||
if len(path) == 0 {
|
||||
return nil
|
||||
}
|
||||
for _, child := range n.Children {
|
||||
for _, candidate := range searchSubtreeBySelector(child, path[0]) {
|
||||
if len(path) == 1 {
|
||||
return candidate
|
||||
}
|
||||
if deeper := candidate.FindBySelectorPath(path[1:]); deeper != nil {
|
||||
return deeper
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FindAllBySelectorPath returns every deepest match for the selector chain
|
||||
// scoped under the receiver.
|
||||
func (n *Node) FindAllBySelectorPath(path []Selector) []*Node {
|
||||
if len(path) == 0 {
|
||||
return nil
|
||||
}
|
||||
var result []*Node
|
||||
for _, child := range n.Children {
|
||||
for _, candidate := range searchSubtreeBySelector(child, path[0]) {
|
||||
if len(path) == 1 {
|
||||
result = append(result, candidate)
|
||||
continue
|
||||
}
|
||||
result = append(result, candidate.FindAllBySelectorPath(path[1:])...)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func findPathNode(root *Node, segments []string) *Node {
|
||||
if root == nil || len(segments) == 0 {
|
||||
return nil
|
||||
|
||||
@@ -528,6 +528,93 @@ func TestElementResourceIDPopulatesFromAccessibilityIdentifier(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
const selectorPathDump = `{
|
||||
"attributes": {"resource-id": "rootView", "bounds": "[0,0,1080,2340]"},
|
||||
"children": [
|
||||
{
|
||||
"attributes": {"testTag": "HomeScreen", "bounds": "[0,0,540,2340]"},
|
||||
"children": [
|
||||
{
|
||||
"attributes": {"testTag": "AccountCard", "bounds": "[0,0,540,200]"},
|
||||
"children": [
|
||||
{"attributes": {"testTag": "AccountName", "text": "Checking", "bounds": "[10,10,200,40]"}, "children": []}
|
||||
]
|
||||
},
|
||||
{
|
||||
"attributes": {"testTag": "AccountCard", "bounds": "[0,200,540,400]"},
|
||||
"children": [
|
||||
{"attributes": {"testTag": "AccountName", "text": "Savings", "bounds": "[10,210,200,240]"}, "children": []}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"attributes": {"testTag": "LedgerScreen", "bounds": "[540,0,1080,2340]"},
|
||||
"children": [
|
||||
{"attributes": {"testTag": "AccountName", "text": "Travel", "bounds": "[600,10,800,40]"}, "children": []}
|
||||
]
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
func TestFindBySelectorPathSingleSegment(t *testing.T) {
|
||||
tree, _ := Parse(selectorPathDump)
|
||||
path := []Selector{{Filters: []AttrFilter{{Attr: "testTag", Value: "HomeScreen"}}}}
|
||||
node := tree.FindBySelectorPath(path)
|
||||
if node == nil {
|
||||
t.Fatal("expected match for HomeScreen")
|
||||
}
|
||||
if got := node.Element.Attributes["testTag"]; got != "HomeScreen" {
|
||||
t.Fatalf("testTag = %q, want HomeScreen", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindBySelectorPathScopedDescent(t *testing.T) {
|
||||
tree, _ := Parse(selectorPathDump)
|
||||
path := []Selector{
|
||||
{Filters: []AttrFilter{{Attr: "testTag", Value: "HomeScreen"}}},
|
||||
{Filters: []AttrFilter{{Attr: "testTag", Value: "AccountCard"}}},
|
||||
{Filters: []AttrFilter{{Attr: "testTag", Value: "AccountName"}}},
|
||||
}
|
||||
node := tree.FindBySelectorPath(path)
|
||||
if node == nil {
|
||||
t.Fatal("expected match for HomeScreen > AccountCard > AccountName")
|
||||
}
|
||||
if node.Element.Text != "Checking" {
|
||||
t.Fatalf("text = %q, want Checking", node.Element.Text)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindBySelectorPathRespectsScope(t *testing.T) {
|
||||
tree, _ := Parse(selectorPathDump)
|
||||
path := []Selector{
|
||||
{Filters: []AttrFilter{{Attr: "testTag", Value: "LedgerScreen"}}},
|
||||
{Filters: []AttrFilter{{Attr: "testTag", Value: "AccountCard"}}},
|
||||
}
|
||||
if node := tree.FindBySelectorPath(path); node != nil {
|
||||
t.Fatalf("AccountCard is under HomeScreen only, expected nil, got %+v", node.Element)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindAllBySelectorPathReturnsAllDeepestMatches(t *testing.T) {
|
||||
tree, _ := Parse(selectorPathDump)
|
||||
path := []Selector{
|
||||
{Filters: []AttrFilter{{Attr: "testTag", Value: "HomeScreen"}}},
|
||||
{Filters: []AttrFilter{{Attr: "testTag", Value: "AccountName"}}},
|
||||
}
|
||||
matches := tree.FindAllBySelectorPath(path)
|
||||
if len(matches) != 2 {
|
||||
t.Fatalf("want 2 matches (Checking, Savings), got %d", len(matches))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindBySelectorPathEmptyPathReturnsNil(t *testing.T) {
|
||||
tree, _ := Parse(selectorPathDump)
|
||||
if tree.FindBySelectorPath(nil) != nil {
|
||||
t.Fatal("empty path should return nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodeFindDoesNotReturnSiblings(t *testing.T) {
|
||||
tree, _ := Parse(pathDump)
|
||||
a2Node := tree.FindNode("id:A2")
|
||||
|
||||
Reference in new issue
Block a user