mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
fix(verifier): canonicalize selector strings
Object/chain JS selectors used to fall through to goja's default stringification, producing "[object Object]" tags that surfaced as garbage in trace.action.selector. Emit canonical "k:v" / " > "-joined strings instead so the tag round-trips back through the hierarchy selector grammar.
This commit is contained in:
1 parent
09c1b8df26
commit
352118c199
2 files changed
+131
-8
No files matched your search
@@ -3,6 +3,7 @@ package verifier
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
@@ -69,7 +70,7 @@ func accessibilityObject(runtime *goja.Runtime, tree *hierarchy.Tree) *goja.Obje
|
||||
if node == nil {
|
||||
return goja.Undefined()
|
||||
}
|
||||
return nodeObject(runtime, node, selectorStringFromJS(call.Argument(0)))
|
||||
return nodeObject(runtime, node, selectorStringFromJS(runtime, call.Argument(0)))
|
||||
}
|
||||
findAll := func(call goja.FunctionCall) goja.Value {
|
||||
if tree == nil {
|
||||
@@ -78,7 +79,7 @@ func accessibilityObject(runtime *goja.Runtime, tree *hierarchy.Tree) *goja.Obje
|
||||
nodes := findAllNodesFromJS(runtime, tree, call.Argument(0))
|
||||
array := runtime.NewArray()
|
||||
for i, n := range nodes {
|
||||
_ = array.Set(fmt.Sprintf("%d", i), nodeObject(runtime, n, selectorStringFromJS(call.Argument(0))))
|
||||
_ = array.Set(fmt.Sprintf("%d", i), nodeObject(runtime, n, selectorStringFromJS(runtime, call.Argument(0))))
|
||||
}
|
||||
return array
|
||||
}
|
||||
@@ -121,14 +122,14 @@ func nodeObject(runtime *goja.Runtime, node *hierarchy.Node, selector string) go
|
||||
if childNode == nil {
|
||||
return goja.Undefined()
|
||||
}
|
||||
return nodeObject(runtime, childNode, selectorStringFromJS(arg))
|
||||
return nodeObject(runtime, childNode, selectorStringFromJS(runtime, arg))
|
||||
}
|
||||
childFindAll := func(call goja.FunctionCall) goja.Value {
|
||||
arg := call.Argument(0)
|
||||
childNodes := findAllNodesInSubtreeFromJS(runtime, node, arg)
|
||||
array := runtime.NewArray()
|
||||
for i, n := range childNodes {
|
||||
_ = array.Set(fmt.Sprintf("%d", i), nodeObject(runtime, n, selectorStringFromJS(arg)))
|
||||
_ = array.Set(fmt.Sprintf("%d", i), nodeObject(runtime, n, selectorStringFromJS(runtime, arg)))
|
||||
}
|
||||
return array
|
||||
}
|
||||
@@ -266,15 +267,62 @@ func selectorPathFromJS(runtime *goja.Runtime, arg goja.Value) ([]hierarchy.Sele
|
||||
|
||||
// selectorStringFromJS returns a string representation of the selector argument
|
||||
// for tagging returned element objects (used by selectorOf to reconstruct the
|
||||
// selector when the element is passed back as an action target).
|
||||
func selectorStringFromJS(arg goja.Value) string {
|
||||
if goja.IsUndefined(arg) || goja.IsNull(arg) {
|
||||
// selector when the element is passed back as an action target). Output
|
||||
// follows the canonical hierarchy selector grammar: "k:v" pairs space-joined
|
||||
// per object, chains joined by " > ".
|
||||
func selectorStringFromJS(runtime *goja.Runtime, arg goja.Value) string {
|
||||
if arg == nil || goja.IsUndefined(arg) || goja.IsNull(arg) {
|
||||
return ""
|
||||
}
|
||||
if s, ok := arg.Export().(string); ok {
|
||||
return s
|
||||
}
|
||||
return arg.String()
|
||||
exported := arg.Export()
|
||||
if slice, ok := exported.([]any); ok {
|
||||
object := arg.ToObject(runtime)
|
||||
if object == nil {
|
||||
return ""
|
||||
}
|
||||
parts := make([]string, 0, len(slice))
|
||||
for index := range slice {
|
||||
entry := object.Get(fmt.Sprintf("%d", index))
|
||||
if entry == nil || goja.IsUndefined(entry) || goja.IsNull(entry) {
|
||||
continue
|
||||
}
|
||||
segment := selectorObjectToString(runtime, entry)
|
||||
if segment == "" {
|
||||
continue
|
||||
}
|
||||
parts = append(parts, segment)
|
||||
}
|
||||
return strings.Join(parts, " > ")
|
||||
}
|
||||
return selectorObjectToString(runtime, arg)
|
||||
}
|
||||
|
||||
// selectorObjectToString formats a single JS object as a space-joined sequence
|
||||
// of "k:v" pairs, mirroring the hierarchy package's predicate grammar.
|
||||
func selectorObjectToString(runtime *goja.Runtime, arg goja.Value) string {
|
||||
if arg == nil || goja.IsUndefined(arg) || goja.IsNull(arg) {
|
||||
return ""
|
||||
}
|
||||
object := arg.ToObject(runtime)
|
||||
if object == nil {
|
||||
return ""
|
||||
}
|
||||
keys := object.Keys()
|
||||
parts := make([]string, 0, len(keys))
|
||||
for _, key := range keys {
|
||||
if key == tagSelector {
|
||||
continue
|
||||
}
|
||||
value := object.Get(key)
|
||||
if value == nil || goja.IsUndefined(value) || goja.IsNull(value) {
|
||||
continue
|
||||
}
|
||||
parts = append(parts, fmt.Sprintf("%s:%s", key, value.String()))
|
||||
}
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
func lastActionObject(runtime *goja.Runtime, action *Action) goja.Value {
|
||||
|
||||
@@ -831,6 +831,81 @@ globalThis.properties = {
|
||||
globalThis.actions = __sanderling__.actions(() => []);
|
||||
`
|
||||
|
||||
// TestSelectorStringFromJS_CanonicalGrammar guarantees the selector tag stamped
|
||||
// on returned AX nodes round-trips back to a parseable selector string when the
|
||||
// node is later used as an action target. Without this, an action emitted from
|
||||
// `tap({ on: state.ax.find({ testTag: "LoginEmail" }) })` ends up with
|
||||
// `action.selector = "[object Object]"` in the trace.
|
||||
func TestSelectorStringFromJS_CanonicalGrammar(t *testing.T) {
|
||||
const treeJSON = `{
|
||||
"attributes": {"resource-id": "root", "bounds": "[0,0,100,100]"},
|
||||
"children": [
|
||||
{"attributes": {"testTag": "LoginScreen", "bounds": "[0,0,100,40]"},
|
||||
"children": [
|
||||
{"attributes": {"testTag": "LoginEmail", "bounds": "[0,0,100,20]"}, "editable": true, "enabled": true, "children": []}
|
||||
]}
|
||||
]
|
||||
}`
|
||||
verifier := newVerifier(t)
|
||||
mustLoad(t, verifier, `
|
||||
globalThis.objectSelector = __sanderling__.extract(state =>
|
||||
state.ax.find({ testTag: "LoginScreen" })
|
||||
);
|
||||
globalThis.chainSelector = __sanderling__.extract(state =>
|
||||
state.ax.find([{ testTag: "LoginScreen" }, { testTag: "LoginEmail" }])
|
||||
);
|
||||
globalThis.stringSelector = __sanderling__.extract(state =>
|
||||
state.ax.find("testTag:LoginScreen")
|
||||
);
|
||||
`)
|
||||
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)
|
||||
}
|
||||
|
||||
read := func(name string) string {
|
||||
handle := verifier.runtime.GlobalObject().Get(name).ToObject(verifier.runtime)
|
||||
current := handle.Get("current")
|
||||
if goja.IsUndefined(current) || goja.IsNull(current) {
|
||||
return ""
|
||||
}
|
||||
object := current.ToObject(verifier.runtime)
|
||||
return object.Get(tagSelector).String()
|
||||
}
|
||||
cases := []struct {
|
||||
name string
|
||||
want string
|
||||
}{
|
||||
{"objectSelector", "testTag:LoginScreen"},
|
||||
{"chainSelector", "testTag:LoginScreen > testTag:LoginEmail"},
|
||||
{"stringSelector", "testTag:LoginScreen"},
|
||||
}
|
||||
for _, testCase := range cases {
|
||||
got := read(testCase.name)
|
||||
if got != testCase.want {
|
||||
t.Errorf("%s: got %q, want %q", testCase.name, got, testCase.want)
|
||||
}
|
||||
if strings.Contains(got, "[object") {
|
||||
t.Errorf("%s: selector contains garbage %q", testCase.name, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestSelectorStringFromJS_NullEmpty verifies that nil/undefined args produce
|
||||
// an empty string instead of "null"/"undefined" garbage.
|
||||
func TestSelectorStringFromJS_NullEmpty(t *testing.T) {
|
||||
verifier := newVerifier(t)
|
||||
if got := selectorStringFromJS(verifier.runtime, goja.Undefined()); got != "" {
|
||||
t.Errorf("undefined: got %q, want empty", got)
|
||||
}
|
||||
if got := selectorStringFromJS(verifier.runtime, goja.Null()); got != "" {
|
||||
t.Errorf("null: got %q, want empty", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOverrideExtractorValues_PropagatesNestedObjectFields(t *testing.T) {
|
||||
verifier := newVerifier(t)
|
||||
mustLoad(t, verifier, objectExtractorSpec)
|
||||
|
||||
Reference in new issue
Block a user