mobile-debug-mcp 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -282,6 +282,150 @@ Get the currently visible activity on an Android device.
282
282
  }
283
283
  ```
284
284
 
285
+ ### wait_for_element
286
+ Wait until a UI element with matching text appears on screen or timeout is reached. Useful for handling loading states or transitions.
287
+
288
+ **Input:**
289
+ ```jsonc
290
+ {
291
+ "platform": "android" | "ios",
292
+ "text": "Home", // Text to wait for
293
+ "timeout": 5000, // Max wait time in ms (default 10000)
294
+ "deviceId": "emulator-5554" // Optional
295
+ }
296
+ ```
297
+
298
+ **Response:**
299
+ ```json
300
+ {
301
+ "device": { /* device info */ },
302
+ "found": true,
303
+ "element": { /* UIElement object if found */ }
304
+ }
305
+ ```
306
+
307
+ If the element is not found within the timeout, `found` will be `false`. If a system error occurs (e.g., ADB failure), an `error` field will be present.
308
+
309
+ ```json
310
+ {
311
+ "device": { /* device info */ },
312
+ "found": false,
313
+ "error": "Optional error message"
314
+ }
315
+ ```
316
+
317
+ ### tap
318
+ Simulate a finger tap on the device screen at specific coordinates.
319
+
320
+ Platform support and constraints:
321
+ - Android: Implemented via `adb shell input tap` and works when `adb` is available in PATH or configured via `ADB_PATH`.
322
+ - iOS: Requires Facebook's `idb` tooling. The iOS implementation uses `idb` to deliver UI events and is simulator-oriented (works reliably on a booted simulator). Physical device support depends on `idb` capabilities and a running `idb_companion` on the target device; it may not work in all environments.
323
+
324
+ Prerequisites for iOS (if you intend to use tap on iOS):
325
+ ```bash
326
+ brew tap facebook/fb
327
+ brew install idb-companion
328
+ pip3 install fb-idb
329
+ ```
330
+ Ensure `idb` and `idb_companion` are in your PATH. If you use non-standard tool locations, set `XCRUN_PATH` and/or `ADB_PATH` environment variables as appropriate.
331
+
332
+ Behavior notes:
333
+ - The tool is a primitive input: it only sends a tap at the provided coordinates. It does not inspect or interpret the UI.
334
+ - If `idb` is missing or the simulator/device is not available, the tool will return an error explaining the failure.
335
+
336
+ **Input:**
337
+ ```jsonc
338
+ {
339
+ "platform": "android" | "ios", // Optional, defaults to "android"
340
+ "x": 200, // X coordinate (Required)
341
+ "y": 400, // Y coordinate (Required)
342
+ "deviceId": "emulator-5554" // Optional
343
+ }
344
+ ```
345
+
346
+ **Response:**
347
+ ```json
348
+ {
349
+ "device": { /* device info */ },
350
+ "success": true,
351
+ "x": 200,
352
+ "y": 400
353
+ }
354
+ ```
355
+
356
+ If the tap fails (e.g., missing `adb`/`idb`, device not found), `success` will be `false` and an `error` field will be present.
357
+
358
+ ### swipe
359
+ Simulate a swipe gesture on an Android device.
360
+
361
+ **Input:**
362
+ ```jsonc
363
+ {
364
+ "platform": "android", // Optional, defaults to "android"
365
+ "x1": 500, // Start X (Required)
366
+ "y1": 1500, // Start Y (Required)
367
+ "x2": 500, // End X (Required)
368
+ "y2": 500, // End Y (Required)
369
+ "duration": 300, // Duration in ms (Required)
370
+ "deviceId": "emulator-5554" // Optional
371
+ }
372
+ ```
373
+
374
+ **Response:**
375
+ ```json
376
+ {
377
+ "device": { /* device info */ },
378
+ "success": true,
379
+ "start": [500, 1500],
380
+ "end": [500, 500],
381
+ "duration": 300
382
+ }
383
+ ```
384
+
385
+ If the swipe fails, `success` will be `false` and an `error` field will be present.
386
+
387
+ ### type_text
388
+ Type text into the currently focused input field on an Android device.
389
+
390
+ **Input:**
391
+ ```jsonc
392
+ {
393
+ "platform": "android", // Optional, defaults to "android"
394
+ "text": "hello world", // Text to type (Required)
395
+ "deviceId": "emulator-5554" // Optional
396
+ }
397
+ ```
398
+
399
+ **Response:**
400
+ ```json
401
+ {
402
+ "device": { /* device info */ },
403
+ "success": true,
404
+ "text": "hello world"
405
+ }
406
+ ```
407
+
408
+ If the command fails, `success` will be `false` and an `error` field will be present.
409
+
410
+ ### press_back
411
+ Simulate pressing the Android Back button.
412
+
413
+ **Input:**
414
+ ```jsonc
415
+ {
416
+ "platform": "android", // Optional
417
+ "deviceId": "emulator-5554" // Optional
418
+ }
419
+ ```
420
+
421
+ **Response:**
422
+ ```json
423
+ {
424
+ "device": { /* device info */ },
425
+ "success": true
426
+ }
427
+ ```
428
+
285
429
  ---
286
430
 
287
431
  ## Recommended Workflow
@@ -290,8 +434,9 @@ Get the currently visible activity on an Android device.
290
434
  2. Use `start_app` to launch the app.
291
435
  3. Use `get_logs` to read the latest logs.
292
436
  4. Use `capture_screenshot` to visually inspect the app if needed.
293
- 5. Use `reset_app_data` to clear state if debugging fresh install scenarios.
294
- 6. Use `restart_app` to quickly reboot the app during development cycles.
437
+ 5. Use `wait_for_element` to ensure the app is in the expected state before proceeding (e.g., after login).
438
+ 6. Use `reset_app_data` to clear state if debugging fresh install scenarios.
439
+ 7. Use `restart_app` to quickly reboot the app during development cycles.
295
440
 
296
441
  ---
297
442
 
@@ -1,5 +1,81 @@
1
1
  import { execAdb, getAndroidDeviceMetadata, getDeviceInfo } from "./utils.js";
2
+ import { AndroidObserve } from "./observe.js";
2
3
  export class AndroidInteract {
4
+ observe = new AndroidObserve();
5
+ async waitForElement(text, timeout, deviceId) {
6
+ const metadata = await getAndroidDeviceMetadata("", deviceId);
7
+ const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
8
+ const startTime = Date.now();
9
+ while (Date.now() - startTime < timeout) {
10
+ try {
11
+ const tree = await this.observe.getUITree(deviceId);
12
+ if (tree.error) {
13
+ return { device: deviceInfo, found: false, error: tree.error };
14
+ }
15
+ const element = tree.elements.find(e => e.text === text);
16
+ if (element) {
17
+ return { device: deviceInfo, found: true, element };
18
+ }
19
+ }
20
+ catch (e) {
21
+ // Ignore errors during polling and retry
22
+ console.error("Error polling UI tree:", e);
23
+ }
24
+ const elapsed = Date.now() - startTime;
25
+ const remaining = timeout - elapsed;
26
+ if (remaining <= 0)
27
+ break;
28
+ await new Promise(resolve => setTimeout(resolve, Math.min(500, remaining)));
29
+ }
30
+ return { device: deviceInfo, found: false };
31
+ }
32
+ async tap(x, y, deviceId) {
33
+ const metadata = await getAndroidDeviceMetadata("", deviceId);
34
+ const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
35
+ try {
36
+ await execAdb(['shell', 'input', 'tap', x.toString(), y.toString()], deviceId);
37
+ return { device: deviceInfo, success: true, x, y };
38
+ }
39
+ catch (e) {
40
+ return { device: deviceInfo, success: false, x, y, error: e instanceof Error ? e.message : String(e) };
41
+ }
42
+ }
43
+ async swipe(x1, y1, x2, y2, duration, deviceId) {
44
+ const metadata = await getAndroidDeviceMetadata("", deviceId);
45
+ const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
46
+ try {
47
+ await execAdb(['shell', 'input', 'swipe', x1.toString(), y1.toString(), x2.toString(), y2.toString(), duration.toString()], deviceId);
48
+ return { device: deviceInfo, success: true, start: [x1, y1], end: [x2, y2], duration };
49
+ }
50
+ catch (e) {
51
+ return { device: deviceInfo, success: false, start: [x1, y1], end: [x2, y2], duration, error: e instanceof Error ? e.message : String(e) };
52
+ }
53
+ }
54
+ async typeText(text, deviceId) {
55
+ const metadata = await getAndroidDeviceMetadata("", deviceId);
56
+ const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
57
+ try {
58
+ // Encode spaces as %s to ensure proper input handling by adb shell input text
59
+ const encodedText = text.replace(/\s/g, '%s');
60
+ // Note: 'input text' might fail with some characters or if keyboard isn't ready, but it's the standard ADB way.
61
+ await execAdb(['shell', 'input', 'text', encodedText], deviceId);
62
+ return { device: deviceInfo, success: true, text };
63
+ }
64
+ catch (e) {
65
+ return { device: deviceInfo, success: false, text, error: e instanceof Error ? e.message : String(e) };
66
+ }
67
+ }
68
+ async pressBack(deviceId) {
69
+ const metadata = await getAndroidDeviceMetadata("", deviceId);
70
+ const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
71
+ try {
72
+ await execAdb(['shell', 'input', 'keyevent', '4'], deviceId);
73
+ return { device: deviceInfo, success: true };
74
+ }
75
+ catch (e) {
76
+ return { device: deviceInfo, success: false, error: e instanceof Error ? e.message : String(e) };
77
+ }
78
+ }
3
79
  async startApp(appId, deviceId) {
4
80
  const metadata = await getAndroidDeviceMetadata(appId, deviceId);
5
81
  const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
@@ -1,6 +1,76 @@
1
1
  import { promises as fs } from "fs";
2
- import { execCommand, getIOSDeviceMetadata, validateBundleId } from "./utils.js";
2
+ import { spawn } from "child_process";
3
+ import { execCommand, getIOSDeviceMetadata, validateBundleId, IDB } from "./utils.js";
4
+ import { iOSObserve } from "./observe.js";
3
5
  export class iOSInteract {
6
+ observe = new iOSObserve();
7
+ async waitForElement(text, timeout, deviceId = "booted") {
8
+ const device = await getIOSDeviceMetadata(deviceId);
9
+ const startTime = Date.now();
10
+ while (Date.now() - startTime < timeout) {
11
+ try {
12
+ const tree = await this.observe.getUITree(deviceId);
13
+ if (tree.error) {
14
+ return { device, found: false, error: tree.error };
15
+ }
16
+ const element = tree.elements.find(e => e.text === text);
17
+ if (element) {
18
+ return { device, found: true, element };
19
+ }
20
+ }
21
+ catch (e) {
22
+ // Ignore errors during polling and retry
23
+ console.error("Error polling UI tree:", e);
24
+ }
25
+ const elapsed = Date.now() - startTime;
26
+ const remaining = timeout - elapsed;
27
+ if (remaining <= 0)
28
+ break;
29
+ await new Promise(resolve => setTimeout(resolve, Math.min(500, remaining)));
30
+ }
31
+ return { device, found: false };
32
+ }
33
+ async tap(x, y, deviceId = "booted") {
34
+ const device = await getIOSDeviceMetadata(deviceId);
35
+ // Check for idb
36
+ const child = spawn(IDB, ['--version']);
37
+ const idbExists = await new Promise((resolve) => {
38
+ child.on('error', () => resolve(false));
39
+ child.on('close', (code) => resolve(code === 0));
40
+ });
41
+ if (!idbExists) {
42
+ return {
43
+ device,
44
+ success: false,
45
+ x,
46
+ y,
47
+ error: "iOS tap requires 'idb' (iOS Device Bridge)."
48
+ };
49
+ }
50
+ try {
51
+ const targetUdid = (device.id && device.id !== 'booted') ? device.id : undefined;
52
+ const args = ['ui', 'tap', x.toString(), y.toString()];
53
+ if (targetUdid) {
54
+ args.push('--udid', targetUdid);
55
+ }
56
+ await new Promise((resolve, reject) => {
57
+ const proc = spawn(IDB, args);
58
+ let stderr = '';
59
+ proc.stderr.on('data', d => stderr += d.toString());
60
+ proc.on('close', code => {
61
+ if (code === 0)
62
+ resolve();
63
+ else
64
+ reject(new Error(`idb ui tap failed: ${stderr}`));
65
+ });
66
+ proc.on('error', err => reject(err));
67
+ });
68
+ return { device, success: true, x, y };
69
+ }
70
+ catch (e) {
71
+ return { device, success: false, x, y, error: e instanceof Error ? e.message : String(e) };
72
+ }
73
+ }
4
74
  async startApp(bundleId, deviceId = "booted") {
5
75
  validateBundleId(bundleId);
6
76
  const result = await execCommand(['simctl', 'launch', deviceId, bundleId], deviceId);
package/dist/server.js CHANGED
@@ -12,7 +12,7 @@ const iosObserve = new iOSObserve();
12
12
  const iosInteract = new iOSInteract();
13
13
  const server = new Server({
14
14
  name: "mobile-debug-mcp",
15
- version: "0.6.0"
15
+ version: "0.7.0"
16
16
  }, {
17
17
  capabilities: {
18
18
  tools: {}
@@ -191,6 +191,126 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
191
191
  }
192
192
  }
193
193
  }
194
+ },
195
+ {
196
+ name: "wait_for_element",
197
+ description: "Wait until a UI element with matching text appears on screen or timeout is reached.",
198
+ inputSchema: {
199
+ type: "object",
200
+ properties: {
201
+ platform: {
202
+ type: "string",
203
+ enum: ["android", "ios"],
204
+ description: "Platform to check"
205
+ },
206
+ text: {
207
+ type: "string",
208
+ description: "Text content of the element to wait for"
209
+ },
210
+ timeout: {
211
+ type: "number",
212
+ description: "Max wait time in ms (default 10000)",
213
+ default: 10000
214
+ },
215
+ deviceId: {
216
+ type: "string",
217
+ description: "Device Serial/UDID. Defaults to connected/booted device."
218
+ }
219
+ },
220
+ required: ["platform", "text"]
221
+ }
222
+ },
223
+ {
224
+ name: "tap",
225
+ description: "Simulate a finger tap on the device screen at specific coordinates.",
226
+ inputSchema: {
227
+ type: "object",
228
+ properties: {
229
+ platform: {
230
+ type: "string",
231
+ enum: ["android", "ios"],
232
+ description: "Platform to tap on"
233
+ },
234
+ x: {
235
+ type: "number",
236
+ description: "X coordinate"
237
+ },
238
+ y: {
239
+ type: "number",
240
+ description: "Y coordinate"
241
+ },
242
+ deviceId: {
243
+ type: "string",
244
+ description: "Device Serial/UDID. Defaults to connected/booted device."
245
+ }
246
+ },
247
+ required: ["x", "y"]
248
+ }
249
+ },
250
+ {
251
+ name: "swipe",
252
+ description: "Simulate a swipe gesture on an Android device.",
253
+ inputSchema: {
254
+ type: "object",
255
+ properties: {
256
+ platform: {
257
+ type: "string",
258
+ enum: ["android"],
259
+ description: "Platform to swipe on (currently only android supported)"
260
+ },
261
+ x1: { type: "number", description: "Start X coordinate" },
262
+ y1: { type: "number", description: "Start Y coordinate" },
263
+ x2: { type: "number", description: "End X coordinate" },
264
+ y2: { type: "number", description: "End Y coordinate" },
265
+ duration: { type: "number", description: "Duration in ms" },
266
+ deviceId: {
267
+ type: "string",
268
+ description: "Device Serial/UDID. Defaults to connected/booted device."
269
+ }
270
+ },
271
+ required: ["x1", "y1", "x2", "y2", "duration"]
272
+ }
273
+ },
274
+ {
275
+ name: "type_text",
276
+ description: "Type text into the currently focused input field on an Android device.",
277
+ inputSchema: {
278
+ type: "object",
279
+ properties: {
280
+ platform: {
281
+ type: "string",
282
+ enum: ["android"],
283
+ description: "Platform to type on (currently only android supported)"
284
+ },
285
+ text: {
286
+ type: "string",
287
+ description: "The text to type"
288
+ },
289
+ deviceId: {
290
+ type: "string",
291
+ description: "Device Serial/UDID. Defaults to connected/booted device."
292
+ }
293
+ },
294
+ required: ["text"]
295
+ }
296
+ },
297
+ {
298
+ name: "press_back",
299
+ description: "Simulate pressing the Android Back button.",
300
+ inputSchema: {
301
+ type: "object",
302
+ properties: {
303
+ platform: {
304
+ type: "string",
305
+ enum: ["android"],
306
+ description: "Platform (currently only android supported)"
307
+ },
308
+ deviceId: {
309
+ type: "string",
310
+ description: "Device Serial/UDID. Defaults to connected/booted device."
311
+ }
312
+ }
313
+ }
194
314
  }
195
315
  ]
196
316
  }));
@@ -376,6 +496,73 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
376
496
  const result = await androidObserve.getCurrentScreen(deviceId);
377
497
  return wrapResponse(result);
378
498
  }
499
+ if (name === "wait_for_element") {
500
+ const { platform, text, timeout, deviceId } = (args || {});
501
+ const effectiveTimeout = timeout ?? 10000;
502
+ let result;
503
+ if (platform === "android") {
504
+ result = await androidInteract.waitForElement(text, effectiveTimeout, deviceId);
505
+ }
506
+ else {
507
+ result = await iosInteract.waitForElement(text, effectiveTimeout, deviceId);
508
+ }
509
+ return wrapResponse(result);
510
+ }
511
+ if (name === "tap") {
512
+ const { platform, x, y, deviceId } = (args || {});
513
+ const effectivePlatform = platform || "android";
514
+ // Basic validation
515
+ if (typeof x !== 'number' || typeof y !== 'number') {
516
+ throw new Error("x and y coordinates are required and must be numbers");
517
+ }
518
+ let result;
519
+ if (effectivePlatform === "android") {
520
+ result = await androidInteract.tap(x, y, deviceId);
521
+ }
522
+ else {
523
+ result = await iosInteract.tap(x, y, deviceId);
524
+ }
525
+ return wrapResponse(result);
526
+ }
527
+ if (name === "swipe") {
528
+ const { platform, x1, y1, x2, y2, duration, deviceId } = (args || {});
529
+ const effectivePlatform = platform || "android";
530
+ if (typeof x1 !== 'number' || typeof y1 !== 'number' || typeof x2 !== 'number' || typeof y2 !== 'number' || typeof duration !== 'number') {
531
+ throw new Error("x1, y1, x2, y2, and duration are required and must be numbers");
532
+ }
533
+ let result;
534
+ if (effectivePlatform === "android") {
535
+ result = await androidInteract.swipe(x1, y1, x2, y2, duration, deviceId);
536
+ }
537
+ else {
538
+ throw new Error(`Platform ${effectivePlatform} not supported for swipe`);
539
+ }
540
+ return wrapResponse(result);
541
+ }
542
+ if (name === "type_text") {
543
+ const { platform, text, deviceId } = (args || {});
544
+ const effectivePlatform = platform || "android";
545
+ if (typeof text !== 'string') {
546
+ throw new Error("text is required and must be a string");
547
+ }
548
+ let result;
549
+ if (effectivePlatform === "android") {
550
+ result = await androidInteract.typeText(text, deviceId);
551
+ }
552
+ else {
553
+ throw new Error(`Platform ${effectivePlatform} not supported for type_text`);
554
+ }
555
+ return wrapResponse(result);
556
+ }
557
+ if (name === "press_back") {
558
+ const { platform, deviceId } = (args || {});
559
+ const effectivePlatform = platform || "android";
560
+ if (effectivePlatform !== "android") {
561
+ throw new Error(`Platform ${effectivePlatform} not supported for press_back`);
562
+ }
563
+ const result = await androidInteract.pressBack(deviceId);
564
+ return wrapResponse(result);
565
+ }
379
566
  }
380
567
  catch (error) {
381
568
  return {
package/docs/CHANGELOG.md CHANGED
@@ -2,12 +2,17 @@
2
2
 
3
3
  All notable changes to the **Mobile Debug MCP** project will be documented in this file.
4
4
 
5
- ## [0.6.0] - 2026-03-11
5
+ ## [0.7.0]
6
6
 
7
7
  ### Added
8
+ - **`wait_for_element` tool**: Added ability to wait for a specific UI element to appear on screen. Polls `get_ui_tree` until timeout. Useful for waiting on app transitions or loading states.
8
9
  - **`get_current_screen` tool**: Added ability to determine the currently visible activity on an Android device using `dumpsys activity activities`. Includes robust regex parsing to handle various Android versions.
10
+ - **`tap` tool**: Added ability to tap at specific screen coordinates on Android and iOS devices.
11
+ - **`swipe` tool**: Added ability to simulate swipe gestures (scroll, drag) on Android devices.
12
+ - **`type_text` tool**: Added ability to type text into focused input fields on Android devices.
13
+ - **`press_back` tool**: Added ability to simulate the Android Back button.
9
14
 
10
- ## [0.4.0] - 2026-03-09
15
+ ## [0.4.0]
11
16
 
12
17
  ### Added
13
18
  - **`terminate_app` tool**: Added ability to terminate apps on Android and iOS.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mobile-debug-mcp",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "MCP server for mobile app debugging (Android + iOS), with focus on security and reliability",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,92 @@
1
- import { StartAppResponse, TerminateAppResponse, RestartAppResponse, ResetAppDataResponse } from "../types.js"
1
+ import { StartAppResponse, TerminateAppResponse, RestartAppResponse, ResetAppDataResponse, WaitForElementResponse, TapResponse, SwipeResponse, TypeTextResponse, PressBackResponse } from "../types.js"
2
2
  import { execAdb, getAndroidDeviceMetadata, getDeviceInfo } from "./utils.js"
3
+ import { AndroidObserve } from "./observe.js"
3
4
 
4
5
  export class AndroidInteract {
6
+ private observe = new AndroidObserve();
7
+
8
+ async waitForElement(text: string, timeout: number, deviceId?: string): Promise<WaitForElementResponse> {
9
+ const metadata = await getAndroidDeviceMetadata("", deviceId)
10
+ const deviceInfo = getDeviceInfo(deviceId || 'default', metadata)
11
+ const startTime = Date.now();
12
+
13
+ while (Date.now() - startTime < timeout) {
14
+ try {
15
+ const tree = await this.observe.getUITree(deviceId);
16
+
17
+ if (tree.error) {
18
+ return { device: deviceInfo, found: false, error: tree.error };
19
+ }
20
+
21
+ const element = tree.elements.find(e => e.text === text);
22
+ if (element) {
23
+ return { device: deviceInfo, found: true, element };
24
+ }
25
+ } catch (e) {
26
+ // Ignore errors during polling and retry
27
+ console.error("Error polling UI tree:", e);
28
+ }
29
+
30
+ const elapsed = Date.now() - startTime;
31
+ const remaining = timeout - elapsed;
32
+ if (remaining <= 0) break;
33
+
34
+ await new Promise(resolve => setTimeout(resolve, Math.min(500, remaining)));
35
+ }
36
+ return { device: deviceInfo, found: false };
37
+ }
38
+
39
+ async tap(x: number, y: number, deviceId?: string): Promise<TapResponse> {
40
+ const metadata = await getAndroidDeviceMetadata("", deviceId)
41
+ const deviceInfo = getDeviceInfo(deviceId || 'default', metadata)
42
+
43
+ try {
44
+ await execAdb(['shell', 'input', 'tap', x.toString(), y.toString()], deviceId)
45
+ return { device: deviceInfo, success: true, x, y }
46
+ } catch (e) {
47
+ return { device: deviceInfo, success: false, x, y, error: e instanceof Error ? e.message : String(e) }
48
+ }
49
+ }
50
+
51
+ async swipe(x1: number, y1: number, x2: number, y2: number, duration: number, deviceId?: string): Promise<SwipeResponse> {
52
+ const metadata = await getAndroidDeviceMetadata("", deviceId)
53
+ const deviceInfo = getDeviceInfo(deviceId || 'default', metadata)
54
+
55
+ try {
56
+ await execAdb(['shell', 'input', 'swipe', x1.toString(), y1.toString(), x2.toString(), y2.toString(), duration.toString()], deviceId)
57
+ return { device: deviceInfo, success: true, start: [x1, y1], end: [x2, y2], duration }
58
+ } catch (e) {
59
+ return { device: deviceInfo, success: false, start: [x1, y1], end: [x2, y2], duration, error: e instanceof Error ? e.message : String(e) }
60
+ }
61
+ }
62
+
63
+ async typeText(text: string, deviceId?: string): Promise<TypeTextResponse> {
64
+ const metadata = await getAndroidDeviceMetadata("", deviceId)
65
+ const deviceInfo = getDeviceInfo(deviceId || 'default', metadata)
66
+
67
+ try {
68
+ // Encode spaces as %s to ensure proper input handling by adb shell input text
69
+ const encodedText = text.replace(/\s/g, '%s')
70
+ // Note: 'input text' might fail with some characters or if keyboard isn't ready, but it's the standard ADB way.
71
+ await execAdb(['shell', 'input', 'text', encodedText], deviceId)
72
+ return { device: deviceInfo, success: true, text }
73
+ } catch (e) {
74
+ return { device: deviceInfo, success: false, text, error: e instanceof Error ? e.message : String(e) }
75
+ }
76
+ }
77
+
78
+ async pressBack(deviceId?: string): Promise<PressBackResponse> {
79
+ const metadata = await getAndroidDeviceMetadata("", deviceId)
80
+ const deviceInfo = getDeviceInfo(deviceId || 'default', metadata)
81
+
82
+ try {
83
+ await execAdb(['shell', 'input', 'keyevent', '4'], deviceId)
84
+ return { device: deviceInfo, success: true }
85
+ } catch (e) {
86
+ return { device: deviceInfo, success: false, error: e instanceof Error ? e.message : String(e) }
87
+ }
88
+ }
89
+
5
90
  async startApp(appId: string, deviceId?: string): Promise<StartAppResponse> {
6
91
  const metadata = await getAndroidDeviceMetadata(appId, deviceId)
7
92
  const deviceInfo = getDeviceInfo(deviceId || 'default', metadata)
@@ -1,8 +1,86 @@
1
1
  import { promises as fs } from "fs"
2
- import { StartAppResponse, TerminateAppResponse, RestartAppResponse, ResetAppDataResponse } from "../types.js"
3
- import { execCommand, getIOSDeviceMetadata, validateBundleId } from "./utils.js"
2
+ import { spawn } from "child_process"
3
+ import { StartAppResponse, TerminateAppResponse, RestartAppResponse, ResetAppDataResponse, WaitForElementResponse, TapResponse } from "../types.js"
4
+ import { execCommand, getIOSDeviceMetadata, validateBundleId, IDB } from "./utils.js"
5
+ import { iOSObserve } from "./observe.js"
4
6
 
5
7
  export class iOSInteract {
8
+ private observe = new iOSObserve();
9
+
10
+ async waitForElement(text: string, timeout: number, deviceId: string = "booted"): Promise<WaitForElementResponse> {
11
+ const device = await getIOSDeviceMetadata(deviceId);
12
+ const startTime = Date.now();
13
+
14
+ while (Date.now() - startTime < timeout) {
15
+ try {
16
+ const tree = await this.observe.getUITree(deviceId);
17
+
18
+ if (tree.error) {
19
+ return { device, found: false, error: tree.error };
20
+ }
21
+
22
+ const element = tree.elements.find(e => e.text === text);
23
+ if (element) {
24
+ return { device, found: true, element };
25
+ }
26
+ } catch (e) {
27
+ // Ignore errors during polling and retry
28
+ console.error("Error polling UI tree:", e);
29
+ }
30
+
31
+ const elapsed = Date.now() - startTime;
32
+ const remaining = timeout - elapsed;
33
+ if (remaining <= 0) break;
34
+
35
+ await new Promise(resolve => setTimeout(resolve, Math.min(500, remaining)));
36
+ }
37
+ return { device, found: false };
38
+ }
39
+
40
+ async tap(x: number, y: number, deviceId: string = "booted"): Promise<TapResponse> {
41
+ const device = await getIOSDeviceMetadata(deviceId)
42
+
43
+ // Check for idb
44
+ const child = spawn(IDB, ['--version']);
45
+ const idbExists = await new Promise<boolean>((resolve) => {
46
+ child.on('error', () => resolve(false));
47
+ child.on('close', (code) => resolve(code === 0));
48
+ });
49
+
50
+ if (!idbExists) {
51
+ return {
52
+ device,
53
+ success: false,
54
+ x,
55
+ y,
56
+ error: "iOS tap requires 'idb' (iOS Device Bridge)."
57
+ }
58
+ }
59
+
60
+ try {
61
+ const targetUdid = (device.id && device.id !== 'booted') ? device.id : undefined;
62
+ const args = ['ui', 'tap', x.toString(), y.toString()];
63
+ if (targetUdid) {
64
+ args.push('--udid', targetUdid);
65
+ }
66
+
67
+ await new Promise<void>((resolve, reject) => {
68
+ const proc = spawn(IDB, args);
69
+ let stderr = '';
70
+ proc.stderr.on('data', d => stderr += d.toString());
71
+ proc.on('close', code => {
72
+ if (code === 0) resolve();
73
+ else reject(new Error(`idb ui tap failed: ${stderr}`));
74
+ });
75
+ proc.on('error', err => reject(err));
76
+ });
77
+
78
+ return { device, success: true, x, y };
79
+ } catch (e) {
80
+ return { device, success: false, x, y, error: e instanceof Error ? e.message : String(e) };
81
+ }
82
+ }
83
+
6
84
  async startApp(bundleId: string, deviceId: string = "booted"): Promise<StartAppResponse> {
7
85
  validateBundleId(bundleId)
8
86
  const result = await execCommand(['simctl', 'launch', deviceId, bundleId], deviceId)
package/src/server.ts CHANGED
@@ -13,7 +13,12 @@ import {
13
13
  RestartAppResponse,
14
14
  ResetAppDataResponse,
15
15
  GetUITreeResponse,
16
- GetCurrentScreenResponse
16
+ GetCurrentScreenResponse,
17
+ WaitForElementResponse,
18
+ TapResponse,
19
+ SwipeResponse,
20
+ TypeTextResponse,
21
+ PressBackResponse
17
22
  } from "./types.js"
18
23
 
19
24
  import { AndroidObserve } from "./android/observe.js"
@@ -29,7 +34,7 @@ const iosInteract = new iOSInteract()
29
34
  const server = new Server(
30
35
  {
31
36
  name: "mobile-debug-mcp",
32
- version: "0.6.0"
37
+ version: "0.7.0"
33
38
  },
34
39
  {
35
40
  capabilities: {
@@ -212,6 +217,126 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
212
217
  }
213
218
  }
214
219
  }
220
+ },
221
+ {
222
+ name: "wait_for_element",
223
+ description: "Wait until a UI element with matching text appears on screen or timeout is reached.",
224
+ inputSchema: {
225
+ type: "object",
226
+ properties: {
227
+ platform: {
228
+ type: "string",
229
+ enum: ["android", "ios"],
230
+ description: "Platform to check"
231
+ },
232
+ text: {
233
+ type: "string",
234
+ description: "Text content of the element to wait for"
235
+ },
236
+ timeout: {
237
+ type: "number",
238
+ description: "Max wait time in ms (default 10000)",
239
+ default: 10000
240
+ },
241
+ deviceId: {
242
+ type: "string",
243
+ description: "Device Serial/UDID. Defaults to connected/booted device."
244
+ }
245
+ },
246
+ required: ["platform", "text"]
247
+ }
248
+ },
249
+ {
250
+ name: "tap",
251
+ description: "Simulate a finger tap on the device screen at specific coordinates.",
252
+ inputSchema: {
253
+ type: "object",
254
+ properties: {
255
+ platform: {
256
+ type: "string",
257
+ enum: ["android", "ios"],
258
+ description: "Platform to tap on"
259
+ },
260
+ x: {
261
+ type: "number",
262
+ description: "X coordinate"
263
+ },
264
+ y: {
265
+ type: "number",
266
+ description: "Y coordinate"
267
+ },
268
+ deviceId: {
269
+ type: "string",
270
+ description: "Device Serial/UDID. Defaults to connected/booted device."
271
+ }
272
+ },
273
+ required: ["x", "y"]
274
+ }
275
+ },
276
+ {
277
+ name: "swipe",
278
+ description: "Simulate a swipe gesture on an Android device.",
279
+ inputSchema: {
280
+ type: "object",
281
+ properties: {
282
+ platform: {
283
+ type: "string",
284
+ enum: ["android"],
285
+ description: "Platform to swipe on (currently only android supported)"
286
+ },
287
+ x1: { type: "number", description: "Start X coordinate" },
288
+ y1: { type: "number", description: "Start Y coordinate" },
289
+ x2: { type: "number", description: "End X coordinate" },
290
+ y2: { type: "number", description: "End Y coordinate" },
291
+ duration: { type: "number", description: "Duration in ms" },
292
+ deviceId: {
293
+ type: "string",
294
+ description: "Device Serial/UDID. Defaults to connected/booted device."
295
+ }
296
+ },
297
+ required: ["x1", "y1", "x2", "y2", "duration"]
298
+ }
299
+ },
300
+ {
301
+ name: "type_text",
302
+ description: "Type text into the currently focused input field on an Android device.",
303
+ inputSchema: {
304
+ type: "object",
305
+ properties: {
306
+ platform: {
307
+ type: "string",
308
+ enum: ["android"],
309
+ description: "Platform to type on (currently only android supported)"
310
+ },
311
+ text: {
312
+ type: "string",
313
+ description: "The text to type"
314
+ },
315
+ deviceId: {
316
+ type: "string",
317
+ description: "Device Serial/UDID. Defaults to connected/booted device."
318
+ }
319
+ },
320
+ required: ["text"]
321
+ }
322
+ },
323
+ {
324
+ name: "press_back",
325
+ description: "Simulate pressing the Android Back button.",
326
+ inputSchema: {
327
+ type: "object",
328
+ properties: {
329
+ platform: {
330
+ type: "string",
331
+ enum: ["android"],
332
+ description: "Platform (currently only android supported)"
333
+ },
334
+ deviceId: {
335
+ type: "string",
336
+ description: "Device Serial/UDID. Defaults to connected/booted device."
337
+ }
338
+ }
339
+ }
215
340
  }
216
341
  ]
217
342
  }))
@@ -444,6 +569,113 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
444
569
  const result = await androidObserve.getCurrentScreen(deviceId)
445
570
  return wrapResponse(result)
446
571
  }
572
+
573
+ if (name === "wait_for_element") {
574
+ const { platform, text, timeout, deviceId } = (args || {}) as {
575
+ platform: "android" | "ios"
576
+ text: string
577
+ timeout?: number
578
+ deviceId?: string
579
+ }
580
+
581
+ const effectiveTimeout = timeout ?? 10000;
582
+
583
+ let result: WaitForElementResponse;
584
+ if (platform === "android") {
585
+ result = await androidInteract.waitForElement(text, effectiveTimeout, deviceId)
586
+ } else {
587
+ result = await iosInteract.waitForElement(text, effectiveTimeout, deviceId)
588
+ }
589
+ return wrapResponse(result)
590
+ }
591
+
592
+ if (name === "tap") {
593
+ const { platform, x, y, deviceId } = (args || {}) as {
594
+ platform?: "android" | "ios"
595
+ x: number
596
+ y: number
597
+ deviceId?: string
598
+ }
599
+
600
+ const effectivePlatform = platform || "android";
601
+
602
+ // Basic validation
603
+ if (typeof x !== 'number' || typeof y !== 'number') {
604
+ throw new Error("x and y coordinates are required and must be numbers");
605
+ }
606
+
607
+ let result: TapResponse;
608
+ if (effectivePlatform === "android") {
609
+ result = await androidInteract.tap(x, y, deviceId)
610
+ } else {
611
+ result = await iosInteract.tap(x, y, deviceId)
612
+ }
613
+ return wrapResponse(result)
614
+ }
615
+
616
+ if (name === "swipe") {
617
+ const { platform, x1, y1, x2, y2, duration, deviceId } = (args || {}) as {
618
+ platform?: "android"
619
+ x1: number
620
+ y1: number
621
+ x2: number
622
+ y2: number
623
+ duration: number
624
+ deviceId?: string
625
+ }
626
+
627
+ const effectivePlatform = platform || "android";
628
+
629
+ if (typeof x1 !== 'number' || typeof y1 !== 'number' || typeof x2 !== 'number' || typeof y2 !== 'number' || typeof duration !== 'number') {
630
+ throw new Error("x1, y1, x2, y2, and duration are required and must be numbers");
631
+ }
632
+
633
+ let result: SwipeResponse;
634
+ if (effectivePlatform === "android") {
635
+ result = await androidInteract.swipe(x1, y1, x2, y2, duration, deviceId)
636
+ } else {
637
+ throw new Error(`Platform ${effectivePlatform} not supported for swipe`)
638
+ }
639
+ return wrapResponse(result)
640
+ }
641
+
642
+ if (name === "type_text") {
643
+ const { platform, text, deviceId } = (args || {}) as {
644
+ platform?: "android"
645
+ text: string
646
+ deviceId?: string
647
+ }
648
+
649
+ const effectivePlatform = platform || "android";
650
+
651
+ if (typeof text !== 'string') {
652
+ throw new Error("text is required and must be a string");
653
+ }
654
+
655
+ let result: TypeTextResponse;
656
+ if (effectivePlatform === "android") {
657
+ result = await androidInteract.typeText(text, deviceId)
658
+ } else {
659
+ throw new Error(`Platform ${effectivePlatform} not supported for type_text`)
660
+ }
661
+ return wrapResponse(result)
662
+ }
663
+
664
+ if (name === "press_back") {
665
+ const { platform, deviceId } = (args || {}) as {
666
+ platform?: "android"
667
+ deviceId?: string
668
+ }
669
+
670
+ const effectivePlatform = platform || "android";
671
+
672
+ if (effectivePlatform !== "android") {
673
+ throw new Error(`Platform ${effectivePlatform} not supported for press_back`)
674
+ }
675
+
676
+ const result = await androidInteract.pressBack(deviceId)
677
+ return wrapResponse(result)
678
+ }
447
679
  } catch (error) {
448
680
  return {
449
681
  content: [{ type: "text", text: `Error executing tool ${name}: ${error instanceof Error ? error.message : String(error)}` }]
package/src/types.ts CHANGED
@@ -90,3 +90,40 @@ export interface GetCurrentScreenResponse {
90
90
  shortActivity: string;
91
91
  error?: string;
92
92
  }
93
+
94
+ export interface WaitForElementResponse {
95
+ device: DeviceInfo;
96
+ found: boolean;
97
+ element?: UIElement;
98
+ error?: string;
99
+ }
100
+
101
+ export interface TapResponse {
102
+ device: DeviceInfo;
103
+ success: boolean;
104
+ x: number;
105
+ y: number;
106
+ error?: string;
107
+ }
108
+
109
+ export interface SwipeResponse {
110
+ device: DeviceInfo;
111
+ success: boolean;
112
+ start: [number, number];
113
+ end: [number, number];
114
+ duration: number;
115
+ error?: string;
116
+ }
117
+
118
+ export interface TypeTextResponse {
119
+ device: DeviceInfo;
120
+ success: boolean;
121
+ text: string;
122
+ error?: string;
123
+ }
124
+
125
+ export interface PressBackResponse {
126
+ device: DeviceInfo;
127
+ success: boolean;
128
+ error?: string;
129
+ }
@@ -0,0 +1,24 @@
1
+ // This script wraps the real test execution for ease of use
2
+ // It sets ADB_PATH and invokes the test file
3
+ import { spawn } from 'child_process';
4
+ import path from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+ const ADB_PATH = process.env.ADB_PATH || process.env.ADB || 'adb';
9
+ const TEST_FILE = path.join(__dirname, 'wait_for_element_real.js');
10
+
11
+ // Merge ADB_PATH into the child environment if provided
12
+ const childEnv = { ...process.env, ADB_PATH };
13
+
14
+ // Use NODE to execute the JS test by default, or respect RUNNER env if set (e.g., 'npx tsx')
15
+ const runner = process.env.RUNNER || 'node';
16
+ const runnerArgs = [TEST_FILE];
17
+
18
+ const child = spawn(runner, runnerArgs, {
19
+ env: childEnv,
20
+ stdio: 'inherit'
21
+ });
22
+ child.on('exit', (code) => {
23
+ process.exit(code || 0);
24
+ });
@@ -0,0 +1,113 @@
1
+ import { AndroidInteract } from "../src/android/interact.js";
2
+ import { AndroidObserve } from "../src/android/observe.js";
3
+ // Mock the observe method
4
+ // We need to override the prototype method directly
5
+ // Since we are using ES modules, we need to handle the import correctly or just override the instance method if possible.
6
+ // But getUITree is an instance method on AndroidObserve, which AndroidInteract instantiates internally.
7
+ // We can override the prototype of AndroidObserve to affect all instances.
8
+ const originalGetUITree = AndroidObserve.prototype.getUITree;
9
+ async function runTests() {
10
+ console.log("Starting tests for wait_for_element...");
11
+ const interact = new AndroidInteract();
12
+ // ---------------------------------------------------------
13
+ // Test 1: Element found immediately
14
+ // ---------------------------------------------------------
15
+ console.log("\nTest 1: Element found immediately");
16
+ AndroidObserve.prototype.getUITree = async () => ({
17
+ device: { platform: "android", id: "mock", osVersion: "12", model: "Pixel", simulator: true },
18
+ screen: "",
19
+ resolution: { width: 1080, height: 1920 },
20
+ elements: [{
21
+ text: "Target",
22
+ type: "Button",
23
+ contentDescription: null,
24
+ clickable: true,
25
+ enabled: true,
26
+ visible: true,
27
+ bounds: [0, 0, 100, 100],
28
+ resourceId: null
29
+ }]
30
+ });
31
+ const start1 = Date.now();
32
+ const result1 = await interact.waitForElement("Target", 1000);
33
+ const elapsed1 = Date.now() - start1;
34
+ console.log("Result:", result1.found === true ? "PASS" : "FAIL");
35
+ console.log("Element:", result1.element ? "FOUND" : "MISSING");
36
+ console.log("Elapsed:", elapsed1, "ms");
37
+ // ---------------------------------------------------------
38
+ // Test 2: Element not found (timeout)
39
+ // ---------------------------------------------------------
40
+ console.log("\nTest 2: Element not found (timeout)");
41
+ AndroidObserve.prototype.getUITree = async () => ({
42
+ device: { platform: "android", id: "mock", osVersion: "12", model: "Pixel", simulator: true },
43
+ screen: "",
44
+ resolution: { width: 1080, height: 1920 },
45
+ elements: []
46
+ });
47
+ const start2 = Date.now();
48
+ // Use a short timeout for test speed, but long enough to sleep at least once
49
+ const result2 = await interact.waitForElement("Target", 1200);
50
+ const elapsed2 = Date.now() - start2;
51
+ console.log("Result:", result2.found === false ? "PASS" : "FAIL");
52
+ // Should wait at least 1200ms
53
+ console.log("Elapsed time (should be >= 1200ms):", elapsed2, elapsed2 >= 1200 ? "PASS" : "FAIL");
54
+ // ---------------------------------------------------------
55
+ // Test 3: Element found after polling
56
+ // ---------------------------------------------------------
57
+ console.log("\nTest 3: Element found after polling");
58
+ let calls = 0;
59
+ AndroidObserve.prototype.getUITree = async () => {
60
+ calls++;
61
+ if (calls < 3) {
62
+ return {
63
+ device: { platform: "android", id: "mock", osVersion: "12", model: "Pixel", simulator: true },
64
+ screen: "",
65
+ resolution: { width: 1080, height: 1920 },
66
+ elements: []
67
+ };
68
+ }
69
+ return {
70
+ device: { platform: "android", id: "mock", osVersion: "12", model: "Pixel", simulator: true },
71
+ screen: "",
72
+ resolution: { width: 1080, height: 1920 },
73
+ elements: [{
74
+ text: "Target",
75
+ type: "Button",
76
+ contentDescription: null,
77
+ clickable: true,
78
+ enabled: true,
79
+ visible: true,
80
+ bounds: [0, 0, 100, 100],
81
+ resourceId: null
82
+ }]
83
+ };
84
+ };
85
+ const start3 = Date.now();
86
+ const result3 = await interact.waitForElement("Target", 2000);
87
+ const elapsed3 = Date.now() - start3;
88
+ console.log("Result:", result3.found === true ? "PASS" : "FAIL");
89
+ console.log("Calls:", calls, calls === 3 ? "PASS" : "FAIL");
90
+ // Expected calls: 0ms (fail), 500ms (fail), 1000ms (success). Elapsed should be >= 1000ms.
91
+ console.log("Elapsed time (should be >= 1000ms):", elapsed3, elapsed3 >= 1000 ? "PASS" : "FAIL");
92
+ // ---------------------------------------------------------
93
+ // Test 4: Error handling (fast failure)
94
+ // ---------------------------------------------------------
95
+ console.log("\nTest 4: Error handling (fast failure)");
96
+ AndroidObserve.prototype.getUITree = async () => ({
97
+ device: { platform: "android", id: "mock", osVersion: "12", model: "Pixel", simulator: true },
98
+ screen: "",
99
+ resolution: { width: 0, height: 0 },
100
+ elements: [],
101
+ error: "ADB Connection Failed"
102
+ });
103
+ const start4 = Date.now();
104
+ const result4 = await interact.waitForElement("Target", 5000);
105
+ const elapsed4 = Date.now() - start4;
106
+ console.log("Result:", result4.found === false && result4.error === "ADB Connection Failed" ? "PASS" : "FAIL");
107
+ console.log("Error Message:", result4.error);
108
+ // Should fail fast, not wait for timeout
109
+ console.log("Elapsed time (should be < 500ms):", elapsed4, elapsed4 < 500 ? "PASS" : "FAIL");
110
+ // Restore original
111
+ AndroidObserve.prototype.getUITree = originalGetUITree;
112
+ }
113
+ runTests().catch(console.error);
@@ -0,0 +1,67 @@
1
+ import { AndroidInteract } from "../src/android/interact.js";
2
+ import { AndroidObserve } from "../src/android/observe.js";
3
+ // Configure ADB path and target via env vars or CLI args.
4
+ // Usage: node test/wait_for_element_real.js [deviceId] [appId]
5
+ // Priority: CLI args > environment variables
6
+ const args = process.argv.slice(2);
7
+ const DEVICE_ID = args[0] || process.env.DEVICE_ID;
8
+ const APP_ID = args[1] || process.env.APP_ID;
9
+
10
+ // Do not hard-code ADB_PATH here. If the user supplied ADB_PATH in the environment, leave it as-is.
11
+ // (process.env.ADB_PATH may be set externally before running this script.)
12
+
13
+ if (!DEVICE_ID || !APP_ID) {
14
+ console.error("Usage: node test/wait_for_element_real.js <deviceId> <appId> or set DEVICE_ID and APP_ID env vars");
15
+ process.exit(1);
16
+ }
17
+ async function runRealTest() {
18
+ console.log(`Connecting to device ${DEVICE_ID}...`);
19
+ const interact = new AndroidInteract();
20
+ const observe = new AndroidObserve();
21
+ try {
22
+ // 1. Start App
23
+ console.log(`\nStarting app ${APP_ID}...`);
24
+ await interact.startApp(APP_ID, DEVICE_ID);
25
+ // Give it a moment to render
26
+ console.log("Waiting 3s for app to render...");
27
+ await new Promise(r => setTimeout(r, 3000));
28
+ // 2. Get UI Tree to find a valid text target
29
+ console.log("\nFetching UI Tree to find a target text...");
30
+ const tree = await observe.getUITree(DEVICE_ID);
31
+ if (tree.error) {
32
+ console.error("Failed to get UI Tree:", tree.error);
33
+ return;
34
+ }
35
+ // Find first visible element with text
36
+ const targetElement = tree.elements.find(e => e.text && e.text.length > 0 && e.visible);
37
+ if (!targetElement || !targetElement.text) {
38
+ console.warn("No visible text elements found on screen to test with.");
39
+ console.log("Elements found:", tree.elements.length);
40
+ return;
41
+ }
42
+ const targetText = targetElement.text;
43
+ console.log(`Found target element: "${targetText}"`);
44
+ // 3. Test waitForElement (Success Case)
45
+ console.log(`\nTest 1: Waiting for existing element "${targetText}" (should succeed)...`);
46
+ const start1 = Date.now();
47
+ const result1 = await interact.waitForElement(targetText, 5000, DEVICE_ID);
48
+ const elapsed1 = Date.now() - start1;
49
+ console.log(`Result: ${result1.found ? "PASS" : "FAIL"}`);
50
+ console.log(`Found Element: ${result1.element?.text}`);
51
+ console.log(`Time taken: ${elapsed1}ms`);
52
+ // 4. Test waitForElement (Timeout Case)
53
+ const missingText = "THIS_TEXT_SHOULD_NOT_EXIST_XYZ_123";
54
+ console.log(`\nTest 2: Waiting for missing element "${missingText}" (should timeout)...`);
55
+ const start2 = Date.now();
56
+ // Use short timeout 2s
57
+ const result2 = await interact.waitForElement(missingText, 2000, DEVICE_ID);
58
+ const elapsed2 = Date.now() - start2;
59
+ console.log(`Result: ${!result2.found ? "PASS" : "FAIL"}`);
60
+ console.log(`Found: ${result2.found}`);
61
+ console.log(`Time taken: ${elapsed2}ms (expected ~2000ms)`);
62
+ }
63
+ catch (error) {
64
+ console.error("Test failed with error:", error);
65
+ }
66
+ }
67
+ runRealTest();