mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
feat(verifier): fail the spec on a selector key that cannot match
An empty match is indistinguishable from a screen with no such element, so a mistyped key generates no action for the whole run and the campaign finishes clean having explored nothing. The goja boundary now throws, naming the key and the accepted list.
This commit is contained in:
1 parent
35b0856e6c
commit
1332471b72
3 files changed
+120
No files matched your search
@@ -2,6 +2,7 @@ package verifier
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/priyanshujain/sanderling/internal/hierarchy"
|
||||
@@ -99,3 +100,79 @@ func TestStateAxFindWorks(t *testing.T) {
|
||||
t.Fatalf("findAll count = %d, want 1", count)
|
||||
}
|
||||
}
|
||||
|
||||
// A selector key that can never match is a spec bug, and an empty result hides
|
||||
// it: the generator yields no action, the runner waits out the step, and the
|
||||
// run ends clean having explored nothing. The spec must fail instead.
|
||||
func TestStateAxObjectSelectorRejectsAnUnknownKey(t *testing.T) {
|
||||
tree, err := hierarchy.Parse(`{
|
||||
"attributes": {"resource-id": "root"},
|
||||
"children": [{"attributes": {"content-desc": "Supplier"}, "children": []}]
|
||||
}`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
verifier := newVerifier(t)
|
||||
mustLoad(t, verifier, `
|
||||
globalThis.probe = __sanderling__.extract(state => !!state.ax.find({ descripton: "Supplier" }));
|
||||
`)
|
||||
|
||||
err = verifier.PushSnapshot(SnapshotInput{Snapshots: Snapshots{}, Tree: tree})
|
||||
if err == nil {
|
||||
t.Fatal("expected an unknown selector key to fail the spec")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "descripton") {
|
||||
t.Errorf("error does not name the offending key: %v", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "accepted keys") {
|
||||
t.Errorf("error does not list the accepted keys: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// desc names the accessibility description in the element fields and in the
|
||||
// string form, so the object form answers to it too rather than reporting it as
|
||||
// a mistake.
|
||||
func TestStateAxObjectSelectorAcceptsDesc(t *testing.T) {
|
||||
tree, err := hierarchy.Parse(`{
|
||||
"attributes": {"resource-id": "root"},
|
||||
"children": [{"attributes": {"content-desc": "Supplier"}, "children": []}]
|
||||
}`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
verifier := newVerifier(t)
|
||||
mustLoad(t, verifier, `
|
||||
globalThis.probe = __sanderling__.extract(state => state.ax.find({ desc: "Supplier" }) ? "matched" : "miss");
|
||||
`)
|
||||
if err := verifier.PushSnapshot(SnapshotInput{Snapshots: Snapshots{}, Tree: tree}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := verifier.runtime.GlobalObject().Get("probe").ToObject(verifier.runtime).Get("current").String()
|
||||
if got != "matched" {
|
||||
t.Fatalf("probe = %q, want matched", got)
|
||||
}
|
||||
}
|
||||
|
||||
// A key that belongs to another platform must stay silent: one spec runs on
|
||||
// every platform, and iOS-only attributes are absent from an Android tree by
|
||||
// design rather than by mistake.
|
||||
func TestStateAxObjectSelectorKeepsCrossPlatformKeysSilent(t *testing.T) {
|
||||
tree, err := hierarchy.Parse(`{
|
||||
"attributes": {"resource-id": "root"},
|
||||
"children": []
|
||||
}`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
verifier := newVerifier(t)
|
||||
mustLoad(t, verifier, `
|
||||
globalThis.probe = __sanderling__.extract(state => state.ax.find({ title: "Settings" }) ? "matched" : "miss");
|
||||
`)
|
||||
if err := verifier.PushSnapshot(SnapshotInput{Snapshots: Snapshots{}, Tree: tree}); err != nil {
|
||||
t.Fatalf("a platform-specific key must not fail the run: %v", err)
|
||||
}
|
||||
got := verifier.runtime.GlobalObject().Get("probe").ToObject(verifier.runtime).Get("current").String()
|
||||
if got != "miss" {
|
||||
t.Fatalf("probe = %q, want miss", got)
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package verifier
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -151,12 +152,14 @@ func findNodeFromJS(runtime *goja.Runtime, tree *hierarchy.Tree, arg goja.Value)
|
||||
return tree.FindNode(s)
|
||||
}
|
||||
if path, ok := selectorPathFromJS(runtime, arg); ok {
|
||||
requireKnownSelectorKeys(runtime, tree, path...)
|
||||
return tree.FindBySelectorPath(path)
|
||||
}
|
||||
sel := selectorFromJSObject(runtime, arg)
|
||||
if len(sel.Filters) == 0 {
|
||||
return nil
|
||||
}
|
||||
requireKnownSelectorKeys(runtime, tree, sel)
|
||||
return tree.Root.FindBySelector(sel)
|
||||
}
|
||||
|
||||
@@ -169,12 +172,14 @@ func findAllNodesFromJS(runtime *goja.Runtime, tree *hierarchy.Tree, arg goja.Va
|
||||
return tree.FindAllNodes(s)
|
||||
}
|
||||
if path, ok := selectorPathFromJS(runtime, arg); ok {
|
||||
requireKnownSelectorKeys(runtime, tree, path...)
|
||||
return tree.FindAllBySelectorPath(path)
|
||||
}
|
||||
sel := selectorFromJSObject(runtime, arg)
|
||||
if len(sel.Filters) == 0 {
|
||||
return nil
|
||||
}
|
||||
requireKnownSelectorKeys(runtime, tree, sel)
|
||||
return tree.Root.FindAllBySelector(sel)
|
||||
}
|
||||
|
||||
@@ -187,12 +192,14 @@ func findNodeInSubtreeFromJS(runtime *goja.Runtime, node *hierarchy.Node, arg go
|
||||
return node.Find(s)
|
||||
}
|
||||
if path, ok := selectorPathFromJS(runtime, arg); ok {
|
||||
requireKnownSelectorKeys(runtime, node.Tree(), path...)
|
||||
return node.FindBySelectorPath(path)
|
||||
}
|
||||
sel := selectorFromJSObject(runtime, arg)
|
||||
if len(sel.Filters) == 0 {
|
||||
return nil
|
||||
}
|
||||
requireKnownSelectorKeys(runtime, node.Tree(), sel)
|
||||
return node.FindBySelector(sel)
|
||||
}
|
||||
|
||||
@@ -205,15 +212,41 @@ func findAllNodesInSubtreeFromJS(runtime *goja.Runtime, node *hierarchy.Node, ar
|
||||
return node.FindAll(s)
|
||||
}
|
||||
if path, ok := selectorPathFromJS(runtime, arg); ok {
|
||||
requireKnownSelectorKeys(runtime, node.Tree(), path...)
|
||||
return node.FindAllBySelectorPath(path)
|
||||
}
|
||||
sel := selectorFromJSObject(runtime, arg)
|
||||
if len(sel.Filters) == 0 {
|
||||
return nil
|
||||
}
|
||||
requireKnownSelectorKeys(runtime, node.Tree(), sel)
|
||||
return node.FindAllBySelector(sel)
|
||||
}
|
||||
|
||||
// requireKnownSelectorKeys throws a JS error when a selector names a key that
|
||||
// can never match. Returning an empty result instead is indistinguishable from
|
||||
// a screen that simply has no such element, so a spec built on a mistyped key
|
||||
// generates no action, the runner waits out every step, and the campaign
|
||||
// finishes clean having explored nothing.
|
||||
func requireKnownSelectorKeys(
|
||||
runtime *goja.Runtime,
|
||||
tree *hierarchy.Tree,
|
||||
selectors ...hierarchy.Selector,
|
||||
) {
|
||||
var unknown []string
|
||||
for _, sel := range selectors {
|
||||
for _, key := range tree.UnknownSelectorKeys(sel) {
|
||||
if !slices.Contains(unknown, key) {
|
||||
unknown = append(unknown, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(unknown) == 0 {
|
||||
return
|
||||
}
|
||||
panic(runtime.NewTypeError(hierarchy.UnknownSelectorKeyMessage(unknown)))
|
||||
}
|
||||
|
||||
// selectorFromJSObject converts a JS object {attr: value, ...} into a Selector.
|
||||
func selectorFromJSObject(runtime *goja.Runtime, arg goja.Value) hierarchy.Selector {
|
||||
obj := arg.ToObject(runtime)
|
||||
|
||||
Reference in new issue
Block a user