react-native-ai-debugger 1.1.6 → 1.4.33
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.
- package/LICENSE +1 -1
- package/README.md +386 -45
- package/build/__tests__/helpers/tap-test-helpers.d.ts +36 -0
- package/build/__tests__/helpers/tap-test-helpers.d.ts.map +1 -0
- package/build/__tests__/helpers/tap-test-helpers.js +130 -0
- package/build/__tests__/helpers/tap-test-helpers.js.map +1 -0
- package/build/__tests__/integration/connection-health.test.js +12 -6
- package/build/__tests__/integration/connection-health.test.js.map +1 -1
- package/build/__tests__/integration/execute-in-app.test.js +2 -1
- package/build/__tests__/integration/execute-in-app.test.js.map +1 -1
- package/build/__tests__/integration/tap-strategies.test.d.ts +2 -0
- package/build/__tests__/integration/tap-strategies.test.d.ts.map +1 -0
- package/build/__tests__/integration/tap-strategies.test.js +329 -0
- package/build/__tests__/integration/tap-strategies.test.js.map +1 -0
- package/build/__tests__/integration/tools.test.js +9 -10
- package/build/__tests__/integration/tools.test.js.map +1 -1
- package/build/__tests__/unit/burst.test.d.ts +2 -0
- package/build/__tests__/unit/burst.test.d.ts.map +1 -0
- package/build/__tests__/unit/burst.test.js +50 -0
- package/build/__tests__/unit/burst.test.js.map +1 -0
- package/build/__tests__/unit/connection-health.test.js +26 -16
- package/build/__tests__/unit/connection-health.test.js.map +1 -1
- package/build/__tests__/unit/fiberMatch.test.d.ts +2 -0
- package/build/__tests__/unit/fiberMatch.test.d.ts.map +1 -0
- package/build/__tests__/unit/fiberMatch.test.js +114 -0
- package/build/__tests__/unit/fiberMatch.test.js.map +1 -0
- package/build/__tests__/unit/fingerprint.test.d.ts +2 -0
- package/build/__tests__/unit/fingerprint.test.d.ts.map +1 -0
- package/build/__tests__/unit/fingerprint.test.js +89 -0
- package/build/__tests__/unit/fingerprint.test.js.map +1 -0
- package/build/__tests__/unit/imageBuffer.test.d.ts +2 -0
- package/build/__tests__/unit/imageBuffer.test.d.ts.map +1 -0
- package/build/__tests__/unit/imageBuffer.test.js +128 -0
- package/build/__tests__/unit/imageBuffer.test.js.map +1 -0
- package/build/__tests__/unit/logs.test.js +9 -0
- package/build/__tests__/unit/logs.test.js.map +1 -1
- package/build/__tests__/unit/networkInterceptor.test.d.ts +2 -0
- package/build/__tests__/unit/networkInterceptor.test.d.ts.map +1 -0
- package/build/__tests__/unit/networkInterceptor.test.js +104 -0
- package/build/__tests__/unit/networkInterceptor.test.js.map +1 -0
- package/build/__tests__/unit/screenshot-diff.test.d.ts +2 -0
- package/build/__tests__/unit/screenshot-diff.test.d.ts.map +1 -0
- package/build/__tests__/unit/screenshot-diff.test.js +97 -0
- package/build/__tests__/unit/screenshot-diff.test.js.map +1 -0
- package/build/__tests__/unit/tap.test.js +542 -25
- package/build/__tests__/unit/tap.test.js.map +1 -1
- package/build/core/android.d.ts.map +1 -1
- package/build/core/android.js +104 -113
- package/build/core/android.js.map +1 -1
- package/build/core/appDetection.d.ts +8 -0
- package/build/core/appDetection.d.ts.map +1 -0
- package/build/core/appDetection.js +115 -0
- package/build/core/appDetection.js.map +1 -0
- package/build/core/bundle.d.ts.map +1 -1
- package/build/core/bundle.js +15 -23
- package/build/core/bundle.js.map +1 -1
- package/build/core/config.d.ts +8 -0
- package/build/core/config.d.ts.map +1 -0
- package/build/core/config.js +27 -0
- package/build/core/config.js.map +1 -0
- package/build/core/connection.d.ts +47 -3
- package/build/core/connection.d.ts.map +1 -1
- package/build/core/connection.js +519 -124
- package/build/core/connection.js.map +1 -1
- package/build/core/executor.d.ts +82 -14
- package/build/core/executor.d.ts.map +1 -1
- package/build/core/executor.js +1869 -418
- package/build/core/executor.js.map +1 -1
- package/build/core/feedback.d.ts +18 -0
- package/build/core/feedback.d.ts.map +1 -0
- package/build/core/feedback.js +73 -0
- package/build/core/feedback.js.map +1 -0
- package/build/core/fiberMatch.d.ts +25 -0
- package/build/core/fiberMatch.d.ts.map +1 -0
- package/build/core/fiberMatch.js +35 -0
- package/build/core/fiberMatch.js.map +1 -0
- package/build/core/fingerprint.d.ts +4 -0
- package/build/core/fingerprint.d.ts.map +1 -0
- package/build/core/fingerprint.js +47 -0
- package/build/core/fingerprint.js.map +1 -0
- package/build/core/guides.d.ts.map +1 -1
- package/build/core/guides.js +66 -3
- package/build/core/guides.js.map +1 -1
- package/build/core/imageBuffer.d.ts +37 -0
- package/build/core/imageBuffer.d.ts.map +1 -0
- package/build/core/imageBuffer.js +58 -0
- package/build/core/imageBuffer.js.map +1 -0
- package/build/core/index.d.ts +22 -17
- package/build/core/index.d.ts.map +1 -1
- package/build/core/index.js +23 -19
- package/build/core/index.js.map +1 -1
- package/build/core/ios.d.ts +36 -7
- package/build/core/ios.d.ts.map +1 -1
- package/build/core/ios.js +561 -326
- package/build/core/ios.js.map +1 -1
- package/build/core/license.d.ts +38 -0
- package/build/core/license.d.ts.map +1 -0
- package/build/core/license.js +243 -0
- package/build/core/license.js.map +1 -0
- package/build/core/logbox.d.ts +57 -0
- package/build/core/logbox.d.ts.map +1 -0
- package/build/core/logbox.js +282 -0
- package/build/core/logbox.js.map +1 -0
- package/build/core/logs.d.ts +1 -0
- package/build/core/logs.d.ts.map +1 -1
- package/build/core/logs.js +5 -0
- package/build/core/logs.js.map +1 -1
- package/build/core/metro.d.ts +8 -0
- package/build/core/metro.d.ts.map +1 -1
- package/build/core/metro.js +28 -0
- package/build/core/metro.js.map +1 -1
- package/build/core/networkInterceptor.d.ts +29 -2
- package/build/core/networkInterceptor.d.ts.map +1 -1
- package/build/core/networkInterceptor.js +195 -191
- package/build/core/networkInterceptor.js.map +1 -1
- package/build/core/ocr.d.ts +8 -3
- package/build/core/ocr.d.ts.map +1 -1
- package/build/core/ocr.js +116 -15
- package/build/core/ocr.js.map +1 -1
- package/build/core/posthog.d.ts +5 -0
- package/build/core/posthog.d.ts.map +1 -0
- package/build/core/posthog.js +46 -0
- package/build/core/posthog.js.map +1 -0
- package/build/core/sdkBridge.d.ts +67 -0
- package/build/core/sdkBridge.d.ts.map +1 -0
- package/build/core/sdkBridge.js +148 -0
- package/build/core/sdkBridge.js.map +1 -0
- package/build/core/state.d.ts +25 -5
- package/build/core/state.d.ts.map +1 -1
- package/build/core/state.js +79 -10
- package/build/core/state.js.map +1 -1
- package/build/core/telemetry.d.ts +21 -1
- package/build/core/telemetry.d.ts.map +1 -1
- package/build/core/telemetry.js +166 -10
- package/build/core/telemetry.js.map +1 -1
- package/build/core/types.d.ts +11 -0
- package/build/core/types.d.ts.map +1 -1
- package/build/index.js +1490 -324
- package/build/index.js.map +1 -1
- package/build/pro/screenshot-diff.d.ts +10 -0
- package/build/pro/screenshot-diff.d.ts.map +1 -0
- package/build/pro/screenshot-diff.js +38 -0
- package/build/pro/screenshot-diff.js.map +1 -0
- package/build/pro/tap.d.ts +84 -2
- package/build/pro/tap.d.ts.map +1 -1
- package/build/pro/tap.js +835 -129
- package/build/pro/tap.js.map +1 -1
- package/build/tools/accountTools.d.ts +44 -0
- package/build/tools/accountTools.d.ts.map +1 -0
- package/build/tools/accountTools.js +217 -0
- package/build/tools/accountTools.js.map +1 -0
- package/package.json +8 -7
- package/build/core/httpServer.d.ts +0 -14
- package/build/core/httpServer.d.ts.map +0 -1
- package/build/core/httpServer.js +0 -2459
- package/build/core/httpServer.js.map +0 -1
- package/build/core/httpServerProcess.d.ts +0 -25
- package/build/core/httpServerProcess.d.ts.map +0 -1
- package/build/core/httpServerProcess.js +0 -153
- package/build/core/httpServerProcess.js.map +0 -1
- package/build/core/tap.d.ts +0 -91
- package/build/core/tap.d.ts.map +0 -1
- package/build/core/tap.js +0 -542
- package/build/core/tap.js.map +0 -1
- package/build/httpServerStandalone.d.ts +0 -7
- package/build/httpServerStandalone.d.ts.map +0 -1
- package/build/httpServerStandalone.js +0 -31
- package/build/httpServerStandalone.js.map +0 -1
|
@@ -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,
|
|
3
|
+
import { buildQuery, getAvailableStrategies, hasProblematicUnicode, convertScreenshotToTapCoords, formatTapSuccess, formatTapFailure, buildVerificationExplanation, isTapTimeout, findOcrMatch, } 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("
|
|
72
|
+
describe("hasProblematicUnicode", () => {
|
|
61
73
|
it("returns false for ASCII text", () => {
|
|
62
|
-
expect(
|
|
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);
|
|
63
81
|
});
|
|
64
|
-
it("returns
|
|
65
|
-
expect(
|
|
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);
|
|
90
|
+
});
|
|
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(
|
|
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
|
|
73
|
-
expect(getAvailableStrategies({ text: "Submit" }, "auto")).toEqual(["
|
|
114
|
+
it("returns accessibility-first for text query", () => {
|
|
115
|
+
expect(getAvailableStrategies({ text: "Submit" }, "auto")).toEqual(["accessibility", "fiber", "ocr"]);
|
|
116
|
+
});
|
|
117
|
+
it("includes fiber for non-ASCII accented text", () => {
|
|
118
|
+
expect(getAvailableStrategies({ text: "Отправить" }, "auto")).toEqual(["accessibility", "fiber", "ocr"]);
|
|
119
|
+
});
|
|
120
|
+
it("includes fiber for Polish text", () => {
|
|
121
|
+
expect(getAvailableStrategies({ text: "Potwierdź" }, "auto")).toEqual(["accessibility", "fiber", "ocr"]);
|
|
74
122
|
});
|
|
75
|
-
it("
|
|
76
|
-
expect(getAvailableStrategies({ text: "
|
|
123
|
+
it("includes fiber for Vietnamese text", () => {
|
|
124
|
+
expect(getAvailableStrategies({ text: "Tin nhắn" }, "auto")).toEqual(["accessibility", "fiber", "ocr"]);
|
|
77
125
|
});
|
|
78
|
-
it("
|
|
79
|
-
expect(getAvailableStrategies({
|
|
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
|
|
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("
|
|
92
|
-
it("
|
|
93
|
-
expect(
|
|
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 });
|
|
172
|
+
});
|
|
173
|
+
it("converts iOS screenshot pixels to points (2x iPad)", () => {
|
|
174
|
+
expect(convertScreenshotToTapCoords(200, 400, "ios", 2)).toEqual({ x: 100, y: 200 });
|
|
94
175
|
});
|
|
95
|
-
it("
|
|
96
|
-
expect(
|
|
176
|
+
it("undoes image downscaling before dividing by DPR", () => {
|
|
177
|
+
expect(convertScreenshotToTapCoords(219, 438, "ios", 3, 1.368)).toEqual({ x: 100, y: 200 });
|
|
97
178
|
});
|
|
98
|
-
it("
|
|
99
|
-
expect(
|
|
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(
|
|
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("
|
|
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
|
-
|
|
140
|
-
//
|
|
141
|
-
//
|
|
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,209 @@ 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
|
+
});
|
|
562
|
+
describe("isTapTimeout", () => {
|
|
563
|
+
it("returns true when a strategy wrapper fired at its cap", () => {
|
|
564
|
+
expect(isTapTimeout([
|
|
565
|
+
{ strategy: "fiber", reason: "fiber timed out after 5000ms" },
|
|
566
|
+
])).toBe(true);
|
|
567
|
+
});
|
|
568
|
+
it("returns true when a later strategy was skipped due to exhausted budget", () => {
|
|
569
|
+
expect(isTapTimeout([
|
|
570
|
+
{ strategy: "fiber", reason: "fiber timed out after 5000ms" },
|
|
571
|
+
{ strategy: "ocr", reason: "Skipped — only 200ms remaining (budget 20000ms)" },
|
|
572
|
+
])).toBe(true);
|
|
573
|
+
});
|
|
574
|
+
it("returns false when strategy errored quickly with a nested sub-op timeout", () => {
|
|
575
|
+
expect(isTapTimeout([
|
|
576
|
+
{ strategy: "fiber", reason: "CDP getProperties timed out after 150ms" },
|
|
577
|
+
])).toBe(false);
|
|
578
|
+
});
|
|
579
|
+
it("returns false when no element was found and no strategy actually timed out", () => {
|
|
580
|
+
expect(isTapTimeout([
|
|
581
|
+
{ strategy: "fiber", reason: "No element found matching testID=\"foo\"" },
|
|
582
|
+
{ strategy: "ocr", reason: "No OCR text matched" },
|
|
583
|
+
])).toBe(false);
|
|
584
|
+
});
|
|
585
|
+
it("returns false for an empty attempted list", () => {
|
|
586
|
+
expect(isTapTimeout([])).toBe(false);
|
|
587
|
+
});
|
|
588
|
+
it("accepts accessibility, ocr, and coordinate wrapper formats", () => {
|
|
589
|
+
expect(isTapTimeout([{ strategy: "accessibility", reason: "accessibility timed out after 3000ms" }])).toBe(true);
|
|
590
|
+
expect(isTapTimeout([{ strategy: "ocr", reason: "ocr timed out after 5000ms" }])).toBe(true);
|
|
591
|
+
expect(isTapTimeout([{ strategy: "coordinate", reason: "coordinate timed out after 3000ms" }])).toBe(true);
|
|
592
|
+
});
|
|
593
|
+
});
|
|
594
|
+
describe("findOcrMatch", () => {
|
|
595
|
+
function makeWord(text, tap) {
|
|
596
|
+
return {
|
|
597
|
+
text,
|
|
598
|
+
confidence: 0.99,
|
|
599
|
+
bbox: { x0: 0, y0: 0, x1: 10, y1: 10 },
|
|
600
|
+
center: tap,
|
|
601
|
+
tapCenter: tap,
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
function makeLine(text, tap) {
|
|
605
|
+
return {
|
|
606
|
+
text,
|
|
607
|
+
confidence: 0.99,
|
|
608
|
+
bbox: { x0: 0, y0: 0, x1: 100, y1: 10 },
|
|
609
|
+
center: tap,
|
|
610
|
+
tapCenter: tap,
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
function makeResult(opts) {
|
|
614
|
+
return {
|
|
615
|
+
success: true,
|
|
616
|
+
fullText: "",
|
|
617
|
+
confidence: 0.99,
|
|
618
|
+
words: opts.words ?? [],
|
|
619
|
+
lines: opts.lines ?? [],
|
|
620
|
+
processingTimeMs: 0,
|
|
621
|
+
engine: "cloud",
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
it("matches a single-word query against words (regression guard)", () => {
|
|
625
|
+
const result = makeResult({
|
|
626
|
+
words: [makeWord("Submit", { x: 50, y: 100 })],
|
|
627
|
+
});
|
|
628
|
+
const match = findOcrMatch(result, "Submit");
|
|
629
|
+
expect(match).not.toBeNull();
|
|
630
|
+
expect(match.text).toBe("Submit");
|
|
631
|
+
expect(match.tapCenter).toEqual({ x: 50, y: 100 });
|
|
632
|
+
});
|
|
633
|
+
it("matches a multi-word query against a line when the line equals the phrase", () => {
|
|
634
|
+
const result = makeResult({
|
|
635
|
+
words: [makeWord("Save", { x: 10, y: 20 }), makeWord("a", { x: 30, y: 20 })],
|
|
636
|
+
lines: [makeLine("Save a full copy", { x: 200, y: 300 })],
|
|
637
|
+
});
|
|
638
|
+
const match = findOcrMatch(result, "Save a full copy");
|
|
639
|
+
expect(match).not.toBeNull();
|
|
640
|
+
expect(match.tapCenter).toEqual({ x: 200, y: 300 });
|
|
641
|
+
});
|
|
642
|
+
it("matches a multi-word query against a line that contains the phrase as a substring", () => {
|
|
643
|
+
const result = makeResult({
|
|
644
|
+
lines: [makeLine("Please Sign In with Google to continue", { x: 400, y: 500 })],
|
|
645
|
+
});
|
|
646
|
+
const match = findOcrMatch(result, "Sign In with Google");
|
|
647
|
+
expect(match).not.toBeNull();
|
|
648
|
+
expect(match.tapCenter).toEqual({ x: 400, y: 500 });
|
|
649
|
+
});
|
|
650
|
+
it("is case-insensitive on both word and line paths", () => {
|
|
651
|
+
const wordOnly = makeResult({ words: [makeWord("SUBMIT", { x: 1, y: 2 })] });
|
|
652
|
+
expect(findOcrMatch(wordOnly, "submit").tapCenter).toEqual({ x: 1, y: 2 });
|
|
653
|
+
const lineOnly = makeResult({ lines: [makeLine("save A FULL copy", { x: 3, y: 4 })] });
|
|
654
|
+
expect(findOcrMatch(lineOnly, "Save a Full Copy").tapCenter).toEqual({ x: 3, y: 4 });
|
|
655
|
+
});
|
|
656
|
+
it("survives whitespace/punctuation differences and non-Latin characters", () => {
|
|
657
|
+
const result = makeResult({
|
|
658
|
+
lines: [
|
|
659
|
+
makeLine("Moeez's Cookbook", { x: 5, y: 6 }),
|
|
660
|
+
makeLine("Pozwalaj, gdy używam aplikacji", { x: 7, y: 8 }),
|
|
661
|
+
],
|
|
662
|
+
});
|
|
663
|
+
expect(findOcrMatch(result, " Moeez's Cookbook ").tapCenter).toEqual({ x: 5, y: 6 });
|
|
664
|
+
expect(findOcrMatch(result, "Pozwalaj, gdy używam aplikacji").tapCenter).toEqual({ x: 7, y: 8 });
|
|
665
|
+
});
|
|
666
|
+
it("returns null when neither words nor lines contain the query", () => {
|
|
667
|
+
const result = makeResult({
|
|
668
|
+
words: [makeWord("Hello", { x: 0, y: 0 })],
|
|
669
|
+
lines: [makeLine("World peace", { x: 0, y: 0 })],
|
|
670
|
+
});
|
|
671
|
+
expect(findOcrMatch(result, "Sign In with Google")).toBeNull();
|
|
672
|
+
});
|
|
673
|
+
});
|
|
157
674
|
//# sourceMappingURL=tap.test.js.map
|