mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 11:07:10 +00:00
fix(hierarchy): rank spatial-fallback matches by specificity
The bounds-containment fallback returned the first pre-order match, so a screen-sized container could win over the intended small element. Matches are now ordered smallest-area first; equal-area matches keep pre-order, preserving the iOS-flat equal-bounds sibling pattern.
This commit is contained in:
1 parent
29ec13adcd
commit
c356340563
2 files changed
+82
No files matched your search
@@ -28,6 +28,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
"maps"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@@ -426,9 +427,22 @@ func (n *Node) scopedNodes(accept func(*Element) bool) []*Node {
|
|||||||
result = append(result, candidate)
|
result = append(result, candidate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Spatial containment alone lets a large container outrank the intended
|
||||||
|
// small element; the most specific (smallest) match wins instead.
|
||||||
|
sortBySpecificity(result)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sortBySpecificity orders nodes ascending by bounds area, so the smallest
|
||||||
|
// (most specific) containing match comes first. Equal-area nodes keep their
|
||||||
|
// pre-order position.
|
||||||
|
func sortBySpecificity(nodes []*Node) {
|
||||||
|
sort.SliceStable(nodes, func(i, j int) bool {
|
||||||
|
return nodes[i].Bounds.Width()*nodes[i].Bounds.Height() <
|
||||||
|
nodes[j].Bounds.Width()*nodes[j].Bounds.Height()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func collectMatches(node *Node, accept func(*Element) bool, result *[]*Node) {
|
func collectMatches(node *Node, accept func(*Element) bool, result *[]*Node) {
|
||||||
if accept(&node.Element) {
|
if accept(&node.Element) {
|
||||||
*result = append(*result, node)
|
*result = append(*result, node)
|
||||||
|
|||||||
@@ -856,6 +856,74 @@ func TestIOSFlatSpatialScopeExcludesOutsideBounds(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// spatialSpecificityDump scopes a leaf testTag (iOS-flat pattern) over a
|
||||||
|
// screen where both a screen-sized container and the small element inside it
|
||||||
|
// match the same attribute, plus two equal-bounds siblings for tie ordering.
|
||||||
|
const spatialSpecificityDump = `{
|
||||||
|
"attributes": {"bounds": "[0,0][402,874]"},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"attributes": {"resource-id": "FormScreen", "bounds": "[0,62][402,840]"},
|
||||||
|
"children": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attributes": {"resource-id": "FieldContainer", "accessibilityText": "Amount", "bounds": "[0,62][402,840]"},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"attributes": {"resource-id": "AmountField", "accessibilityText": "Amount", "bounds": "[34,125][368,173]"},
|
||||||
|
"children": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attributes": {"resource-id": "FirstTab", "accessibilityText": "Tab", "bounds": "[20,297][382,345]"},
|
||||||
|
"children": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attributes": {"resource-id": "SecondTab", "accessibilityText": "Tab", "bounds": "[20,297][382,345]"},
|
||||||
|
"children": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`
|
||||||
|
|
||||||
|
func TestSpatialFallbackPrefersSmallestContainingMatch(t *testing.T) {
|
||||||
|
tree, err := Parse(spatialSpecificityDump)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Parse: %v", err)
|
||||||
|
}
|
||||||
|
screen := tree.FindNode("id:FormScreen")
|
||||||
|
if screen == nil {
|
||||||
|
t.Fatal("expected FormScreen node")
|
||||||
|
}
|
||||||
|
// Both FieldContainer (screen-sized, earlier in pre-order) and
|
||||||
|
// AmountField (small) match; the most specific match must win.
|
||||||
|
node := screen.Find("desc:Amount")
|
||||||
|
if node == nil {
|
||||||
|
t.Fatal("expected a spatial-fallback match")
|
||||||
|
}
|
||||||
|
if node.ResourceID != "AmountField" {
|
||||||
|
t.Fatalf("expected the smallest containing match AmountField, got id=%q", node.ResourceID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSpatialFallbackEqualAreaKeepsPreOrder(t *testing.T) {
|
||||||
|
tree, err := Parse(spatialSpecificityDump)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Parse: %v", err)
|
||||||
|
}
|
||||||
|
screen := tree.FindNode("id:FormScreen")
|
||||||
|
if screen == nil {
|
||||||
|
t.Fatal("expected FormScreen node")
|
||||||
|
}
|
||||||
|
node := screen.Find("desc:Tab")
|
||||||
|
if node == nil {
|
||||||
|
t.Fatal("expected a spatial-fallback match")
|
||||||
|
}
|
||||||
|
if node.ResourceID != "FirstTab" {
|
||||||
|
t.Fatalf("equal-area matches must keep pre-order, got id=%q", node.ResourceID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestIOSFlatStructuralChildStillPreferred(t *testing.T) {
|
func TestIOSFlatStructuralChildStillPreferred(t *testing.T) {
|
||||||
tree, _ := Parse(iosFlatDump)
|
tree, _ := Parse(iosFlatDump)
|
||||||
submit := tree.FindNode("id:LoginSubmit")
|
submit := tree.FindNode("id:LoginSubmit")
|
||||||
|
|||||||
Reference in new issue
Block a user