diff --git a/internal/runner/runner_test.go b/internal/runner/runner_test.go index e06a2c9..cf19dca 100644 --- a/internal/runner/runner_test.go +++ b/internal/runner/runner_test.go @@ -294,6 +294,47 @@ func TestApplyAction_V8InputTextAtOriginStillTaps(t *testing.T) { } } +func TestApplyAction_DoubleTapDispatchesTwoTapsAtCoordinates(t *testing.T) { + driverMock := mockdriver.New() + action := verifier.Action{Kind: verifier.ActionKindDoubleTap, X: 100, Y: 200} + + start := time.Now() + if err := applyAction(context.Background(), driverMock, action, nil); err != nil { + t.Fatalf("apply action: %v", err) + } + elapsed := time.Since(start) + if elapsed < 40*time.Millisecond { + t.Errorf("expected >= 40ms gap between taps, elapsed %v", elapsed) + } + taps := 0 + for _, a := range driverMock.Actions() { + if a.Kind == mockdriver.ActionTap && a.X == 100 && a.Y == 200 { + taps++ + } + } + if taps != 2 { + t.Errorf("expected 2 Tap calls at (100,200), got %d in %v", taps, driverMock.Actions()) + } +} + +func TestApplyAction_DoubleTapDispatchesTwoSelectorTaps(t *testing.T) { + driverMock := mockdriver.New() + action := verifier.Action{Kind: verifier.ActionKindDoubleTap, On: "id:save"} + + if err := applyAction(context.Background(), driverMock, action, nil); err != nil { + t.Fatalf("apply action: %v", err) + } + taps := 0 + for _, a := range driverMock.Actions() { + if a.Kind == mockdriver.ActionTapSelector && a.Selector == "id:save" { + taps++ + } + } + if taps != 2 { + t.Errorf("expected 2 TapSelector calls with id:save, got %d in %v", taps, driverMock.Actions()) + } +} + func TestRunner_ParallelFetchCallsAllDriverMethods(t *testing.T) { state := newHarness(t) state.mock.MetricsData = driver.Metrics{CPUPercent: 5.0, HeapBytes: 1024, TotalMemoryBytes: 4096} diff --git a/internal/verifier/verifier_test.go b/internal/verifier/verifier_test.go index 5226063..23e90fe 100644 --- a/internal/verifier/verifier_test.go +++ b/internal/verifier/verifier_test.go @@ -282,6 +282,27 @@ func TestNextAction_NoSetupRegistered(t *testing.T) { } } +func TestDoubleTap_RoundTrip(t *testing.T) { + verifier := newVerifier(t) + mustLoad(t, verifier, ` + globalThis.actions = __sanderling__.actions(() => [ + __sanderling__.doubleTap({ on: "id:save" }), + ]); + `) + _ = verifier.PushSnapshot(SnapshotInput{Snapshots: Snapshots{}}) + + action, err := verifier.NextAction() + if err != nil { + t.Fatal(err) + } + if action.Kind != ActionKindDoubleTap { + t.Errorf("kind: got %v, want DoubleTap", action.Kind) + } + if action.On != "id:save" { + t.Errorf("selector: got %q, want id:save", action.On) + } +} + func TestInputText_RoundTrip(t *testing.T) { verifier := newVerifier(t) mustLoad(t, verifier, ` diff --git a/pkg/spec/test/api.test.ts b/pkg/spec/test/api.test.ts index 5eb545c..d55ea2d 100644 --- a/pkg/spec/test/api.test.ts +++ b/pkg/spec/test/api.test.ts @@ -2,6 +2,7 @@ import assert from "node:assert/strict"; import { test } from "node:test"; import { + DoubleTap, InputText, PressKey, Swipe, @@ -138,6 +139,7 @@ function installFakeRuntime(): RecordedRuntime { return { generate: () => items[0] as T }; }, tap: ({ on }) => ({ kind: "Tap", on }), + doubleTap: ({ on }) => ({ kind: "DoubleTap", on }), inputText: ({ into, text }) => ({ kind: "InputText", into, text }), swipe: ({ from: fromPoint, to, durationMillis }) => ({ kind: "Swipe", @@ -218,6 +220,12 @@ test("Tap returns a TapAction with the supplied selector", () => { assert.deepEqual(action, { kind: "Tap", on: "id:login_continue" }); }); +test("DoubleTap returns a DoubleTapAction with the supplied selector", () => { + installFakeRuntime(); + const action = DoubleTap({ on: "id:save" }); + assert.deepEqual(action, { kind: "DoubleTap", on: "id:save" }); +}); + test("Tap accepts an AccessibilityElement", () => { installFakeRuntime(); const element: AccessibilityElement = { diff --git a/pkg/spec/test/defaults.test.ts b/pkg/spec/test/defaults.test.ts index 4829b46..eb31dd2 100644 --- a/pkg/spec/test/defaults.test.ts +++ b/pkg/spec/test/defaults.test.ts @@ -58,6 +58,7 @@ function installRuntime(initialState: State): RecordedRuntime { }), from: (_items: readonly T[]): Sampler => ({ generate: () => _items[0] as T }), tap: ({ on }) => ({ kind: "Tap", on }), + doubleTap: ({ on }) => ({ kind: "DoubleTap", on }), inputText: ({ into, text }) => ({ kind: "InputText", into, text }), swipe: (p) => ({ kind: "Swipe", from: p.from, to: p.to, durationMillis: p.durationMillis }), pressKey: ({ key }) => ({ kind: "PressKey", key }),