execbro 2.5.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +2 -0
  2. package/build/__tests__/unit/androidPinch.test.d.ts +2 -0
  3. package/build/__tests__/unit/androidPinch.test.d.ts.map +1 -0
  4. package/build/__tests__/unit/androidPinch.test.js +119 -0
  5. package/build/__tests__/unit/androidPinch.test.js.map +1 -0
  6. package/build/__tests__/unit/emulatorBridge.test.d.ts +2 -0
  7. package/build/__tests__/unit/emulatorBridge.test.d.ts.map +1 -0
  8. package/build/__tests__/unit/emulatorBridge.test.js +76 -0
  9. package/build/__tests__/unit/emulatorBridge.test.js.map +1 -0
  10. package/build/__tests__/unit/emulatorGrpc.test.d.ts +2 -0
  11. package/build/__tests__/unit/emulatorGrpc.test.d.ts.map +1 -0
  12. package/build/__tests__/unit/emulatorGrpc.test.js +33 -0
  13. package/build/__tests__/unit/emulatorGrpc.test.js.map +1 -0
  14. package/build/__tests__/unit/pinchGeometry.test.d.ts +2 -0
  15. package/build/__tests__/unit/pinchGeometry.test.d.ts.map +1 -0
  16. package/build/__tests__/unit/pinchGeometry.test.js +139 -0
  17. package/build/__tests__/unit/pinchGeometry.test.js.map +1 -0
  18. package/build/__tests__/unit/pinchTool.test.d.ts +2 -0
  19. package/build/__tests__/unit/pinchTool.test.d.ts.map +1 -0
  20. package/build/__tests__/unit/pinchTool.test.js +39 -0
  21. package/build/__tests__/unit/pinchTool.test.js.map +1 -0
  22. package/build/core/androidPinch.d.ts +21 -0
  23. package/build/core/androidPinch.d.ts.map +1 -0
  24. package/build/core/androidPinch.js +72 -0
  25. package/build/core/androidPinch.js.map +1 -0
  26. package/build/core/androidSystemBars.d.ts +7 -0
  27. package/build/core/androidSystemBars.d.ts.map +1 -0
  28. package/build/core/androidSystemBars.js +21 -0
  29. package/build/core/androidSystemBars.js.map +1 -0
  30. package/build/core/emulatorBridge.d.ts +17 -0
  31. package/build/core/emulatorBridge.d.ts.map +1 -0
  32. package/build/core/emulatorBridge.js +78 -0
  33. package/build/core/emulatorBridge.js.map +1 -0
  34. package/build/core/emulatorGrpc.d.ts +14 -0
  35. package/build/core/emulatorGrpc.d.ts.map +1 -0
  36. package/build/core/emulatorGrpc.js +93 -0
  37. package/build/core/emulatorGrpc.js.map +1 -0
  38. package/build/core/guides.d.ts.map +1 -1
  39. package/build/core/guides.js +18 -2
  40. package/build/core/guides.js.map +1 -1
  41. package/build/core/index.d.ts +2 -0
  42. package/build/core/index.d.ts.map +1 -1
  43. package/build/core/index.js +1 -0
  44. package/build/core/index.js.map +1 -1
  45. package/build/core/pinchGeometry.d.ts +28 -0
  46. package/build/core/pinchGeometry.d.ts.map +1 -0
  47. package/build/core/pinchGeometry.js +97 -0
  48. package/build/core/pinchGeometry.js.map +1 -0
  49. package/build/core/pinchThresholds.d.ts +14 -0
  50. package/build/core/pinchThresholds.d.ts.map +1 -0
  51. package/build/core/pinchThresholds.js +9 -0
  52. package/build/core/pinchThresholds.js.map +1 -0
  53. package/build/tools/interactionTools.d.ts.map +1 -1
  54. package/build/tools/interactionTools.js +190 -2
  55. package/build/tools/interactionTools.js.map +1 -1
  56. package/package.json +1 -1
  57. package/tools.json +15 -1
package/README.md CHANGED
@@ -44,6 +44,7 @@ Linking your installation here is also **required to unlock [ExecBro Pro](#prici
44
44
  - **Android Devices** - Screenshots, app install/launch, package management (via ADB)
45
45
  - **Unified Tap** - Single `tap` tool with automatic fallback chain: fiber tree → accessibility → OCR → coordinates. Auto-detects platform, accepts coordinates from screenshots and layout tools unchanged. Returns post-tap screenshot and verifies visual change by default
46
46
  - **Unified Swipe** - Single `swipe` tool that auto-routes to iOS or Android based on the connected device. Takes coordinates in the same screen space as the layout tools and screenshots — no conversion — and returns a `verification.meaningful` signal so agents detect end-of-list, non-scrollable surfaces, and missed coordinates. Essential for scrolling virtualized lists (FlatList/SectionList) where off-screen items aren't in the fiber tree
47
+ - **Real Multi-Touch Pinch** *(Android emulator only — iOS in progress)* - A `pinch` tool that sends two genuine kernel touch contacts through the Android emulator's multi-touch bridge, so it zooms maps, galleries, WebViews, and native views alike — it works below the app, not through React Native. Returns the same `verification.meaningful` signal as `swipe`, and refuses on unsupported targets instead of faking a result
47
48
  - **UI Automation** - Swipe, long press, key events, and text input on both platforms. On Bridgeless/Fabric apps, `dismiss_keyboard` operates on whatever has focus, and `ios_input_text` / `android_input_text` accept `replace:true` to overwrite pre-filled values — updating React state through `onChangeText` so controlled components (Formik, react-hook-form, useState) stay consistent
48
49
  - **Accessibility Inspection** - Query UI hierarchy to find elements by text, label, or resource ID
49
50
  - **OCR Text Extraction** - Extract visible text with tap-ready coordinates via Google Cloud Vision (works on any screen content)
@@ -113,6 +114,7 @@ See the [full tool reference](docs/tools.md) for all tools with descriptions. Ke
113
114
  | `get_screen_state` | **Orientation snapshot** — active route, overlays, and every element with a tap-ready `(x, y)` |
114
115
  | `get_screen_layout` | Screen map of visible components with positions, sizes, and text content |
115
116
  | `tap` | **Unified tap** — auto-detects platform, tries fiber → accessibility → OCR → coordinates |
117
+ | `pinch` | **Real two-finger pinch-to-zoom** — Android emulator only (iOS in progress) |
116
118
  | `ios_input_text` / `android_input_text` | Type text into the focused field. `replace:true` clears pre-filled values first (Fabric) |
117
119
  | `dismiss_keyboard` | Blur the focused input and close the on-screen keyboard |
118
120
  | `execute_in_app` | Run JS expressions in the app runtime (REPL-style) |
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=androidPinch.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"androidPinch.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/unit/androidPinch.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,119 @@
1
+ import { describe, it, expect, jest, beforeEach } from "@jest/globals";
2
+ const resolveEmulatorBridgeMock = jest.fn();
3
+ const sendTouchFramesMock = jest.fn();
4
+ const androidGetScreenSizeMock = jest.fn();
5
+ const getDefaultAndroidDeviceMock = jest.fn();
6
+ const androidSystemBarInsetsMock = jest.fn();
7
+ jest.unstable_mockModule("../../core/emulatorBridge.js", () => ({
8
+ resolveEmulatorBridge: resolveEmulatorBridgeMock,
9
+ }));
10
+ jest.unstable_mockModule("../../core/emulatorGrpc.js", () => ({
11
+ sendTouchFrames: sendTouchFramesMock,
12
+ }));
13
+ jest.unstable_mockModule("../../core/android.js", () => ({
14
+ androidGetScreenSize: androidGetScreenSizeMock,
15
+ getDefaultAndroidDevice: getDefaultAndroidDeviceMock,
16
+ }));
17
+ jest.unstable_mockModule("../../core/androidSystemBars.js", () => ({
18
+ androidSystemBarInsets: androidSystemBarInsetsMock,
19
+ }));
20
+ const { androidPinch } = await import("../../core/androidPinch.js");
21
+ const OPTS = {
22
+ focalX: 540,
23
+ focalY: 1200,
24
+ direction: "out",
25
+ scale: 3,
26
+ angleDeg: 0,
27
+ durationMs: 300,
28
+ serial: "emulator-5554",
29
+ };
30
+ describe("androidPinch", () => {
31
+ beforeEach(() => {
32
+ resolveEmulatorBridgeMock.mockReset();
33
+ sendTouchFramesMock.mockReset();
34
+ androidGetScreenSizeMock.mockReset();
35
+ getDefaultAndroidDeviceMock.mockReset();
36
+ androidSystemBarInsetsMock.mockReset();
37
+ androidSystemBarInsetsMock.mockResolvedValue({ top: 142, bottom: 126 });
38
+ resolveEmulatorBridgeMock.mockResolvedValue({
39
+ ok: true,
40
+ bridge: { port: 8554, token: "t" },
41
+ });
42
+ androidGetScreenSizeMock.mockResolvedValue({ success: true, width: 1080, height: 2424 });
43
+ sendTouchFramesMock.mockResolvedValue({ success: true });
44
+ });
45
+ it("sends frames and reports the counts", async () => {
46
+ const res = await androidPinch(OPTS);
47
+ expect(res.success).toBe(true);
48
+ expect(sendTouchFramesMock).toHaveBeenCalled();
49
+ expect(res.frameCount).toBeGreaterThan(2);
50
+ expect(res.gestureCount).toBe(1);
51
+ });
52
+ it("surfaces the bridge failure message unchanged", async () => {
53
+ resolveEmulatorBridgeMock.mockResolvedValue({
54
+ ok: false,
55
+ reason: "not-emulator",
56
+ message: "physical devices do not expose the bridge",
57
+ });
58
+ const res = await androidPinch(OPTS);
59
+ expect(res.success).toBe(false);
60
+ expect(res.error).toContain("physical devices do not expose the bridge");
61
+ expect(sendTouchFramesMock).not.toHaveBeenCalled();
62
+ });
63
+ it("refuses a gesture the geometry marks unviable", async () => {
64
+ const res = await androidPinch({ ...OPTS, scale: 1 });
65
+ expect(res.success).toBe(false);
66
+ expect(res.error).toContain("scale");
67
+ expect(sendTouchFramesMock).not.toHaveBeenCalled();
68
+ });
69
+ it("releases both pointers when a frame send fails midway", async () => {
70
+ sendTouchFramesMock.mockResolvedValueOnce({ success: false, error: "connection reset" });
71
+ const res = await androidPinch(OPTS);
72
+ expect(res.success).toBe(false);
73
+ const last = sendTouchFramesMock.mock.calls[sendTouchFramesMock.mock.calls.length - 1];
74
+ const releaseFrames = last[1];
75
+ expect(releaseFrames.flat().every((p) => p.pressure === 0)).toBe(true);
76
+ });
77
+ it("falls back to the default device when no serial is given", async () => {
78
+ getDefaultAndroidDeviceMock.mockResolvedValue("emulator-5556");
79
+ await androidPinch({ ...OPTS, serial: undefined });
80
+ expect(resolveEmulatorBridgeMock).toHaveBeenCalledWith("emulator-5556");
81
+ });
82
+ it("errors when no device is connected at all", async () => {
83
+ getDefaultAndroidDeviceMock.mockResolvedValue(null);
84
+ const res = await androidPinch({ ...OPTS, serial: undefined });
85
+ expect(res.success).toBe(false);
86
+ expect(res.error).toContain("No Android device");
87
+ });
88
+ it("keeps contacts clear of the queried system bars", async () => {
89
+ await androidPinch({ ...OPTS, angleDeg: 90, direction: "in", scale: 6 });
90
+ const frames = sendTouchFramesMock.mock.calls.flatMap((c) => c[1]);
91
+ for (const frame of frames) {
92
+ for (const p of frame) {
93
+ expect(p.y).toBeGreaterThanOrEqual(142);
94
+ expect(p.y).toBeLessThanOrEqual(2424 - 126);
95
+ }
96
+ }
97
+ });
98
+ it("falls back to default guards when the system bars cannot be read", async () => {
99
+ androidSystemBarInsetsMock.mockResolvedValue(null);
100
+ const res = await androidPinch({ ...OPTS, angleDeg: 90 });
101
+ expect(res.success).toBe(true);
102
+ });
103
+ it("forwards span to the geometry", async () => {
104
+ await androidPinch({ ...OPTS, span: 0.5 });
105
+ const wideFrames = sendTouchFramesMock.mock.calls.flatMap((c) => c[1]);
106
+ const wideSpread = Math.max(...wideFrames.map((f) => Math.abs(f[1].x - f[0].x)));
107
+ sendTouchFramesMock.mockClear();
108
+ await androidPinch({ ...OPTS, span: 1 });
109
+ const fullFrames = sendTouchFramesMock.mock.calls.flatMap((c) => c[1]);
110
+ const fullSpread = Math.max(...fullFrames.map((f) => Math.abs(f[1].x - f[0].x)));
111
+ expect(wideSpread).toBeLessThan(fullSpread);
112
+ });
113
+ it("pauses between chained sub-gestures", async () => {
114
+ const res = await androidPinch({ ...OPTS, scale: 40 });
115
+ expect(res.gestureCount).toBeGreaterThan(1);
116
+ expect(sendTouchFramesMock.mock.calls.length).toBe(res.gestureCount);
117
+ });
118
+ });
119
+ //# sourceMappingURL=androidPinch.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"androidPinch.test.js","sourceRoot":"","sources":["../../../src/__tests__/unit/androidPinch.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=emulatorBridge.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emulatorBridge.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/unit/emulatorBridge.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,76 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "@jest/globals";
2
+ import { mkdtemp, writeFile, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { emulatorRunningDir, resolveEmulatorBridge } from "../../core/emulatorBridge.js";
6
+ const INI = [
7
+ "emulator.build=15917651",
8
+ "avd.id=Pixel_9",
9
+ "port.serial=5554",
10
+ "port.adb=5555",
11
+ "avd.name=Pixel 9",
12
+ "emulator.version=37.1.11.0",
13
+ "grpc.token=dG9rZW4rd2l0aC9iYXNlNjQ9",
14
+ "grpc.port=8554",
15
+ ].join("\n");
16
+ describe("emulatorRunningDir", () => {
17
+ it("returns the macOS discovery directory", () => {
18
+ expect(emulatorRunningDir("darwin", "/Users/x")).toBe("/Users/x/Library/Caches/TemporaryItems/avd/running");
19
+ });
20
+ it("returns null on hosts where the path is unverified", () => {
21
+ expect(emulatorRunningDir("linux", "/home/x")).toBeNull();
22
+ expect(emulatorRunningDir("win32", "C:\\Users\\x")).toBeNull();
23
+ });
24
+ });
25
+ describe("resolveEmulatorBridge", () => {
26
+ let dir;
27
+ beforeEach(async () => {
28
+ dir = await mkdtemp(join(tmpdir(), "execbro-avd-"));
29
+ });
30
+ afterEach(async () => {
31
+ await rm(dir, { recursive: true, force: true });
32
+ });
33
+ it("resolves port and token from the ini whose port.serial matches", async () => {
34
+ await writeFile(join(dir, "pid_26727.ini"), INI);
35
+ const res = await resolveEmulatorBridge("emulator-5554", dir);
36
+ expect(res).toEqual({
37
+ ok: true,
38
+ bridge: { port: 8554, token: "dG9rZW4rd2l0aC9iYXNlNjQ9", avdName: "Pixel 9" },
39
+ });
40
+ });
41
+ it("picks the right emulator when several are running", async () => {
42
+ await writeFile(join(dir, "pid_1.ini"), INI);
43
+ await writeFile(join(dir, "pid_2.ini"), INI.replace("port.serial=5554", "port.serial=5556").replace("grpc.port=8554", "grpc.port=8556"));
44
+ const res = await resolveEmulatorBridge("emulator-5556", dir);
45
+ expect(res.ok).toBe(true);
46
+ if (res.ok)
47
+ expect(res.bridge.port).toBe(8556);
48
+ });
49
+ it("rejects a physical device serial without touching the filesystem", async () => {
50
+ const res = await resolveEmulatorBridge("R5CT30XXXXX", dir);
51
+ expect(res.ok).toBe(false);
52
+ if (!res.ok)
53
+ expect(res.reason).toBe("not-emulator");
54
+ });
55
+ it("reports no-discovery-file when nothing matches the serial", async () => {
56
+ await writeFile(join(dir, "pid_1.ini"), INI);
57
+ const res = await resolveEmulatorBridge("emulator-5560", dir);
58
+ expect(res.ok).toBe(false);
59
+ if (!res.ok)
60
+ expect(res.reason).toBe("no-discovery-file");
61
+ });
62
+ it("reports no-grpc-keys when the ini lacks the gRPC entries", async () => {
63
+ await writeFile(join(dir, "pid_1.ini"), "port.serial=5554\navd.name=Old AVD\n");
64
+ const res = await resolveEmulatorBridge("emulator-5554", dir);
65
+ expect(res.ok).toBe(false);
66
+ if (!res.ok)
67
+ expect(res.reason).toBe("no-grpc-keys");
68
+ });
69
+ it("keeps base64 tokens intact when they contain '='", async () => {
70
+ await writeFile(join(dir, "pid_1.ini"), INI);
71
+ const res = await resolveEmulatorBridge("emulator-5554", dir);
72
+ if (res.ok)
73
+ expect(res.bridge.token).toBe("dG9rZW4rd2l0aC9iYXNlNjQ9");
74
+ });
75
+ });
76
+ //# sourceMappingURL=emulatorBridge.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emulatorBridge.test.js","sourceRoot":"","sources":["../../../src/__tests__/unit/emulatorBridge.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=emulatorGrpc.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emulatorGrpc.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/unit/emulatorGrpc.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,33 @@
1
+ import { describe, it, expect } from "@jest/globals";
2
+ import { encodeTouchEvent, frameGrpcMessage } from "../../core/emulatorGrpc.js";
3
+ describe("encodeTouchEvent", () => {
4
+ it("encodes two contacts byte-identically to the official serializer", () => {
5
+ const bytes = encodeTouchEvent([
6
+ { x: 100, y: 200, identifier: 0, pressure: 1024 },
7
+ { x: 900, y: 200, identifier: 1, pressure: 1024 },
8
+ ]);
9
+ expect(bytes.toString("hex")).toBe("0a0a086410c80118002080080a0b08840710c8011801208008");
10
+ });
11
+ it("encodes a zero-pressure release explicitly", () => {
12
+ const bytes = encodeTouchEvent([{ x: 5, y: 0, identifier: 1, pressure: 0 }]);
13
+ expect(bytes.toString("hex")).toBe("0a080805100018012000");
14
+ });
15
+ it("encodes multi-byte varints for coordinates above 127", () => {
16
+ const bytes = encodeTouchEvent([{ x: 900, y: 0, identifier: 0, pressure: 0 }]);
17
+ expect(bytes.toString("hex")).toContain("088407");
18
+ });
19
+ });
20
+ describe("frameGrpcMessage", () => {
21
+ it("prefixes an uncompressed flag and a big-endian length", () => {
22
+ const framed = frameGrpcMessage(Buffer.from([0xaa, 0xbb]));
23
+ expect(framed.toString("hex")).toBe("00000000 02 aabb".replace(/ /g, ""));
24
+ });
25
+ it("keeps the payload intact after the 5-byte header", () => {
26
+ const payload = encodeTouchEvent([{ x: 1, y: 2, identifier: 0, pressure: 1 }]);
27
+ const framed = frameGrpcMessage(payload);
28
+ expect(framed.length).toBe(payload.length + 5);
29
+ expect(framed.subarray(5).equals(payload)).toBe(true);
30
+ expect(framed.readUInt32BE(1)).toBe(payload.length);
31
+ });
32
+ });
33
+ //# sourceMappingURL=emulatorGrpc.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emulatorGrpc.test.js","sourceRoot":"","sources":["../../../src/__tests__/unit/emulatorGrpc.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAEhF,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;IAC9B,EAAE,CAAC,kEAAkE,EAAE,GAAG,EAAE;QACxE,MAAM,KAAK,GAAG,gBAAgB,CAAC;YAC3B,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,UAAU,EAAE,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE;YACjD,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,UAAU,EAAE,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE;SACpD,CAAC,CAAC;QACH,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAC9B,oDAAoD,CACvD,CAAC;IACN,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4CAA4C,EAAE,GAAG,EAAE;QAElD,MAAM,KAAK,GAAG,gBAAgB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;QAC7E,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,sBAAsB,CAAC,CAAC;IAC/D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sDAAsD,EAAE,GAAG,EAAE;QAE5D,MAAM,KAAK,GAAG,gBAAgB,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;QAC/E,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;IACtD,CAAC,CAAC,CAAC;AACP,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;IAC9B,EAAE,CAAC,uDAAuD,EAAE,GAAG,EAAE;QAC7D,MAAM,MAAM,GAAG,gBAAgB,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;QAC3D,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,kBAAkB,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC;IAC9E,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kDAAkD,EAAE,GAAG,EAAE;QACxD,MAAM,OAAO,GAAG,gBAAgB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;QAC/E,MAAM,MAAM,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;QACzC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QAC/C,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACtD,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;AACP,CAAC,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=pinchGeometry.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pinchGeometry.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/unit/pinchGeometry.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,139 @@
1
+ import { describe, it, expect } from "@jest/globals";
2
+ import { planPinch } from "../../core/pinchGeometry.js";
3
+ import { MIN_HALF_SEPARATION_PX, EDGE_GUARD_PX, PRESSURE_DOWN, } from "../../core/pinchThresholds.js";
4
+ const base = {
5
+ focalX: 540,
6
+ focalY: 1200,
7
+ direction: "out",
8
+ scale: 3,
9
+ angleDeg: 0,
10
+ durationMs: 300,
11
+ screenWidth: 1080,
12
+ screenHeight: 2424,
13
+ };
14
+ const allFrames = (plan) => plan.gestures.flat();
15
+ describe("planPinch", () => {
16
+ it("produces two pointers in every frame", () => {
17
+ for (const frame of allFrames(planPinch(base))) {
18
+ expect(frame).toHaveLength(2);
19
+ }
20
+ });
21
+ it("gives the two pointers distinct identifiers", () => {
22
+ const [a, b] = allFrames(planPinch(base))[0];
23
+ expect(a.identifier).toBe(0);
24
+ expect(b.identifier).toBe(1);
25
+ });
26
+ it("releases every sub-gesture with zero pressure", () => {
27
+ for (const gesture of planPinch({ ...base, scale: 20 }).gestures) {
28
+ const last = gesture[gesture.length - 1];
29
+ expect(last.every((p) => p.pressure === 0)).toBe(true);
30
+ }
31
+ });
32
+ it("holds non-zero pressure for every frame before the release", () => {
33
+ const gesture = planPinch(base).gestures[0];
34
+ for (const frame of gesture.slice(0, -1)) {
35
+ expect(frame.every((p) => p.pressure === PRESSURE_DOWN)).toBe(true);
36
+ }
37
+ });
38
+ it("separates the pointers over time when pinching out", () => {
39
+ const frames = planPinch(base).gestures[0];
40
+ const spread = (f) => Math.abs(f[1].x - f[0].x);
41
+ expect(spread(frames[frames.length - 1])).toBeGreaterThan(spread(frames[0]));
42
+ });
43
+ it("converges the pointers when pinching in", () => {
44
+ const frames = planPinch({ ...base, direction: "in" }).gestures[0];
45
+ const spread = (f) => Math.abs(f[1].x - f[0].x);
46
+ expect(spread(frames[frames.length - 1])).toBeLessThan(spread(frames[0]));
47
+ });
48
+ it("keeps the pointers symmetric about the focal point", () => {
49
+ for (const frame of allFrames(planPinch(base))) {
50
+ expect((frame[0].x + frame[1].x) / 2).toBeCloseTo(base.focalX, 0);
51
+ expect((frame[0].y + frame[1].y) / 2).toBeCloseTo(base.focalY, 0);
52
+ }
53
+ });
54
+ it("places pointers on the vertical axis at 90 degrees", () => {
55
+ const frame = planPinch({ ...base, angleDeg: 90 }).gestures[0][0];
56
+ expect(frame[0].x).toBeCloseTo(frame[1].x, 0);
57
+ expect(frame[0].y).not.toBeCloseTo(frame[1].y, 0);
58
+ });
59
+ it("never starts a contact inside an edge guard", () => {
60
+ const plan = planPinch({ ...base, focalX: 200, scale: 8 });
61
+ expect(plan.gestures.length).toBeGreaterThan(0);
62
+ for (const frame of allFrames(plan)) {
63
+ for (const p of frame) {
64
+ expect(p.x).toBeGreaterThanOrEqual(EDGE_GUARD_PX.left);
65
+ expect(p.x).toBeLessThanOrEqual(base.screenWidth - EDGE_GUARD_PX.right);
66
+ expect(p.y).toBeGreaterThanOrEqual(EDGE_GUARD_PX.top);
67
+ expect(p.y).toBeLessThanOrEqual(base.screenHeight - EDGE_GUARD_PX.bottom);
68
+ }
69
+ }
70
+ });
71
+ it("keeps contacts far enough apart to read as two fingers", () => {
72
+ const plan = planPinch({ ...base, scale: 50 });
73
+ for (const frame of allFrames(plan)) {
74
+ const gap = Math.hypot(frame[1].x - frame[0].x, frame[1].y - frame[0].y);
75
+ expect(gap).toBeGreaterThanOrEqual(MIN_HALF_SEPARATION_PX * 2 - 1);
76
+ }
77
+ });
78
+ it("splits a large scale into chained sub-gestures", () => {
79
+ expect(planPinch({ ...base, scale: 3 }).gestures.length).toBe(1);
80
+ expect(planPinch({ ...base, scale: 40 }).gestures.length).toBeGreaterThan(1);
81
+ });
82
+ it("scales frame count with duration", () => {
83
+ const short = planPinch({ ...base, durationMs: 100 }).gestures[0].length;
84
+ const long = planPinch({ ...base, durationMs: 600 }).gestures[0].length;
85
+ expect(long).toBeGreaterThan(short);
86
+ expect(short).toBeGreaterThanOrEqual(3);
87
+ });
88
+ it("shrinks the gesture footprint when span is reduced", () => {
89
+ const spread = (plan) => {
90
+ const frames = plan.gestures[0];
91
+ return Math.max(...frames.map((f) => Math.abs(f[1].x - f[0].x)));
92
+ };
93
+ const full = spread(planPinch({ ...base, span: 1 }));
94
+ const half = spread(planPinch({ ...base, span: 0.5 }));
95
+ expect(half).toBeLessThan(full);
96
+ expect(half).toBeCloseTo(full / 2, -1);
97
+ });
98
+ it("keeps a reduced-span pinch-in away from the screen extremes", () => {
99
+ const guards = { left: 4, right: 4, top: 200, bottom: 200 };
100
+ const wide = planPinch({ ...base, direction: "in", angleDeg: 90, span: 1, guards });
101
+ const narrow = planPinch({ ...base, direction: "in", angleDeg: 90, span: 0.4, guards });
102
+ const topContact = (plan) => plan.gestures[0][0][0].y;
103
+ expect(topContact(narrow)).toBeGreaterThan(topContact(wide));
104
+ });
105
+ it("keeps the zoom ratio unchanged when span shrinks", () => {
106
+ const ratio = (plan) => plan.endHalf / plan.startHalf;
107
+ expect(ratio(planPinch({ ...base, span: 0.4 }))).toBeCloseTo(ratio(planPinch({ ...base, span: 1 })), 5);
108
+ });
109
+ it("refuses a span too small to hold two distinct contacts", () => {
110
+ const plan = planPinch({ ...base, span: 0.001 });
111
+ expect(plan.viable).toBe(false);
112
+ expect(plan.note).toMatch(/span/);
113
+ });
114
+ it("defaults to the full span when span is omitted", () => {
115
+ const omitted = planPinch(base);
116
+ const explicit = planPinch({ ...base, span: 1 });
117
+ expect(omitted.endHalf).toBeCloseTo(explicit.endHalf, 5);
118
+ });
119
+ it("marks a scale of 1 as not viable", () => {
120
+ const plan = planPinch({ ...base, scale: 1 });
121
+ expect(plan.viable).toBe(false);
122
+ expect(plan.note).toBeTruthy();
123
+ });
124
+ it("marks a viable ordinary pinch as viable", () => {
125
+ expect(planPinch(base).viable).toBe(true);
126
+ });
127
+ it("clamps a focal point outside the screen back inside the guards", () => {
128
+ const plan = planPinch({ ...base, focalX: -200, angleDeg: 90 });
129
+ expect(plan.viable).toBe(true);
130
+ for (const frame of allFrames(plan)) {
131
+ for (const p of frame) {
132
+ expect(p.x).toBe(EDGE_GUARD_PX.left);
133
+ expect(p.y).toBeGreaterThanOrEqual(EDGE_GUARD_PX.top);
134
+ expect(p.y).toBeLessThanOrEqual(base.screenHeight - EDGE_GUARD_PX.bottom);
135
+ }
136
+ }
137
+ });
138
+ });
139
+ //# sourceMappingURL=pinchGeometry.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pinchGeometry.test.js","sourceRoot":"","sources":["../../../src/__tests__/unit/pinchGeometry.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=pinchTool.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pinchTool.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/unit/pinchTool.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,39 @@
1
+ import { describe, it, expect, jest, beforeEach } from "@jest/globals";
2
+ const registered = [];
3
+ jest.unstable_mockModule("../../core/register.js", () => ({
4
+ registerToolWithTelemetry: (_server, name, config) => {
5
+ registered.push({ name, config });
6
+ },
7
+ }));
8
+ const { registerInteractionTools } = await import("../../tools/interactionTools.js");
9
+ const stubServer = {
10
+ registerTool: (name, config) => {
11
+ registered.push({ name, config });
12
+ },
13
+ };
14
+ describe("pinch tool registration", () => {
15
+ beforeEach(() => {
16
+ registered.length = 0;
17
+ registerInteractionTools(stubServer);
18
+ });
19
+ it("registers a pinch tool", () => {
20
+ expect(registered.map((t) => t.name)).toContain("pinch");
21
+ });
22
+ it("exposes the documented parameters", () => {
23
+ const pinch = registered.find((t) => t.name === "pinch");
24
+ const keys = Object.keys(pinch.config.inputSchema);
25
+ for (const k of ["direction", "scale", "x", "y", "angle", "span", "durationMs", "device", "verify", "screenshot", "burst"]) {
26
+ expect(keys).toContain(k);
27
+ }
28
+ });
29
+ it("states that it sends real two-finger events and is emulator-only", () => {
30
+ const desc = registered.find((t) => t.name === "pinch").config.description;
31
+ expect(desc).toMatch(/two-finger|multi-touch/i);
32
+ expect(desc).toMatch(/emulator/i);
33
+ });
34
+ it("documents the screenshot-pixel coordinate space", () => {
35
+ const schema = registered.find((t) => t.name === "pinch").config.inputSchema;
36
+ expect(JSON.stringify(schema)).toMatch(/screenshot pixel/i);
37
+ });
38
+ });
39
+ //# sourceMappingURL=pinchTool.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pinchTool.test.js","sourceRoot":"","sources":["../../../src/__tests__/unit/pinchTool.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAGvE,MAAM,UAAU,GAAc,EAAE,CAAC;AAEjC,IAAI,CAAC,mBAAmB,CAAC,wBAAwB,EAAE,GAAG,EAAE,CAAC,CAAC;IACtD,yBAAyB,EAAE,CACvB,OAAgB,EAChB,IAAY,EACZ,MAAyB,EAC3B,EAAE;QACA,UAAU,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;IACtC,CAAC;CACJ,CAAC,CAAC,CAAC;AAEJ,MAAM,EAAE,wBAAwB,EAAE,GAAG,MAAM,MAAM,CAAC,iCAAiC,CAAC,CAAC;AAIrF,MAAM,UAAU,GAAG;IACf,YAAY,EAAE,CAAC,IAAY,EAAE,MAAyB,EAAE,EAAE;QACtD,UAAU,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;IACtC,CAAC;CACJ,CAAC;AAEF,QAAQ,CAAC,yBAAyB,EAAE,GAAG,EAAE;IACrC,UAAU,CAAC,GAAG,EAAE;QACZ,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC;QACtB,wBAAwB,CAAC,UAAmB,CAAC,CAAC;IAClD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wBAAwB,EAAE,GAAG,EAAE;QAC9B,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;IAC7D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mCAAmC,EAAE,GAAG,EAAE;QACzC,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,OAAO,CAAC,CAAC;QACzD,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,KAAM,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;QACpD,KAAK,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,QAAQ,EAAE,QAAQ,EAAE,YAAY,EAAE,OAAO,CAAC,EAAE,CAAC;YACzH,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;QAC9B,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kEAAkE,EAAE,GAAG,EAAE;QACxE,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,OAAO,CAAE,CAAC,MAAM,CAAC,WAAW,CAAC;QAC5E,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,yBAAyB,CAAC,CAAC;QAChD,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;IACtC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iDAAiD,EAAE,GAAG,EAAE;QACvD,MAAM,MAAM,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,OAAO,CAAE,CAAC,MAAM,CAAC,WAGjE,CAAC;QACF,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,mBAAmB,CAAC,CAAC;IAChE,CAAC,CAAC,CAAC;AACP,CAAC,CAAC,CAAC"}
@@ -0,0 +1,21 @@
1
+ export interface AndroidPinchOptions {
2
+ focalX: number;
3
+ focalY: number;
4
+ direction: "in" | "out";
5
+ scale: number;
6
+ angleDeg: number;
7
+ durationMs: number;
8
+ span?: number;
9
+ serial?: string;
10
+ }
11
+ export interface AndroidPinchResult {
12
+ success: boolean;
13
+ result?: string;
14
+ error?: string;
15
+ frameCount?: number;
16
+ gestureCount?: number;
17
+ startHalf?: number;
18
+ endHalf?: number;
19
+ }
20
+ export declare function androidPinch(options: AndroidPinchOptions): Promise<AndroidPinchResult>;
21
+ //# sourceMappingURL=androidPinch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"androidPinch.d.ts","sourceRoot":"","sources":["../../src/core/androidPinch.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,mBAAmB;IAEhC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,IAAI,GAAG,KAAK,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IAEnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,kBAAkB;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CACpB;AAuBD,wBAAsB,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAwE5F"}
@@ -0,0 +1,72 @@
1
+ import { resolveEmulatorBridge } from "./emulatorBridge.js";
2
+ import { sendTouchFrames } from "./emulatorGrpc.js";
3
+ import { planPinch } from "./pinchGeometry.js";
4
+ import { EDGE_GUARD_PX, FRAME_INTERVAL_MS, SETTLE_MS } from "./pinchThresholds.js";
5
+ import { androidGetScreenSize, getDefaultAndroidDevice } from "./android.js";
6
+ import { androidSystemBarInsets } from "./androidSystemBars.js";
7
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
8
+ async function releasePointers(bridge, lastFrame) {
9
+ if (!lastFrame)
10
+ return;
11
+ await sendTouchFrames(bridge, [lastFrame.map((p) => ({ ...p, pressure: 0 }))], 0);
12
+ }
13
+ export async function androidPinch(options) {
14
+ const serial = options.serial ?? (await getDefaultAndroidDevice());
15
+ if (!serial) {
16
+ return { success: false, error: "No Android device connected. Run list_devices to see what is available." };
17
+ }
18
+ const resolution = await resolveEmulatorBridge(serial);
19
+ if (!resolution.ok) {
20
+ return { success: false, error: resolution.message };
21
+ }
22
+ const bridge = resolution.bridge;
23
+ const size = await androidGetScreenSize(serial);
24
+ if (!size.success || !size.width || !size.height) {
25
+ return {
26
+ success: false,
27
+ error: `Could not read the screen size for ${serial}: ${size.error ?? "no dimensions returned"}`,
28
+ };
29
+ }
30
+ const bars = await androidSystemBarInsets(serial);
31
+ const guards = bars
32
+ ? { left: EDGE_GUARD_PX.left, right: EDGE_GUARD_PX.right, top: bars.top, bottom: bars.bottom }
33
+ : EDGE_GUARD_PX;
34
+ const plan = planPinch({
35
+ focalX: options.focalX,
36
+ focalY: options.focalY,
37
+ direction: options.direction,
38
+ scale: options.scale,
39
+ angleDeg: options.angleDeg,
40
+ durationMs: options.durationMs,
41
+ screenWidth: size.width,
42
+ screenHeight: size.height,
43
+ guards,
44
+ span: options.span,
45
+ });
46
+ if (!plan.viable) {
47
+ return { success: false, error: plan.note ?? "The requested pinch would not be recognised." };
48
+ }
49
+ let lastFrame;
50
+ for (let i = 0; i < plan.gestures.length; i++) {
51
+ const frames = plan.gestures[i];
52
+ lastFrame = frames[frames.length - 1];
53
+ const sent = await sendTouchFrames(bridge, frames, FRAME_INTERVAL_MS);
54
+ if (!sent.success) {
55
+ await releasePointers(bridge, lastFrame);
56
+ return { success: false, error: `Failed to send touch frames: ${sent.error}` };
57
+ }
58
+ if (i < plan.gestures.length - 1)
59
+ await sleep(SETTLE_MS);
60
+ }
61
+ const frameCount = plan.gestures.reduce((n, g) => n + g.length, 0);
62
+ return {
63
+ success: true,
64
+ result: `Pinched ${options.direction} at (${Math.round(options.focalX)}, ${Math.round(options.focalY)}) ` +
65
+ `over ${plan.gestures.length} gesture(s), ${frameCount} frames`,
66
+ frameCount,
67
+ gestureCount: plan.gestures.length,
68
+ startHalf: plan.startHalf,
69
+ endHalf: plan.endHalf,
70
+ };
71
+ }
72
+ //# sourceMappingURL=androidPinch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"androidPinch.js","sourceRoot":"","sources":["../../src/core/androidPinch.ts"],"names":[],"mappings":"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"}