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
package/build/core/ios.js
CHANGED
|
@@ -5,14 +5,23 @@ import path from "path";
|
|
|
5
5
|
import os from "os";
|
|
6
6
|
import sharp from "sharp";
|
|
7
7
|
import { getActiveSimulatorUdid } from "./state.js";
|
|
8
|
+
import { notifyDriverMissing } from "./logbox.js";
|
|
8
9
|
const execAsync = promisify(exec);
|
|
9
10
|
const execFileAsync = promisify(execFile);
|
|
10
11
|
// simctl command timeout in milliseconds
|
|
11
12
|
const SIMCTL_TIMEOUT = 30000;
|
|
12
13
|
// IDB command timeout in milliseconds
|
|
13
14
|
const IDB_TIMEOUT = 30000;
|
|
15
|
+
// AXe command timeout in milliseconds
|
|
16
|
+
const AXE_TIMEOUT = 30000;
|
|
14
17
|
// Valid button types for IDB ui button command
|
|
15
|
-
export const IOS_BUTTON_TYPES = [
|
|
18
|
+
export const IOS_BUTTON_TYPES = [
|
|
19
|
+
"HOME",
|
|
20
|
+
"LOCK",
|
|
21
|
+
"SIDE_BUTTON",
|
|
22
|
+
"SIRI",
|
|
23
|
+
"APPLE_PAY",
|
|
24
|
+
];
|
|
16
25
|
// Track connected IDB simulators to avoid redundant connect calls
|
|
17
26
|
const connectedIdbSimulators = new Set();
|
|
18
27
|
/**
|
|
@@ -22,6 +31,36 @@ const connectedIdbSimulators = new Set();
|
|
|
22
31
|
function getIdbPath() {
|
|
23
32
|
return process.env.IDB_PATH || "idb";
|
|
24
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Get the active iOS UI driver.
|
|
36
|
+
* Set IOS_DRIVER=axe in MCP server env to use AXe CLI instead of IDB.
|
|
37
|
+
*/
|
|
38
|
+
function getIosDriver() {
|
|
39
|
+
const driver = process.env.IOS_DRIVER?.toLowerCase();
|
|
40
|
+
if (driver === "axe")
|
|
41
|
+
return "axe";
|
|
42
|
+
return "idb";
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Get the AXe executable path.
|
|
46
|
+
* Supports AXE_PATH environment variable for custom installations.
|
|
47
|
+
*/
|
|
48
|
+
function getAxePath() {
|
|
49
|
+
return process.env.AXE_PATH || "axe";
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Run AXe command with execFile (no shell) for proper argument handling.
|
|
53
|
+
*/
|
|
54
|
+
async function runAxe(...args) {
|
|
55
|
+
const axePath = getAxePath();
|
|
56
|
+
const { stdout, stderr } = await execFileAsync(axePath, args, {
|
|
57
|
+
timeout: AXE_TIMEOUT,
|
|
58
|
+
});
|
|
59
|
+
return {
|
|
60
|
+
stdout: stdout.trim(),
|
|
61
|
+
stderr: stderr.trim(),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
25
64
|
/**
|
|
26
65
|
* Run IDB command with execFile (no shell) for proper argument handling
|
|
27
66
|
* This matches the original ios-simulator-mcp implementation
|
|
@@ -29,11 +68,11 @@ function getIdbPath() {
|
|
|
29
68
|
async function runIdb(...args) {
|
|
30
69
|
const idbPath = getIdbPath();
|
|
31
70
|
const { stdout, stderr } = await execFileAsync(idbPath, args, {
|
|
32
|
-
timeout: IDB_TIMEOUT
|
|
71
|
+
timeout: IDB_TIMEOUT,
|
|
33
72
|
});
|
|
34
73
|
return {
|
|
35
74
|
stdout: stdout.trim(),
|
|
36
|
-
stderr: stderr.trim()
|
|
75
|
+
stderr: stderr.trim(),
|
|
37
76
|
};
|
|
38
77
|
}
|
|
39
78
|
/**
|
|
@@ -48,6 +87,49 @@ export async function isIdbAvailable() {
|
|
|
48
87
|
return false;
|
|
49
88
|
}
|
|
50
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Check if AXe CLI is available
|
|
92
|
+
*/
|
|
93
|
+
export async function isAxeAvailable() {
|
|
94
|
+
try {
|
|
95
|
+
await runAxe("--version");
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Check if the active iOS UI driver is available.
|
|
104
|
+
* Used by tap.ts for early fail-fast checks.
|
|
105
|
+
*/
|
|
106
|
+
export async function isUiDriverAvailable() {
|
|
107
|
+
const driver = getIosDriver();
|
|
108
|
+
return driver === "axe" ? isAxeAvailable() : isIdbAvailable();
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Get install instructions for the active iOS UI driver.
|
|
112
|
+
*/
|
|
113
|
+
export function getUiDriverInstallHint() {
|
|
114
|
+
const driver = getIosDriver();
|
|
115
|
+
if (driver === "axe") {
|
|
116
|
+
return "AXe is not installed. Install with: brew install cameroncooke/axe/axe\nAXe is the recommended iOS UI driver. See: https://github.com/cameroncooke/AXe";
|
|
117
|
+
}
|
|
118
|
+
return "IDB is not installed. Install with: brew install idb-companion\nTip: Consider switching to AXe (recommended) — brew install cameroncooke/axe/axe and set IOS_DRIVER=axe";
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Map IDB button names (uppercase/underscores) to AXe button names (lowercase/hyphens).
|
|
122
|
+
*/
|
|
123
|
+
function toAxeButtonName(idbButton) {
|
|
124
|
+
const map = {
|
|
125
|
+
HOME: "home",
|
|
126
|
+
LOCK: "lock",
|
|
127
|
+
SIDE_BUTTON: "side-button",
|
|
128
|
+
SIRI: "siri",
|
|
129
|
+
APPLE_PAY: "apple-pay",
|
|
130
|
+
};
|
|
131
|
+
return map[idbButton];
|
|
132
|
+
}
|
|
51
133
|
/**
|
|
52
134
|
* Ensure IDB is connected to the specified simulator
|
|
53
135
|
* IDB requires `idb connect <UDID>` before any UI commands work
|
|
@@ -65,15 +147,63 @@ async function ensureIdbConnected(udid) {
|
|
|
65
147
|
catch (error) {
|
|
66
148
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
67
149
|
// "Already connected" is not an error
|
|
68
|
-
if (errorMessage.includes("already connected") ||
|
|
150
|
+
if (errorMessage.includes("already connected") ||
|
|
151
|
+
errorMessage.includes("Connected")) {
|
|
69
152
|
connectedIdbSimulators.add(udid);
|
|
70
153
|
return { success: true };
|
|
71
154
|
}
|
|
72
155
|
return {
|
|
73
156
|
success: false,
|
|
74
|
-
error: `Failed to connect IDB to simulator: ${errorMessage}
|
|
157
|
+
error: `Failed to connect IDB to simulator: ${errorMessage}`,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Shared pre-flight check for all UI driver functions:
|
|
163
|
+
* 1. Check driver availability
|
|
164
|
+
* 2. Resolve target simulator UDID
|
|
165
|
+
* 3. Ensure IDB connection (IDB only)
|
|
166
|
+
*/
|
|
167
|
+
async function ensureUiDriverReady(udid) {
|
|
168
|
+
const driver = getIosDriver();
|
|
169
|
+
if (driver === "axe") {
|
|
170
|
+
if (!(await isAxeAvailable())) {
|
|
171
|
+
notifyDriverMissing("ios");
|
|
172
|
+
return {
|
|
173
|
+
ready: false,
|
|
174
|
+
error: { success: false, error: getUiDriverInstallHint() },
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
if (!(await isIdbAvailable())) {
|
|
180
|
+
notifyDriverMissing("ios");
|
|
181
|
+
return {
|
|
182
|
+
ready: false,
|
|
183
|
+
error: { success: false, error: getUiDriverInstallHint() },
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const targetUdid = udid || (await getActiveOrBootedSimulatorUdid());
|
|
188
|
+
if (!targetUdid) {
|
|
189
|
+
return {
|
|
190
|
+
ready: false,
|
|
191
|
+
error: {
|
|
192
|
+
success: false,
|
|
193
|
+
error: "No iOS simulator is currently running. Start a simulator first.",
|
|
194
|
+
},
|
|
75
195
|
};
|
|
76
196
|
}
|
|
197
|
+
if (driver === "idb") {
|
|
198
|
+
const connectResult = await ensureIdbConnected(targetUdid);
|
|
199
|
+
if (!connectResult.success) {
|
|
200
|
+
return {
|
|
201
|
+
ready: false,
|
|
202
|
+
error: { success: false, error: connectResult.error },
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return { ready: true, driver, targetUdid };
|
|
77
207
|
}
|
|
78
208
|
/**
|
|
79
209
|
* Check if simctl is available
|
|
@@ -96,11 +226,11 @@ export async function listIOSSimulators(onlyBooted = false) {
|
|
|
96
226
|
if (!simctlAvailable) {
|
|
97
227
|
return {
|
|
98
228
|
success: false,
|
|
99
|
-
error: "Xcode command line tools not available. Install Xcode from the App Store."
|
|
229
|
+
error: "Xcode command line tools not available. Install Xcode from the App Store.",
|
|
100
230
|
};
|
|
101
231
|
}
|
|
102
232
|
const { stdout } = await execAsync("xcrun simctl list devices -j", {
|
|
103
|
-
timeout: SIMCTL_TIMEOUT
|
|
233
|
+
timeout: SIMCTL_TIMEOUT,
|
|
104
234
|
});
|
|
105
235
|
const data = JSON.parse(stdout);
|
|
106
236
|
const simulators = [];
|
|
@@ -115,14 +245,16 @@ export async function listIOSSimulators(onlyBooted = false) {
|
|
|
115
245
|
continue;
|
|
116
246
|
// Extract iOS version from runtime string
|
|
117
247
|
const runtimeMatch = runtime.match(/iOS[- ](\d+[.-]\d+)/i);
|
|
118
|
-
const runtimeVersion = runtimeMatch
|
|
248
|
+
const runtimeVersion = runtimeMatch
|
|
249
|
+
? `iOS ${runtimeMatch[1].replace("-", ".")}`
|
|
250
|
+
: runtime;
|
|
119
251
|
simulators.push({
|
|
120
252
|
udid: device.udid,
|
|
121
253
|
name: device.name,
|
|
122
254
|
state: device.state,
|
|
123
255
|
runtime: runtimeVersion,
|
|
124
256
|
deviceType: device.deviceTypeIdentifier,
|
|
125
|
-
isAvailable: device.isAvailable
|
|
257
|
+
isAvailable: device.isAvailable,
|
|
126
258
|
});
|
|
127
259
|
}
|
|
128
260
|
}
|
|
@@ -132,7 +264,7 @@ export async function listIOSSimulators(onlyBooted = false) {
|
|
|
132
264
|
result: onlyBooted
|
|
133
265
|
? "No booted iOS simulators. Start a simulator first."
|
|
134
266
|
: "No available iOS simulators found.",
|
|
135
|
-
simulators: []
|
|
267
|
+
simulators: [],
|
|
136
268
|
};
|
|
137
269
|
}
|
|
138
270
|
// Sort: Booted first, then by name
|
|
@@ -152,13 +284,13 @@ export async function listIOSSimulators(onlyBooted = false) {
|
|
|
152
284
|
return {
|
|
153
285
|
success: true,
|
|
154
286
|
result: `iOS Simulators:\n\n${formatted}`,
|
|
155
|
-
simulators
|
|
287
|
+
simulators,
|
|
156
288
|
};
|
|
157
289
|
}
|
|
158
290
|
catch (error) {
|
|
159
291
|
return {
|
|
160
292
|
success: false,
|
|
161
|
-
error: `Failed to list simulators: ${error instanceof Error ? error.message : String(error)}
|
|
293
|
+
error: `Failed to list simulators: ${error instanceof Error ? error.message : String(error)}`,
|
|
162
294
|
};
|
|
163
295
|
}
|
|
164
296
|
}
|
|
@@ -168,7 +300,7 @@ export async function listIOSSimulators(onlyBooted = false) {
|
|
|
168
300
|
export async function getBootedSimulatorUdid() {
|
|
169
301
|
try {
|
|
170
302
|
const { stdout } = await execAsync("xcrun simctl list devices booted -j", {
|
|
171
|
-
timeout: SIMCTL_TIMEOUT
|
|
303
|
+
timeout: SIMCTL_TIMEOUT,
|
|
172
304
|
});
|
|
173
305
|
const data = JSON.parse(stdout);
|
|
174
306
|
for (const devices of Object.values(data.devices)) {
|
|
@@ -193,7 +325,7 @@ export async function getBootedSimulatorUdid() {
|
|
|
193
325
|
export async function findSimulatorByName(deviceName) {
|
|
194
326
|
try {
|
|
195
327
|
const { stdout } = await execAsync("xcrun simctl list devices booted -j", {
|
|
196
|
-
timeout: SIMCTL_TIMEOUT
|
|
328
|
+
timeout: SIMCTL_TIMEOUT,
|
|
197
329
|
});
|
|
198
330
|
const data = JSON.parse(stdout);
|
|
199
331
|
const normalizedDeviceName = deviceName.toLowerCase().trim();
|
|
@@ -249,7 +381,7 @@ export async function iosScreenshot(outputPath, udid) {
|
|
|
249
381
|
if (!simctlAvailable) {
|
|
250
382
|
return {
|
|
251
383
|
success: false,
|
|
252
|
-
error: "Xcode command line tools not available. Install Xcode from the App Store."
|
|
384
|
+
error: "Xcode command line tools not available. Install Xcode from the App Store.",
|
|
253
385
|
};
|
|
254
386
|
}
|
|
255
387
|
// Resolve target UDID (prefer Metro-connected simulator)
|
|
@@ -257,14 +389,14 @@ export async function iosScreenshot(outputPath, udid) {
|
|
|
257
389
|
if (!targetUdid) {
|
|
258
390
|
return {
|
|
259
391
|
success: false,
|
|
260
|
-
error: "No iOS simulator is currently running. Start a simulator first."
|
|
392
|
+
error: "No iOS simulator is currently running. Start a simulator first.",
|
|
261
393
|
};
|
|
262
394
|
}
|
|
263
395
|
// Generate output path if not provided
|
|
264
396
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
265
397
|
const finalOutputPath = outputPath || path.join(os.tmpdir(), `ios-screenshot-${timestamp}.png`);
|
|
266
398
|
await execAsync(`xcrun simctl io ${targetUdid} screenshot "${finalOutputPath}"`, {
|
|
267
|
-
timeout: SIMCTL_TIMEOUT
|
|
399
|
+
timeout: SIMCTL_TIMEOUT,
|
|
268
400
|
});
|
|
269
401
|
// Resize image if needed (API limit: 2000px max for multi-image requests)
|
|
270
402
|
// Return scale factor so AI can convert image coords to device coords
|
|
@@ -281,15 +413,13 @@ export async function iosScreenshot(outputPath, udid) {
|
|
|
281
413
|
imageData = await image
|
|
282
414
|
.resize(MAX_DIMENSION, MAX_DIMENSION, {
|
|
283
415
|
fit: "inside",
|
|
284
|
-
withoutEnlargement: true
|
|
416
|
+
withoutEnlargement: true,
|
|
285
417
|
})
|
|
286
418
|
.jpeg({ quality: 85 })
|
|
287
419
|
.toBuffer();
|
|
288
420
|
}
|
|
289
421
|
else {
|
|
290
|
-
imageData = await image
|
|
291
|
-
.jpeg({ quality: 85 })
|
|
292
|
-
.toBuffer();
|
|
422
|
+
imageData = await image.jpeg({ quality: 85 }).toBuffer();
|
|
293
423
|
}
|
|
294
424
|
return {
|
|
295
425
|
success: true,
|
|
@@ -297,13 +427,13 @@ export async function iosScreenshot(outputPath, udid) {
|
|
|
297
427
|
data: imageData,
|
|
298
428
|
scaleFactor,
|
|
299
429
|
originalWidth,
|
|
300
|
-
originalHeight
|
|
430
|
+
originalHeight,
|
|
301
431
|
};
|
|
302
432
|
}
|
|
303
433
|
catch (error) {
|
|
304
434
|
return {
|
|
305
435
|
success: false,
|
|
306
|
-
error: `Failed to capture screenshot: ${error instanceof Error ? error.message : String(error)}
|
|
436
|
+
error: `Failed to capture screenshot: ${error instanceof Error ? error.message : String(error)}`,
|
|
307
437
|
};
|
|
308
438
|
}
|
|
309
439
|
}
|
|
@@ -316,14 +446,14 @@ export async function iosInstallApp(appPath, udid) {
|
|
|
316
446
|
if (!simctlAvailable) {
|
|
317
447
|
return {
|
|
318
448
|
success: false,
|
|
319
|
-
error: "Xcode command line tools not available. Install Xcode from the App Store."
|
|
449
|
+
error: "Xcode command line tools not available. Install Xcode from the App Store.",
|
|
320
450
|
};
|
|
321
451
|
}
|
|
322
452
|
// Verify app exists
|
|
323
453
|
if (!existsSync(appPath)) {
|
|
324
454
|
return {
|
|
325
455
|
success: false,
|
|
326
|
-
error: `App bundle not found: ${appPath}
|
|
456
|
+
error: `App bundle not found: ${appPath}`,
|
|
327
457
|
};
|
|
328
458
|
}
|
|
329
459
|
// Resolve target UDID (prefer Metro-connected simulator)
|
|
@@ -331,21 +461,21 @@ export async function iosInstallApp(appPath, udid) {
|
|
|
331
461
|
if (!targetUdid) {
|
|
332
462
|
return {
|
|
333
463
|
success: false,
|
|
334
|
-
error: "No iOS simulator is currently running. Start a simulator first."
|
|
464
|
+
error: "No iOS simulator is currently running. Start a simulator first.",
|
|
335
465
|
};
|
|
336
466
|
}
|
|
337
467
|
await execAsync(`xcrun simctl install ${targetUdid} "${appPath}"`, {
|
|
338
|
-
timeout: 120000 // 2 minute timeout for install
|
|
468
|
+
timeout: 120000, // 2 minute timeout for install
|
|
339
469
|
});
|
|
340
470
|
return {
|
|
341
471
|
success: true,
|
|
342
|
-
result: `Successfully installed ${path.basename(appPath)}
|
|
472
|
+
result: `Successfully installed ${path.basename(appPath)}`,
|
|
343
473
|
};
|
|
344
474
|
}
|
|
345
475
|
catch (error) {
|
|
346
476
|
return {
|
|
347
477
|
success: false,
|
|
348
|
-
error: `Failed to install app: ${error instanceof Error ? error.message : String(error)}
|
|
478
|
+
error: `Failed to install app: ${error instanceof Error ? error.message : String(error)}`,
|
|
349
479
|
};
|
|
350
480
|
}
|
|
351
481
|
}
|
|
@@ -358,7 +488,7 @@ export async function iosLaunchApp(bundleId, udid) {
|
|
|
358
488
|
if (!simctlAvailable) {
|
|
359
489
|
return {
|
|
360
490
|
success: false,
|
|
361
|
-
error: "Xcode command line tools not available. Install Xcode from the App Store."
|
|
491
|
+
error: "Xcode command line tools not available. Install Xcode from the App Store.",
|
|
362
492
|
};
|
|
363
493
|
}
|
|
364
494
|
// Resolve target UDID (prefer Metro-connected simulator)
|
|
@@ -366,21 +496,27 @@ export async function iosLaunchApp(bundleId, udid) {
|
|
|
366
496
|
if (!targetUdid) {
|
|
367
497
|
return {
|
|
368
498
|
success: false,
|
|
369
|
-
error: "No iOS simulator is currently running. Start a simulator first."
|
|
499
|
+
error: "No iOS simulator is currently running. Start a simulator first.",
|
|
370
500
|
};
|
|
371
501
|
}
|
|
372
502
|
await execAsync(`xcrun simctl launch ${targetUdid} ${bundleId}`, {
|
|
373
|
-
timeout: SIMCTL_TIMEOUT
|
|
503
|
+
timeout: SIMCTL_TIMEOUT,
|
|
374
504
|
});
|
|
375
505
|
return {
|
|
376
506
|
success: true,
|
|
377
|
-
result: `Launched ${bundleId}
|
|
507
|
+
result: `Launched ${bundleId}`,
|
|
378
508
|
};
|
|
379
509
|
}
|
|
380
510
|
catch (error) {
|
|
511
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
512
|
+
let hint = "";
|
|
513
|
+
if (msg.includes("FBSOpenApplicationErrorDomain")) {
|
|
514
|
+
hint =
|
|
515
|
+
"\n\nCommon causes:\n- App is not installed on this simulator (install it first with ios_install_app)\n- Bundle ID is incorrect (check with: xcrun simctl listapps booted)";
|
|
516
|
+
}
|
|
381
517
|
return {
|
|
382
518
|
success: false,
|
|
383
|
-
error: `Failed to launch app: ${
|
|
519
|
+
error: `Failed to launch app: ${msg}${hint}`,
|
|
384
520
|
};
|
|
385
521
|
}
|
|
386
522
|
}
|
|
@@ -393,7 +529,7 @@ export async function iosOpenUrl(url, udid) {
|
|
|
393
529
|
if (!simctlAvailable) {
|
|
394
530
|
return {
|
|
395
531
|
success: false,
|
|
396
|
-
error: "Xcode command line tools not available. Install Xcode from the App Store."
|
|
532
|
+
error: "Xcode command line tools not available. Install Xcode from the App Store.",
|
|
397
533
|
};
|
|
398
534
|
}
|
|
399
535
|
// Resolve target UDID (prefer Metro-connected simulator)
|
|
@@ -401,21 +537,28 @@ export async function iosOpenUrl(url, udid) {
|
|
|
401
537
|
if (!targetUdid) {
|
|
402
538
|
return {
|
|
403
539
|
success: false,
|
|
404
|
-
error: "No iOS simulator is currently running. Start a simulator first."
|
|
540
|
+
error: "No iOS simulator is currently running. Start a simulator first.",
|
|
405
541
|
};
|
|
406
542
|
}
|
|
407
543
|
await execAsync(`xcrun simctl openurl ${targetUdid} "${url}"`, {
|
|
408
|
-
timeout: SIMCTL_TIMEOUT
|
|
544
|
+
timeout: SIMCTL_TIMEOUT,
|
|
409
545
|
});
|
|
410
546
|
return {
|
|
411
547
|
success: true,
|
|
412
|
-
result: `Opened URL: ${url}
|
|
548
|
+
result: `Opened URL: ${url}`,
|
|
413
549
|
};
|
|
414
550
|
}
|
|
415
551
|
catch (error) {
|
|
552
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
553
|
+
let hint = "";
|
|
554
|
+
if (msg.includes("LSApplicationWorkspaceError") ||
|
|
555
|
+
msg.includes("OpenApplicationErrorDomain")) {
|
|
556
|
+
hint =
|
|
557
|
+
"\n\nCommon causes:\n- Custom URL scheme not registered in the app's Info.plist\n- App that handles this URL scheme is not installed\n- For Expo apps, use the exp:// URL format";
|
|
558
|
+
}
|
|
416
559
|
return {
|
|
417
560
|
success: false,
|
|
418
|
-
error: `Failed to open URL: ${
|
|
561
|
+
error: `Failed to open URL: ${msg}${hint}`,
|
|
419
562
|
};
|
|
420
563
|
}
|
|
421
564
|
}
|
|
@@ -428,7 +571,7 @@ export async function iosTerminateApp(bundleId, udid) {
|
|
|
428
571
|
if (!simctlAvailable) {
|
|
429
572
|
return {
|
|
430
573
|
success: false,
|
|
431
|
-
error: "Xcode command line tools not available. Install Xcode from the App Store."
|
|
574
|
+
error: "Xcode command line tools not available. Install Xcode from the App Store.",
|
|
432
575
|
};
|
|
433
576
|
}
|
|
434
577
|
// Resolve target UDID (prefer Metro-connected simulator)
|
|
@@ -436,21 +579,21 @@ export async function iosTerminateApp(bundleId, udid) {
|
|
|
436
579
|
if (!targetUdid) {
|
|
437
580
|
return {
|
|
438
581
|
success: false,
|
|
439
|
-
error: "No iOS simulator is currently running. Start a simulator first."
|
|
582
|
+
error: "No iOS simulator is currently running. Start a simulator first.",
|
|
440
583
|
};
|
|
441
584
|
}
|
|
442
585
|
await execAsync(`xcrun simctl terminate ${targetUdid} ${bundleId}`, {
|
|
443
|
-
timeout: SIMCTL_TIMEOUT
|
|
586
|
+
timeout: SIMCTL_TIMEOUT,
|
|
444
587
|
});
|
|
445
588
|
return {
|
|
446
589
|
success: true,
|
|
447
|
-
result: `Terminated ${bundleId}
|
|
590
|
+
result: `Terminated ${bundleId}`,
|
|
448
591
|
};
|
|
449
592
|
}
|
|
450
593
|
catch (error) {
|
|
451
594
|
return {
|
|
452
595
|
success: false,
|
|
453
|
-
error: `Failed to terminate app: ${error instanceof Error ? error.message : String(error)}
|
|
596
|
+
error: `Failed to terminate app: ${error instanceof Error ? error.message : String(error)}`,
|
|
454
597
|
};
|
|
455
598
|
}
|
|
456
599
|
}
|
|
@@ -463,11 +606,11 @@ export async function iosBootSimulator(udid) {
|
|
|
463
606
|
if (!simctlAvailable) {
|
|
464
607
|
return {
|
|
465
608
|
success: false,
|
|
466
|
-
error: "Xcode command line tools not available. Install Xcode from the App Store."
|
|
609
|
+
error: "Xcode command line tools not available. Install Xcode from the App Store.",
|
|
467
610
|
};
|
|
468
611
|
}
|
|
469
612
|
await execAsync(`xcrun simctl boot ${udid}`, {
|
|
470
|
-
timeout: 60000 // 1 minute timeout for boot
|
|
613
|
+
timeout: 60000, // 1 minute timeout for boot
|
|
471
614
|
});
|
|
472
615
|
// Open Simulator app
|
|
473
616
|
await execAsync("open -a Simulator", { timeout: 10000 }).catch(() => {
|
|
@@ -475,7 +618,7 @@ export async function iosBootSimulator(udid) {
|
|
|
475
618
|
});
|
|
476
619
|
return {
|
|
477
620
|
success: true,
|
|
478
|
-
result: `Simulator ${udid} is now booting
|
|
621
|
+
result: `Simulator ${udid} is now booting`,
|
|
479
622
|
};
|
|
480
623
|
}
|
|
481
624
|
catch (error) {
|
|
@@ -484,301 +627,277 @@ export async function iosBootSimulator(udid) {
|
|
|
484
627
|
if (errorMessage.includes("Unable to boot device in current state: Booted")) {
|
|
485
628
|
return {
|
|
486
629
|
success: true,
|
|
487
|
-
result: "Simulator is already booted"
|
|
630
|
+
result: "Simulator is already booted",
|
|
488
631
|
};
|
|
489
632
|
}
|
|
490
633
|
return {
|
|
491
634
|
success: false,
|
|
492
|
-
error: `Failed to boot simulator: ${errorMessage}
|
|
635
|
+
error: `Failed to boot simulator: ${errorMessage}`,
|
|
493
636
|
};
|
|
494
637
|
}
|
|
495
638
|
}
|
|
496
639
|
// ============================================================================
|
|
497
|
-
//
|
|
498
|
-
// These tools require
|
|
499
|
-
//
|
|
640
|
+
// iOS UI Interaction Tools
|
|
641
|
+
// These tools require an iOS UI driver to be installed:
|
|
642
|
+
// Recommended: AXe — brew install cameroncooke/axe/axe
|
|
643
|
+
// Alternative: IDB — brew install idb-companion
|
|
644
|
+
// Set IOS_DRIVER=axe env var to use AXe (default is idb)
|
|
500
645
|
// ============================================================================
|
|
501
646
|
/**
|
|
502
647
|
* Tap at coordinates on an iOS simulator using IDB
|
|
503
648
|
*/
|
|
504
649
|
export async function iosTap(x, y, options) {
|
|
505
650
|
try {
|
|
506
|
-
const
|
|
507
|
-
if (!
|
|
508
|
-
return
|
|
509
|
-
|
|
510
|
-
error: "IDB is not installed. Install with: brew install idb-companion"
|
|
511
|
-
};
|
|
512
|
-
}
|
|
513
|
-
// Get simulator UDID (prefer Metro-connected, then fall back to booted)
|
|
514
|
-
const targetUdid = options?.udid || (await getActiveOrBootedSimulatorUdid());
|
|
515
|
-
if (!targetUdid) {
|
|
516
|
-
return {
|
|
517
|
-
success: false,
|
|
518
|
-
error: "No iOS simulator is currently running. Start a simulator first."
|
|
519
|
-
};
|
|
520
|
-
}
|
|
521
|
-
// Ensure IDB is connected to the simulator
|
|
522
|
-
const connectResult = await ensureIdbConnected(targetUdid);
|
|
523
|
-
if (!connectResult.success) {
|
|
524
|
-
return { success: false, error: connectResult.error };
|
|
525
|
-
}
|
|
651
|
+
const preflight = await ensureUiDriverReady(options?.udid);
|
|
652
|
+
if (!preflight.ready)
|
|
653
|
+
return preflight.error;
|
|
654
|
+
const { driver, targetUdid } = preflight;
|
|
526
655
|
const xRounded = Math.round(x);
|
|
527
656
|
const yRounded = Math.round(y);
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
657
|
+
if (driver === "axe") {
|
|
658
|
+
if (options?.duration !== undefined) {
|
|
659
|
+
// Long press: use axe touch with delay (convert ms to seconds)
|
|
660
|
+
const delaySecs = String(options.duration / 1000);
|
|
661
|
+
await runAxe("touch", "-x", String(xRounded), "-y", String(yRounded), "--down", "--up", "--delay", delaySecs, "--udid", targetUdid);
|
|
662
|
+
}
|
|
663
|
+
else {
|
|
664
|
+
await runAxe("tap", "-x", String(xRounded), "-y", String(yRounded), "--udid", targetUdid);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
else {
|
|
668
|
+
const args = ["ui", "tap", "--udid", targetUdid];
|
|
669
|
+
if (options?.duration !== undefined) {
|
|
670
|
+
args.push("--duration", String(options.duration));
|
|
671
|
+
}
|
|
672
|
+
args.push("--json", "--", String(xRounded), String(yRounded));
|
|
673
|
+
const { stderr } = await runIdb(...args);
|
|
674
|
+
if (stderr)
|
|
675
|
+
throw new Error(stderr);
|
|
676
|
+
}
|
|
537
677
|
return {
|
|
538
678
|
success: true,
|
|
539
|
-
result: `Tapped at (${xRounded}, ${yRounded})
|
|
679
|
+
result: `Tapped at (${xRounded}, ${yRounded})`,
|
|
540
680
|
};
|
|
541
681
|
}
|
|
542
682
|
catch (error) {
|
|
543
683
|
return {
|
|
544
684
|
success: false,
|
|
545
|
-
error: `Failed to tap: ${error instanceof Error ? error.message : String(error)}
|
|
685
|
+
error: `Failed to tap: ${error instanceof Error ? error.message : String(error)}`,
|
|
546
686
|
};
|
|
547
687
|
}
|
|
548
688
|
}
|
|
549
689
|
/**
|
|
550
|
-
* Swipe gesture on an iOS simulator
|
|
690
|
+
* Swipe gesture on an iOS simulator
|
|
551
691
|
*/
|
|
552
692
|
export async function iosSwipe(startX, startY, endX, endY, options) {
|
|
553
693
|
try {
|
|
554
|
-
const
|
|
555
|
-
if (!
|
|
556
|
-
return
|
|
557
|
-
|
|
558
|
-
error: "IDB is not installed. Install with: brew install idb-companion"
|
|
559
|
-
};
|
|
560
|
-
}
|
|
561
|
-
const targetUdid = options?.udid || (await getActiveOrBootedSimulatorUdid());
|
|
562
|
-
if (!targetUdid) {
|
|
563
|
-
return {
|
|
564
|
-
success: false,
|
|
565
|
-
error: "No iOS simulator is currently running. Start a simulator first."
|
|
566
|
-
};
|
|
567
|
-
}
|
|
568
|
-
// Ensure IDB is connected to the simulator
|
|
569
|
-
const connectResult = await ensureIdbConnected(targetUdid);
|
|
570
|
-
if (!connectResult.success) {
|
|
571
|
-
return { success: false, error: connectResult.error };
|
|
572
|
-
}
|
|
694
|
+
const preflight = await ensureUiDriverReady(options?.udid);
|
|
695
|
+
if (!preflight.ready)
|
|
696
|
+
return preflight.error;
|
|
697
|
+
const { driver, targetUdid } = preflight;
|
|
573
698
|
const x1 = Math.round(startX);
|
|
574
699
|
const y1 = Math.round(startY);
|
|
575
700
|
const x2 = Math.round(endX);
|
|
576
701
|
const y2 = Math.round(endY);
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
702
|
+
if (driver === "axe") {
|
|
703
|
+
const args = [
|
|
704
|
+
"swipe",
|
|
705
|
+
"--start-x",
|
|
706
|
+
String(x1),
|
|
707
|
+
"--start-y",
|
|
708
|
+
String(y1),
|
|
709
|
+
"--end-x",
|
|
710
|
+
String(x2),
|
|
711
|
+
"--end-y",
|
|
712
|
+
String(y2),
|
|
713
|
+
];
|
|
714
|
+
if (options?.duration !== undefined) {
|
|
715
|
+
args.push("--duration", String(options.duration / 1000));
|
|
716
|
+
}
|
|
717
|
+
if (options?.delta !== undefined) {
|
|
718
|
+
args.push("--delta", String(options.delta));
|
|
719
|
+
}
|
|
720
|
+
args.push("--udid", targetUdid);
|
|
721
|
+
await runAxe(...args);
|
|
722
|
+
}
|
|
723
|
+
else {
|
|
724
|
+
const args = ["ui", "swipe", "--udid", targetUdid];
|
|
725
|
+
if (options?.duration !== undefined) {
|
|
726
|
+
args.push("--duration", String(options.duration));
|
|
727
|
+
}
|
|
728
|
+
if (options?.delta !== undefined) {
|
|
729
|
+
args.push("--delta", String(options.delta));
|
|
730
|
+
}
|
|
731
|
+
args.push("--json", "--", String(x1), String(y1), String(x2), String(y2));
|
|
732
|
+
const { stderr } = await runIdb(...args);
|
|
733
|
+
if (stderr)
|
|
734
|
+
throw new Error(stderr);
|
|
735
|
+
}
|
|
589
736
|
return {
|
|
590
737
|
success: true,
|
|
591
|
-
result: `Swiped from (${x1}, ${y1}) to (${x2}, ${y2})
|
|
738
|
+
result: `Swiped from (${x1}, ${y1}) to (${x2}, ${y2})`,
|
|
592
739
|
};
|
|
593
740
|
}
|
|
594
741
|
catch (error) {
|
|
595
742
|
return {
|
|
596
743
|
success: false,
|
|
597
|
-
error: `Failed to swipe: ${error instanceof Error ? error.message : String(error)}
|
|
744
|
+
error: `Failed to swipe: ${error instanceof Error ? error.message : String(error)}`,
|
|
598
745
|
};
|
|
599
746
|
}
|
|
600
747
|
}
|
|
601
748
|
/**
|
|
602
|
-
* Input text into the active field on an iOS simulator
|
|
749
|
+
* Input text into the active field on an iOS simulator
|
|
603
750
|
*/
|
|
604
751
|
export async function iosInputText(text, udid) {
|
|
605
752
|
try {
|
|
606
|
-
const
|
|
607
|
-
if (!
|
|
608
|
-
return
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
}
|
|
613
|
-
const targetUdid = udid || (await getActiveOrBootedSimulatorUdid());
|
|
614
|
-
if (!targetUdid) {
|
|
615
|
-
return {
|
|
616
|
-
success: false,
|
|
617
|
-
error: "No iOS simulator is currently running. Start a simulator first."
|
|
618
|
-
};
|
|
753
|
+
const preflight = await ensureUiDriverReady(udid);
|
|
754
|
+
if (!preflight.ready)
|
|
755
|
+
return preflight.error;
|
|
756
|
+
const { driver, targetUdid } = preflight;
|
|
757
|
+
if (driver === "axe") {
|
|
758
|
+
await runAxe("type", text, "--udid", targetUdid);
|
|
619
759
|
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
760
|
+
else {
|
|
761
|
+
const { stderr } = await runIdb("ui", "text", "--udid", targetUdid, text);
|
|
762
|
+
if (stderr)
|
|
763
|
+
throw new Error(stderr);
|
|
624
764
|
}
|
|
625
|
-
// Use execFile with args array (no shell escaping needed)
|
|
626
|
-
const { stderr } = await runIdb("ui", "text", "--udid", targetUdid, text);
|
|
627
|
-
if (stderr)
|
|
628
|
-
throw new Error(stderr);
|
|
629
765
|
return {
|
|
630
766
|
success: true,
|
|
631
|
-
result: `Typed text: "${text}"
|
|
767
|
+
result: `Typed text: "${text}"`,
|
|
632
768
|
};
|
|
633
769
|
}
|
|
634
770
|
catch (error) {
|
|
635
771
|
return {
|
|
636
772
|
success: false,
|
|
637
|
-
error: `Failed to input text: ${error instanceof Error ? error.message : String(error)}
|
|
773
|
+
error: `Failed to input text: ${error instanceof Error ? error.message : String(error)}`,
|
|
638
774
|
};
|
|
639
775
|
}
|
|
640
776
|
}
|
|
641
777
|
/**
|
|
642
|
-
* Press a hardware button on an iOS simulator
|
|
778
|
+
* Press a hardware button on an iOS simulator
|
|
643
779
|
*/
|
|
644
780
|
export async function iosButton(button, options) {
|
|
645
781
|
try {
|
|
646
|
-
const idbAvailable = await isIdbAvailable();
|
|
647
|
-
if (!idbAvailable) {
|
|
648
|
-
return {
|
|
649
|
-
success: false,
|
|
650
|
-
error: "IDB is not installed. Install with: brew install idb-companion"
|
|
651
|
-
};
|
|
652
|
-
}
|
|
653
782
|
// Validate button type
|
|
654
783
|
if (!IOS_BUTTON_TYPES.includes(button)) {
|
|
655
784
|
return {
|
|
656
785
|
success: false,
|
|
657
|
-
error: `Invalid button type: ${button}. Valid options: ${IOS_BUTTON_TYPES.join(", ")}
|
|
786
|
+
error: `Invalid button type: ${button}. Valid options: ${IOS_BUTTON_TYPES.join(", ")}`,
|
|
658
787
|
};
|
|
659
788
|
}
|
|
660
|
-
const
|
|
661
|
-
if (!
|
|
662
|
-
return
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
789
|
+
const preflight = await ensureUiDriverReady(options?.udid);
|
|
790
|
+
if (!preflight.ready)
|
|
791
|
+
return preflight.error;
|
|
792
|
+
const { driver, targetUdid } = preflight;
|
|
793
|
+
if (driver === "axe") {
|
|
794
|
+
const args = ["button", toAxeButtonName(button)];
|
|
795
|
+
if (options?.duration !== undefined) {
|
|
796
|
+
args.push("--duration", String(options.duration));
|
|
797
|
+
}
|
|
798
|
+
args.push("--udid", targetUdid);
|
|
799
|
+
await runAxe(...args);
|
|
671
800
|
}
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
801
|
+
else {
|
|
802
|
+
const args = ["ui", "button", "--udid", targetUdid];
|
|
803
|
+
if (options?.duration !== undefined) {
|
|
804
|
+
args.push("--duration", String(options.duration));
|
|
805
|
+
}
|
|
806
|
+
args.push(button);
|
|
807
|
+
const { stderr } = await runIdb(...args);
|
|
808
|
+
if (stderr)
|
|
809
|
+
throw new Error(stderr);
|
|
676
810
|
}
|
|
677
|
-
args.push(button);
|
|
678
|
-
const { stderr } = await runIdb(...args);
|
|
679
|
-
if (stderr)
|
|
680
|
-
throw new Error(stderr);
|
|
681
811
|
return {
|
|
682
812
|
success: true,
|
|
683
|
-
result: `Pressed ${button} button
|
|
813
|
+
result: `Pressed ${button} button`,
|
|
684
814
|
};
|
|
685
815
|
}
|
|
686
816
|
catch (error) {
|
|
687
817
|
return {
|
|
688
818
|
success: false,
|
|
689
|
-
error: `Failed to press button: ${error instanceof Error ? error.message : String(error)}
|
|
819
|
+
error: `Failed to press button: ${error instanceof Error ? error.message : String(error)}`,
|
|
690
820
|
};
|
|
691
821
|
}
|
|
692
822
|
}
|
|
693
823
|
/**
|
|
694
|
-
* Send a key event to an iOS simulator
|
|
824
|
+
* Send a key event to an iOS simulator
|
|
695
825
|
*/
|
|
696
826
|
export async function iosKeyEvent(keycode, options) {
|
|
697
827
|
try {
|
|
698
|
-
const
|
|
699
|
-
if (!
|
|
700
|
-
return
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
error: "No iOS simulator is currently running. Start a simulator first."
|
|
710
|
-
};
|
|
711
|
-
}
|
|
712
|
-
// Ensure IDB is connected to the simulator
|
|
713
|
-
const connectResult = await ensureIdbConnected(targetUdid);
|
|
714
|
-
if (!connectResult.success) {
|
|
715
|
-
return { success: false, error: connectResult.error };
|
|
828
|
+
const preflight = await ensureUiDriverReady(options?.udid);
|
|
829
|
+
if (!preflight.ready)
|
|
830
|
+
return preflight.error;
|
|
831
|
+
const { driver, targetUdid } = preflight;
|
|
832
|
+
if (driver === "axe") {
|
|
833
|
+
const args = ["key", String(keycode)];
|
|
834
|
+
if (options?.duration !== undefined) {
|
|
835
|
+
args.push("--duration", String(options.duration));
|
|
836
|
+
}
|
|
837
|
+
args.push("--udid", targetUdid);
|
|
838
|
+
await runAxe(...args);
|
|
716
839
|
}
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
840
|
+
else {
|
|
841
|
+
const args = ["ui", "key", "--udid", targetUdid];
|
|
842
|
+
if (options?.duration !== undefined) {
|
|
843
|
+
args.push("--duration", String(options.duration));
|
|
844
|
+
}
|
|
845
|
+
args.push(String(keycode));
|
|
846
|
+
const { stderr } = await runIdb(...args);
|
|
847
|
+
if (stderr)
|
|
848
|
+
throw new Error(stderr);
|
|
721
849
|
}
|
|
722
|
-
args.push(String(keycode));
|
|
723
|
-
const { stderr } = await runIdb(...args);
|
|
724
|
-
if (stderr)
|
|
725
|
-
throw new Error(stderr);
|
|
726
850
|
return {
|
|
727
851
|
success: true,
|
|
728
|
-
result: `Sent key event: ${keycode}
|
|
852
|
+
result: `Sent key event: ${keycode}`,
|
|
729
853
|
};
|
|
730
854
|
}
|
|
731
855
|
catch (error) {
|
|
732
856
|
return {
|
|
733
857
|
success: false,
|
|
734
|
-
error: `Failed to send key event: ${error instanceof Error ? error.message : String(error)}
|
|
858
|
+
error: `Failed to send key event: ${error instanceof Error ? error.message : String(error)}`,
|
|
735
859
|
};
|
|
736
860
|
}
|
|
737
861
|
}
|
|
738
862
|
/**
|
|
739
|
-
* Send a sequence of key events to an iOS simulator
|
|
863
|
+
* Send a sequence of key events to an iOS simulator
|
|
740
864
|
*/
|
|
741
865
|
export async function iosKeySequence(keycodes, udid) {
|
|
742
866
|
try {
|
|
743
|
-
const idbAvailable = await isIdbAvailable();
|
|
744
|
-
if (!idbAvailable) {
|
|
745
|
-
return {
|
|
746
|
-
success: false,
|
|
747
|
-
error: "IDB is not installed. Install with: brew install idb-companion"
|
|
748
|
-
};
|
|
749
|
-
}
|
|
750
867
|
if (!keycodes || keycodes.length === 0) {
|
|
751
868
|
return {
|
|
752
869
|
success: false,
|
|
753
|
-
error: "At least one keycode is required"
|
|
870
|
+
error: "At least one keycode is required",
|
|
754
871
|
};
|
|
755
872
|
}
|
|
756
|
-
const
|
|
757
|
-
if (!
|
|
758
|
-
return
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
873
|
+
const preflight = await ensureUiDriverReady(udid);
|
|
874
|
+
if (!preflight.ready)
|
|
875
|
+
return preflight.error;
|
|
876
|
+
const { driver, targetUdid } = preflight;
|
|
877
|
+
if (driver === "axe") {
|
|
878
|
+
await runAxe("key-sequence", "--keycodes", keycodes.join(","), "--udid", targetUdid);
|
|
762
879
|
}
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
880
|
+
else {
|
|
881
|
+
const args = [
|
|
882
|
+
"ui",
|
|
883
|
+
"key-sequence",
|
|
884
|
+
"--udid",
|
|
885
|
+
targetUdid,
|
|
886
|
+
...keycodes.map(String),
|
|
887
|
+
];
|
|
888
|
+
const { stderr } = await runIdb(...args);
|
|
889
|
+
if (stderr)
|
|
890
|
+
throw new Error(stderr);
|
|
767
891
|
}
|
|
768
|
-
// Build args array for execFile (no shell)
|
|
769
|
-
const args = ["ui", "key-sequence", "--udid", targetUdid, ...keycodes.map(String)];
|
|
770
|
-
const { stderr } = await runIdb(...args);
|
|
771
|
-
if (stderr)
|
|
772
|
-
throw new Error(stderr);
|
|
773
892
|
return {
|
|
774
893
|
success: true,
|
|
775
|
-
result: `Sent key sequence: ${keycodes.join(", ")}
|
|
894
|
+
result: `Sent key sequence: ${keycodes.join(", ")}`,
|
|
776
895
|
};
|
|
777
896
|
}
|
|
778
897
|
catch (error) {
|
|
779
898
|
return {
|
|
780
899
|
success: false,
|
|
781
|
-
error: `Failed to send key sequence: ${error instanceof Error ? error.message : String(error)}
|
|
900
|
+
error: `Failed to send key sequence: ${error instanceof Error ? error.message : String(error)}`,
|
|
782
901
|
};
|
|
783
902
|
}
|
|
784
903
|
}
|
|
@@ -812,80 +931,65 @@ function formatAccessibilityTree(elements, depth = 0) {
|
|
|
812
931
|
return lines.join("\n");
|
|
813
932
|
}
|
|
814
933
|
/**
|
|
815
|
-
* Get accessibility info for the entire screen
|
|
934
|
+
* Get accessibility info for the entire screen
|
|
816
935
|
*/
|
|
817
936
|
export async function iosDescribeAll(udid) {
|
|
818
937
|
try {
|
|
819
|
-
const
|
|
820
|
-
if (!
|
|
821
|
-
return
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
if (!targetUdid) {
|
|
828
|
-
return {
|
|
829
|
-
success: false,
|
|
830
|
-
error: "No iOS simulator is currently running. Start a simulator first."
|
|
831
|
-
};
|
|
938
|
+
const preflight = await ensureUiDriverReady(udid);
|
|
939
|
+
if (!preflight.ready)
|
|
940
|
+
return preflight.error;
|
|
941
|
+
const { driver, targetUdid } = preflight;
|
|
942
|
+
let stdout;
|
|
943
|
+
if (driver === "axe") {
|
|
944
|
+
const result = await runAxe("describe-ui", "--udid", targetUdid);
|
|
945
|
+
stdout = result.stdout;
|
|
832
946
|
}
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
947
|
+
else {
|
|
948
|
+
const result = await runIdb("ui", "describe-all", "--udid", targetUdid, "--json", "--nested");
|
|
949
|
+
if (result.stderr)
|
|
950
|
+
throw new Error(result.stderr);
|
|
951
|
+
stdout = result.stdout;
|
|
837
952
|
}
|
|
838
|
-
// Use execFile with args array (no shell)
|
|
839
|
-
const { stdout, stderr } = await runIdb("ui", "describe-all", "--udid", targetUdid, "--json", "--nested");
|
|
840
|
-
if (stderr)
|
|
841
|
-
throw new Error(stderr);
|
|
842
953
|
// Parse JSON response
|
|
843
954
|
const elements = JSON.parse(stdout);
|
|
844
955
|
// Format for human-readable output
|
|
845
956
|
const formatted = formatAccessibilityTree(elements);
|
|
957
|
+
const header = 'Note: All coordinates are in iOS points. Use tap(text="...") for tapping — do not pass these coordinates to tap(x, y) directly.\n\n';
|
|
846
958
|
return {
|
|
847
959
|
success: true,
|
|
848
|
-
result: formatted || "No accessibility elements found",
|
|
849
|
-
elements
|
|
960
|
+
result: header + (formatted || "No accessibility elements found"),
|
|
961
|
+
elements,
|
|
850
962
|
};
|
|
851
963
|
}
|
|
852
964
|
catch (error) {
|
|
853
965
|
return {
|
|
854
966
|
success: false,
|
|
855
|
-
error: `Failed to describe screen: ${error instanceof Error ? error.message : String(error)}
|
|
967
|
+
error: `Failed to describe screen: ${error instanceof Error ? error.message : String(error)}`,
|
|
856
968
|
};
|
|
857
969
|
}
|
|
858
970
|
}
|
|
859
971
|
/**
|
|
860
|
-
* Get accessibility info at a specific point
|
|
972
|
+
* Get accessibility info at a specific point
|
|
861
973
|
*/
|
|
862
974
|
export async function iosDescribePoint(x, y, udid) {
|
|
863
975
|
try {
|
|
864
|
-
const
|
|
865
|
-
if (!
|
|
866
|
-
return
|
|
867
|
-
|
|
868
|
-
error: "IDB is not installed. Install with: brew install idb-companion"
|
|
869
|
-
};
|
|
870
|
-
}
|
|
871
|
-
const targetUdid = udid || (await getActiveOrBootedSimulatorUdid());
|
|
872
|
-
if (!targetUdid) {
|
|
873
|
-
return {
|
|
874
|
-
success: false,
|
|
875
|
-
error: "No iOS simulator is currently running. Start a simulator first."
|
|
876
|
-
};
|
|
877
|
-
}
|
|
878
|
-
// Ensure IDB is connected to the simulator
|
|
879
|
-
const connectResult = await ensureIdbConnected(targetUdid);
|
|
880
|
-
if (!connectResult.success) {
|
|
881
|
-
return { success: false, error: connectResult.error };
|
|
882
|
-
}
|
|
976
|
+
const preflight = await ensureUiDriverReady(udid);
|
|
977
|
+
if (!preflight.ready)
|
|
978
|
+
return preflight.error;
|
|
979
|
+
const { driver, targetUdid } = preflight;
|
|
883
980
|
const xRounded = Math.round(x);
|
|
884
981
|
const yRounded = Math.round(y);
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
982
|
+
let stdout;
|
|
983
|
+
if (driver === "axe") {
|
|
984
|
+
const result = await runAxe("describe-ui", "--point", `${xRounded},${yRounded}`, "--udid", targetUdid);
|
|
985
|
+
stdout = result.stdout;
|
|
986
|
+
}
|
|
987
|
+
else {
|
|
988
|
+
const result = await runIdb("ui", "describe-point", "--udid", targetUdid, "--json", "--", String(xRounded), String(yRounded));
|
|
989
|
+
if (result.stderr)
|
|
990
|
+
throw new Error(result.stderr);
|
|
991
|
+
stdout = result.stdout;
|
|
992
|
+
}
|
|
889
993
|
// Parse JSON response - may be single element or array
|
|
890
994
|
let element;
|
|
891
995
|
try {
|
|
@@ -896,7 +1000,7 @@ export async function iosDescribePoint(x, y, udid) {
|
|
|
896
1000
|
return {
|
|
897
1001
|
success: true,
|
|
898
1002
|
result: `No accessibility element found at (${xRounded}, ${yRounded})`,
|
|
899
|
-
elements: []
|
|
1003
|
+
elements: [],
|
|
900
1004
|
};
|
|
901
1005
|
}
|
|
902
1006
|
// Format for human-readable output
|
|
@@ -919,13 +1023,13 @@ export async function iosDescribePoint(x, y, udid) {
|
|
|
919
1023
|
result: parts.length > 0
|
|
920
1024
|
? `Element at (${xRounded}, ${yRounded}):\n${parts.join("\n")}`
|
|
921
1025
|
: `No accessibility element found at (${xRounded}, ${yRounded})`,
|
|
922
|
-
elements: element ? [element] : []
|
|
1026
|
+
elements: element ? [element] : [],
|
|
923
1027
|
};
|
|
924
1028
|
}
|
|
925
1029
|
catch (error) {
|
|
926
1030
|
return {
|
|
927
1031
|
success: false,
|
|
928
|
-
error: `Failed to describe point: ${error instanceof Error ? error.message : String(error)}
|
|
1032
|
+
error: `Failed to describe point: ${error instanceof Error ? error.message : String(error)}`,
|
|
929
1033
|
};
|
|
930
1034
|
}
|
|
931
1035
|
}
|
|
@@ -952,7 +1056,7 @@ export async function iosTapElement(options) {
|
|
|
952
1056
|
if (!label && !labelContains) {
|
|
953
1057
|
return {
|
|
954
1058
|
success: false,
|
|
955
|
-
error: "Either 'label' (exact match) or 'labelContains' (partial match) is required"
|
|
1059
|
+
error: "Either 'label' (exact match) or 'labelContains' (partial match) is required",
|
|
956
1060
|
};
|
|
957
1061
|
}
|
|
958
1062
|
// Get all accessibility elements
|
|
@@ -960,12 +1064,12 @@ export async function iosTapElement(options) {
|
|
|
960
1064
|
if (!describeResult.success || !describeResult.elements) {
|
|
961
1065
|
return {
|
|
962
1066
|
success: false,
|
|
963
|
-
error: describeResult.error || "Failed to get accessibility elements"
|
|
1067
|
+
error: describeResult.error || "Failed to get accessibility elements",
|
|
964
1068
|
};
|
|
965
1069
|
}
|
|
966
1070
|
// Flatten the tree and find matching elements
|
|
967
1071
|
const allElements = flattenElements(describeResult.elements);
|
|
968
|
-
const matches = allElements.filter(el => {
|
|
1072
|
+
const matches = allElements.filter((el) => {
|
|
969
1073
|
if (!el.AXLabel)
|
|
970
1074
|
return false;
|
|
971
1075
|
if (label)
|
|
@@ -975,17 +1079,19 @@ export async function iosTapElement(options) {
|
|
|
975
1079
|
return false;
|
|
976
1080
|
});
|
|
977
1081
|
if (matches.length === 0) {
|
|
978
|
-
const searchTerm = label
|
|
1082
|
+
const searchTerm = label
|
|
1083
|
+
? `label="${label}"`
|
|
1084
|
+
: `labelContains="${labelContains}"`;
|
|
979
1085
|
return {
|
|
980
1086
|
success: false,
|
|
981
|
-
error: `No element found with ${searchTerm}
|
|
1087
|
+
error: `No element found with ${searchTerm}`,
|
|
982
1088
|
};
|
|
983
1089
|
}
|
|
984
1090
|
// Select element by index (default 0 = first match)
|
|
985
1091
|
if (index >= matches.length) {
|
|
986
1092
|
return {
|
|
987
1093
|
success: false,
|
|
988
|
-
error: `Index ${index} out of range. Found ${matches.length} matching element(s)
|
|
1094
|
+
error: `Index ${index} out of range. Found ${matches.length} matching element(s).`,
|
|
989
1095
|
};
|
|
990
1096
|
}
|
|
991
1097
|
const element = matches[index];
|
|
@@ -993,7 +1099,7 @@ export async function iosTapElement(options) {
|
|
|
993
1099
|
if (!element.frame) {
|
|
994
1100
|
return {
|
|
995
1101
|
success: false,
|
|
996
|
-
error: `Element "${element.AXLabel}" has no frame coordinates
|
|
1102
|
+
error: `Element "${element.AXLabel}" has no frame coordinates`,
|
|
997
1103
|
};
|
|
998
1104
|
}
|
|
999
1105
|
// Calculate center
|
|
@@ -1004,7 +1110,7 @@ export async function iosTapElement(options) {
|
|
|
1004
1110
|
if (tapResult.success) {
|
|
1005
1111
|
return {
|
|
1006
1112
|
success: true,
|
|
1007
|
-
result: `Tapped "${element.AXLabel}" at (${centerX}, ${centerY})
|
|
1113
|
+
result: `Tapped "${element.AXLabel}" at (${centerX}, ${centerY})`,
|
|
1008
1114
|
};
|
|
1009
1115
|
}
|
|
1010
1116
|
return tapResult;
|
|
@@ -1012,7 +1118,7 @@ export async function iosTapElement(options) {
|
|
|
1012
1118
|
catch (error) {
|
|
1013
1119
|
return {
|
|
1014
1120
|
success: false,
|
|
1015
|
-
error: `Failed to tap element: ${error instanceof Error ? error.message : String(error)}
|
|
1121
|
+
error: `Failed to tap element: ${error instanceof Error ? error.message : String(error)}`,
|
|
1016
1122
|
};
|
|
1017
1123
|
}
|
|
1018
1124
|
}
|
|
@@ -1030,20 +1136,27 @@ function parseIdbAccessibilityForFindElement(output) {
|
|
|
1030
1136
|
label: node.AXLabel || node.label || "",
|
|
1031
1137
|
value: node.AXValue || node.value || "",
|
|
1032
1138
|
type: node.type || node.AXType || "",
|
|
1139
|
+
identifier: node.AXIdentifier ||
|
|
1140
|
+
node.identifier ||
|
|
1141
|
+
node.AXUniqueId ||
|
|
1142
|
+
"",
|
|
1033
1143
|
frame: {
|
|
1034
1144
|
x: frame.x || 0,
|
|
1035
1145
|
y: frame.y || 0,
|
|
1036
1146
|
width: frame.width || 0,
|
|
1037
|
-
height: frame.height || 0
|
|
1147
|
+
height: frame.height || 0,
|
|
1038
1148
|
},
|
|
1039
1149
|
center: {
|
|
1040
1150
|
x: Math.round((frame.x || 0) + (frame.width || 0) / 2),
|
|
1041
|
-
y: Math.round((frame.y || 0) + (frame.height || 0) / 2)
|
|
1151
|
+
y: Math.round((frame.y || 0) + (frame.height || 0) / 2),
|
|
1042
1152
|
},
|
|
1043
1153
|
enabled: node.enabled !== false,
|
|
1044
|
-
traits: node.traits || []
|
|
1154
|
+
traits: node.traits || [],
|
|
1045
1155
|
};
|
|
1046
|
-
if (element.label ||
|
|
1156
|
+
if (element.label ||
|
|
1157
|
+
element.value ||
|
|
1158
|
+
element.type ||
|
|
1159
|
+
element.identifier) {
|
|
1047
1160
|
elements.push(element);
|
|
1048
1161
|
}
|
|
1049
1162
|
}
|
|
@@ -1068,6 +1181,35 @@ function parseIdbAccessibilityForFindElement(output) {
|
|
|
1068
1181
|
}
|
|
1069
1182
|
return elements;
|
|
1070
1183
|
}
|
|
1184
|
+
/**
|
|
1185
|
+
* Check if an element's center is within the screen viewport.
|
|
1186
|
+
* Filters out off-screen elements in scroll views, horizontal pagers, etc.
|
|
1187
|
+
*/
|
|
1188
|
+
function isIOSElementInViewport(element, screenWidth, screenHeight) {
|
|
1189
|
+
const { center } = element;
|
|
1190
|
+
return (center.x >= 0 &&
|
|
1191
|
+
center.x <= screenWidth &&
|
|
1192
|
+
center.y >= 0 &&
|
|
1193
|
+
center.y <= screenHeight);
|
|
1194
|
+
}
|
|
1195
|
+
/**
|
|
1196
|
+
* Extract screen dimensions from the parsed UI tree.
|
|
1197
|
+
* The root Application element's frame represents the screen size.
|
|
1198
|
+
*/
|
|
1199
|
+
function extractIOSScreenSize(rawOutput) {
|
|
1200
|
+
try {
|
|
1201
|
+
const data = JSON.parse(rawOutput);
|
|
1202
|
+
const root = Array.isArray(data) ? data[0] : data;
|
|
1203
|
+
const frame = root?.frame;
|
|
1204
|
+
if (frame?.width && frame?.height) {
|
|
1205
|
+
return { width: frame.width, height: frame.height };
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
catch {
|
|
1209
|
+
// Fall through
|
|
1210
|
+
}
|
|
1211
|
+
return null;
|
|
1212
|
+
}
|
|
1071
1213
|
/**
|
|
1072
1214
|
* Match iOS element against find options
|
|
1073
1215
|
*/
|
|
@@ -1092,6 +1234,16 @@ function matchesIOSFindElement(element, options) {
|
|
|
1092
1234
|
if (!element.type.toLowerCase().includes(options.type.toLowerCase()))
|
|
1093
1235
|
return false;
|
|
1094
1236
|
}
|
|
1237
|
+
if (options.identifier !== undefined) {
|
|
1238
|
+
if (element.identifier !== options.identifier)
|
|
1239
|
+
return false;
|
|
1240
|
+
}
|
|
1241
|
+
if (options.identifierContains !== undefined) {
|
|
1242
|
+
if (!element.identifier
|
|
1243
|
+
.toLowerCase()
|
|
1244
|
+
.includes(options.identifierContains.toLowerCase()))
|
|
1245
|
+
return false;
|
|
1246
|
+
}
|
|
1095
1247
|
return true;
|
|
1096
1248
|
}
|
|
1097
1249
|
/**
|
|
@@ -1099,36 +1251,30 @@ function matchesIOSFindElement(element, options) {
|
|
|
1099
1251
|
*/
|
|
1100
1252
|
export async function iosGetUITree(udid) {
|
|
1101
1253
|
try {
|
|
1102
|
-
const
|
|
1103
|
-
if (!
|
|
1104
|
-
return
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1254
|
+
const preflight = await ensureUiDriverReady(udid);
|
|
1255
|
+
if (!preflight.ready)
|
|
1256
|
+
return preflight.error;
|
|
1257
|
+
const { driver, targetUdid } = preflight;
|
|
1258
|
+
let stdout;
|
|
1259
|
+
if (driver === "axe") {
|
|
1260
|
+
const result = await runAxe("describe-ui", "--udid", targetUdid);
|
|
1261
|
+
stdout = result.stdout;
|
|
1108
1262
|
}
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
success: false,
|
|
1113
|
-
error: "No iOS simulator is currently running. Start a simulator first."
|
|
1114
|
-
};
|
|
1115
|
-
}
|
|
1116
|
-
const connectResult = await ensureIdbConnected(targetUdid);
|
|
1117
|
-
if (!connectResult.success) {
|
|
1118
|
-
return { success: false, error: connectResult.error };
|
|
1263
|
+
else {
|
|
1264
|
+
const result = await runIdb("ui", "describe-all", "--udid", targetUdid);
|
|
1265
|
+
stdout = result.stdout;
|
|
1119
1266
|
}
|
|
1120
|
-
const { stdout } = await runIdb("ui", "describe-all", "--udid", targetUdid);
|
|
1121
1267
|
const elements = parseIdbAccessibilityForFindElement(stdout);
|
|
1122
1268
|
return {
|
|
1123
1269
|
success: true,
|
|
1124
1270
|
elements,
|
|
1125
|
-
rawOutput: stdout
|
|
1271
|
+
rawOutput: stdout,
|
|
1126
1272
|
};
|
|
1127
1273
|
}
|
|
1128
1274
|
catch (error) {
|
|
1129
1275
|
return {
|
|
1130
1276
|
success: false,
|
|
1131
|
-
error: `Failed to get UI tree: ${error instanceof Error ? error.message : String(error)}
|
|
1277
|
+
error: `Failed to get UI tree: ${error instanceof Error ? error.message : String(error)}`,
|
|
1132
1278
|
};
|
|
1133
1279
|
}
|
|
1134
1280
|
}
|
|
@@ -1137,12 +1283,17 @@ export async function iosGetUITree(udid) {
|
|
|
1137
1283
|
*/
|
|
1138
1284
|
export async function iosFindElement(options, udid) {
|
|
1139
1285
|
try {
|
|
1140
|
-
if (!options.label &&
|
|
1141
|
-
!options.
|
|
1286
|
+
if (!options.label &&
|
|
1287
|
+
!options.labelContains &&
|
|
1288
|
+
!options.value &&
|
|
1289
|
+
!options.valueContains &&
|
|
1290
|
+
!options.type &&
|
|
1291
|
+
!options.identifier &&
|
|
1292
|
+
!options.identifierContains) {
|
|
1142
1293
|
return {
|
|
1143
1294
|
success: false,
|
|
1144
1295
|
found: false,
|
|
1145
|
-
error: "At least one search criteria (label, labelContains, value, valueContains, or type) must be provided"
|
|
1296
|
+
error: "At least one search criteria (label, labelContains, value, valueContains, identifier, identifierContains, or type) must be provided",
|
|
1146
1297
|
};
|
|
1147
1298
|
}
|
|
1148
1299
|
const treeResult = await iosGetUITree(udid);
|
|
@@ -1150,15 +1301,28 @@ export async function iosFindElement(options, udid) {
|
|
|
1150
1301
|
return {
|
|
1151
1302
|
success: false,
|
|
1152
1303
|
found: false,
|
|
1153
|
-
error: treeResult.error
|
|
1304
|
+
error: treeResult.error,
|
|
1154
1305
|
};
|
|
1155
1306
|
}
|
|
1156
|
-
|
|
1307
|
+
// Filter to viewport-visible elements first, then match criteria.
|
|
1308
|
+
// This prevents selecting off-screen duplicates in scroll views / pagers.
|
|
1309
|
+
const screenSize = treeResult.rawOutput
|
|
1310
|
+
? extractIOSScreenSize(treeResult.rawOutput)
|
|
1311
|
+
: null;
|
|
1312
|
+
const visibleElements = screenSize
|
|
1313
|
+
? treeResult.elements.filter((el) => isIOSElementInViewport(el, screenSize.width, screenSize.height))
|
|
1314
|
+
: treeResult.elements;
|
|
1315
|
+
let matches = visibleElements.filter((el) => matchesIOSFindElement(el, options));
|
|
1316
|
+
// If no visible matches, fall back to all elements (element may be partially visible
|
|
1317
|
+
// or the screen size extraction failed)
|
|
1318
|
+
if (matches.length === 0) {
|
|
1319
|
+
matches = treeResult.elements.filter((el) => matchesIOSFindElement(el, options));
|
|
1320
|
+
}
|
|
1157
1321
|
if (matches.length === 0) {
|
|
1158
1322
|
return {
|
|
1159
1323
|
success: true,
|
|
1160
1324
|
found: false,
|
|
1161
|
-
matchCount: 0
|
|
1325
|
+
matchCount: 0,
|
|
1162
1326
|
};
|
|
1163
1327
|
}
|
|
1164
1328
|
const index = options.index ?? 0;
|
|
@@ -1168,7 +1332,7 @@ export async function iosFindElement(options, udid) {
|
|
|
1168
1332
|
success: true,
|
|
1169
1333
|
found: false,
|
|
1170
1334
|
matchCount: matches.length,
|
|
1171
|
-
error: `Index ${index} out of bounds. Found ${matches.length} matching element(s)
|
|
1335
|
+
error: `Index ${index} out of bounds. Found ${matches.length} matching element(s).`,
|
|
1172
1336
|
};
|
|
1173
1337
|
}
|
|
1174
1338
|
return {
|
|
@@ -1176,17 +1340,85 @@ export async function iosFindElement(options, udid) {
|
|
|
1176
1340
|
found: true,
|
|
1177
1341
|
element: selectedElement,
|
|
1178
1342
|
allMatches: matches,
|
|
1179
|
-
matchCount: matches.length
|
|
1343
|
+
matchCount: matches.length,
|
|
1180
1344
|
};
|
|
1181
1345
|
}
|
|
1182
1346
|
catch (error) {
|
|
1183
1347
|
return {
|
|
1184
1348
|
success: false,
|
|
1185
1349
|
found: false,
|
|
1186
|
-
error: `Failed to find element: ${error instanceof Error ? error.message : String(error)}
|
|
1350
|
+
error: `Failed to find element: ${error instanceof Error ? error.message : String(error)}`,
|
|
1187
1351
|
};
|
|
1188
1352
|
}
|
|
1189
1353
|
}
|
|
1354
|
+
// DPR cache: UDID → device pixel ratio (never changes during session)
|
|
1355
|
+
const dprCache = new Map();
|
|
1356
|
+
/**
|
|
1357
|
+
* Pure calculation: DPR = screenshotPixelWidth / screenPointWidth.
|
|
1358
|
+
* Exported for unit testing.
|
|
1359
|
+
*/
|
|
1360
|
+
export function calculateDPR(screenshotPixelWidth, screenPointWidth) {
|
|
1361
|
+
return Math.round(screenshotPixelWidth / screenPointWidth);
|
|
1362
|
+
}
|
|
1363
|
+
/**
|
|
1364
|
+
* Query the actual device pixel ratio for an iOS simulator.
|
|
1365
|
+
* Uses screenshot pixel width vs accessibility root frame width.
|
|
1366
|
+
* Result is cached per UDID for the session.
|
|
1367
|
+
*/
|
|
1368
|
+
export async function getDevicePixelRatio(udid) {
|
|
1369
|
+
const cacheKey = udid || "__default__";
|
|
1370
|
+
const cached = dprCache.get(cacheKey);
|
|
1371
|
+
if (cached)
|
|
1372
|
+
return cached;
|
|
1373
|
+
const resolvedUdid = udid || (await getActiveOrBootedSimulatorUdid());
|
|
1374
|
+
if (!resolvedUdid) {
|
|
1375
|
+
throw new Error("No iOS simulator is currently running. Start a simulator first.");
|
|
1376
|
+
}
|
|
1377
|
+
// Take screenshot to get pixel dimensions
|
|
1378
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
1379
|
+
const screenshotPath = path.join(os.tmpdir(), `ios-dpr-screenshot-${timestamp}.png`);
|
|
1380
|
+
await execAsync(`xcrun simctl io ${resolvedUdid} screenshot "${screenshotPath}"`, {
|
|
1381
|
+
timeout: SIMCTL_TIMEOUT,
|
|
1382
|
+
});
|
|
1383
|
+
const metadata = await sharp(screenshotPath).metadata();
|
|
1384
|
+
const pixelWidth = metadata.width;
|
|
1385
|
+
const pixelHeight = metadata.height;
|
|
1386
|
+
// Try accessibility tree for exact DPR, fall back to inference
|
|
1387
|
+
const driver = getIosDriver();
|
|
1388
|
+
const driverOk = driver === "axe" ? await isAxeAvailable() : await isIdbAvailable();
|
|
1389
|
+
if (driverOk) {
|
|
1390
|
+
try {
|
|
1391
|
+
let stdout;
|
|
1392
|
+
if (driver === "axe") {
|
|
1393
|
+
const result = await runAxe("describe-ui", "--udid", resolvedUdid);
|
|
1394
|
+
stdout = result.stdout;
|
|
1395
|
+
}
|
|
1396
|
+
else {
|
|
1397
|
+
await ensureIdbConnected(resolvedUdid);
|
|
1398
|
+
const result = await runIdb("ui", "describe-all", "--udid", resolvedUdid, "--json", "--nested");
|
|
1399
|
+
stdout = result.stdout;
|
|
1400
|
+
}
|
|
1401
|
+
const screenSize = extractIOSScreenSize(stdout);
|
|
1402
|
+
if (screenSize) {
|
|
1403
|
+
const dpr = calculateDPR(pixelWidth, screenSize.width);
|
|
1404
|
+
dprCache.set(cacheKey, dpr);
|
|
1405
|
+
return dpr;
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
catch {
|
|
1409
|
+
// driver failed — fall through to inference
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
// Fallback: infer DPR from screenshot pixel dimensions
|
|
1413
|
+
const { inferIOSDevicePixelRatio } = await import("./ocr.js");
|
|
1414
|
+
const dpr = inferIOSDevicePixelRatio(pixelWidth, pixelHeight);
|
|
1415
|
+
dprCache.set(cacheKey, dpr);
|
|
1416
|
+
return dpr;
|
|
1417
|
+
}
|
|
1418
|
+
/** Clear DPR cache (for testing or when devices change) */
|
|
1419
|
+
export function clearDPRCache() {
|
|
1420
|
+
dprCache.clear();
|
|
1421
|
+
}
|
|
1190
1422
|
/**
|
|
1191
1423
|
* Wait for element to appear on iOS screen with polling
|
|
1192
1424
|
*/
|
|
@@ -1194,13 +1426,16 @@ export async function iosWaitForElement(options, udid) {
|
|
|
1194
1426
|
const timeoutMs = options.timeoutMs ?? 10000;
|
|
1195
1427
|
const pollIntervalMs = options.pollIntervalMs ?? 500;
|
|
1196
1428
|
const startTime = Date.now();
|
|
1197
|
-
if (!options.label &&
|
|
1198
|
-
!options.
|
|
1429
|
+
if (!options.label &&
|
|
1430
|
+
!options.labelContains &&
|
|
1431
|
+
!options.value &&
|
|
1432
|
+
!options.valueContains &&
|
|
1433
|
+
!options.type) {
|
|
1199
1434
|
return {
|
|
1200
1435
|
success: false,
|
|
1201
1436
|
found: false,
|
|
1202
1437
|
timedOut: false,
|
|
1203
|
-
error: "At least one search criteria (label, labelContains, value, valueContains, or type) must be provided"
|
|
1438
|
+
error: "At least one search criteria (label, labelContains, value, valueContains, or type) must be provided",
|
|
1204
1439
|
};
|
|
1205
1440
|
}
|
|
1206
1441
|
while (Date.now() - startTime < timeoutMs) {
|
|
@@ -1209,24 +1444,24 @@ export async function iosWaitForElement(options, udid) {
|
|
|
1209
1444
|
return {
|
|
1210
1445
|
...result,
|
|
1211
1446
|
elapsedMs: Date.now() - startTime,
|
|
1212
|
-
timedOut: false
|
|
1447
|
+
timedOut: false,
|
|
1213
1448
|
};
|
|
1214
1449
|
}
|
|
1215
1450
|
if (!result.success) {
|
|
1216
1451
|
return {
|
|
1217
1452
|
...result,
|
|
1218
1453
|
elapsedMs: Date.now() - startTime,
|
|
1219
|
-
timedOut: false
|
|
1454
|
+
timedOut: false,
|
|
1220
1455
|
};
|
|
1221
1456
|
}
|
|
1222
|
-
await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
|
|
1457
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
1223
1458
|
}
|
|
1224
1459
|
return {
|
|
1225
1460
|
success: true,
|
|
1226
1461
|
found: false,
|
|
1227
1462
|
elapsedMs: Date.now() - startTime,
|
|
1228
1463
|
timedOut: true,
|
|
1229
|
-
error: `Timed out after ${timeoutMs}ms waiting for element
|
|
1464
|
+
error: `Timed out after ${timeoutMs}ms waiting for element`,
|
|
1230
1465
|
};
|
|
1231
1466
|
}
|
|
1232
1467
|
//# sourceMappingURL=ios.js.map
|