diff --git a/internal/verifier/bindings.go b/internal/verifier/bindings.go index 595f9fb..49cf8fd 100644 --- a/internal/verifier/bindings.go +++ b/internal/verifier/bindings.go @@ -62,11 +62,12 @@ const ( internalKindActions = "actions" internalKindWeighted = "weighted" - internalKindBuiltinTaps = "taps" - internalKindBuiltinTyping = "typing" - internalKindBuiltinSwipes = "swipes" - internalKindBuiltinWaitOnce = "waitOnce" - internalKindBuiltinPressKey = "pressKey" + internalKindBuiltinTaps = "taps" + internalKindBuiltinDoubleTaps = "doubleTaps" + internalKindBuiltinTyping = "typing" + internalKindBuiltinSwipes = "swipes" + internalKindBuiltinWaitOnce = "waitOnce" + internalKindBuiltinPressKey = "pressKey" ) // 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 { return err } + if err := sanderling.Set("doubleTaps", v.builtinGenerator(internalKindBuiltinDoubleTaps)); err != nil { + return err + } if err := sanderling.Set("typing", v.builtinGenerator(internalKindBuiltinTyping)); err != nil { return err } diff --git a/internal/verifier/verifier_test.go b/internal/verifier/verifier_test.go index 23e90fe..32f8ffa 100644 --- a/internal/verifier/verifier_test.go +++ b/internal/verifier/verifier_test.go @@ -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) { verifier := newVerifier(t) mustLoad(t, verifier, ` diff --git a/internal/verifier/worker.go b/internal/verifier/worker.go index 678a1ae..5616b72 100644 --- a/internal/verifier/worker.go +++ b/internal/verifier/worker.go @@ -452,6 +452,8 @@ func (v *Verifier) resolveGenerator(generator goja.Value) (Action, error) { return v.resolveGenerator(picked) case internalKindBuiltinTaps: return v.generateRandomTap() + case internalKindBuiltinDoubleTaps: + return v.generateRandomDoubleTap() case internalKindBuiltinTyping: return v.generateRandomInput() case internalKindBuiltinSwipes: @@ -468,6 +470,18 @@ func (v *Verifier) resolveGenerator(generator goja.Value) (Action, error) { // generateRandomTap picks a visible, tappable element from the last // hierarchy snapshot and returns a Tap action targeting its center. 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 { return Action{}, ErrNoAction } @@ -486,7 +500,7 @@ func (v *Verifier) generateRandomTap() (Action, error) { } picked := candidates[v.rng.IntN(len(candidates))] 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