react-native-ai-debugger 1.1.6 → 1.4.21

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 (144) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +371 -42
  3. package/build/__tests__/helpers/tap-test-helpers.d.ts +36 -0
  4. package/build/__tests__/helpers/tap-test-helpers.d.ts.map +1 -0
  5. package/build/__tests__/helpers/tap-test-helpers.js +130 -0
  6. package/build/__tests__/helpers/tap-test-helpers.js.map +1 -0
  7. package/build/__tests__/integration/connection-health.test.js +12 -6
  8. package/build/__tests__/integration/connection-health.test.js.map +1 -1
  9. package/build/__tests__/integration/execute-in-app.test.js +2 -1
  10. package/build/__tests__/integration/execute-in-app.test.js.map +1 -1
  11. package/build/__tests__/integration/tap-strategies.test.d.ts +2 -0
  12. package/build/__tests__/integration/tap-strategies.test.d.ts.map +1 -0
  13. package/build/__tests__/integration/tap-strategies.test.js +329 -0
  14. package/build/__tests__/integration/tap-strategies.test.js.map +1 -0
  15. package/build/__tests__/integration/tools.test.js +9 -10
  16. package/build/__tests__/integration/tools.test.js.map +1 -1
  17. package/build/__tests__/unit/burst.test.d.ts +2 -0
  18. package/build/__tests__/unit/burst.test.d.ts.map +1 -0
  19. package/build/__tests__/unit/burst.test.js +50 -0
  20. package/build/__tests__/unit/burst.test.js.map +1 -0
  21. package/build/__tests__/unit/connection-health.test.js +26 -16
  22. package/build/__tests__/unit/connection-health.test.js.map +1 -1
  23. package/build/__tests__/unit/fingerprint.test.d.ts +2 -0
  24. package/build/__tests__/unit/fingerprint.test.d.ts.map +1 -0
  25. package/build/__tests__/unit/fingerprint.test.js +89 -0
  26. package/build/__tests__/unit/fingerprint.test.js.map +1 -0
  27. package/build/__tests__/unit/imageBuffer.test.d.ts +2 -0
  28. package/build/__tests__/unit/imageBuffer.test.d.ts.map +1 -0
  29. package/build/__tests__/unit/imageBuffer.test.js +128 -0
  30. package/build/__tests__/unit/imageBuffer.test.js.map +1 -0
  31. package/build/__tests__/unit/logs.test.js +9 -0
  32. package/build/__tests__/unit/logs.test.js.map +1 -1
  33. package/build/__tests__/unit/networkInterceptor.test.d.ts +2 -0
  34. package/build/__tests__/unit/networkInterceptor.test.d.ts.map +1 -0
  35. package/build/__tests__/unit/networkInterceptor.test.js +104 -0
  36. package/build/__tests__/unit/networkInterceptor.test.js.map +1 -0
  37. package/build/__tests__/unit/screenshot-diff.test.d.ts +2 -0
  38. package/build/__tests__/unit/screenshot-diff.test.d.ts.map +1 -0
  39. package/build/__tests__/unit/screenshot-diff.test.js +97 -0
  40. package/build/__tests__/unit/screenshot-diff.test.js.map +1 -0
  41. package/build/__tests__/unit/tap.test.js +430 -25
  42. package/build/__tests__/unit/tap.test.js.map +1 -1
  43. package/build/core/android.d.ts.map +1 -1
  44. package/build/core/android.js +64 -29
  45. package/build/core/android.js.map +1 -1
  46. package/build/core/appDetection.d.ts +8 -0
  47. package/build/core/appDetection.d.ts.map +1 -0
  48. package/build/core/appDetection.js +91 -0
  49. package/build/core/appDetection.js.map +1 -0
  50. package/build/core/bundle.d.ts.map +1 -1
  51. package/build/core/bundle.js +15 -23
  52. package/build/core/bundle.js.map +1 -1
  53. package/build/core/config.d.ts +8 -0
  54. package/build/core/config.d.ts.map +1 -0
  55. package/build/core/config.js +27 -0
  56. package/build/core/config.js.map +1 -0
  57. package/build/core/connection.d.ts +47 -3
  58. package/build/core/connection.d.ts.map +1 -1
  59. package/build/core/connection.js +519 -124
  60. package/build/core/connection.js.map +1 -1
  61. package/build/core/executor.d.ts +57 -14
  62. package/build/core/executor.d.ts.map +1 -1
  63. package/build/core/executor.js +936 -208
  64. package/build/core/executor.js.map +1 -1
  65. package/build/core/fingerprint.d.ts +4 -0
  66. package/build/core/fingerprint.d.ts.map +1 -0
  67. package/build/core/fingerprint.js +47 -0
  68. package/build/core/fingerprint.js.map +1 -0
  69. package/build/core/guides.d.ts.map +1 -1
  70. package/build/core/guides.js +27 -3
  71. package/build/core/guides.js.map +1 -1
  72. package/build/core/httpServer.js.map +1 -1
  73. package/build/core/imageBuffer.d.ts +37 -0
  74. package/build/core/imageBuffer.d.ts.map +1 -0
  75. package/build/core/imageBuffer.js +58 -0
  76. package/build/core/imageBuffer.js.map +1 -0
  77. package/build/core/index.d.ts +21 -17
  78. package/build/core/index.d.ts.map +1 -1
  79. package/build/core/index.js +21 -19
  80. package/build/core/index.js.map +1 -1
  81. package/build/core/ios.d.ts +36 -7
  82. package/build/core/ios.d.ts.map +1 -1
  83. package/build/core/ios.js +553 -323
  84. package/build/core/ios.js.map +1 -1
  85. package/build/core/license.d.ts +38 -0
  86. package/build/core/license.d.ts.map +1 -0
  87. package/build/core/license.js +265 -0
  88. package/build/core/license.js.map +1 -0
  89. package/build/core/logbox.d.ts +51 -0
  90. package/build/core/logbox.d.ts.map +1 -0
  91. package/build/core/logbox.js +258 -0
  92. package/build/core/logbox.js.map +1 -0
  93. package/build/core/logs.d.ts +1 -0
  94. package/build/core/logs.d.ts.map +1 -1
  95. package/build/core/logs.js +5 -0
  96. package/build/core/logs.js.map +1 -1
  97. package/build/core/metro.d.ts +8 -0
  98. package/build/core/metro.d.ts.map +1 -1
  99. package/build/core/metro.js +28 -0
  100. package/build/core/metro.js.map +1 -1
  101. package/build/core/networkInterceptor.d.ts +29 -2
  102. package/build/core/networkInterceptor.d.ts.map +1 -1
  103. package/build/core/networkInterceptor.js +195 -191
  104. package/build/core/networkInterceptor.js.map +1 -1
  105. package/build/core/ocr.d.ts +8 -3
  106. package/build/core/ocr.d.ts.map +1 -1
  107. package/build/core/ocr.js +116 -15
  108. package/build/core/ocr.js.map +1 -1
  109. package/build/core/posthog.d.ts +5 -0
  110. package/build/core/posthog.d.ts.map +1 -0
  111. package/build/core/posthog.js +46 -0
  112. package/build/core/posthog.js.map +1 -0
  113. package/build/core/sdkBridge.d.ts +67 -0
  114. package/build/core/sdkBridge.d.ts.map +1 -0
  115. package/build/core/sdkBridge.js +148 -0
  116. package/build/core/sdkBridge.js.map +1 -0
  117. package/build/core/state.d.ts +25 -5
  118. package/build/core/state.d.ts.map +1 -1
  119. package/build/core/state.js +79 -10
  120. package/build/core/state.js.map +1 -1
  121. package/build/core/telemetry.d.ts +19 -1
  122. package/build/core/telemetry.d.ts.map +1 -1
  123. package/build/core/telemetry.js +163 -8
  124. package/build/core/telemetry.js.map +1 -1
  125. package/build/core/types.d.ts +11 -0
  126. package/build/core/types.d.ts.map +1 -1
  127. package/build/index.js +1302 -302
  128. package/build/index.js.map +1 -1
  129. package/build/pro/screenshot-diff.d.ts +10 -0
  130. package/build/pro/screenshot-diff.d.ts.map +1 -0
  131. package/build/pro/screenshot-diff.js +38 -0
  132. package/build/pro/screenshot-diff.js.map +1 -0
  133. package/build/pro/tap.d.ts +71 -2
  134. package/build/pro/tap.d.ts.map +1 -1
  135. package/build/pro/tap.js +797 -124
  136. package/build/pro/tap.js.map +1 -1
  137. package/build/tools/accountTools.d.ts +44 -0
  138. package/build/tools/accountTools.d.ts.map +1 -0
  139. package/build/tools/accountTools.js +217 -0
  140. package/build/tools/accountTools.js.map +1 -0
  141. package/package.json +9 -7
  142. package/build/core/httpServer.js +0 -2459
  143. package/build/core/httpServerProcess.js +0 -153
  144. package/build/httpServerStandalone.js +0 -31
@@ -0,0 +1,97 @@
1
+ import { describe, it, expect } from "@jest/globals";
2
+ import sharp from "sharp";
3
+ import { compareScreenshots } from "../../pro/screenshot-diff.js";
4
+ // Helper: create a solid-color 100x100 PNG buffer
5
+ async function solidImage(r, g, b) {
6
+ return sharp({
7
+ create: { width: 100, height: 100, channels: 3, background: { r, g, b } },
8
+ })
9
+ .png()
10
+ .toBuffer();
11
+ }
12
+ describe("compareScreenshots", () => {
13
+ it("returns not changed for identical images", async () => {
14
+ const img = await solidImage(255, 0, 0);
15
+ const result = await compareScreenshots(img, img);
16
+ expect(result.changed).toBe(false);
17
+ expect(result.changeRate).toBe(0);
18
+ expect(result.changedPixels).toBe(0);
19
+ expect(result.totalPixels).toBe(10000);
20
+ });
21
+ it("returns changed for completely different images", async () => {
22
+ const red = await solidImage(255, 0, 0);
23
+ const blue = await solidImage(0, 0, 255);
24
+ const result = await compareScreenshots(red, blue);
25
+ expect(result.changed).toBe(true);
26
+ expect(result.changeRate).toBeGreaterThan(0.5);
27
+ expect(result.changedPixels).toBe(10000);
28
+ });
29
+ it("detects small changes above threshold", async () => {
30
+ // Create an image with a 10x10 changed region (1% of 100x100)
31
+ const base = await solidImage(255, 255, 255);
32
+ const modified = await sharp({
33
+ create: { width: 100, height: 100, channels: 3, background: { r: 255, g: 255, b: 255 } },
34
+ })
35
+ .composite([
36
+ {
37
+ input: await sharp({
38
+ create: { width: 10, height: 10, channels: 3, background: { r: 0, g: 0, b: 0 } },
39
+ })
40
+ .png()
41
+ .toBuffer(),
42
+ left: 0,
43
+ top: 0,
44
+ },
45
+ ])
46
+ .png()
47
+ .toBuffer();
48
+ const result = await compareScreenshots(base, modified);
49
+ expect(result.changed).toBe(true);
50
+ expect(result.changeRate).toBeCloseTo(0.01, 1);
51
+ });
52
+ it("ignores changes below threshold", async () => {
53
+ // Create images with very subtle difference (1 pixel)
54
+ const base = await solidImage(200, 200, 200);
55
+ // Change just slightly — within anti-aliasing tolerance
56
+ const modified = await sharp({
57
+ create: { width: 100, height: 100, channels: 3, background: { r: 201, g: 200, b: 200 } },
58
+ })
59
+ .png()
60
+ .toBuffer();
61
+ const result = await compareScreenshots(base, modified);
62
+ // Pixelmatch with threshold 0.1 should tolerate very small color differences
63
+ expect(result.changed).toBe(false);
64
+ });
65
+ it("handles JPEG buffers (not just PNG)", async () => {
66
+ const jpg1 = await sharp({
67
+ create: { width: 50, height: 50, channels: 3, background: { r: 100, g: 100, b: 100 } },
68
+ })
69
+ .jpeg()
70
+ .toBuffer();
71
+ const jpg2 = await sharp({
72
+ create: { width: 50, height: 50, channels: 3, background: { r: 255, g: 0, b: 0 } },
73
+ })
74
+ .jpeg()
75
+ .toBuffer();
76
+ const result = await compareScreenshots(jpg1, jpg2);
77
+ expect(result.changed).toBe(true);
78
+ expect(result.totalPixels).toBe(2500);
79
+ });
80
+ it("handles images of different sizes by returning changed", async () => {
81
+ const small = await sharp({
82
+ create: { width: 50, height: 50, channels: 3, background: { r: 0, g: 0, b: 0 } },
83
+ })
84
+ .png()
85
+ .toBuffer();
86
+ const big = await sharp({
87
+ create: { width: 100, height: 100, channels: 3, background: { r: 0, g: 0, b: 0 } },
88
+ })
89
+ .png()
90
+ .toBuffer();
91
+ const result = await compareScreenshots(small, big);
92
+ // Different sizes = assume changed (can't diff properly)
93
+ expect(result.changed).toBe(true);
94
+ expect(result.changeRate).toBe(1);
95
+ });
96
+ });
97
+ //# sourceMappingURL=screenshot-diff.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"screenshot-diff.test.js","sourceRoot":"","sources":["../../../src/__tests__/unit/screenshot-diff.test.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  // src/__tests__/unit/tap.test.ts
2
2
  import { describe, it, expect } from "@jest/globals";
3
- import { buildQuery, getAvailableStrategies, isNonAscii, convertPixelsToPoints, formatTapSuccess, formatTapFailure, } from "../../pro/tap.js";
3
+ import { buildQuery, getAvailableStrategies, hasProblematicUnicode, convertScreenshotToTapCoords, formatTapSuccess, formatTapFailure, buildVerificationExplanation, } from "../../pro/tap.js";
4
4
  describe("ConnectedApp type", () => {
5
5
  it("accepts platform and lastScreenshot fields", () => {
6
6
  const app = {
@@ -56,27 +56,78 @@ describe("buildQuery", () => {
56
56
  const q = buildQuery({ text: "Submit", testID: "btn" });
57
57
  expect(q).toEqual({ text: "Submit", testID: "btn" });
58
58
  });
59
+ it("builds query from all search params combined", () => {
60
+ const q = buildQuery({ text: "Submit", testID: "btn", component: "Button" });
61
+ expect(q).toEqual({ text: "Submit", testID: "btn", component: "Button" });
62
+ });
63
+ it("ignores non-query options like strategy and native", () => {
64
+ const q = buildQuery({ text: "Submit", strategy: "fiber", native: true, verify: true });
65
+ expect(q).toEqual({ text: "Submit" });
66
+ });
67
+ it("builds empty query from empty options", () => {
68
+ const q = buildQuery({});
69
+ expect(q).toEqual({});
70
+ });
59
71
  });
60
- describe("isNonAscii", () => {
72
+ describe("hasProblematicUnicode", () => {
61
73
  it("returns false for ASCII text", () => {
62
- expect(isNonAscii("Submit")).toBe(false);
74
+ expect(hasProblematicUnicode("Submit")).toBe(false);
75
+ });
76
+ it("returns false for Polish accented text", () => {
77
+ expect(hasProblematicUnicode("Potwierdź")).toBe(false);
78
+ });
79
+ it("returns false for Vietnamese accented text", () => {
80
+ expect(hasProblematicUnicode("Tin nhắn")).toBe(false);
81
+ });
82
+ it("returns false for German umlauts", () => {
83
+ expect(hasProblematicUnicode("Übersicht")).toBe(false);
84
+ });
85
+ it("returns false for French accented text", () => {
86
+ expect(hasProblematicUnicode("Paramètres")).toBe(false);
87
+ });
88
+ it("returns false for Cyrillic text", () => {
89
+ expect(hasProblematicUnicode("Отправить")).toBe(false);
63
90
  });
64
- it("returns true for Cyrillic", () => {
65
- expect(isNonAscii("Отправить")).toBe(true);
91
+ it("returns false for Chinese text", () => {
92
+ expect(hasProblematicUnicode("提交")).toBe(false);
93
+ });
94
+ it("returns false for Japanese text", () => {
95
+ expect(hasProblematicUnicode("送信")).toBe(false);
66
96
  });
67
97
  it("returns true for emoji", () => {
68
- expect(isNonAscii("🔥")).toBe(true);
98
+ expect(hasProblematicUnicode("🔥")).toBe(true);
99
+ });
100
+ it("returns true for mixed text with emoji", () => {
101
+ expect(hasProblematicUnicode("Save 🔥")).toBe(true);
102
+ });
103
+ it("returns true for flag emoji", () => {
104
+ expect(hasProblematicUnicode("🇺🇸")).toBe(true);
105
+ });
106
+ it("returns true for zero-width joiner sequences", () => {
107
+ expect(hasProblematicUnicode("👨‍👩‍👧")).toBe(true);
108
+ });
109
+ it("returns true for weather symbols", () => {
110
+ expect(hasProblematicUnicode("☀")).toBe(true);
69
111
  });
70
112
  });
71
113
  describe("getAvailableStrategies", () => {
72
- it("returns all strategies for text query", () => {
73
- expect(getAvailableStrategies({ text: "Submit" }, "auto")).toEqual(["fiber", "accessibility", "ocr"]);
114
+ it("returns accessibility-first for text query", () => {
115
+ expect(getAvailableStrategies({ text: "Submit" }, "auto")).toEqual(["accessibility", "fiber", "ocr"]);
74
116
  });
75
- it("skips fiber for non-ASCII text", () => {
76
- expect(getAvailableStrategies({ text: "Отправить" }, "auto")).toEqual(["accessibility", "ocr"]);
117
+ it("includes fiber for non-ASCII accented text", () => {
118
+ expect(getAvailableStrategies({ text: "Отправить" }, "auto")).toEqual(["accessibility", "fiber", "ocr"]);
77
119
  });
78
- it("returns fiber+accessibility for testID", () => {
79
- expect(getAvailableStrategies({ testID: "btn" }, "auto")).toEqual(["fiber", "accessibility"]);
120
+ it("includes fiber for Polish text", () => {
121
+ expect(getAvailableStrategies({ text: "Potwierdź" }, "auto")).toEqual(["accessibility", "fiber", "ocr"]);
122
+ });
123
+ it("includes fiber for Vietnamese text", () => {
124
+ expect(getAvailableStrategies({ text: "Tin nhắn" }, "auto")).toEqual(["accessibility", "fiber", "ocr"]);
125
+ });
126
+ it("skips fiber for emoji text", () => {
127
+ expect(getAvailableStrategies({ text: "🔥 Fire" }, "auto")).toEqual(["accessibility", "ocr"]);
128
+ });
129
+ it("returns accessibility+fiber for testID", () => {
130
+ expect(getAvailableStrategies({ testID: "btn" }, "auto")).toEqual(["accessibility", "fiber"]);
80
131
  });
81
132
  it("returns only fiber for component", () => {
82
133
  expect(getAvailableStrategies({ component: "Button" }, "auto")).toEqual(["fiber"]);
@@ -84,22 +135,67 @@ describe("getAvailableStrategies", () => {
84
135
  it("returns coordinate for x,y", () => {
85
136
  expect(getAvailableStrategies({ x: 100, y: 200 }, "auto")).toEqual(["coordinate"]);
86
137
  });
87
- it("returns single strategy when explicitly set", () => {
138
+ it("returns explicit strategy with OCR fallback for text query", () => {
139
+ expect(getAvailableStrategies({ text: "Submit" }, "fiber")).toEqual(["fiber", "ocr"]);
140
+ expect(getAvailableStrategies({ text: "Submit" }, "accessibility")).toEqual(["accessibility", "ocr"]);
141
+ });
142
+ it("returns only OCR when explicitly set with text query", () => {
88
143
  expect(getAvailableStrategies({ text: "Submit" }, "ocr")).toEqual(["ocr"]);
89
144
  });
145
+ it("returns single strategy when explicitly set without text", () => {
146
+ expect(getAvailableStrategies({ testID: "btn" }, "fiber")).toEqual(["fiber"]);
147
+ });
148
+ it("returns accessibility+fiber+ocr for testID+text combo in auto mode", () => {
149
+ expect(getAvailableStrategies({ testID: "btn", text: "Submit" }, "auto")).toEqual([
150
+ "accessibility",
151
+ "fiber",
152
+ "ocr",
153
+ ]);
154
+ });
155
+ it("returns fallback chain for component+text in auto mode", () => {
156
+ expect(getAvailableStrategies({ component: "Button", text: "OK" }, "auto")).toEqual([
157
+ "accessibility",
158
+ "fiber",
159
+ "ocr",
160
+ ]);
161
+ });
162
+ it("returns coordinate only even when text is also provided", () => {
163
+ expect(getAvailableStrategies({ x: 100, y: 200, text: "Submit" }, "auto")).toEqual(["coordinate"]);
164
+ });
165
+ it("returns fiber+ocr for explicit fiber with emoji text", () => {
166
+ expect(getAvailableStrategies({ text: "🔥 Fire" }, "fiber")).toEqual(["fiber", "ocr"]);
167
+ });
90
168
  });
91
- describe("convertPixelsToPoints", () => {
92
- it("divides by pixel ratio for iOS", () => {
93
- expect(convertPixelsToPoints(300, 600, "ios", 3)).toEqual({ x: 100, y: 200 });
169
+ describe("convertScreenshotToTapCoords", () => {
170
+ it("converts iOS screenshot pixels to points (3x device)", () => {
171
+ expect(convertScreenshotToTapCoords(300, 600, "ios", 3)).toEqual({ x: 100, y: 200 });
94
172
  });
95
- it("passes through for Android", () => {
96
- expect(convertPixelsToPoints(300, 600, "android", 3)).toEqual({ x: 300, y: 600 });
173
+ it("converts iOS screenshot pixels to points (2x iPad)", () => {
174
+ expect(convertScreenshotToTapCoords(200, 400, "ios", 2)).toEqual({ x: 100, y: 200 });
97
175
  });
98
- it("applies scaleFactor before conversion", () => {
99
- expect(convertPixelsToPoints(150, 300, "ios", 3, 2)).toEqual({ x: 100, y: 200 });
176
+ it("undoes image downscaling before dividing by DPR", () => {
177
+ expect(convertScreenshotToTapCoords(219, 438, "ios", 3, 1.368)).toEqual({ x: 100, y: 200 });
178
+ });
179
+ it("passes through for Android with no downscaling", () => {
180
+ expect(convertScreenshotToTapCoords(300, 600, "android", 1)).toEqual({ x: 300, y: 600 });
181
+ });
182
+ it("undoes Android image downscaling", () => {
183
+ expect(convertScreenshotToTapCoords(250, 500, "android", 1, 1.2)).toEqual({ x: 300, y: 600 });
100
184
  });
101
185
  it("rounds to integers", () => {
102
- expect(convertPixelsToPoints(301, 599, "ios", 3)).toEqual({ x: 100, y: 200 });
186
+ expect(convertScreenshotToTapCoords(301, 599, "ios", 3)).toEqual({ x: 100, y: 200 });
187
+ });
188
+ it("handles origin coordinates (0, 0)", () => {
189
+ expect(convertScreenshotToTapCoords(0, 0, "ios", 3)).toEqual({ x: 0, y: 0 });
190
+ expect(convertScreenshotToTapCoords(0, 0, "android", 1)).toEqual({ x: 0, y: 0 });
191
+ });
192
+ it("handles large coordinates for high-res displays", () => {
193
+ const result = convertScreenshotToTapCoords(3840, 2160, "ios", 3);
194
+ expect(result.x).toBe(1280);
195
+ expect(result.y).toBe(720);
196
+ });
197
+ it("handles scaleFactor=1 as no-op for downscaling", () => {
198
+ expect(convertScreenshotToTapCoords(300, 600, "ios", 3, 1)).toEqual({ x: 100, y: 200 });
103
199
  });
104
200
  });
105
201
  describe("formatTapSuccess", () => {
@@ -129,16 +225,232 @@ describe("formatTapFailure", () => {
129
225
  expect(result.attempted).toHaveLength(1);
130
226
  expect(result.suggestion).toBe("Use screenshot");
131
227
  });
228
+ it("sets method to last attempted strategy for telemetry tracking", () => {
229
+ const result = formatTapFailure({
230
+ query: { text: "Submit" },
231
+ attempted: [
232
+ { strategy: "fiber", reason: "No match" },
233
+ { strategy: "accessibility", reason: "No match" },
234
+ { strategy: "ocr", reason: "OCR did not find text" },
235
+ ],
236
+ suggestion: "Use screenshot",
237
+ });
238
+ expect(result.method).toBe("ocr");
239
+ });
240
+ it("sets method to undefined when no strategies attempted", () => {
241
+ const result = formatTapFailure({
242
+ query: { text: "Submit" },
243
+ attempted: [],
244
+ suggestion: "Use screenshot",
245
+ });
246
+ expect(result.method).toBeUndefined();
247
+ });
248
+ });
249
+ describe("formatTapSuccess with coordinate conversion info", () => {
250
+ it("includes tappedAt and convertedTo when provided", () => {
251
+ const result = formatTapSuccess({
252
+ method: "fiber+native",
253
+ query: { text: "Submit" },
254
+ pressed: "PrimaryButton",
255
+ tappedAt: { x: 300, y: 600 },
256
+ convertedTo: { x: 100, y: 200, unit: "points" },
257
+ platform: "ios",
258
+ });
259
+ expect(result.tappedAt).toEqual({ x: 300, y: 600 });
260
+ expect(result.convertedTo).toEqual({ x: 100, y: 200, unit: "points" });
261
+ expect(result.platform).toBe("ios");
262
+ });
263
+ it("includes device name when provided", () => {
264
+ const result = formatTapSuccess({
265
+ method: "accessibility",
266
+ query: { testID: "btn" },
267
+ pressed: "Button",
268
+ device: "iPhone 16 Pro",
269
+ });
270
+ expect(result.device).toBe("iPhone 16 Pro");
271
+ });
272
+ });
273
+ describe("formatTapSuccess with screenshot and verification", () => {
274
+ it("includes screenshot field when provided", () => {
275
+ const result = formatTapSuccess({
276
+ method: "fiber",
277
+ query: { text: "Submit" },
278
+ pressed: "Button",
279
+ screenshot: {
280
+ image: "data:image/jpeg;base64,abc123",
281
+ width: 1170,
282
+ height: 2532,
283
+ scaleFactor: 1.0,
284
+ },
285
+ });
286
+ expect(result.success).toBe(true);
287
+ expect(result.screenshot).toEqual({
288
+ image: "data:image/jpeg;base64,abc123",
289
+ width: 1170,
290
+ height: 2532,
291
+ scaleFactor: 1.0,
292
+ });
293
+ });
294
+ it("includes verification field when provided", () => {
295
+ const result = formatTapSuccess({
296
+ method: "coordinate",
297
+ query: { x: 300, y: 600 },
298
+ verification: {
299
+ meaningful: true,
300
+ changeRate: 0.12,
301
+ changedPixels: 48210,
302
+ totalPixels: 2961720,
303
+ explanation: "Tap caused a visible UI change (12.0% pixel diff). The screen updated as expected.",
304
+ },
305
+ screenshot: {
306
+ image: "data:image/jpeg;base64,abc123",
307
+ width: 1170,
308
+ height: 2532,
309
+ scaleFactor: 1.0,
310
+ },
311
+ });
312
+ expect(result.verification).toEqual({
313
+ meaningful: true,
314
+ changeRate: 0.12,
315
+ changedPixels: 48210,
316
+ totalPixels: 2961720,
317
+ explanation: "Tap caused a visible UI change (12.0% pixel diff). The screen updated as expected.",
318
+ });
319
+ expect(result.screenshot).toBeDefined();
320
+ });
321
+ it("omits screenshot and verification when not provided", () => {
322
+ const result = formatTapSuccess({
323
+ method: "fiber",
324
+ query: { text: "Submit" },
325
+ });
326
+ expect(result.screenshot).toBeUndefined();
327
+ expect(result.verification).toBeUndefined();
328
+ });
329
+ });
330
+ describe("formatTapFailure with timeout error", () => {
331
+ it("includes error field when timeout info is provided", () => {
332
+ const result = formatTapFailure({
333
+ query: { text: "Submit" },
334
+ attempted: [
335
+ { strategy: "fiber", reason: "fiber timed out after 5000ms" },
336
+ { strategy: "accessibility", reason: "accessibility timed out after 3500ms" },
337
+ { strategy: "ocr", reason: "Skipped — only 200ms remaining (budget 20000ms)" },
338
+ ],
339
+ error: "Tap timed out after 20000ms (budget 20000ms)",
340
+ suggestion: "Use screenshot and retry",
341
+ });
342
+ expect(result.success).toBe(false);
343
+ expect(result.error).toContain("timed out");
344
+ expect(result.attempted).toHaveLength(3);
345
+ expect(result.attempted[0].reason).toContain("timed out after 5000ms");
346
+ expect(result.attempted[2].reason).toContain("Skipped");
347
+ });
348
+ it("uses default error message when no explicit error field", () => {
349
+ const result = formatTapFailure({
350
+ query: { text: "Submit" },
351
+ attempted: [{ strategy: "fiber", reason: "No match" }],
352
+ suggestion: "Try OCR",
353
+ });
354
+ expect(result.success).toBe(false);
355
+ expect(result.error).toBeDefined();
356
+ // Default error from buildErrorMessage, not timeout
357
+ expect(result.error).not.toContain("timed out");
358
+ });
359
+ });
360
+ describe("formatTapFailure with screenshot and verification", () => {
361
+ it("includes warning when verification shows not meaningful", () => {
362
+ const result = formatTapFailure({
363
+ query: { x: 300, y: 600 },
364
+ attempted: [{ strategy: "coordinate", reason: "executed" }],
365
+ suggestion: "Retry with adjusted coordinates",
366
+ verification: {
367
+ meaningful: false,
368
+ changeRate: 0.001,
369
+ changedPixels: 312,
370
+ totalPixels: 2961720,
371
+ explanation: "No visual change detected between before and after screenshots. The element may not respond visually or the tap may have missed.",
372
+ },
373
+ screenshot: {
374
+ image: "data:image/jpeg;base64,abc123",
375
+ width: 1170,
376
+ height: 2532,
377
+ scaleFactor: 1.0,
378
+ },
379
+ });
380
+ expect(result.verification?.meaningful).toBe(false);
381
+ expect(result.screenshot).toBeDefined();
382
+ expect(result.warning).toContain("no visual change detected");
383
+ });
384
+ });
385
+ describe("toTapCoord returns image-pixel coordinates", () => {
386
+ it("returns raw OCR coordinate (scaleFactor ignored)", async () => {
387
+ const { toTapCoord } = await import("../../core/ocr.js");
388
+ // OCR coord 100 on downscaled image → returns 100 (image-pixel space)
389
+ // tap() handles un-downscaling and platform conversion
390
+ expect(toTapCoord(100, 1.368)).toBe(100);
391
+ });
392
+ it("rounds to integer", async () => {
393
+ const { toTapCoord } = await import("../../core/ocr.js");
394
+ expect(toTapCoord(100.6, 1.2)).toBe(101);
395
+ });
396
+ it("returns raw coord when scaleFactor is 1", async () => {
397
+ const { toTapCoord } = await import("../../core/ocr.js");
398
+ expect(toTapCoord(250, 1)).toBe(250);
399
+ });
400
+ });
401
+ describe("buildVerificationExplanation", () => {
402
+ it("explains persistent visual change", () => {
403
+ const explanation = buildVerificationExplanation({
404
+ meaningful: true, changeRate: 0.032, changedPixels: 32000, totalPixels: 1000000,
405
+ });
406
+ expect(explanation).toContain("visible UI change");
407
+ expect(explanation).toContain("3.2%");
408
+ });
409
+ it("explains transient change from burst", () => {
410
+ const explanation = buildVerificationExplanation({
411
+ meaningful: true, changeRate: 0.001, changedPixels: 1000, totalPixels: 1000000,
412
+ transientChangeDetected: true, peakChangeRate: 0.041, peakFrame: 2,
413
+ });
414
+ expect(explanation).toContain("transient visual feedback");
415
+ expect(explanation).toContain("frame 2");
416
+ expect(explanation).toContain("4.1%");
417
+ });
418
+ it("explains no change in standard mode", () => {
419
+ const explanation = buildVerificationExplanation({
420
+ meaningful: false, changeRate: 0.0, changedPixels: 0, totalPixels: 1000000,
421
+ });
422
+ expect(explanation).toContain("No visual change");
423
+ expect(explanation).toContain("before and after");
424
+ });
425
+ it("explains no change in burst mode", () => {
426
+ const explanation = buildVerificationExplanation({
427
+ meaningful: false, changeRate: 0.0, changedPixels: 0, totalPixels: 1000000,
428
+ transientChangeDetected: false, peakChangeRate: 0.001, peakFrame: 0,
429
+ });
430
+ expect(explanation).toContain("No visual change");
431
+ expect(explanation).toContain("burst frames");
432
+ });
433
+ });
434
+ describe("getIOSDevicePixelRatio", () => {
435
+ it("calculates DPR from screenshot width and accessibility root frame", async () => {
436
+ const { calculateDPR } = await import("../../core/ios.js");
437
+ // iPhone 3x: 1260px screenshot, 420pt root frame width
438
+ expect(calculateDPR(1260, 420)).toBe(3);
439
+ // iPad 2x: 2048px screenshot, 1024pt root frame width
440
+ expect(calculateDPR(2048, 1024)).toBe(2);
441
+ // iPhone SE 2x: 750px screenshot, 375pt root frame width
442
+ expect(calculateDPR(750, 375)).toBe(2);
443
+ });
132
444
  });
133
445
  describe("tap orchestrator", () => {
134
- it("returns error when no app is connected and auto-connect fails", async () => {
446
+ it("attempts strategies when no app is connected", async () => {
135
447
  const { tap } = await import("../../pro/tap.js");
136
448
  const { connectedApps } = await import("../../core/state.js");
137
449
  connectedApps.clear();
138
450
  const result = await tap({ text: "Submit" });
139
- expect(result.success).toBe(false);
140
- // Auto-connect will be attempted (scans ports) but may fail or succeed
141
- // depending on whether Metro is running in the test environment
451
+ // With Metro decoupling, tap may succeed via accessibility/OCR even without
452
+ // a Metro connection, or it may fail if no devices are available.
453
+ // Either way, the result should be a valid TapResult.
142
454
  expect(result.error || result.method).toBeTruthy();
143
455
  }, 15000);
144
456
  it("validates that at least one search param is provided", async () => {
@@ -154,4 +466,97 @@ describe("tap orchestrator", () => {
154
466
  expect(result.error).toContain("Both x and y");
155
467
  });
156
468
  });
469
+ describe("verification thresholds", () => {
470
+ it("buildVerificationExplanation describes meaningful change at 0.1%", () => {
471
+ const explanation = buildVerificationExplanation({
472
+ meaningful: true,
473
+ changeRate: 0.001,
474
+ changedPixels: 1842,
475
+ totalPixels: 1842000,
476
+ });
477
+ expect(explanation).toContain("visible UI change");
478
+ });
479
+ it("buildVerificationExplanation describes no change at 0.03%", () => {
480
+ const explanation = buildVerificationExplanation({
481
+ meaningful: false,
482
+ changeRate: 0.0003,
483
+ changedPixels: 553,
484
+ totalPixels: 1842000,
485
+ });
486
+ expect(explanation).toContain("No visual change");
487
+ });
488
+ });
489
+ describe("formatTapFailure with all strategies exhausted", () => {
490
+ it("includes all attempted strategies in order", () => {
491
+ const result = formatTapFailure({
492
+ query: { text: "Submit" },
493
+ attempted: [
494
+ { strategy: "accessibility", reason: "No iOS accessibility match" },
495
+ { strategy: "fiber", reason: "pressElement failed" },
496
+ { strategy: "ocr", reason: "OCR did not find text" },
497
+ ],
498
+ suggestion: "Take a screenshot and use coordinates",
499
+ });
500
+ expect(result.attempted).toHaveLength(3);
501
+ expect(result.attempted[0].strategy).toBe("accessibility");
502
+ expect(result.attempted[1].strategy).toBe("fiber");
503
+ expect(result.attempted[2].strategy).toBe("ocr");
504
+ });
505
+ it("includes device name in failure response", () => {
506
+ const result = formatTapFailure({
507
+ query: { text: "Submit" },
508
+ attempted: [{ strategy: "fiber", reason: "No match" }],
509
+ suggestion: "Try OCR",
510
+ device: "Pixel 7",
511
+ });
512
+ expect(result.device).toBe("Pixel 7");
513
+ });
514
+ });
515
+ describe("tap without Metro connection", () => {
516
+ it("does not hard-fail when Metro is unavailable and strategy is accessibility", async () => {
517
+ const { tap } = await import("../../pro/tap.js");
518
+ const { connectedApps } = await import("../../core/state.js");
519
+ connectedApps.clear();
520
+ const result = await tap({
521
+ text: "Submit",
522
+ strategy: "accessibility",
523
+ platform: "ios",
524
+ });
525
+ // It may still fail (no simulator running in test env), but the error
526
+ // should NOT be "No connected app"
527
+ if (!result.success) {
528
+ expect(result.error).not.toContain("No connected app");
529
+ }
530
+ }, 15000);
531
+ it("does not hard-fail when Metro is unavailable and strategy is ocr", async () => {
532
+ const { tap } = await import("../../pro/tap.js");
533
+ const { connectedApps } = await import("../../core/state.js");
534
+ connectedApps.clear();
535
+ const result = await tap({
536
+ text: "Submit",
537
+ strategy: "ocr",
538
+ platform: "ios",
539
+ });
540
+ if (!result.success) {
541
+ expect(result.error).not.toContain("No connected app");
542
+ }
543
+ }, 15000);
544
+ it("still requires Metro for fiber strategy", async () => {
545
+ const { tap } = await import("../../pro/tap.js");
546
+ const { connectedApps } = await import("../../core/state.js");
547
+ connectedApps.clear();
548
+ const result = await tap({
549
+ text: "Submit",
550
+ strategy: "fiber",
551
+ platform: "ios",
552
+ });
553
+ // If Metro auto-connect succeeded, fiber may work — that's fine.
554
+ // But if it failed, the error or attempted strategies should mention Metro.
555
+ if (!result.success) {
556
+ const mentionsMetro = result.error?.includes("Metro") ||
557
+ result.attempted?.some((a) => a.reason.includes("Metro"));
558
+ expect(mentionsMetro).toBe(true);
559
+ }
560
+ }, 15000);
561
+ });
157
562
  //# sourceMappingURL=tap.test.js.map