feat(verifier): add doubleTaps random-target generator

This commit is contained in:
pj committed 2026-05-30 16:00:09 +05:30
1 parent 177b571b0a
commit 801f09d455
3 files changed
+54 -6

No files matched your search

+9 -5
View File
@@ -62,11 +62,12 @@ const (
internalKindActions = "actions" internalKindActions = "actions"
internalKindWeighted = "weighted" internalKindWeighted = "weighted"
internalKindBuiltinTaps = "taps" internalKindBuiltinTaps = "taps"
internalKindBuiltinTyping = "typing" internalKindBuiltinDoubleTaps = "doubleTaps"
internalKindBuiltinSwipes = "swipes" internalKindBuiltinTyping = "typing"
internalKindBuiltinWaitOnce = "waitOnce" internalKindBuiltinSwipes = "swipes"
internalKindBuiltinPressKey = "pressKey" internalKindBuiltinWaitOnce = "waitOnce"
internalKindBuiltinPressKey = "pressKey"
) )
// installRuntimeBindings exposes globalThis.__sanderling__ to the loaded spec. // installRuntimeBindings exposes globalThis.__sanderling__ to the loaded spec.
@@ -118,6 +119,9 @@ func (v *Verifier) installRuntimeBindings() error {
if err := sanderling.Set("taps", v.builtinGenerator(internalKindBuiltinTaps)); err != nil { if err := sanderling.Set("taps", v.builtinGenerator(internalKindBuiltinTaps)); err != nil {
return err return err
} }
if err := sanderling.Set("doubleTaps", v.builtinGenerator(internalKindBuiltinDoubleTaps)); err != nil {
return err
}
if err := sanderling.Set("typing", v.builtinGenerator(internalKindBuiltinTyping)); err != nil { if err := sanderling.Set("typing", v.builtinGenerator(internalKindBuiltinTyping)); err != nil {
return err return err
} }
+30
View File
@@ -282,6 +282,36 @@ func TestNextAction_NoSetupRegistered(t *testing.T) {
} }
} }
// TestDoubleTapsBuiltin_TargetsClickable verifies the doubleTaps builtin emits
// a DoubleTap action targeting a clickable, enabled element's center.
func TestDoubleTapsBuiltin_TargetsClickable(t *testing.T) {
const treeJSON = `{
"attributes": {"resource-id": "root", "bounds": "[0,0,100,100]"},
"children": [
{"attributes": {"testTag": "SubmitButton", "bounds": "[0,40,100,80]"}, "clickable": true, "enabled": true, "children": []}
]
}`
verifier := newVerifier(t)
mustLoad(t, verifier, `globalThis.actions = __sanderling__.doubleTaps;`)
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)
}
action, err := verifier.NextAction()
if err != nil {
t.Fatal(err)
}
if action.Kind != ActionKindDoubleTap {
t.Fatalf("kind = %v, want DoubleTap", action.Kind)
}
if action.X != 50 || action.Y != 60 {
t.Errorf("coords = (%d,%d), want (50,60) at SubmitButton center", action.X, action.Y)
}
}
func TestDoubleTap_RoundTrip(t *testing.T) { func TestDoubleTap_RoundTrip(t *testing.T) {
verifier := newVerifier(t) verifier := newVerifier(t)
mustLoad(t, verifier, ` mustLoad(t, verifier, `
+15 -1
View File
@@ -452,6 +452,8 @@ func (v *Verifier) resolveGenerator(generator goja.Value) (Action, error) {
return v.resolveGenerator(picked) return v.resolveGenerator(picked)
case internalKindBuiltinTaps: case internalKindBuiltinTaps:
return v.generateRandomTap() return v.generateRandomTap()
case internalKindBuiltinDoubleTaps:
return v.generateRandomDoubleTap()
case internalKindBuiltinTyping: case internalKindBuiltinTyping:
return v.generateRandomInput() return v.generateRandomInput()
case internalKindBuiltinSwipes: case internalKindBuiltinSwipes:
@@ -468,6 +470,18 @@ func (v *Verifier) resolveGenerator(generator goja.Value) (Action, error) {
// generateRandomTap picks a visible, tappable element from the last // generateRandomTap picks a visible, tappable element from the last
// hierarchy snapshot and returns a Tap action targeting its center. // hierarchy snapshot and returns a Tap action targeting its center.
func (v *Verifier) generateRandomTap() (Action, error) { func (v *Verifier) generateRandomTap() (Action, error) {
return v.generateRandomTapKind(ActionKindTap)
}
// generateRandomDoubleTap is the DoubleTap counterpart of generateRandomTap.
// Real user gestures include double-tap (image zoom, like-to-favorite,
// play/pause); a fuzzer that never emits one cannot exercise either those
// features or the sub-100ms race windows that single-step Tap cadence misses.
func (v *Verifier) generateRandomDoubleTap() (Action, error) {
return v.generateRandomTapKind(ActionKindDoubleTap)
}
func (v *Verifier) generateRandomTapKind(kind ActionKind) (Action, error) {
if v.lastTree == nil { if v.lastTree == nil {
return Action{}, ErrNoAction return Action{}, ErrNoAction
} }
@@ -486,7 +500,7 @@ func (v *Verifier) generateRandomTap() (Action, error) {
} }
picked := candidates[v.rng.IntN(len(candidates))] picked := candidates[v.rng.IntN(len(candidates))]
x, y := picked.Bounds.Center() x, y := picked.Bounds.Center()
return Action{Kind: ActionKindTap, X: x, Y: y}, nil return Action{Kind: kind, X: x, Y: y}, nil
} }
// inputCorpus is the edge-case string pool the typing builtin draws from to // inputCorpus is the edge-case string pool the typing builtin draws from to