mcp-baepsae 5.1.0 → 6.2.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 (62) hide show
  1. package/README-KR.md +98 -33
  2. package/README.md +101 -35
  3. package/bundled/baepsae-native +0 -0
  4. package/dist/backend.d.ts +26 -0
  5. package/dist/backend.d.ts.map +1 -0
  6. package/dist/backend.js +79 -0
  7. package/dist/backend.js.map +1 -0
  8. package/dist/index.js +3 -1
  9. package/dist/index.js.map +1 -1
  10. package/dist/tool-manifest.d.ts +12 -0
  11. package/dist/tool-manifest.d.ts.map +1 -0
  12. package/dist/tool-manifest.js +79 -0
  13. package/dist/tool-manifest.js.map +1 -0
  14. package/dist/tools/info.d.ts.map +1 -1
  15. package/dist/tools/info.js +104 -5
  16. package/dist/tools/info.js.map +1 -1
  17. package/dist/tools/input.js +7 -6
  18. package/dist/tools/input.js.map +1 -1
  19. package/dist/tools/media.d.ts.map +1 -1
  20. package/dist/tools/media.js +137 -11
  21. package/dist/tools/media.js.map +1 -1
  22. package/dist/tools/simulator.js +7 -7
  23. package/dist/tools/simulator.js.map +1 -1
  24. package/dist/tools/system.d.ts.map +1 -1
  25. package/dist/tools/system.js +2 -2
  26. package/dist/tools/system.js.map +1 -1
  27. package/dist/tools/ui.d.ts.map +1 -1
  28. package/dist/tools/ui.js +126 -8
  29. package/dist/tools/ui.js.map +1 -1
  30. package/dist/tools/workflow.d.ts +3 -0
  31. package/dist/tools/workflow.d.ts.map +1 -0
  32. package/dist/tools/workflow.js +434 -0
  33. package/dist/tools/workflow.js.map +1 -0
  34. package/dist/types.d.ts +15 -0
  35. package/dist/types.d.ts.map +1 -1
  36. package/dist/utils.d.ts +19 -3
  37. package/dist/utils.d.ts.map +1 -1
  38. package/dist/utils.js +110 -5
  39. package/dist/utils.js.map +1 -1
  40. package/dist/version.d.ts +1 -1
  41. package/dist/version.js +1 -1
  42. package/native/Sources/Commands/InputCommands.swift +53 -33
  43. package/native/Sources/Commands/SystemCommands.swift +86 -0
  44. package/native/Sources/Commands/UICommands.swift +254 -35
  45. package/native/Sources/Commands/WindowCommands.swift +11 -4
  46. package/native/Sources/IndigoHID/IndigoHIDClient.swift +222 -0
  47. package/native/Sources/IndigoHID/IndigoHIDCoordinates.swift +74 -0
  48. package/native/Sources/IndigoHID/IndigoHIDEvents.swift +63 -0
  49. package/native/Sources/IndigoHID/IndigoHIDLoader.swift +102 -0
  50. package/native/Sources/IndigoHID/IndigoHIDTypes.swift +41 -0
  51. package/native/Sources/Types.swift +26 -0
  52. package/native/Sources/Utils.swift +653 -13
  53. package/native/Sources/Version.swift +1 -1
  54. package/native/Sources/main.swift +55 -8
  55. package/native/Tests/BaepsaeNativeTests/BinaryInvocationTests.swift +54 -6
  56. package/package.json +12 -3
  57. package/scripts/dump-tabbar-actions.mjs +312 -0
  58. package/scripts/generate-tool-manifest.mjs +75 -0
  59. package/scripts/research-coordinate-calibration.mjs +276 -0
  60. package/scripts/research-input-channels.mjs +327 -0
  61. package/scripts/research-tap-tab-grid.mjs +271 -0
  62. package/scripts/verify-media-capture.mjs +99 -0
@@ -0,0 +1,276 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { existsSync } from "node:fs";
4
+ import { execFileSync } from "node:child_process";
5
+
6
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
7
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+ const projectRoot = path.resolve(__dirname, "..");
12
+ const realNativePath = path.join(projectRoot, "native", ".build", "release", "baepsae-native");
13
+
14
+ function extractText(result) {
15
+ return result.content
16
+ .filter((item) => item.type === "text")
17
+ .map((item) => item.text)
18
+ .join("\n");
19
+ }
20
+
21
+ function parseFrameFromText(text) {
22
+ const match = text.match(/frame=\(x:([\d.]+),y:([\d.]+),w:([\d.]+),h:([\d.]+)\)/);
23
+ if (!match) return null;
24
+ return {
25
+ x: parseFloat(match[1]),
26
+ y: parseFloat(match[2]),
27
+ width: parseFloat(match[3]),
28
+ height: parseFloat(match[4]),
29
+ };
30
+ }
31
+
32
+ function extractLargestFrameFromLines(text, predicate) {
33
+ const lines = text.split("\n").filter((line) => predicate(line));
34
+ let best = null;
35
+ for (const line of lines) {
36
+ const frame = parseFrameFromText(line);
37
+ if (!frame) continue;
38
+ const area = frame.width * frame.height;
39
+ if (!best || area > best.area) {
40
+ best = { line, frame, area };
41
+ }
42
+ }
43
+ return best ? { line: best.line, frame: best.frame } : null;
44
+ }
45
+
46
+ function chooseSimulatorUdid() {
47
+ const output = execFileSync("xcrun", ["simctl", "list", "devices", "available"], {
48
+ cwd: projectRoot,
49
+ encoding: "utf8",
50
+ stdio: ["ignore", "pipe", "pipe"],
51
+ });
52
+ const booted = output.match(/\(([0-9A-F-]{36})\)\s+\(Booted\)/i);
53
+ if (booted) return booted[1];
54
+
55
+ const shutdownIPhone = output.match(/^\s+iPhone .* \(([0-9A-F-]{36})\) \(Shutdown\)\s*$/m);
56
+ if (shutdownIPhone) return shutdownIPhone[1];
57
+
58
+ throw new Error("No available iPhone Simulator device found.");
59
+ }
60
+
61
+ async function sleep(ms) {
62
+ return await new Promise((resolve) => setTimeout(resolve, ms));
63
+ }
64
+
65
+ async function ensureBootedSimulator(udid) {
66
+ try {
67
+ execFileSync("xcrun", ["simctl", "boot", udid], { cwd: projectRoot, stdio: "pipe" });
68
+ } catch {
69
+ // ignore
70
+ }
71
+ try {
72
+ execFileSync("/usr/bin/open", ["-a", "Simulator", "--args", "-CurrentDeviceUDID", udid], {
73
+ cwd: projectRoot,
74
+ stdio: "pipe",
75
+ });
76
+ } catch {
77
+ // ignore
78
+ }
79
+
80
+ const deadline = Date.now() + 15_000;
81
+ while (Date.now() < deadline) {
82
+ const output = execFileSync("xcrun", ["simctl", "list", "devices", udid], {
83
+ cwd: projectRoot,
84
+ encoding: "utf8",
85
+ stdio: ["ignore", "pipe", "pipe"],
86
+ });
87
+ if (/\(Booted\)/.test(output)) {
88
+ return;
89
+ }
90
+ await sleep(500);
91
+ }
92
+ throw new Error(`Simulator did not boot in time: ${udid}`);
93
+ }
94
+
95
+ const sampleAppPath = [
96
+ path.join(projectRoot, "test-fixtures", "SampleApp", "build", "Debug-iphonesimulator", "SampleApp.app"),
97
+ path.join(projectRoot, "test-fixtures", "SampleApp", "Build", "Products", "Debug-iphonesimulator", "SampleApp.app"),
98
+ ].find((candidate) => existsSync(candidate));
99
+
100
+ async function withClient(run) {
101
+ const client = new Client({ name: "baepsae-coordinate-calibration", version: "1.0.0" });
102
+ const transport = new StdioClientTransport({
103
+ command: "node",
104
+ args: ["dist/index.js"],
105
+ cwd: projectRoot,
106
+ stderr: "pipe",
107
+ env: {
108
+ ...process.env,
109
+ BAEPSAE_NATIVE_PATH: process.env.BAEPSAE_NATIVE_PATH ?? realNativePath,
110
+ },
111
+ });
112
+
113
+ await client.connect(transport);
114
+ try {
115
+ return await run(client);
116
+ } finally {
117
+ await client.close();
118
+ }
119
+ }
120
+
121
+ async function call(client, name, args) {
122
+ const result = await client.callTool({ name, arguments: args });
123
+ return { result, text: extractText(result) };
124
+ }
125
+
126
+ async function relaunchDefaultSampleApp(client, udid) {
127
+ await call(client, "terminate_app", { udid, bundleId: "com.baepsae.sampleapp" }).catch(() => {});
128
+ await sleep(500);
129
+ if (sampleAppPath) {
130
+ await call(client, "install_app", { udid, path: sampleAppPath });
131
+ }
132
+ await call(client, "launch_app", { udid, bundleId: "com.baepsae.sampleapp" });
133
+ const deadline = Date.now() + 10_000;
134
+ while (Date.now() < deadline) {
135
+ const probe = await call(client, "analyze_ui", { udid, focusId: "nav-basic", maxDepth: 1 });
136
+ if (!probe.result.isError && probe.text.includes("Basic")) {
137
+ return;
138
+ }
139
+ await sleep(500);
140
+ }
141
+ throw new Error("SampleApp default screen did not become ready in time.");
142
+ }
143
+
144
+ function toContentRelativePoint(frame, contentFrame, xRatio = 0.5, yRatio = 0.5) {
145
+ return {
146
+ x: Math.round(frame.x - contentFrame.x + frame.width * xRatio),
147
+ y: Math.round(frame.y - contentFrame.y + frame.height * yRatio),
148
+ };
149
+ }
150
+
151
+ function toWindowRelativePoint(frame, windowFrame, xRatio = 0.5, yRatio = 0.5) {
152
+ return {
153
+ x: Math.round(frame.x - windowFrame.x + frame.width * xRatio),
154
+ y: Math.round(frame.y - windowFrame.y + frame.height * yRatio),
155
+ };
156
+ }
157
+
158
+ async function waitForAnchor(client, udid, focusId, pattern, timeoutMs = 5000) {
159
+ const deadline = Date.now() + timeoutMs;
160
+ let lastText = "";
161
+ while (Date.now() < deadline) {
162
+ const result = await call(client, "analyze_ui", { udid, focusId, maxDepth: 1 });
163
+ lastText = result.text;
164
+ if (!result.result.isError && pattern.test(result.text)) {
165
+ return { ok: true, text: result.text };
166
+ }
167
+ await sleep(400);
168
+ }
169
+ return { ok: false, text: lastText };
170
+ }
171
+
172
+ async function measureCase(client, udid, config) {
173
+ await relaunchDefaultSampleApp(client, udid);
174
+
175
+ const targetResult = await call(client, "analyze_ui", { udid, focusId: config.sourceFocusId, maxDepth: 1 });
176
+ const targetFrame = parseFrameFromText(targetResult.text);
177
+ const allTree = await call(client, "analyze_ui", { udid, all: true, maxDepth: 8 });
178
+ const contentCandidate = extractLargestFrameFromLines(
179
+ allTree.text,
180
+ (line) => line.includes("subrole=iOSContentGroup"),
181
+ );
182
+ const windowCandidate = extractLargestFrameFromLines(
183
+ allTree.text,
184
+ (line) => line.includes("role=AXWindow subrole=AXStandardWindow"),
185
+ );
186
+ const contentFrame = contentCandidate?.frame ?? null;
187
+ const windowFrame = windowCandidate?.frame ?? null;
188
+
189
+ if (!targetFrame || !contentFrame || !windowFrame) {
190
+ return {
191
+ ...config,
192
+ ok: false,
193
+ reason: "frame_missing",
194
+ targetFrame,
195
+ contentFrame,
196
+ windowFrame,
197
+ };
198
+ }
199
+
200
+ const contentPoint = toContentRelativePoint(targetFrame, contentFrame);
201
+ const windowPoint = toWindowRelativePoint(targetFrame, windowFrame);
202
+ const point = config.base === "content" ? contentPoint : windowPoint;
203
+
204
+ await call(client, "tap", { udid, x: point.x, y: point.y });
205
+ await sleep(800);
206
+
207
+ const verification = await waitForAnchor(client, udid, config.expectedFocusId, config.expectedPattern, 5000);
208
+
209
+ return {
210
+ ...config,
211
+ ok: verification.ok,
212
+ point,
213
+ targetFrame,
214
+ contentFrame,
215
+ windowFrame,
216
+ verificationText: verification.text,
217
+ };
218
+ }
219
+
220
+ async function main() {
221
+ const udid = chooseSimulatorUdid();
222
+ await ensureBootedSimulator(udid);
223
+
224
+ const summary = await withClient(async (client) => {
225
+ const cases = [
226
+ {
227
+ caseId: "nav-scroll-content",
228
+ sourceFocusId: "nav-scroll",
229
+ expectedFocusId: "scroll-position",
230
+ expectedPattern: /Visible:\s*Item\s+\d+\s*~\s*Item\s+\d+/,
231
+ base: "content",
232
+ },
233
+ {
234
+ caseId: "nav-scroll-window",
235
+ sourceFocusId: "nav-scroll",
236
+ expectedFocusId: "scroll-position",
237
+ expectedPattern: /Visible:\s*Item\s+\d+\s*~\s*Item\s+\d+/,
238
+ base: "window",
239
+ },
240
+ {
241
+ caseId: "test-button-content",
242
+ sourceFocusId: "test-button",
243
+ expectedFocusId: "test-label",
244
+ expectedPattern: /Tapped!/,
245
+ base: "content",
246
+ },
247
+ {
248
+ caseId: "test-button-window",
249
+ sourceFocusId: "test-button",
250
+ expectedFocusId: "test-label",
251
+ expectedPattern: /Tapped!/,
252
+ base: "window",
253
+ },
254
+ ];
255
+
256
+ const results = [];
257
+ for (const config of cases) {
258
+ results.push(await measureCase(client, udid, config));
259
+ }
260
+
261
+ return {
262
+ udid,
263
+ results,
264
+ contentWins: results.filter((entry) => entry.base === "content" && entry.ok).length,
265
+ windowWins: results.filter((entry) => entry.base === "window" && entry.ok).length,
266
+ };
267
+ });
268
+
269
+ console.log(JSON.stringify(summary, null, 2));
270
+ process.exitCode = summary.results.some((entry) => entry.ok) ? 0 : 1;
271
+ }
272
+
273
+ main().catch((error) => {
274
+ console.error(error instanceof Error ? error.message : String(error));
275
+ process.exitCode = 1;
276
+ });
@@ -0,0 +1,327 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { existsSync } from "node:fs";
4
+ import { execFileSync } from "node:child_process";
5
+
6
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
7
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+ const projectRoot = path.resolve(__dirname, "..");
12
+ const realNativePath = path.join(projectRoot, "native", ".build", "release", "baepsae-native");
13
+
14
+ function extractText(result) {
15
+ return result.content
16
+ .filter((item) => item.type === "text")
17
+ .map((item) => item.text)
18
+ .join("\n");
19
+ }
20
+
21
+ function parseFrameFromText(text) {
22
+ const match = text.match(/frame=\(x:([\d.]+),y:([\d.]+),w:([\d.]+),h:([\d.]+)\)/);
23
+ if (!match) return null;
24
+ return {
25
+ x: parseFloat(match[1]),
26
+ y: parseFloat(match[2]),
27
+ width: parseFloat(match[3]),
28
+ height: parseFloat(match[4]),
29
+ };
30
+ }
31
+
32
+ function extractLargestFrameFromLines(text, predicate) {
33
+ const lines = text.split("\n").filter((line) => predicate(line));
34
+ let best = null;
35
+ for (const line of lines) {
36
+ const frame = parseFrameFromText(line);
37
+ if (!frame) continue;
38
+ const area = frame.width * frame.height;
39
+ if (!best || area > best.area) {
40
+ best = { line, frame, area };
41
+ }
42
+ }
43
+ return best ? { line: best.line, frame: best.frame } : null;
44
+ }
45
+
46
+ function chooseSimulatorUdid() {
47
+ const output = execFileSync("xcrun", ["simctl", "list", "devices", "available"], {
48
+ cwd: projectRoot,
49
+ encoding: "utf8",
50
+ stdio: ["ignore", "pipe", "pipe"],
51
+ });
52
+ const booted = output.match(/\(([0-9A-F-]{36})\)\s+\(Booted\)/i);
53
+ if (booted) return booted[1];
54
+
55
+ const shutdownIPhone = output.match(/^\s+iPhone .* \(([0-9A-F-]{36})\) \(Shutdown\)\s*$/m);
56
+ if (shutdownIPhone) return shutdownIPhone[1];
57
+
58
+ throw new Error("No available iPhone Simulator device found.");
59
+ }
60
+
61
+ async function sleep(ms) {
62
+ return await new Promise((resolve) => setTimeout(resolve, ms));
63
+ }
64
+
65
+ async function ensureBootedSimulator(udid) {
66
+ try {
67
+ execFileSync("xcrun", ["simctl", "boot", udid], { cwd: projectRoot, stdio: "pipe" });
68
+ } catch {
69
+ // ignore
70
+ }
71
+ try {
72
+ execFileSync("/usr/bin/open", ["-a", "Simulator", "--args", "-CurrentDeviceUDID", udid], {
73
+ cwd: projectRoot,
74
+ stdio: "pipe",
75
+ });
76
+ } catch {
77
+ // ignore
78
+ }
79
+
80
+ const deadline = Date.now() + 15_000;
81
+ while (Date.now() < deadline) {
82
+ const output = execFileSync("xcrun", ["simctl", "list", "devices", udid], {
83
+ cwd: projectRoot,
84
+ encoding: "utf8",
85
+ stdio: ["ignore", "pipe", "pipe"],
86
+ });
87
+ if (/\(Booted\)/.test(output)) {
88
+ return;
89
+ }
90
+ await sleep(500);
91
+ }
92
+
93
+ throw new Error(`Simulator did not boot in time: ${udid}`);
94
+ }
95
+
96
+ const sampleAppPath = [
97
+ path.join(projectRoot, "test-fixtures", "SampleApp", "build", "Debug-iphonesimulator", "SampleApp.app"),
98
+ path.join(projectRoot, "test-fixtures", "SampleApp", "Build", "Products", "Debug-iphonesimulator", "SampleApp.app"),
99
+ ].find((candidate) => existsSync(candidate));
100
+
101
+ async function withClient(run) {
102
+ const client = new Client({ name: "baepsae-input-channel-research", version: "1.0.0" });
103
+ const transport = new StdioClientTransport({
104
+ command: "node",
105
+ args: ["dist/index.js"],
106
+ cwd: projectRoot,
107
+ stderr: "pipe",
108
+ env: {
109
+ ...process.env,
110
+ BAEPSAE_NATIVE_PATH: process.env.BAEPSAE_NATIVE_PATH ?? realNativePath,
111
+ },
112
+ });
113
+
114
+ await client.connect(transport);
115
+ try {
116
+ return await run(client);
117
+ } finally {
118
+ await client.close();
119
+ }
120
+ }
121
+
122
+ async function call(client, name, args) {
123
+ const result = await client.callTool({ name, arguments: args });
124
+ return { result, text: extractText(result) };
125
+ }
126
+
127
+ async function relaunchDefaultSampleApp(client, udid) {
128
+ await call(client, "terminate_app", { udid, bundleId: "com.baepsae.sampleapp" }).catch(() => {});
129
+ await sleep(500);
130
+ if (sampleAppPath) {
131
+ await call(client, "install_app", { udid, path: sampleAppPath });
132
+ }
133
+ await call(client, "launch_app", { udid, bundleId: "com.baepsae.sampleapp" });
134
+ const deadline = Date.now() + 10_000;
135
+ while (Date.now() < deadline) {
136
+ const probe = await call(client, "analyze_ui", { udid, focusId: "nav-basic", maxDepth: 1 });
137
+ if (!probe.result.isError && probe.text.includes("Basic")) {
138
+ return;
139
+ }
140
+ await sleep(500);
141
+ }
142
+ throw new Error("SampleApp default screen did not become ready in time.");
143
+ }
144
+
145
+ function toContentRelativePoint(frame, contentFrame, xRatio = 0.5, yRatio = 0.5) {
146
+ return {
147
+ x: Math.round(frame.x - contentFrame.x + frame.width * xRatio),
148
+ y: Math.round(frame.y - contentFrame.y + frame.height * yRatio),
149
+ };
150
+ }
151
+
152
+ function toWindowRelativePoint(frame, windowFrame, xRatio = 0.5, yRatio = 0.5) {
153
+ return {
154
+ x: Math.round(frame.x - windowFrame.x + frame.width * xRatio),
155
+ y: Math.round(frame.y - windowFrame.y + frame.height * yRatio),
156
+ };
157
+ }
158
+
159
+ async function waitForAnchor(client, udid, focusId, pattern, timeoutMs = 5000) {
160
+ const deadline = Date.now() + timeoutMs;
161
+ let lastText = "";
162
+ while (Date.now() < deadline) {
163
+ const result = await call(client, "analyze_ui", { udid, focusId, maxDepth: 1 });
164
+ lastText = result.text;
165
+ if (!result.result.isError && pattern.test(result.text)) {
166
+ return { ok: true, text: result.text };
167
+ }
168
+ await sleep(400);
169
+ }
170
+ return { ok: false, text: lastText };
171
+ }
172
+
173
+ async function runChannel(client, udid, channel, sourceFocusId, contentPoint, windowPoint) {
174
+ switch (channel) {
175
+ case "selector_tap":
176
+ return await call(client, "tap", { udid, id: sourceFocusId });
177
+ case "coord_tap_content":
178
+ return await call(client, "tap", { udid, x: contentPoint.x, y: contentPoint.y });
179
+ case "coord_tap_window":
180
+ return await call(client, "tap", { udid, x: windowPoint.x, y: windowPoint.y });
181
+ case "touch_content":
182
+ return await call(client, "touch", { udid, x: contentPoint.x, y: contentPoint.y, down: true, up: true, delay: 0.02 });
183
+ case "touch_window":
184
+ return await call(client, "touch", { udid, x: windowPoint.x, y: windowPoint.y, down: true, up: true, delay: 0.02 });
185
+ case "micro_drag_content":
186
+ return await call(client, "drag_drop", {
187
+ udid,
188
+ startX: contentPoint.x,
189
+ startY: contentPoint.y,
190
+ endX: contentPoint.x + 2,
191
+ endY: contentPoint.y + 2,
192
+ duration: 0.02,
193
+ holdDuration: 0,
194
+ });
195
+ case "micro_drag_window":
196
+ return await call(client, "drag_drop", {
197
+ udid,
198
+ startX: windowPoint.x,
199
+ startY: windowPoint.y,
200
+ endX: windowPoint.x + 2,
201
+ endY: windowPoint.y + 2,
202
+ duration: 0.02,
203
+ holdDuration: 0,
204
+ });
205
+ default:
206
+ throw new Error(`Unknown channel: ${channel}`);
207
+ }
208
+ }
209
+
210
+ async function measureCase(client, udid, config) {
211
+ await relaunchDefaultSampleApp(client, udid);
212
+
213
+ const targetResult = await call(client, "analyze_ui", { udid, focusId: config.sourceFocusId, maxDepth: 1 });
214
+ const targetFrame = parseFrameFromText(targetResult.text);
215
+ const allTree = await call(client, "analyze_ui", { udid, all: true, maxDepth: 8 });
216
+ const contentCandidate = extractLargestFrameFromLines(
217
+ allTree.text,
218
+ (line) => line.includes("subrole=iOSContentGroup"),
219
+ );
220
+ const windowCandidate = extractLargestFrameFromLines(
221
+ allTree.text,
222
+ (line) => line.includes("role=AXWindow subrole=AXStandardWindow"),
223
+ );
224
+ const contentFrame = contentCandidate?.frame ?? null;
225
+ const windowFrame = windowCandidate?.frame ?? null;
226
+
227
+ if (!targetFrame || !contentFrame || !windowFrame) {
228
+ return {
229
+ ...config,
230
+ ok: false,
231
+ reason: "frame_missing",
232
+ targetFrame,
233
+ contentFrame,
234
+ windowFrame,
235
+ };
236
+ }
237
+
238
+ const contentPoint = toContentRelativePoint(targetFrame, contentFrame);
239
+ const windowPoint = toWindowRelativePoint(targetFrame, windowFrame);
240
+
241
+ const channelResult = await runChannel(
242
+ client,
243
+ udid,
244
+ config.channel,
245
+ config.sourceFocusId,
246
+ contentPoint,
247
+ windowPoint,
248
+ );
249
+ await sleep(900);
250
+
251
+ const verification = await waitForAnchor(client, udid, config.expectedFocusId, config.expectedPattern, 5000);
252
+
253
+ return {
254
+ ...config,
255
+ ok: verification.ok,
256
+ channelIsError: !!channelResult.result.isError,
257
+ contentPoint,
258
+ windowPoint,
259
+ targetFrame,
260
+ contentFrame,
261
+ windowFrame,
262
+ channelText: channelResult.text,
263
+ verificationText: verification.text,
264
+ };
265
+ }
266
+
267
+ async function main() {
268
+ const udid = chooseSimulatorUdid();
269
+ await ensureBootedSimulator(udid);
270
+
271
+ const summary = await withClient(async (client) => {
272
+ const cases = [
273
+ {
274
+ sourceFocusId: "nav-scroll",
275
+ expectedFocusId: "scroll-position",
276
+ expectedPattern: /Visible:\s*Item\s+\d+\s*~\s*Item\s+\d+/,
277
+ channels: [
278
+ "selector_tap",
279
+ "coord_tap_content",
280
+ "coord_tap_window",
281
+ "touch_content",
282
+ "touch_window",
283
+ "micro_drag_content",
284
+ "micro_drag_window",
285
+ ],
286
+ },
287
+ {
288
+ sourceFocusId: "test-button",
289
+ expectedFocusId: "test-label",
290
+ expectedPattern: /Tapped!/,
291
+ channels: [
292
+ "selector_tap",
293
+ "coord_tap_content",
294
+ "coord_tap_window",
295
+ "touch_content",
296
+ "touch_window",
297
+ "micro_drag_content",
298
+ "micro_drag_window",
299
+ ],
300
+ },
301
+ ];
302
+
303
+ const results = [];
304
+ for (const group of cases) {
305
+ for (const channel of group.channels) {
306
+ results.push(await measureCase(client, udid, { ...group, channel }));
307
+ }
308
+ }
309
+
310
+ return {
311
+ udid,
312
+ results,
313
+ successes: results.filter((entry) => entry.ok).map((entry) => ({
314
+ sourceFocusId: entry.sourceFocusId,
315
+ channel: entry.channel,
316
+ })),
317
+ };
318
+ });
319
+
320
+ console.log(JSON.stringify(summary, null, 2));
321
+ process.exitCode = summary.successes.length > 0 ? 0 : 1;
322
+ }
323
+
324
+ main().catch((error) => {
325
+ console.error(error instanceof Error ? error.message : String(error));
326
+ process.exitCode = 1;
327
+ });