mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
feat(verifier): wire longPresses and scrolls generators
This commit is contained in:
1 parent
2397a65b9f
commit
dc0026ad37
3 files changed
+116
-50
No files matched your search
@@ -66,14 +66,16 @@ const (
|
|||||||
tagInternalKind = "__sanderlingKind"
|
tagInternalKind = "__sanderlingKind"
|
||||||
tagSelector = "__sanderlingSelector"
|
tagSelector = "__sanderlingSelector"
|
||||||
|
|
||||||
internalKindActions = "actions"
|
internalKindActions = "actions"
|
||||||
internalKindWeighted = "weighted"
|
internalKindWeighted = "weighted"
|
||||||
internalKindBuiltinTaps = "taps"
|
internalKindBuiltinTaps = "taps"
|
||||||
internalKindBuiltinDoubleTaps = "doubleTaps"
|
internalKindBuiltinDoubleTaps = "doubleTaps"
|
||||||
internalKindBuiltinTyping = "typing"
|
internalKindBuiltinTyping = "typing"
|
||||||
internalKindBuiltinSwipes = "swipes"
|
internalKindBuiltinSwipes = "swipes"
|
||||||
internalKindBuiltinWaitOnce = "waitOnce"
|
internalKindBuiltinWaitOnce = "waitOnce"
|
||||||
internalKindBuiltinPressKey = "pressKey"
|
internalKindBuiltinPressKey = "pressKey"
|
||||||
|
internalKindBuiltinLongPresses = "longPresses"
|
||||||
|
internalKindBuiltinScrolls = "scrolls"
|
||||||
)
|
)
|
||||||
|
|
||||||
// installRuntimeBindings exposes globalThis.__sanderling__ to the loaded spec.
|
// installRuntimeBindings exposes globalThis.__sanderling__ to the loaded spec.
|
||||||
@@ -110,6 +112,12 @@ func (v *Verifier) installRuntimeBindings() error {
|
|||||||
if err := sanderling.Set("doubleTap", v.bindDoubleTap); err != nil {
|
if err := sanderling.Set("doubleTap", v.bindDoubleTap); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := sanderling.Set("longPress", v.bindLongPress); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := sanderling.Set("scroll", v.bindScroll); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err := sanderling.Set("inputText", v.bindInputText); err != nil {
|
if err := sanderling.Set("inputText", v.bindInputText); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -140,6 +148,12 @@ func (v *Verifier) installRuntimeBindings() error {
|
|||||||
if err := sanderling.Set("pressKeys", v.builtinGenerator(internalKindBuiltinPressKey)); err != nil {
|
if err := sanderling.Set("pressKeys", v.builtinGenerator(internalKindBuiltinPressKey)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := sanderling.Set("longPresses", v.builtinGenerator(internalKindBuiltinLongPresses)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := sanderling.Set("scrolls", v.builtinGenerator(internalKindBuiltinScrolls)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return v.runtime.GlobalObject().Set("__sanderling__", sanderling)
|
return v.runtime.GlobalObject().Set("__sanderling__", sanderling)
|
||||||
}
|
}
|
||||||
@@ -425,6 +439,29 @@ func (v *Verifier) bindDoubleTap(call goja.FunctionCall) goja.Value {
|
|||||||
return handle
|
return handle
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *Verifier) bindLongPress(call goja.FunctionCall) goja.Value {
|
||||||
|
parameters := call.Argument(0).ToObject(v.runtime)
|
||||||
|
if parameters == nil {
|
||||||
|
panic(v.runtime.NewTypeError("LongPress requires {on}"))
|
||||||
|
}
|
||||||
|
handle := v.runtime.NewObject()
|
||||||
|
_ = handle.Set("kind", "LongPress")
|
||||||
|
_ = handle.Set("on", parameters.Get("on"))
|
||||||
|
return handle
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Verifier) bindScroll(call goja.FunctionCall) goja.Value {
|
||||||
|
parameters := call.Argument(0).ToObject(v.runtime)
|
||||||
|
if parameters == nil {
|
||||||
|
panic(v.runtime.NewTypeError("Scroll requires {direction}"))
|
||||||
|
}
|
||||||
|
handle := v.runtime.NewObject()
|
||||||
|
_ = handle.Set("kind", "Scroll")
|
||||||
|
_ = handle.Set("direction", parameters.Get("direction"))
|
||||||
|
_ = handle.Set("in", parameters.Get("in"))
|
||||||
|
return handle
|
||||||
|
}
|
||||||
|
|
||||||
func (v *Verifier) bindInputText(call goja.FunctionCall) goja.Value {
|
func (v *Verifier) bindInputText(call goja.FunctionCall) goja.Value {
|
||||||
parameters := call.Argument(0).ToObject(v.runtime)
|
parameters := call.Argument(0).ToObject(v.runtime)
|
||||||
if parameters == nil {
|
if parameters == nil {
|
||||||
|
|||||||
@@ -556,6 +556,10 @@ func (v *Verifier) resolveGenerator(generator goja.Value) (Action, error) {
|
|||||||
return Action{Kind: ActionKindWait, DurationMillis: 500}, nil
|
return Action{Kind: ActionKindWait, DurationMillis: 500}, nil
|
||||||
case internalKindBuiltinPressKey:
|
case internalKindBuiltinPressKey:
|
||||||
return v.generateRandomPressKey()
|
return v.generateRandomPressKey()
|
||||||
|
case internalKindBuiltinLongPresses:
|
||||||
|
return v.generateRandomLongPress()
|
||||||
|
case internalKindBuiltinScrolls:
|
||||||
|
return v.generateRandomScroll()
|
||||||
default:
|
default:
|
||||||
return Action{}, fmt.Errorf("unknown generator kind %q", kindValue.String())
|
return Action{}, fmt.Errorf("unknown generator kind %q", kindValue.String())
|
||||||
}
|
}
|
||||||
@@ -712,6 +716,73 @@ func (v *Verifier) generateRandomSwipe() (Action, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// generateRandomLongPress mirrors generateRandomTap: it picks a visible,
|
||||||
|
// clickable, enabled, in-scope element and targets its center. Real users
|
||||||
|
// long-press (context menus, reorder handles, multi-select), so a fuzzer that
|
||||||
|
// never emits one cannot reach those affordances.
|
||||||
|
func (v *Verifier) generateRandomLongPress() (Action, error) {
|
||||||
|
return v.generateRandomTapKind(ActionKindLongPress)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateRandomScroll picks a scrollable, in-scope container and emits a swipe
|
||||||
|
// across it in a random direction.
|
||||||
|
func (v *Verifier) generateRandomScroll() (Action, error) {
|
||||||
|
if v.lastTree == nil {
|
||||||
|
return Action{}, ErrNoAction
|
||||||
|
}
|
||||||
|
candidates := make([]*hierarchy.Element, 0, len(v.lastTree.Elements))
|
||||||
|
for _, element := range v.lastTree.Elements {
|
||||||
|
if element.Attributes["scrollable"] != "true" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !v.inScope(element) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if element.Bounds.Width() <= 0 || element.Bounds.Height() <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
candidates = append(candidates, element)
|
||||||
|
}
|
||||||
|
if len(candidates) == 0 {
|
||||||
|
return Action{}, ErrNoAction
|
||||||
|
}
|
||||||
|
picked := candidates[v.rng.IntN(len(candidates))]
|
||||||
|
cx, cy := picked.Bounds.Center()
|
||||||
|
width := picked.Bounds.Width()
|
||||||
|
height := picked.Bounds.Height()
|
||||||
|
directions := []string{"up", "down", "left", "right"}
|
||||||
|
dir := directions[v.rng.IntN(len(directions))]
|
||||||
|
toX, toY := cx, cy
|
||||||
|
// Scroll direction names the content motion; the gesture swipes the
|
||||||
|
// opposite way. Revealing content below ("down") means dragging the
|
||||||
|
// finger up, so toY decreases, and likewise for the other directions.
|
||||||
|
switch dir {
|
||||||
|
case "down":
|
||||||
|
toY = cy - (4*height)/10
|
||||||
|
case "up":
|
||||||
|
toY = cy + (4*height)/10
|
||||||
|
case "left":
|
||||||
|
toX = cx + (4*width)/10
|
||||||
|
case "right":
|
||||||
|
toX = cx - (4*width)/10
|
||||||
|
}
|
||||||
|
if toX < 0 {
|
||||||
|
toX = 0
|
||||||
|
}
|
||||||
|
if toY < 0 {
|
||||||
|
toY = 0
|
||||||
|
}
|
||||||
|
return Action{
|
||||||
|
Kind: ActionKindScroll,
|
||||||
|
Direction: dir,
|
||||||
|
FromX: cx,
|
||||||
|
FromY: cy,
|
||||||
|
ToX: toX,
|
||||||
|
ToY: toY,
|
||||||
|
DurationMillis: 300,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (v *Verifier) generateRandomPressKey() (Action, error) {
|
func (v *Verifier) generateRandomPressKey() (Action, error) {
|
||||||
// Keep exploration gentle: only "back" for now. Home/menu would navigate
|
// Keep exploration gentle: only "back" for now. Home/menu would navigate
|
||||||
// away from the app under test.
|
// away from the app under test.
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
import assert from "node:assert/strict";
|
|
||||||
import { test } from "node:test";
|
|
||||||
|
|
||||||
import {
|
|
||||||
balanceMatchesAddedSum,
|
|
||||||
type LedgerRow,
|
|
||||||
} from "../../../examples/folio/sanderling/predicates.ts";
|
|
||||||
|
|
||||||
function row(key: string, signed: number): LedgerRow {
|
|
||||||
return { key, signed };
|
|
||||||
}
|
|
||||||
|
|
||||||
test("single new row whose signed amount matches the balance delta holds", () => {
|
|
||||||
const previous: LedgerRow[] = [row("a", 100)];
|
|
||||||
const current: LedgerRow[] = [row("a", 100), row("b", 500)];
|
|
||||||
assert.equal(balanceMatchesAddedSum(previous, current, 100, 600), true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("two new rows whose sum matches the balance delta holds", () => {
|
|
||||||
const previous: LedgerRow[] = [];
|
|
||||||
const current: LedgerRow[] = [row("a", 500), row("b", 300)];
|
|
||||||
assert.equal(balanceMatchesAddedSum(previous, current, 0, 800), true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("double-submit case: two identical rows with sum exceeding the delta violates", () => {
|
|
||||||
const previous: LedgerRow[] = [];
|
|
||||||
const current: LedgerRow[] = [row("a", 500), row("b", 500)];
|
|
||||||
assert.equal(balanceMatchesAddedSum(previous, current, 0, 500), false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("partial-credit case: two new rows whose sum falls short of the delta violates", () => {
|
|
||||||
const previous: LedgerRow[] = [];
|
|
||||||
const current: LedgerRow[] = [row("a", 200), row("b", 100)];
|
|
||||||
assert.equal(balanceMatchesAddedSum(previous, current, 0, 500), false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("no new rows holds vacuously regardless of balance delta", () => {
|
|
||||||
const previous: LedgerRow[] = [row("a", 100)];
|
|
||||||
const current: LedgerRow[] = [row("a", 100)];
|
|
||||||
assert.equal(balanceMatchesAddedSum(previous, current, 100, 100), true);
|
|
||||||
assert.equal(balanceMatchesAddedSum(previous, current, 100, 999), true);
|
|
||||||
});
|
|
||||||
Reference in new issue
Block a user