react-native-ai-devtools 1.4.16 → 1.4.18
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/README.md +43 -18
- package/build/core/index.d.ts +18 -18
- package/build/core/index.d.ts.map +1 -1
- package/build/core/index.js +16 -16
- package/build/core/index.js.map +1 -1
- package/build/core/ios.d.ts +20 -7
- package/build/core/ios.d.ts.map +1 -1
- package/build/core/ios.js +453 -339
- package/build/core/ios.js.map +1 -1
- package/build/core/telemetry.d.ts +1 -1
- package/build/core/telemetry.d.ts.map +1 -1
- package/build/core/telemetry.js +5 -1
- package/build/core/telemetry.js.map +1 -1
- package/build/index.js +32 -46
- package/build/index.js.map +1 -1
- package/build/pro/tap.d.ts.map +1 -1
- package/build/pro/tap.js +157 -99
- package/build/pro/tap.js.map +1 -1
- package/package.json +1 -1
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,16 +146,62 @@ 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}`,
|
|
75
157
|
};
|
|
76
158
|
}
|
|
77
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
|
+
},
|
|
192
|
+
};
|
|
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 };
|
|
204
|
+
}
|
|
78
205
|
/**
|
|
79
206
|
* Check if simctl is available
|
|
80
207
|
*/
|
|
@@ -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,26 +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) {
|
|
381
508
|
const msg = error instanceof Error ? error.message : String(error);
|
|
382
509
|
let hint = "";
|
|
383
510
|
if (msg.includes("FBSOpenApplicationErrorDomain")) {
|
|
384
|
-
hint =
|
|
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)";
|
|
385
513
|
}
|
|
386
514
|
return {
|
|
387
515
|
success: false,
|
|
388
|
-
error: `Failed to launch app: ${msg}${hint}
|
|
516
|
+
error: `Failed to launch app: ${msg}${hint}`,
|
|
389
517
|
};
|
|
390
518
|
}
|
|
391
519
|
}
|
|
@@ -398,7 +526,7 @@ export async function iosOpenUrl(url, udid) {
|
|
|
398
526
|
if (!simctlAvailable) {
|
|
399
527
|
return {
|
|
400
528
|
success: false,
|
|
401
|
-
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.",
|
|
402
530
|
};
|
|
403
531
|
}
|
|
404
532
|
// Resolve target UDID (prefer Metro-connected simulator)
|
|
@@ -406,26 +534,28 @@ export async function iosOpenUrl(url, udid) {
|
|
|
406
534
|
if (!targetUdid) {
|
|
407
535
|
return {
|
|
408
536
|
success: false,
|
|
409
|
-
error: "No iOS simulator is currently running. Start a simulator first."
|
|
537
|
+
error: "No iOS simulator is currently running. Start a simulator first.",
|
|
410
538
|
};
|
|
411
539
|
}
|
|
412
540
|
await execAsync(`xcrun simctl openurl ${targetUdid} "${url}"`, {
|
|
413
|
-
timeout: SIMCTL_TIMEOUT
|
|
541
|
+
timeout: SIMCTL_TIMEOUT,
|
|
414
542
|
});
|
|
415
543
|
return {
|
|
416
544
|
success: true,
|
|
417
|
-
result: `Opened URL: ${url}
|
|
545
|
+
result: `Opened URL: ${url}`,
|
|
418
546
|
};
|
|
419
547
|
}
|
|
420
548
|
catch (error) {
|
|
421
549
|
const msg = error instanceof Error ? error.message : String(error);
|
|
422
550
|
let hint = "";
|
|
423
|
-
if (msg.includes("LSApplicationWorkspaceError") ||
|
|
424
|
-
|
|
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";
|
|
425
555
|
}
|
|
426
556
|
return {
|
|
427
557
|
success: false,
|
|
428
|
-
error: `Failed to open URL: ${msg}${hint}
|
|
558
|
+
error: `Failed to open URL: ${msg}${hint}`,
|
|
429
559
|
};
|
|
430
560
|
}
|
|
431
561
|
}
|
|
@@ -438,7 +568,7 @@ export async function iosTerminateApp(bundleId, udid) {
|
|
|
438
568
|
if (!simctlAvailable) {
|
|
439
569
|
return {
|
|
440
570
|
success: false,
|
|
441
|
-
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.",
|
|
442
572
|
};
|
|
443
573
|
}
|
|
444
574
|
// Resolve target UDID (prefer Metro-connected simulator)
|
|
@@ -446,21 +576,21 @@ export async function iosTerminateApp(bundleId, udid) {
|
|
|
446
576
|
if (!targetUdid) {
|
|
447
577
|
return {
|
|
448
578
|
success: false,
|
|
449
|
-
error: "No iOS simulator is currently running. Start a simulator first."
|
|
579
|
+
error: "No iOS simulator is currently running. Start a simulator first.",
|
|
450
580
|
};
|
|
451
581
|
}
|
|
452
582
|
await execAsync(`xcrun simctl terminate ${targetUdid} ${bundleId}`, {
|
|
453
|
-
timeout: SIMCTL_TIMEOUT
|
|
583
|
+
timeout: SIMCTL_TIMEOUT,
|
|
454
584
|
});
|
|
455
585
|
return {
|
|
456
586
|
success: true,
|
|
457
|
-
result: `Terminated ${bundleId}
|
|
587
|
+
result: `Terminated ${bundleId}`,
|
|
458
588
|
};
|
|
459
589
|
}
|
|
460
590
|
catch (error) {
|
|
461
591
|
return {
|
|
462
592
|
success: false,
|
|
463
|
-
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)}`,
|
|
464
594
|
};
|
|
465
595
|
}
|
|
466
596
|
}
|
|
@@ -473,11 +603,11 @@ export async function iosBootSimulator(udid) {
|
|
|
473
603
|
if (!simctlAvailable) {
|
|
474
604
|
return {
|
|
475
605
|
success: false,
|
|
476
|
-
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.",
|
|
477
607
|
};
|
|
478
608
|
}
|
|
479
609
|
await execAsync(`xcrun simctl boot ${udid}`, {
|
|
480
|
-
timeout: 60000 // 1 minute timeout for boot
|
|
610
|
+
timeout: 60000, // 1 minute timeout for boot
|
|
481
611
|
});
|
|
482
612
|
// Open Simulator app
|
|
483
613
|
await execAsync("open -a Simulator", { timeout: 10000 }).catch(() => {
|
|
@@ -485,7 +615,7 @@ export async function iosBootSimulator(udid) {
|
|
|
485
615
|
});
|
|
486
616
|
return {
|
|
487
617
|
success: true,
|
|
488
|
-
result: `Simulator ${udid} is now booting
|
|
618
|
+
result: `Simulator ${udid} is now booting`,
|
|
489
619
|
};
|
|
490
620
|
}
|
|
491
621
|
catch (error) {
|
|
@@ -494,12 +624,12 @@ export async function iosBootSimulator(udid) {
|
|
|
494
624
|
if (errorMessage.includes("Unable to boot device in current state: Booted")) {
|
|
495
625
|
return {
|
|
496
626
|
success: true,
|
|
497
|
-
result: "Simulator is already booted"
|
|
627
|
+
result: "Simulator is already booted",
|
|
498
628
|
};
|
|
499
629
|
}
|
|
500
630
|
return {
|
|
501
631
|
success: false,
|
|
502
|
-
error: `Failed to boot simulator: ${errorMessage}
|
|
632
|
+
error: `Failed to boot simulator: ${errorMessage}`,
|
|
503
633
|
};
|
|
504
634
|
}
|
|
505
635
|
}
|
|
@@ -513,282 +643,256 @@ export async function iosBootSimulator(udid) {
|
|
|
513
643
|
*/
|
|
514
644
|
export async function iosTap(x, y, options) {
|
|
515
645
|
try {
|
|
516
|
-
const
|
|
517
|
-
if (!
|
|
518
|
-
return
|
|
519
|
-
|
|
520
|
-
error: "IDB is not installed. Install with: brew install idb-companion"
|
|
521
|
-
};
|
|
522
|
-
}
|
|
523
|
-
// Get simulator UDID (prefer Metro-connected, then fall back to booted)
|
|
524
|
-
const targetUdid = options?.udid || (await getActiveOrBootedSimulatorUdid());
|
|
525
|
-
if (!targetUdid) {
|
|
526
|
-
return {
|
|
527
|
-
success: false,
|
|
528
|
-
error: "No iOS simulator is currently running. Start a simulator first."
|
|
529
|
-
};
|
|
530
|
-
}
|
|
531
|
-
// Ensure IDB is connected to the simulator
|
|
532
|
-
const connectResult = await ensureIdbConnected(targetUdid);
|
|
533
|
-
if (!connectResult.success) {
|
|
534
|
-
return { success: false, error: connectResult.error };
|
|
535
|
-
}
|
|
646
|
+
const preflight = await ensureUiDriverReady(options?.udid);
|
|
647
|
+
if (!preflight.ready)
|
|
648
|
+
return preflight.error;
|
|
649
|
+
const { driver, targetUdid } = preflight;
|
|
536
650
|
const xRounded = Math.round(x);
|
|
537
651
|
const yRounded = Math.round(y);
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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
|
+
}
|
|
547
672
|
return {
|
|
548
673
|
success: true,
|
|
549
|
-
result: `Tapped at (${xRounded}, ${yRounded})
|
|
674
|
+
result: `Tapped at (${xRounded}, ${yRounded})`,
|
|
550
675
|
};
|
|
551
676
|
}
|
|
552
677
|
catch (error) {
|
|
553
678
|
return {
|
|
554
679
|
success: false,
|
|
555
|
-
error: `Failed to tap: ${error instanceof Error ? error.message : String(error)}
|
|
680
|
+
error: `Failed to tap: ${error instanceof Error ? error.message : String(error)}`,
|
|
556
681
|
};
|
|
557
682
|
}
|
|
558
683
|
}
|
|
559
684
|
/**
|
|
560
|
-
* Swipe gesture on an iOS simulator
|
|
685
|
+
* Swipe gesture on an iOS simulator
|
|
561
686
|
*/
|
|
562
687
|
export async function iosSwipe(startX, startY, endX, endY, options) {
|
|
563
688
|
try {
|
|
564
|
-
const
|
|
565
|
-
if (!
|
|
566
|
-
return
|
|
567
|
-
|
|
568
|
-
error: "IDB is not installed. Install with: brew install idb-companion"
|
|
569
|
-
};
|
|
570
|
-
}
|
|
571
|
-
const targetUdid = options?.udid || (await getActiveOrBootedSimulatorUdid());
|
|
572
|
-
if (!targetUdid) {
|
|
573
|
-
return {
|
|
574
|
-
success: false,
|
|
575
|
-
error: "No iOS simulator is currently running. Start a simulator first."
|
|
576
|
-
};
|
|
577
|
-
}
|
|
578
|
-
// Ensure IDB is connected to the simulator
|
|
579
|
-
const connectResult = await ensureIdbConnected(targetUdid);
|
|
580
|
-
if (!connectResult.success) {
|
|
581
|
-
return { success: false, error: connectResult.error };
|
|
582
|
-
}
|
|
689
|
+
const preflight = await ensureUiDriverReady(options?.udid);
|
|
690
|
+
if (!preflight.ready)
|
|
691
|
+
return preflight.error;
|
|
692
|
+
const { driver, targetUdid } = preflight;
|
|
583
693
|
const x1 = Math.round(startX);
|
|
584
694
|
const y1 = Math.round(startY);
|
|
585
695
|
const x2 = Math.round(endX);
|
|
586
696
|
const y2 = Math.round(endY);
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
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
|
+
}
|
|
599
731
|
return {
|
|
600
732
|
success: true,
|
|
601
|
-
result: `Swiped from (${x1}, ${y1}) to (${x2}, ${y2})
|
|
733
|
+
result: `Swiped from (${x1}, ${y1}) to (${x2}, ${y2})`,
|
|
602
734
|
};
|
|
603
735
|
}
|
|
604
736
|
catch (error) {
|
|
605
737
|
return {
|
|
606
738
|
success: false,
|
|
607
|
-
error: `Failed to swipe: ${error instanceof Error ? error.message : String(error)}
|
|
739
|
+
error: `Failed to swipe: ${error instanceof Error ? error.message : String(error)}`,
|
|
608
740
|
};
|
|
609
741
|
}
|
|
610
742
|
}
|
|
611
743
|
/**
|
|
612
|
-
* Input text into the active field on an iOS simulator
|
|
744
|
+
* Input text into the active field on an iOS simulator
|
|
613
745
|
*/
|
|
614
746
|
export async function iosInputText(text, udid) {
|
|
615
747
|
try {
|
|
616
|
-
const
|
|
617
|
-
if (!
|
|
618
|
-
return
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
}
|
|
623
|
-
const targetUdid = udid || (await getActiveOrBootedSimulatorUdid());
|
|
624
|
-
if (!targetUdid) {
|
|
625
|
-
return {
|
|
626
|
-
success: false,
|
|
627
|
-
error: "No iOS simulator is currently running. Start a simulator first."
|
|
628
|
-
};
|
|
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);
|
|
629
754
|
}
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
755
|
+
else {
|
|
756
|
+
const { stderr } = await runIdb("ui", "text", "--udid", targetUdid, text);
|
|
757
|
+
if (stderr)
|
|
758
|
+
throw new Error(stderr);
|
|
634
759
|
}
|
|
635
|
-
// Use execFile with args array (no shell escaping needed)
|
|
636
|
-
const { stderr } = await runIdb("ui", "text", "--udid", targetUdid, text);
|
|
637
|
-
if (stderr)
|
|
638
|
-
throw new Error(stderr);
|
|
639
760
|
return {
|
|
640
761
|
success: true,
|
|
641
|
-
result: `Typed text: "${text}"
|
|
762
|
+
result: `Typed text: "${text}"`,
|
|
642
763
|
};
|
|
643
764
|
}
|
|
644
765
|
catch (error) {
|
|
645
766
|
return {
|
|
646
767
|
success: false,
|
|
647
|
-
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)}`,
|
|
648
769
|
};
|
|
649
770
|
}
|
|
650
771
|
}
|
|
651
772
|
/**
|
|
652
|
-
* Press a hardware button on an iOS simulator
|
|
773
|
+
* Press a hardware button on an iOS simulator
|
|
653
774
|
*/
|
|
654
775
|
export async function iosButton(button, options) {
|
|
655
776
|
try {
|
|
656
|
-
const idbAvailable = await isIdbAvailable();
|
|
657
|
-
if (!idbAvailable) {
|
|
658
|
-
return {
|
|
659
|
-
success: false,
|
|
660
|
-
error: "IDB is not installed. Install with: brew install idb-companion"
|
|
661
|
-
};
|
|
662
|
-
}
|
|
663
777
|
// Validate button type
|
|
664
778
|
if (!IOS_BUTTON_TYPES.includes(button)) {
|
|
665
779
|
return {
|
|
666
780
|
success: false,
|
|
667
|
-
error: `Invalid button type: ${button}. Valid options: ${IOS_BUTTON_TYPES.join(", ")}
|
|
668
|
-
};
|
|
669
|
-
}
|
|
670
|
-
const targetUdid = options?.udid || (await getActiveOrBootedSimulatorUdid());
|
|
671
|
-
if (!targetUdid) {
|
|
672
|
-
return {
|
|
673
|
-
success: false,
|
|
674
|
-
error: "No iOS simulator is currently running. Start a simulator first."
|
|
781
|
+
error: `Invalid button type: ${button}. Valid options: ${IOS_BUTTON_TYPES.join(", ")}`,
|
|
675
782
|
};
|
|
676
783
|
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
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);
|
|
681
795
|
}
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
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);
|
|
686
805
|
}
|
|
687
|
-
args.push(button);
|
|
688
|
-
const { stderr } = await runIdb(...args);
|
|
689
|
-
if (stderr)
|
|
690
|
-
throw new Error(stderr);
|
|
691
806
|
return {
|
|
692
807
|
success: true,
|
|
693
|
-
result: `Pressed ${button} button
|
|
808
|
+
result: `Pressed ${button} button`,
|
|
694
809
|
};
|
|
695
810
|
}
|
|
696
811
|
catch (error) {
|
|
697
812
|
return {
|
|
698
813
|
success: false,
|
|
699
|
-
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)}`,
|
|
700
815
|
};
|
|
701
816
|
}
|
|
702
817
|
}
|
|
703
818
|
/**
|
|
704
|
-
* Send a key event to an iOS simulator
|
|
819
|
+
* Send a key event to an iOS simulator
|
|
705
820
|
*/
|
|
706
821
|
export async function iosKeyEvent(keycode, options) {
|
|
707
822
|
try {
|
|
708
|
-
const
|
|
709
|
-
if (!
|
|
710
|
-
return
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
error: "No iOS simulator is currently running. Start a simulator first."
|
|
720
|
-
};
|
|
721
|
-
}
|
|
722
|
-
// Ensure IDB is connected to the simulator
|
|
723
|
-
const connectResult = await ensureIdbConnected(targetUdid);
|
|
724
|
-
if (!connectResult.success) {
|
|
725
|
-
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);
|
|
726
834
|
}
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
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);
|
|
731
844
|
}
|
|
732
|
-
args.push(String(keycode));
|
|
733
|
-
const { stderr } = await runIdb(...args);
|
|
734
|
-
if (stderr)
|
|
735
|
-
throw new Error(stderr);
|
|
736
845
|
return {
|
|
737
846
|
success: true,
|
|
738
|
-
result: `Sent key event: ${keycode}
|
|
847
|
+
result: `Sent key event: ${keycode}`,
|
|
739
848
|
};
|
|
740
849
|
}
|
|
741
850
|
catch (error) {
|
|
742
851
|
return {
|
|
743
852
|
success: false,
|
|
744
|
-
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)}`,
|
|
745
854
|
};
|
|
746
855
|
}
|
|
747
856
|
}
|
|
748
857
|
/**
|
|
749
|
-
* Send a sequence of key events to an iOS simulator
|
|
858
|
+
* Send a sequence of key events to an iOS simulator
|
|
750
859
|
*/
|
|
751
860
|
export async function iosKeySequence(keycodes, udid) {
|
|
752
861
|
try {
|
|
753
|
-
const idbAvailable = await isIdbAvailable();
|
|
754
|
-
if (!idbAvailable) {
|
|
755
|
-
return {
|
|
756
|
-
success: false,
|
|
757
|
-
error: "IDB is not installed. Install with: brew install idb-companion"
|
|
758
|
-
};
|
|
759
|
-
}
|
|
760
862
|
if (!keycodes || keycodes.length === 0) {
|
|
761
863
|
return {
|
|
762
864
|
success: false,
|
|
763
|
-
error: "At least one keycode is required"
|
|
865
|
+
error: "At least one keycode is required",
|
|
764
866
|
};
|
|
765
867
|
}
|
|
766
|
-
const
|
|
767
|
-
if (!
|
|
768
|
-
return
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
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);
|
|
772
874
|
}
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
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);
|
|
777
886
|
}
|
|
778
|
-
// Build args array for execFile (no shell)
|
|
779
|
-
const args = ["ui", "key-sequence", "--udid", targetUdid, ...keycodes.map(String)];
|
|
780
|
-
const { stderr } = await runIdb(...args);
|
|
781
|
-
if (stderr)
|
|
782
|
-
throw new Error(stderr);
|
|
783
887
|
return {
|
|
784
888
|
success: true,
|
|
785
|
-
result: `Sent key sequence: ${keycodes.join(", ")}
|
|
889
|
+
result: `Sent key sequence: ${keycodes.join(", ")}`,
|
|
786
890
|
};
|
|
787
891
|
}
|
|
788
892
|
catch (error) {
|
|
789
893
|
return {
|
|
790
894
|
success: false,
|
|
791
|
-
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)}`,
|
|
792
896
|
};
|
|
793
897
|
}
|
|
794
898
|
}
|
|
@@ -822,81 +926,65 @@ function formatAccessibilityTree(elements, depth = 0) {
|
|
|
822
926
|
return lines.join("\n");
|
|
823
927
|
}
|
|
824
928
|
/**
|
|
825
|
-
* Get accessibility info for the entire screen
|
|
929
|
+
* Get accessibility info for the entire screen
|
|
826
930
|
*/
|
|
827
931
|
export async function iosDescribeAll(udid) {
|
|
828
932
|
try {
|
|
829
|
-
const
|
|
830
|
-
if (!
|
|
831
|
-
return
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
if (!targetUdid) {
|
|
838
|
-
return {
|
|
839
|
-
success: false,
|
|
840
|
-
error: "No iOS simulator is currently running. Start a simulator first."
|
|
841
|
-
};
|
|
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;
|
|
842
941
|
}
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
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;
|
|
847
947
|
}
|
|
848
|
-
// Use execFile with args array (no shell)
|
|
849
|
-
const { stdout, stderr } = await runIdb("ui", "describe-all", "--udid", targetUdid, "--json", "--nested");
|
|
850
|
-
if (stderr)
|
|
851
|
-
throw new Error(stderr);
|
|
852
948
|
// Parse JSON response
|
|
853
949
|
const elements = JSON.parse(stdout);
|
|
854
950
|
// Format for human-readable output
|
|
855
951
|
const formatted = formatAccessibilityTree(elements);
|
|
856
|
-
const header =
|
|
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';
|
|
857
953
|
return {
|
|
858
954
|
success: true,
|
|
859
955
|
result: header + (formatted || "No accessibility elements found"),
|
|
860
|
-
elements
|
|
956
|
+
elements,
|
|
861
957
|
};
|
|
862
958
|
}
|
|
863
959
|
catch (error) {
|
|
864
960
|
return {
|
|
865
961
|
success: false,
|
|
866
|
-
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)}`,
|
|
867
963
|
};
|
|
868
964
|
}
|
|
869
965
|
}
|
|
870
966
|
/**
|
|
871
|
-
* Get accessibility info at a specific point
|
|
967
|
+
* Get accessibility info at a specific point
|
|
872
968
|
*/
|
|
873
969
|
export async function iosDescribePoint(x, y, udid) {
|
|
874
970
|
try {
|
|
875
|
-
const
|
|
876
|
-
if (!
|
|
877
|
-
return
|
|
878
|
-
|
|
879
|
-
error: "IDB is not installed. Install with: brew install idb-companion"
|
|
880
|
-
};
|
|
881
|
-
}
|
|
882
|
-
const targetUdid = udid || (await getActiveOrBootedSimulatorUdid());
|
|
883
|
-
if (!targetUdid) {
|
|
884
|
-
return {
|
|
885
|
-
success: false,
|
|
886
|
-
error: "No iOS simulator is currently running. Start a simulator first."
|
|
887
|
-
};
|
|
888
|
-
}
|
|
889
|
-
// Ensure IDB is connected to the simulator
|
|
890
|
-
const connectResult = await ensureIdbConnected(targetUdid);
|
|
891
|
-
if (!connectResult.success) {
|
|
892
|
-
return { success: false, error: connectResult.error };
|
|
893
|
-
}
|
|
971
|
+
const preflight = await ensureUiDriverReady(udid);
|
|
972
|
+
if (!preflight.ready)
|
|
973
|
+
return preflight.error;
|
|
974
|
+
const { driver, targetUdid } = preflight;
|
|
894
975
|
const xRounded = Math.round(x);
|
|
895
976
|
const yRounded = Math.round(y);
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
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
|
+
}
|
|
900
988
|
// Parse JSON response - may be single element or array
|
|
901
989
|
let element;
|
|
902
990
|
try {
|
|
@@ -907,7 +995,7 @@ export async function iosDescribePoint(x, y, udid) {
|
|
|
907
995
|
return {
|
|
908
996
|
success: true,
|
|
909
997
|
result: `No accessibility element found at (${xRounded}, ${yRounded})`,
|
|
910
|
-
elements: []
|
|
998
|
+
elements: [],
|
|
911
999
|
};
|
|
912
1000
|
}
|
|
913
1001
|
// Format for human-readable output
|
|
@@ -930,13 +1018,13 @@ export async function iosDescribePoint(x, y, udid) {
|
|
|
930
1018
|
result: parts.length > 0
|
|
931
1019
|
? `Element at (${xRounded}, ${yRounded}):\n${parts.join("\n")}`
|
|
932
1020
|
: `No accessibility element found at (${xRounded}, ${yRounded})`,
|
|
933
|
-
elements: element ? [element] : []
|
|
1021
|
+
elements: element ? [element] : [],
|
|
934
1022
|
};
|
|
935
1023
|
}
|
|
936
1024
|
catch (error) {
|
|
937
1025
|
return {
|
|
938
1026
|
success: false,
|
|
939
|
-
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)}`,
|
|
940
1028
|
};
|
|
941
1029
|
}
|
|
942
1030
|
}
|
|
@@ -963,7 +1051,7 @@ export async function iosTapElement(options) {
|
|
|
963
1051
|
if (!label && !labelContains) {
|
|
964
1052
|
return {
|
|
965
1053
|
success: false,
|
|
966
|
-
error: "Either 'label' (exact match) or 'labelContains' (partial match) is required"
|
|
1054
|
+
error: "Either 'label' (exact match) or 'labelContains' (partial match) is required",
|
|
967
1055
|
};
|
|
968
1056
|
}
|
|
969
1057
|
// Get all accessibility elements
|
|
@@ -971,12 +1059,12 @@ export async function iosTapElement(options) {
|
|
|
971
1059
|
if (!describeResult.success || !describeResult.elements) {
|
|
972
1060
|
return {
|
|
973
1061
|
success: false,
|
|
974
|
-
error: describeResult.error || "Failed to get accessibility elements"
|
|
1062
|
+
error: describeResult.error || "Failed to get accessibility elements",
|
|
975
1063
|
};
|
|
976
1064
|
}
|
|
977
1065
|
// Flatten the tree and find matching elements
|
|
978
1066
|
const allElements = flattenElements(describeResult.elements);
|
|
979
|
-
const matches = allElements.filter(el => {
|
|
1067
|
+
const matches = allElements.filter((el) => {
|
|
980
1068
|
if (!el.AXLabel)
|
|
981
1069
|
return false;
|
|
982
1070
|
if (label)
|
|
@@ -986,17 +1074,19 @@ export async function iosTapElement(options) {
|
|
|
986
1074
|
return false;
|
|
987
1075
|
});
|
|
988
1076
|
if (matches.length === 0) {
|
|
989
|
-
const searchTerm = label
|
|
1077
|
+
const searchTerm = label
|
|
1078
|
+
? `label="${label}"`
|
|
1079
|
+
: `labelContains="${labelContains}"`;
|
|
990
1080
|
return {
|
|
991
1081
|
success: false,
|
|
992
|
-
error: `No element found with ${searchTerm}
|
|
1082
|
+
error: `No element found with ${searchTerm}`,
|
|
993
1083
|
};
|
|
994
1084
|
}
|
|
995
1085
|
// Select element by index (default 0 = first match)
|
|
996
1086
|
if (index >= matches.length) {
|
|
997
1087
|
return {
|
|
998
1088
|
success: false,
|
|
999
|
-
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).`,
|
|
1000
1090
|
};
|
|
1001
1091
|
}
|
|
1002
1092
|
const element = matches[index];
|
|
@@ -1004,7 +1094,7 @@ export async function iosTapElement(options) {
|
|
|
1004
1094
|
if (!element.frame) {
|
|
1005
1095
|
return {
|
|
1006
1096
|
success: false,
|
|
1007
|
-
error: `Element "${element.AXLabel}" has no frame coordinates
|
|
1097
|
+
error: `Element "${element.AXLabel}" has no frame coordinates`,
|
|
1008
1098
|
};
|
|
1009
1099
|
}
|
|
1010
1100
|
// Calculate center
|
|
@@ -1015,7 +1105,7 @@ export async function iosTapElement(options) {
|
|
|
1015
1105
|
if (tapResult.success) {
|
|
1016
1106
|
return {
|
|
1017
1107
|
success: true,
|
|
1018
|
-
result: `Tapped "${element.AXLabel}" at (${centerX}, ${centerY})
|
|
1108
|
+
result: `Tapped "${element.AXLabel}" at (${centerX}, ${centerY})`,
|
|
1019
1109
|
};
|
|
1020
1110
|
}
|
|
1021
1111
|
return tapResult;
|
|
@@ -1023,7 +1113,7 @@ export async function iosTapElement(options) {
|
|
|
1023
1113
|
catch (error) {
|
|
1024
1114
|
return {
|
|
1025
1115
|
success: false,
|
|
1026
|
-
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)}`,
|
|
1027
1117
|
};
|
|
1028
1118
|
}
|
|
1029
1119
|
}
|
|
@@ -1041,21 +1131,27 @@ function parseIdbAccessibilityForFindElement(output) {
|
|
|
1041
1131
|
label: node.AXLabel || node.label || "",
|
|
1042
1132
|
value: node.AXValue || node.value || "",
|
|
1043
1133
|
type: node.type || node.AXType || "",
|
|
1044
|
-
identifier: node.AXIdentifier ||
|
|
1134
|
+
identifier: node.AXIdentifier ||
|
|
1135
|
+
node.identifier ||
|
|
1136
|
+
node.AXUniqueId ||
|
|
1137
|
+
"",
|
|
1045
1138
|
frame: {
|
|
1046
1139
|
x: frame.x || 0,
|
|
1047
1140
|
y: frame.y || 0,
|
|
1048
1141
|
width: frame.width || 0,
|
|
1049
|
-
height: frame.height || 0
|
|
1142
|
+
height: frame.height || 0,
|
|
1050
1143
|
},
|
|
1051
1144
|
center: {
|
|
1052
1145
|
x: Math.round((frame.x || 0) + (frame.width || 0) / 2),
|
|
1053
|
-
y: Math.round((frame.y || 0) + (frame.height || 0) / 2)
|
|
1146
|
+
y: Math.round((frame.y || 0) + (frame.height || 0) / 2),
|
|
1054
1147
|
},
|
|
1055
1148
|
enabled: node.enabled !== false,
|
|
1056
|
-
traits: node.traits || []
|
|
1149
|
+
traits: node.traits || [],
|
|
1057
1150
|
};
|
|
1058
|
-
if (element.label ||
|
|
1151
|
+
if (element.label ||
|
|
1152
|
+
element.value ||
|
|
1153
|
+
element.type ||
|
|
1154
|
+
element.identifier) {
|
|
1059
1155
|
elements.push(element);
|
|
1060
1156
|
}
|
|
1061
1157
|
}
|
|
@@ -1086,7 +1182,10 @@ function parseIdbAccessibilityForFindElement(output) {
|
|
|
1086
1182
|
*/
|
|
1087
1183
|
function isIOSElementInViewport(element, screenWidth, screenHeight) {
|
|
1088
1184
|
const { center } = element;
|
|
1089
|
-
return center.x >= 0 &&
|
|
1185
|
+
return (center.x >= 0 &&
|
|
1186
|
+
center.x <= screenWidth &&
|
|
1187
|
+
center.y >= 0 &&
|
|
1188
|
+
center.y <= screenHeight);
|
|
1090
1189
|
}
|
|
1091
1190
|
/**
|
|
1092
1191
|
* Extract screen dimensions from the parsed UI tree.
|
|
@@ -1135,7 +1234,9 @@ function matchesIOSFindElement(element, options) {
|
|
|
1135
1234
|
return false;
|
|
1136
1235
|
}
|
|
1137
1236
|
if (options.identifierContains !== undefined) {
|
|
1138
|
-
if (!element.identifier
|
|
1237
|
+
if (!element.identifier
|
|
1238
|
+
.toLowerCase()
|
|
1239
|
+
.includes(options.identifierContains.toLowerCase()))
|
|
1139
1240
|
return false;
|
|
1140
1241
|
}
|
|
1141
1242
|
return true;
|
|
@@ -1145,36 +1246,30 @@ function matchesIOSFindElement(element, options) {
|
|
|
1145
1246
|
*/
|
|
1146
1247
|
export async function iosGetUITree(udid) {
|
|
1147
1248
|
try {
|
|
1148
|
-
const
|
|
1149
|
-
if (!
|
|
1150
|
-
return
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
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;
|
|
1154
1257
|
}
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
success: false,
|
|
1159
|
-
error: "No iOS simulator is currently running. Start a simulator first."
|
|
1160
|
-
};
|
|
1161
|
-
}
|
|
1162
|
-
const connectResult = await ensureIdbConnected(targetUdid);
|
|
1163
|
-
if (!connectResult.success) {
|
|
1164
|
-
return { success: false, error: connectResult.error };
|
|
1258
|
+
else {
|
|
1259
|
+
const result = await runIdb("ui", "describe-all", "--udid", targetUdid);
|
|
1260
|
+
stdout = result.stdout;
|
|
1165
1261
|
}
|
|
1166
|
-
const { stdout } = await runIdb("ui", "describe-all", "--udid", targetUdid);
|
|
1167
1262
|
const elements = parseIdbAccessibilityForFindElement(stdout);
|
|
1168
1263
|
return {
|
|
1169
1264
|
success: true,
|
|
1170
1265
|
elements,
|
|
1171
|
-
rawOutput: stdout
|
|
1266
|
+
rawOutput: stdout,
|
|
1172
1267
|
};
|
|
1173
1268
|
}
|
|
1174
1269
|
catch (error) {
|
|
1175
1270
|
return {
|
|
1176
1271
|
success: false,
|
|
1177
|
-
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)}`,
|
|
1178
1273
|
};
|
|
1179
1274
|
}
|
|
1180
1275
|
}
|
|
@@ -1183,12 +1278,17 @@ export async function iosGetUITree(udid) {
|
|
|
1183
1278
|
*/
|
|
1184
1279
|
export async function iosFindElement(options, udid) {
|
|
1185
1280
|
try {
|
|
1186
|
-
if (!options.label &&
|
|
1187
|
-
!options.
|
|
1281
|
+
if (!options.label &&
|
|
1282
|
+
!options.labelContains &&
|
|
1283
|
+
!options.value &&
|
|
1284
|
+
!options.valueContains &&
|
|
1285
|
+
!options.type &&
|
|
1286
|
+
!options.identifier &&
|
|
1287
|
+
!options.identifierContains) {
|
|
1188
1288
|
return {
|
|
1189
1289
|
success: false,
|
|
1190
1290
|
found: false,
|
|
1191
|
-
error: "At least one search criteria (label, labelContains, value, valueContains, identifier, identifierContains, or type) must be provided"
|
|
1291
|
+
error: "At least one search criteria (label, labelContains, value, valueContains, identifier, identifierContains, or type) must be provided",
|
|
1192
1292
|
};
|
|
1193
1293
|
}
|
|
1194
1294
|
const treeResult = await iosGetUITree(udid);
|
|
@@ -1196,26 +1296,28 @@ export async function iosFindElement(options, udid) {
|
|
|
1196
1296
|
return {
|
|
1197
1297
|
success: false,
|
|
1198
1298
|
found: false,
|
|
1199
|
-
error: treeResult.error
|
|
1299
|
+
error: treeResult.error,
|
|
1200
1300
|
};
|
|
1201
1301
|
}
|
|
1202
1302
|
// Filter to viewport-visible elements first, then match criteria.
|
|
1203
1303
|
// This prevents selecting off-screen duplicates in scroll views / pagers.
|
|
1204
|
-
const screenSize = treeResult.rawOutput
|
|
1304
|
+
const screenSize = treeResult.rawOutput
|
|
1305
|
+
? extractIOSScreenSize(treeResult.rawOutput)
|
|
1306
|
+
: null;
|
|
1205
1307
|
const visibleElements = screenSize
|
|
1206
|
-
? treeResult.elements.filter(el => isIOSElementInViewport(el, screenSize.width, screenSize.height))
|
|
1308
|
+
? treeResult.elements.filter((el) => isIOSElementInViewport(el, screenSize.width, screenSize.height))
|
|
1207
1309
|
: treeResult.elements;
|
|
1208
|
-
let matches = visibleElements.filter(el => matchesIOSFindElement(el, options));
|
|
1310
|
+
let matches = visibleElements.filter((el) => matchesIOSFindElement(el, options));
|
|
1209
1311
|
// If no visible matches, fall back to all elements (element may be partially visible
|
|
1210
1312
|
// or the screen size extraction failed)
|
|
1211
1313
|
if (matches.length === 0) {
|
|
1212
|
-
matches = treeResult.elements.filter(el => matchesIOSFindElement(el, options));
|
|
1314
|
+
matches = treeResult.elements.filter((el) => matchesIOSFindElement(el, options));
|
|
1213
1315
|
}
|
|
1214
1316
|
if (matches.length === 0) {
|
|
1215
1317
|
return {
|
|
1216
1318
|
success: true,
|
|
1217
1319
|
found: false,
|
|
1218
|
-
matchCount: 0
|
|
1320
|
+
matchCount: 0,
|
|
1219
1321
|
};
|
|
1220
1322
|
}
|
|
1221
1323
|
const index = options.index ?? 0;
|
|
@@ -1225,7 +1327,7 @@ export async function iosFindElement(options, udid) {
|
|
|
1225
1327
|
success: true,
|
|
1226
1328
|
found: false,
|
|
1227
1329
|
matchCount: matches.length,
|
|
1228
|
-
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).`,
|
|
1229
1331
|
};
|
|
1230
1332
|
}
|
|
1231
1333
|
return {
|
|
@@ -1233,14 +1335,14 @@ export async function iosFindElement(options, udid) {
|
|
|
1233
1335
|
found: true,
|
|
1234
1336
|
element: selectedElement,
|
|
1235
1337
|
allMatches: matches,
|
|
1236
|
-
matchCount: matches.length
|
|
1338
|
+
matchCount: matches.length,
|
|
1237
1339
|
};
|
|
1238
1340
|
}
|
|
1239
1341
|
catch (error) {
|
|
1240
1342
|
return {
|
|
1241
1343
|
success: false,
|
|
1242
1344
|
found: false,
|
|
1243
|
-
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)}`,
|
|
1244
1346
|
};
|
|
1245
1347
|
}
|
|
1246
1348
|
}
|
|
@@ -1271,17 +1373,26 @@ export async function getDevicePixelRatio(udid) {
|
|
|
1271
1373
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
1272
1374
|
const screenshotPath = path.join(os.tmpdir(), `ios-dpr-screenshot-${timestamp}.png`);
|
|
1273
1375
|
await execAsync(`xcrun simctl io ${resolvedUdid} screenshot "${screenshotPath}"`, {
|
|
1274
|
-
timeout: SIMCTL_TIMEOUT
|
|
1376
|
+
timeout: SIMCTL_TIMEOUT,
|
|
1275
1377
|
});
|
|
1276
1378
|
const metadata = await sharp(screenshotPath).metadata();
|
|
1277
1379
|
const pixelWidth = metadata.width;
|
|
1278
1380
|
const pixelHeight = metadata.height;
|
|
1279
|
-
// Try
|
|
1280
|
-
const
|
|
1281
|
-
|
|
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) {
|
|
1282
1385
|
try {
|
|
1283
|
-
|
|
1284
|
-
|
|
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
|
+
}
|
|
1285
1396
|
const screenSize = extractIOSScreenSize(stdout);
|
|
1286
1397
|
if (screenSize) {
|
|
1287
1398
|
const dpr = calculateDPR(pixelWidth, screenSize.width);
|
|
@@ -1290,7 +1401,7 @@ export async function getDevicePixelRatio(udid) {
|
|
|
1290
1401
|
}
|
|
1291
1402
|
}
|
|
1292
1403
|
catch {
|
|
1293
|
-
//
|
|
1404
|
+
// driver failed — fall through to inference
|
|
1294
1405
|
}
|
|
1295
1406
|
}
|
|
1296
1407
|
// Fallback: infer DPR from screenshot pixel dimensions
|
|
@@ -1310,13 +1421,16 @@ export async function iosWaitForElement(options, udid) {
|
|
|
1310
1421
|
const timeoutMs = options.timeoutMs ?? 10000;
|
|
1311
1422
|
const pollIntervalMs = options.pollIntervalMs ?? 500;
|
|
1312
1423
|
const startTime = Date.now();
|
|
1313
|
-
if (!options.label &&
|
|
1314
|
-
!options.
|
|
1424
|
+
if (!options.label &&
|
|
1425
|
+
!options.labelContains &&
|
|
1426
|
+
!options.value &&
|
|
1427
|
+
!options.valueContains &&
|
|
1428
|
+
!options.type) {
|
|
1315
1429
|
return {
|
|
1316
1430
|
success: false,
|
|
1317
1431
|
found: false,
|
|
1318
1432
|
timedOut: false,
|
|
1319
|
-
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",
|
|
1320
1434
|
};
|
|
1321
1435
|
}
|
|
1322
1436
|
while (Date.now() - startTime < timeoutMs) {
|
|
@@ -1325,24 +1439,24 @@ export async function iosWaitForElement(options, udid) {
|
|
|
1325
1439
|
return {
|
|
1326
1440
|
...result,
|
|
1327
1441
|
elapsedMs: Date.now() - startTime,
|
|
1328
|
-
timedOut: false
|
|
1442
|
+
timedOut: false,
|
|
1329
1443
|
};
|
|
1330
1444
|
}
|
|
1331
1445
|
if (!result.success) {
|
|
1332
1446
|
return {
|
|
1333
1447
|
...result,
|
|
1334
1448
|
elapsedMs: Date.now() - startTime,
|
|
1335
|
-
timedOut: false
|
|
1449
|
+
timedOut: false,
|
|
1336
1450
|
};
|
|
1337
1451
|
}
|
|
1338
|
-
await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
|
|
1452
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
1339
1453
|
}
|
|
1340
1454
|
return {
|
|
1341
1455
|
success: true,
|
|
1342
1456
|
found: false,
|
|
1343
1457
|
elapsedMs: Date.now() - startTime,
|
|
1344
1458
|
timedOut: true,
|
|
1345
|
-
error: `Timed out after ${timeoutMs}ms waiting for element
|
|
1459
|
+
error: `Timed out after ${timeoutMs}ms waiting for element`,
|
|
1346
1460
|
};
|
|
1347
1461
|
}
|
|
1348
1462
|
//# sourceMappingURL=ios.js.map
|