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:
pj committed 2026-08-13 00:36:05 +05:30
1 parent 35b0856e6c
commit 1332471b72
3 files changed
+120

No files matched your search

+33
View File
@@ -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)