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