mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
fix(verifier): name an element only when the selector names it alone
ax.findAll stamped every result with the query selector, and resolveCoordinates prefers the tree lookup over the element's own coordinates, so N sibling candidates all executed on the first match. On folio's Home screen the fuzzer could never open any account but the first. The gate tests identity rather than cardinality: no node other than this one answers to the rendered string, checked with the same lookup the runner runs. A rendered object selector can resolve somewhere the query never matched, so counting the query would call that unique. Claude-Session: https://claude.ai/code/session_01A5KmftdEJ49A9z5mF5ESrX
This commit is contained in:
1 parent
6d03c5788a
commit
57977cde0a
2 files changed
+106
-6
No files matched your search
@@ -72,16 +72,17 @@ func accessibilityObject(runtime *goja.Runtime, tree *hierarchy.Tree) *goja.Obje
|
|||||||
if node == nil {
|
if node == nil {
|
||||||
return goja.Undefined()
|
return goja.Undefined()
|
||||||
}
|
}
|
||||||
return nodeObject(runtime, node, selectorStringFromJS(runtime, call.Argument(0)))
|
return nodeObject(runtime, tree, node, selectorStringFromJS(runtime, call.Argument(0)))
|
||||||
}
|
}
|
||||||
findAll := func(call goja.FunctionCall) goja.Value {
|
findAll := func(call goja.FunctionCall) goja.Value {
|
||||||
if tree == nil {
|
if tree == nil {
|
||||||
return goja.Undefined()
|
return goja.Undefined()
|
||||||
}
|
}
|
||||||
nodes := findAllNodesFromJS(runtime, tree, call.Argument(0))
|
nodes := findAllNodesFromJS(runtime, tree, call.Argument(0))
|
||||||
|
selector := selectorStringFromJS(runtime, call.Argument(0))
|
||||||
array := runtime.NewArray()
|
array := runtime.NewArray()
|
||||||
for i, n := range nodes {
|
for i, n := range nodes {
|
||||||
_ = array.Set(fmt.Sprintf("%d", i), nodeObject(runtime, n, selectorStringFromJS(runtime, call.Argument(0))))
|
_ = array.Set(fmt.Sprintf("%d", i), nodeObject(runtime, tree, n, selector))
|
||||||
}
|
}
|
||||||
return array
|
return array
|
||||||
}
|
}
|
||||||
@@ -90,7 +91,26 @@ func accessibilityObject(runtime *goja.Runtime, tree *hierarchy.Tree) *goja.Obje
|
|||||||
return accessibility
|
return accessibility
|
||||||
}
|
}
|
||||||
|
|
||||||
func nodeObject(runtime *goja.Runtime, node *hierarchy.Node, selector string) goja.Value {
|
// unambiguousSelector returns selector only when no node other than this one
|
||||||
|
// answers to it. The runner prefers tree.Find(action.On) over the coordinates
|
||||||
|
// the element reported (resolveCoordinates) and Find takes the first match, so
|
||||||
|
// naming an element by a selector its siblings share sends every one of their
|
||||||
|
// actions to the first sibling. An unnamed element keeps its own coordinates,
|
||||||
|
// which are already right, matching what selectorsFor does for the builtin
|
||||||
|
// target enumeration in pkg/spec/src/web-runtime.ts.
|
||||||
|
func unambiguousSelector(tree *hierarchy.Tree, node *hierarchy.Node, selector string) string {
|
||||||
|
if tree == nil || selector == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
for _, match := range tree.FindAllNodes(selector) {
|
||||||
|
if match != node {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return selector
|
||||||
|
}
|
||||||
|
|
||||||
|
func nodeObject(runtime *goja.Runtime, tree *hierarchy.Tree, node *hierarchy.Node, selector string) goja.Value {
|
||||||
element := &node.Element
|
element := &node.Element
|
||||||
object := runtime.NewObject()
|
object := runtime.NewObject()
|
||||||
centerX, centerY := element.Bounds.Center()
|
centerX, centerY := element.Bounds.Center()
|
||||||
@@ -106,7 +126,7 @@ func nodeObject(runtime *goja.Runtime, node *hierarchy.Node, selector string) go
|
|||||||
_ = object.Set("editable", element.Editable)
|
_ = object.Set("editable", element.Editable)
|
||||||
_ = object.Set("x", centerX)
|
_ = object.Set("x", centerX)
|
||||||
_ = object.Set("y", centerY)
|
_ = object.Set("y", centerY)
|
||||||
_ = object.Set(tagSelector, selector)
|
_ = object.Set(tagSelector, unambiguousSelector(tree, node, selector))
|
||||||
bounds := runtime.NewObject()
|
bounds := runtime.NewObject()
|
||||||
_ = bounds.Set("left", element.Bounds.Left)
|
_ = bounds.Set("left", element.Bounds.Left)
|
||||||
_ = bounds.Set("top", element.Bounds.Top)
|
_ = bounds.Set("top", element.Bounds.Top)
|
||||||
@@ -124,14 +144,15 @@ func nodeObject(runtime *goja.Runtime, node *hierarchy.Node, selector string) go
|
|||||||
if childNode == nil {
|
if childNode == nil {
|
||||||
return goja.Undefined()
|
return goja.Undefined()
|
||||||
}
|
}
|
||||||
return nodeObject(runtime, childNode, selectorStringFromJS(runtime, arg))
|
return nodeObject(runtime, tree, childNode, selectorStringFromJS(runtime, arg))
|
||||||
}
|
}
|
||||||
childFindAll := func(call goja.FunctionCall) goja.Value {
|
childFindAll := func(call goja.FunctionCall) goja.Value {
|
||||||
arg := call.Argument(0)
|
arg := call.Argument(0)
|
||||||
childNodes := findAllNodesInSubtreeFromJS(runtime, node, arg)
|
childNodes := findAllNodesInSubtreeFromJS(runtime, node, arg)
|
||||||
|
childSelector := selectorStringFromJS(runtime, arg)
|
||||||
array := runtime.NewArray()
|
array := runtime.NewArray()
|
||||||
for i, n := range childNodes {
|
for i, n := range childNodes {
|
||||||
_ = array.Set(fmt.Sprintf("%d", i), nodeObject(runtime, n, selectorStringFromJS(runtime, arg)))
|
_ = array.Set(fmt.Sprintf("%d", i), nodeObject(runtime, tree, n, childSelector))
|
||||||
}
|
}
|
||||||
return array
|
return array
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ package verifier
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -1433,3 +1435,80 @@ func TestExtractorCount_ReportsEveryRegisteredExtractor(t *testing.T) {
|
|||||||
t.Errorf("ExtractorCount() = %d, want 2 (helloSpec registers screen and balance)", got)
|
t.Errorf("ExtractorCount() = %d, want 2 (helloSpec registers screen and balance)", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestAxSelectorTag_OnlyNamesTheElementItAloneResolvesTo pins the rule the
|
||||||
|
// runner depends on: resolveCoordinates prefers tree.Find(action.On) over the
|
||||||
|
// coordinates the element reported, and Find takes the first match, so a
|
||||||
|
// selector three sibling cards share would send all three taps to the first
|
||||||
|
// card. Siblings therefore carry no selector and keep their own coordinates;
|
||||||
|
// an element the selector alone resolves to still carries it.
|
||||||
|
func TestAxSelectorTag_OnlyNamesTheElementItAloneResolvesTo(t *testing.T) {
|
||||||
|
const treeJSON = `{
|
||||||
|
"attributes": {"resource-id": "root", "bounds": "[0,0,100,400]"},
|
||||||
|
"children": [
|
||||||
|
{"attributes": {"testTag": "AccountCard", "text": "Alpha", "bounds": "[0,0,100,100]"}, "clickable": true, "children": []},
|
||||||
|
{"attributes": {"testTag": "AccountCard", "text": "Beta", "bounds": "[0,100,100,200]"}, "clickable": true, "children": []},
|
||||||
|
{"attributes": {"testTag": "AccountCard", "text": "Gamma", "bounds": "[0,200,100,300]"}, "clickable": true, "children": []},
|
||||||
|
{"attributes": {"testTag": "AddAccount", "bounds": "[0,300,100,400]"}, "clickable": true, "children": []}
|
||||||
|
]
|
||||||
|
}`
|
||||||
|
verifier := newVerifier(t)
|
||||||
|
mustLoad(t, verifier, `
|
||||||
|
globalThis.cards = __sanderling__.extract(state =>
|
||||||
|
state.ax.findAll({ testTag: "AccountCard" })
|
||||||
|
);
|
||||||
|
globalThis.sole = __sanderling__.extract(state =>
|
||||||
|
state.ax.findAll({ testTag: "AddAccount" })
|
||||||
|
);
|
||||||
|
globalThis.soleFind = __sanderling__.extract(state =>
|
||||||
|
state.ax.find({ testTag: "AddAccount" })
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
tree, err := hierarchy.Parse(treeJSON)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := verifier.PushSnapshot(SnapshotInput{Snapshots: Snapshots{}, Tree: tree}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
current := func(name string) *goja.Object {
|
||||||
|
handle := verifier.runtime.GlobalObject().Get(name).ToObject(verifier.runtime)
|
||||||
|
value := handle.Get("current")
|
||||||
|
if goja.IsUndefined(value) || goja.IsNull(value) {
|
||||||
|
t.Fatalf("%s: extractor produced no value", name)
|
||||||
|
}
|
||||||
|
return value.ToObject(verifier.runtime)
|
||||||
|
}
|
||||||
|
element := func(array *goja.Object, index int) *goja.Object {
|
||||||
|
return array.Get(strconv.Itoa(index)).ToObject(verifier.runtime)
|
||||||
|
}
|
||||||
|
|
||||||
|
cards := current("cards")
|
||||||
|
if got := cards.Get("length").ToInteger(); got != 3 {
|
||||||
|
t.Fatalf("findAll returned %d cards, want 3", got)
|
||||||
|
}
|
||||||
|
centers := map[string]bool{}
|
||||||
|
for index := range 3 {
|
||||||
|
card := element(cards, index)
|
||||||
|
if got := card.Get(tagSelector).String(); got != "" {
|
||||||
|
t.Errorf("card %d (%s) carries selector %q; three cards answer to it, so the runner would tap the first card three times",
|
||||||
|
index, card.Get("text"), got)
|
||||||
|
}
|
||||||
|
centers[fmt.Sprintf("%d,%d", card.Get("x").ToInteger(), card.Get("y").ToInteger())] = true
|
||||||
|
}
|
||||||
|
if len(centers) != 3 {
|
||||||
|
t.Errorf("the three cards report %d distinct centers, want 3: %v", len(centers), centers)
|
||||||
|
}
|
||||||
|
|
||||||
|
soleAll := current("sole")
|
||||||
|
if got := soleAll.Get("length").ToInteger(); got != 1 {
|
||||||
|
t.Fatalf("findAll returned %d AddAccount elements, want 1", got)
|
||||||
|
}
|
||||||
|
if got := element(soleAll, 0).Get(tagSelector).String(); got != "testTag:AddAccount" {
|
||||||
|
t.Errorf("findAll over a single match: selector = %q, want %q", got, "testTag:AddAccount")
|
||||||
|
}
|
||||||
|
if got := current("soleFind").Get(tagSelector).String(); got != "testTag:AddAccount" {
|
||||||
|
t.Errorf("find over a single match: selector = %q, want %q", got, "testTag:AddAccount")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user