mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
feat(verifier): wire hierarchy into state.ax and carry element coords in Action
This commit is contained in:
1 parent
e539e89438
commit
5f2a2d52ab
4 files changed
+105
-24
No files matched your search
@@ -17,6 +17,10 @@ type Action struct {
|
||||
Kind ActionKind
|
||||
On string
|
||||
Text string
|
||||
// X, Y hold the element center when the spec passed an ax element to
|
||||
// Tap/InputText. Zero means the runner must resolve On against the
|
||||
// current hierarchy.
|
||||
X, Y int
|
||||
}
|
||||
|
||||
type extractorState struct {
|
||||
@@ -32,6 +36,7 @@ const (
|
||||
tagFormula = "__uatuFormula"
|
||||
tagActionGenerator = "__uatuActionGenerator"
|
||||
tagInternalKind = "__uatuKind"
|
||||
tagSelector = "__uatuSelector"
|
||||
internalKindActions = "actions"
|
||||
internalKindWeighted = "weighted"
|
||||
internalKindBuiltinTaps = "taps"
|
||||
|
||||
@@ -5,14 +5,17 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
|
||||
"github.com/priyanshujain/uatu/internal/hierarchy"
|
||||
)
|
||||
|
||||
// Snapshots is the per-step extractor output forwarded by the SDK.
|
||||
type Snapshots map[string]json.RawMessage
|
||||
|
||||
// stateObject builds a JS-side `{ snapshots, ax }` matching the State type
|
||||
// from pkg/spec-api. ax is currently a stub returning nothing.
|
||||
func stateObject(runtime *goja.Runtime, snapshots Snapshots) (*goja.Object, error) {
|
||||
// from pkg/spec-api. ax is backed by the parsed uiautomator hierarchy when
|
||||
// one is provided.
|
||||
func stateObject(runtime *goja.Runtime, snapshots Snapshots, tree *hierarchy.Tree) (*goja.Object, error) {
|
||||
state := runtime.NewObject()
|
||||
snapshotsObject := runtime.NewObject()
|
||||
for key, raw := range snapshots {
|
||||
@@ -27,20 +30,59 @@ func stateObject(runtime *goja.Runtime, snapshots Snapshots) (*goja.Object, erro
|
||||
if err := state.Set("snapshots", snapshotsObject); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
accessibility := runtime.NewObject()
|
||||
if err := accessibility.Set("find", runtime.ToValue(func(string) goja.Value { return goja.Undefined() })); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := accessibility.Set("findAll", runtime.ToValue(func(string) []goja.Value { return nil })); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := state.Set("ax", accessibility); err != nil {
|
||||
if err := state.Set("ax", accessibilityObject(runtime, tree)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return state, nil
|
||||
}
|
||||
|
||||
func accessibilityObject(runtime *goja.Runtime, tree *hierarchy.Tree) *goja.Object {
|
||||
accessibility := runtime.NewObject()
|
||||
find := func(selector string) goja.Value {
|
||||
if tree == nil {
|
||||
return goja.Undefined()
|
||||
}
|
||||
element := tree.Find(selector)
|
||||
if element == nil {
|
||||
return goja.Undefined()
|
||||
}
|
||||
return elementObject(runtime, element, selector)
|
||||
}
|
||||
findAll := func(selector string) []goja.Value {
|
||||
if tree == nil {
|
||||
return nil
|
||||
}
|
||||
elements := tree.FindAll(selector)
|
||||
result := make([]goja.Value, len(elements))
|
||||
for index, element := range elements {
|
||||
result[index] = elementObject(runtime, element, selector)
|
||||
}
|
||||
return result
|
||||
}
|
||||
_ = accessibility.Set("find", runtime.ToValue(find))
|
||||
_ = accessibility.Set("findAll", runtime.ToValue(findAll))
|
||||
return accessibility
|
||||
}
|
||||
|
||||
func elementObject(runtime *goja.Runtime, element *hierarchy.Element, selector string) goja.Value {
|
||||
object := runtime.NewObject()
|
||||
centerX, centerY := element.Bounds.Center()
|
||||
_ = object.Set("id", element.ResourceID)
|
||||
_ = object.Set("text", element.Text)
|
||||
_ = object.Set("desc", element.Description)
|
||||
_ = object.Set("class", element.Class)
|
||||
_ = object.Set("x", centerX)
|
||||
_ = object.Set("y", centerY)
|
||||
_ = object.Set(tagSelector, selector)
|
||||
bounds := runtime.NewObject()
|
||||
_ = bounds.Set("left", element.Bounds.Left)
|
||||
_ = bounds.Set("top", element.Bounds.Top)
|
||||
_ = bounds.Set("right", element.Bounds.Right)
|
||||
_ = bounds.Set("bottom", element.Bounds.Bottom)
|
||||
_ = object.Set("bounds", bounds)
|
||||
return object
|
||||
}
|
||||
|
||||
func jsonToJSValue(runtime *goja.Runtime, raw json.RawMessage) (goja.Value, error) {
|
||||
if len(raw) == 0 {
|
||||
return goja.Undefined(), nil
|
||||
@@ -66,16 +108,48 @@ func jsValueToAction(runtime *goja.Runtime, value goja.Value) (Action, error) {
|
||||
switch kind {
|
||||
case "Tap":
|
||||
on := object.Get("on")
|
||||
return Action{Kind: ActionKindTap, On: stringOf(on)}, nil
|
||||
x, y := coordinatesOf(runtime, on)
|
||||
return Action{Kind: ActionKindTap, On: selectorOf(runtime, on), X: x, Y: y}, nil
|
||||
case "InputText":
|
||||
into := object.Get("into")
|
||||
text := object.Get("text")
|
||||
return Action{Kind: ActionKindInputText, On: stringOf(into), Text: stringOf(text)}, nil
|
||||
x, y := coordinatesOf(runtime, into)
|
||||
return Action{Kind: ActionKindInputText, On: selectorOf(runtime, into), Text: stringOf(text), X: x, Y: y}, nil
|
||||
default:
|
||||
return Action{}, fmt.Errorf("unknown action kind %q", kind)
|
||||
}
|
||||
}
|
||||
|
||||
func selectorOf(runtime *goja.Runtime, value goja.Value) string {
|
||||
if value == nil || goja.IsNull(value) || goja.IsUndefined(value) {
|
||||
return ""
|
||||
}
|
||||
object := value.ToObject(runtime)
|
||||
if object == nil {
|
||||
return value.String()
|
||||
}
|
||||
if tag := object.Get(tagSelector); tag != nil && !goja.IsUndefined(tag) {
|
||||
return tag.String()
|
||||
}
|
||||
return value.String()
|
||||
}
|
||||
|
||||
func coordinatesOf(runtime *goja.Runtime, value goja.Value) (int, int) {
|
||||
if value == nil || goja.IsNull(value) || goja.IsUndefined(value) {
|
||||
return 0, 0
|
||||
}
|
||||
object := value.ToObject(runtime)
|
||||
if object == nil {
|
||||
return 0, 0
|
||||
}
|
||||
xValue := object.Get("x")
|
||||
yValue := object.Get("y")
|
||||
if xValue == nil || yValue == nil || goja.IsUndefined(xValue) || goja.IsUndefined(yValue) {
|
||||
return 0, 0
|
||||
}
|
||||
return int(xValue.ToInteger()), int(yValue.ToInteger())
|
||||
}
|
||||
|
||||
func stringOf(value goja.Value) string {
|
||||
if value == nil || goja.IsNull(value) || goja.IsUndefined(value) {
|
||||
return ""
|
||||
|
||||
@@ -63,7 +63,7 @@ func TestPushSnapshot_UpdatesExtractorCurrentAndPrevious(t *testing.T) {
|
||||
if err := verifier.PushSnapshot(Snapshots{
|
||||
"screen": json.RawMessage(`"customer_ledger"`),
|
||||
"ledger.balance": json.RawMessage(`1500`),
|
||||
}); err != nil {
|
||||
}, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ func TestPushSnapshot_UpdatesExtractorCurrentAndPrevious(t *testing.T) {
|
||||
}
|
||||
|
||||
// Push again: previous should mirror the prior current.
|
||||
if err := verifier.PushSnapshot(Snapshots{"ledger.balance": json.RawMessage(`2000`)}); err != nil {
|
||||
if err := verifier.PushSnapshot(Snapshots{"ledger.balance": json.RawMessage(`2000`)}, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
balanceValue = verifier.runtime.GlobalObject().Get("balance").ToObject(verifier.runtime)
|
||||
@@ -105,7 +105,7 @@ func TestEvaluateProperties_HoldsThenViolates(t *testing.T) {
|
||||
}
|
||||
for index, testCase := range cases {
|
||||
raw, _ := json.Marshal(testCase.balance)
|
||||
if err := verifier.PushSnapshot(Snapshots{"ledger.balance": raw}); err != nil {
|
||||
if err := verifier.PushSnapshot(Snapshots{"ledger.balance": raw}, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
verdicts := verifier.EvaluateProperties()
|
||||
@@ -118,7 +118,7 @@ func TestEvaluateProperties_HoldsThenViolates(t *testing.T) {
|
||||
func TestNextAction_FromActionsGenerator(t *testing.T) {
|
||||
verifier := newVerifier(t)
|
||||
mustLoad(t, verifier, helloSpec)
|
||||
_ = verifier.PushSnapshot(Snapshots{})
|
||||
_ = verifier.PushSnapshot(Snapshots{}, nil)
|
||||
|
||||
action, err := verifier.NextAction()
|
||||
if err != nil {
|
||||
@@ -142,7 +142,7 @@ func TestNextAction_WeightedSelectsByWeight(t *testing.T) {
|
||||
[99, tapAway],
|
||||
);
|
||||
`)
|
||||
_ = verifier.PushSnapshot(Snapshots{})
|
||||
_ = verifier.PushSnapshot(Snapshots{}, nil)
|
||||
|
||||
awayCount := 0
|
||||
homeCount := 0
|
||||
@@ -168,7 +168,7 @@ func TestNextAction_EmptyGeneratorReturnsErrNoAction(t *testing.T) {
|
||||
mustLoad(t, verifier, `
|
||||
globalThis.actions = __uatu__.actions(() => []);
|
||||
`)
|
||||
_ = verifier.PushSnapshot(Snapshots{})
|
||||
_ = verifier.PushSnapshot(Snapshots{}, nil)
|
||||
|
||||
_, err := verifier.NextAction()
|
||||
if !errors.Is(err, ErrNoAction) {
|
||||
@@ -183,7 +183,7 @@ func TestInputText_RoundTrip(t *testing.T) {
|
||||
__uatu__.inputText({ into: "id:phone", text: "+919876543210" }),
|
||||
]);
|
||||
`)
|
||||
_ = verifier.PushSnapshot(Snapshots{})
|
||||
_ = verifier.PushSnapshot(Snapshots{}, nil)
|
||||
|
||||
action, err := verifier.NextAction()
|
||||
if err != nil {
|
||||
@@ -202,7 +202,7 @@ func TestPushSnapshot_FeedsSnapshotsToExtractorState(t *testing.T) {
|
||||
mustLoad(t, verifier, `
|
||||
globalThis.captured = __uatu__.extract(state => state.snapshots["k"]);
|
||||
`)
|
||||
if err := verifier.PushSnapshot(Snapshots{"k": json.RawMessage(`"hello"`)}); err != nil {
|
||||
if err := verifier.PushSnapshot(Snapshots{"k": json.RawMessage(`"hello"`)}, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
value := verifier.runtime.GlobalObject().Get("captured").ToObject(verifier.runtime).Get("current")
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/dop251/goja"
|
||||
|
||||
"github.com/priyanshujain/uatu/internal/hierarchy"
|
||||
"github.com/priyanshujain/uatu/internal/ltl"
|
||||
)
|
||||
|
||||
@@ -79,9 +80,10 @@ func (v *Verifier) Load(source string) error {
|
||||
}
|
||||
|
||||
// PushSnapshot updates the JS-side state and refreshes every extractor's
|
||||
// current/previous values in registration order.
|
||||
func (v *Verifier) PushSnapshot(snapshots Snapshots) error {
|
||||
state, err := stateObject(v.runtime, snapshots)
|
||||
// current/previous values in registration order. Passing a nil tree is
|
||||
// allowed and yields an empty ax scope.
|
||||
func (v *Verifier) PushSnapshot(snapshots Snapshots, tree *hierarchy.Tree) error {
|
||||
state, err := stateObject(v.runtime, snapshots, tree)
|
||||
if err != nil {
|
||||
return fmt.Errorf("build state: %w", err)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user