Files
Jakub Piasecki 5082b63082 feat(flow): multi-tap via tap times and gesture-tap clickCount (#497)
Flows can now express double-taps declaratively:

```yaml
- tap: { on: Login, times: 2 }          # selector double-tap
- tap: { x: 0.5, y: 0.57, times: 2 }    # coordinate double-tap
```

The selector nests under `on` so option keys never share a map with
selector fields — zod would silently strip them; mixing errors with a
hint instead. `times: 1` normalizes to absent and the serializer emits
the minimal spelling, keeping parse/serialize exact inverses.

The taps dispatch as **one gesture** via `gesture-tap`'s new
`clickCount` (1–10): N Down/Up pairs ~100 ms apart on touch, escalating
CDP `clickCount` per click on Chromium (what makes `dblclick` actually
fire) — never N separate calls whose RPC gaps could miss the OS
double-tap window.

The recorder carries a recorded `clickCount` into `times`, so a recorded
double-tap can't silently replay as a single tap.
2026-07-15 07:55:31 +02:00

63 lines
2.1 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
// Capture the touch-event train the tool sends to the simulator-server — the
// multi-tap contract (N Down/Up pairs, one gesture) lives entirely in it.
interface TouchCmd {
cmd: string;
type: "Down" | "Move" | "Up";
x: number;
y: number;
}
const sent: TouchCmd[] = [];
vi.mock("../../src/utils/simulator-client", () => ({
sendCommand: (_api: unknown, cmd: TouchCmd) => {
sent.push(cmd);
},
}));
import { gestureTapTool } from "../../src/tools/gesture-tap";
const touchServices = { simulatorServer: {} } as never;
beforeEach(() => {
sent.length = 0;
});
describe("gesture-tap", () => {
it("dispatches a single Down/Up pair by default", async () => {
await gestureTapTool.execute(touchServices, { udid: "X", x: 0.5, y: 0.5 });
expect(sent.map((e) => e.type)).toEqual(["Down", "Up"]);
});
it("dispatches clickCount Down/Up pairs as ONE gesture on touch platforms", async () => {
await gestureTapTool.execute(touchServices, { udid: "X", x: 0.4, y: 0.6, clickCount: 3 });
expect(sent.map((e) => e.type)).toEqual(["Down", "Up", "Down", "Up", "Down", "Up"]);
// Every tap lands on the same point — a multi-tap, not a gesture path.
expect(sent.every((e) => e.x === 0.4 && e.y === 0.6)).toBe(true);
});
it("escalates the CDP clickCount per click on chromium so dblclick fires", async () => {
const mouse: Array<{ type: string; clickCount?: number }> = [];
const chromium = {
getViewport: () => ({ width: 1000, height: 800 }),
dispatchMouseEvent: vi.fn(async (e: { type: string; clickCount?: number }) => {
mouse.push(e);
}),
};
await gestureTapTool.execute({ chromium } as never, {
udid: "chromium-cdp-9222",
x: 0.5,
y: 0.5,
clickCount: 2,
});
// The browser's click counter drives dblclick: presses carry 1, then 2.
expect(mouse.map((e) => `${e.type}:${e.clickCount ?? ""}`)).toEqual([
"mouseMoved:",
"mousePressed:1",
"mouseReleased:1",
"mousePressed:2",
"mouseReleased:2",
]);
});
});