mobile-debug-mcp 0.5.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 +244 -31
- package/dist/android/interact.js +106 -0
- package/dist/android/observe.js +313 -0
- package/dist/android/utils.js +82 -0
- package/dist/android.js +131 -1
- package/dist/ios/interact.js +135 -0
- package/dist/ios/observe.js +219 -0
- package/dist/ios/utils.js +114 -0
- package/dist/ios.js +134 -0
- package/dist/server.js +271 -27
- package/docs/CHANGELOG.md +11 -1
- package/package.json +2 -1
- package/smoke-test.ts +17 -10
- package/src/android/interact.ts +126 -0
- package/src/android/observe.ts +360 -0
- package/src/android/utils.ts +94 -0
- package/src/ios/interact.ts +153 -0
- package/src/ios/observe.ts +269 -0
- package/src/ios/utils.ts +133 -0
- package/src/server.ts +322 -28
- package/src/types.ts +71 -0
- package/test/run-real-test.js +24 -0
- package/test/wait_for_element_mock.js +113 -0
- package/test/wait_for_element_real.js +67 -0
- package/test-ui-tree.js +68 -0
- package/test-ui-tree.ts +76 -0
- package/tsconfig.json +2 -1
- package/src/android.ts +0 -222
- package/src/ios.ts +0 -243
package/dist/server.js
CHANGED
|
@@ -2,11 +2,17 @@
|
|
|
2
2
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { AndroidObserve } from "./android/observe.js";
|
|
6
|
+
import { AndroidInteract } from "./android/interact.js";
|
|
7
|
+
import { iOSObserve } from "./ios/observe.js";
|
|
8
|
+
import { iOSInteract } from "./ios/interact.js";
|
|
9
|
+
const androidObserve = new AndroidObserve();
|
|
10
|
+
const androidInteract = new AndroidInteract();
|
|
11
|
+
const iosObserve = new iOSObserve();
|
|
12
|
+
const iosInteract = new iOSInteract();
|
|
7
13
|
const server = new Server({
|
|
8
14
|
name: "mobile-debug-mcp",
|
|
9
|
-
version: "0.
|
|
15
|
+
version: "0.7.0"
|
|
10
16
|
}, {
|
|
11
17
|
capabilities: {
|
|
12
18
|
tools: {}
|
|
@@ -153,6 +159,158 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
153
159
|
},
|
|
154
160
|
required: ["platform"]
|
|
155
161
|
}
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
name: "get_ui_tree",
|
|
165
|
+
description: "Get the current UI hierarchy from an Android device or iOS simulator. Returns a structured JSON representation of the screen content.",
|
|
166
|
+
inputSchema: {
|
|
167
|
+
type: "object",
|
|
168
|
+
properties: {
|
|
169
|
+
platform: {
|
|
170
|
+
type: "string",
|
|
171
|
+
enum: ["android", "ios"],
|
|
172
|
+
description: "Platform to get UI tree for"
|
|
173
|
+
},
|
|
174
|
+
deviceId: {
|
|
175
|
+
type: "string",
|
|
176
|
+
description: "Device Serial (Android) or UDID (iOS). Defaults to connected/booted device."
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
required: ["platform"]
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
name: "get_current_screen",
|
|
184
|
+
description: "Get the currently visible activity on an Android device. Returns package and activity name.",
|
|
185
|
+
inputSchema: {
|
|
186
|
+
type: "object",
|
|
187
|
+
properties: {
|
|
188
|
+
deviceId: {
|
|
189
|
+
type: "string",
|
|
190
|
+
description: "Device Serial (Android). Defaults to connected/booted device."
|
|
191
|
+
}
|
|
192
|
+
}
|
|
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
|
+
}
|
|
156
314
|
}
|
|
157
315
|
]
|
|
158
316
|
}));
|
|
@@ -165,16 +323,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
165
323
|
let launchTimeMs;
|
|
166
324
|
let deviceInfo;
|
|
167
325
|
if (platform === "android") {
|
|
168
|
-
const result = await
|
|
326
|
+
const result = await androidInteract.startApp(appId, deviceId);
|
|
169
327
|
appStarted = result.appStarted;
|
|
170
328
|
launchTimeMs = result.launchTimeMs;
|
|
171
|
-
deviceInfo =
|
|
329
|
+
deviceInfo = result.device;
|
|
172
330
|
}
|
|
173
331
|
else {
|
|
174
|
-
const result = await
|
|
332
|
+
const result = await iosInteract.startApp(appId, deviceId);
|
|
175
333
|
appStarted = result.appStarted;
|
|
176
334
|
launchTimeMs = result.launchTimeMs;
|
|
177
|
-
deviceInfo =
|
|
335
|
+
deviceInfo = result.device;
|
|
178
336
|
}
|
|
179
337
|
const response = {
|
|
180
338
|
device: deviceInfo,
|
|
@@ -188,14 +346,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
188
346
|
let appTerminated;
|
|
189
347
|
let deviceInfo;
|
|
190
348
|
if (platform === "android") {
|
|
191
|
-
const result = await
|
|
349
|
+
const result = await androidInteract.terminateApp(appId, deviceId);
|
|
192
350
|
appTerminated = result.appTerminated;
|
|
193
|
-
deviceInfo =
|
|
351
|
+
deviceInfo = result.device;
|
|
194
352
|
}
|
|
195
353
|
else {
|
|
196
|
-
const result = await
|
|
354
|
+
const result = await iosInteract.terminateApp(appId, deviceId);
|
|
197
355
|
appTerminated = result.appTerminated;
|
|
198
|
-
deviceInfo =
|
|
356
|
+
deviceInfo = result.device;
|
|
199
357
|
}
|
|
200
358
|
const response = {
|
|
201
359
|
device: deviceInfo,
|
|
@@ -209,16 +367,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
209
367
|
let launchTimeMs;
|
|
210
368
|
let deviceInfo;
|
|
211
369
|
if (platform === "android") {
|
|
212
|
-
const result = await
|
|
370
|
+
const result = await androidInteract.restartApp(appId, deviceId);
|
|
213
371
|
appRestarted = result.appRestarted;
|
|
214
372
|
launchTimeMs = result.launchTimeMs;
|
|
215
|
-
deviceInfo =
|
|
373
|
+
deviceInfo = result.device;
|
|
216
374
|
}
|
|
217
375
|
else {
|
|
218
|
-
const result = await
|
|
376
|
+
const result = await iosInteract.restartApp(appId, deviceId);
|
|
219
377
|
appRestarted = result.appRestarted;
|
|
220
378
|
launchTimeMs = result.launchTimeMs;
|
|
221
|
-
deviceInfo =
|
|
379
|
+
deviceInfo = result.device;
|
|
222
380
|
}
|
|
223
381
|
const response = {
|
|
224
382
|
device: deviceInfo,
|
|
@@ -232,14 +390,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
232
390
|
let dataCleared;
|
|
233
391
|
let deviceInfo;
|
|
234
392
|
if (platform === "android") {
|
|
235
|
-
const result = await
|
|
393
|
+
const result = await androidInteract.resetAppData(appId, deviceId);
|
|
236
394
|
dataCleared = result.dataCleared;
|
|
237
|
-
deviceInfo =
|
|
395
|
+
deviceInfo = result.device;
|
|
238
396
|
}
|
|
239
397
|
else {
|
|
240
|
-
const result = await
|
|
398
|
+
const result = await iosInteract.resetAppData(appId, deviceId);
|
|
241
399
|
dataCleared = result.dataCleared;
|
|
242
|
-
deviceInfo =
|
|
400
|
+
deviceInfo = result.device;
|
|
243
401
|
}
|
|
244
402
|
const response = {
|
|
245
403
|
device: deviceInfo,
|
|
@@ -252,13 +410,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
252
410
|
let logs;
|
|
253
411
|
let deviceInfo;
|
|
254
412
|
if (platform === "android") {
|
|
255
|
-
deviceInfo = await
|
|
256
|
-
const response = await
|
|
413
|
+
deviceInfo = await androidObserve.getDeviceMetadata(appId || "", deviceId);
|
|
414
|
+
const response = await androidObserve.getLogs(appId, lines ?? 200, deviceId);
|
|
257
415
|
logs = Array.isArray(response.logs) ? response.logs : [];
|
|
258
416
|
}
|
|
259
417
|
else {
|
|
260
|
-
deviceInfo = await
|
|
261
|
-
const response = await
|
|
418
|
+
deviceInfo = await iosObserve.getDeviceMetadata(deviceId);
|
|
419
|
+
const response = await iosObserve.getLogs(appId, deviceId);
|
|
262
420
|
logs = Array.isArray(response.logs) ? response.logs : [];
|
|
263
421
|
}
|
|
264
422
|
// Filter crash lines (e.g. lines containing 'FATAL EXCEPTION') for internal or AI use
|
|
@@ -289,14 +447,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
289
447
|
let resolution;
|
|
290
448
|
let deviceInfo;
|
|
291
449
|
if (platform === "android") {
|
|
292
|
-
deviceInfo = await
|
|
293
|
-
const result = await
|
|
450
|
+
deviceInfo = await androidObserve.getDeviceMetadata("", deviceId);
|
|
451
|
+
const result = await androidObserve.captureScreen(deviceId);
|
|
294
452
|
screenshot = result.screenshot;
|
|
295
453
|
resolution = result.resolution;
|
|
296
454
|
}
|
|
297
455
|
else {
|
|
298
|
-
deviceInfo = await
|
|
299
|
-
const result = await
|
|
456
|
+
deviceInfo = await iosObserve.getDeviceMetadata(deviceId);
|
|
457
|
+
const result = await iosObserve.captureScreenshot(deviceId);
|
|
300
458
|
screenshot = result.screenshot;
|
|
301
459
|
resolution = result.resolution;
|
|
302
460
|
}
|
|
@@ -319,6 +477,92 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
319
477
|
]
|
|
320
478
|
};
|
|
321
479
|
}
|
|
480
|
+
if (name === "get_ui_tree") {
|
|
481
|
+
const { platform, deviceId } = args;
|
|
482
|
+
let result;
|
|
483
|
+
if (platform === "android") {
|
|
484
|
+
result = await androidObserve.getUITree(deviceId);
|
|
485
|
+
}
|
|
486
|
+
else if (platform === "ios") {
|
|
487
|
+
result = await iosObserve.getUITree(deviceId);
|
|
488
|
+
}
|
|
489
|
+
else {
|
|
490
|
+
throw new Error(`Platform ${platform} not supported for get_ui_tree`);
|
|
491
|
+
}
|
|
492
|
+
return wrapResponse(result);
|
|
493
|
+
}
|
|
494
|
+
if (name === "get_current_screen") {
|
|
495
|
+
const { deviceId } = (args || {});
|
|
496
|
+
const result = await androidObserve.getCurrentScreen(deviceId);
|
|
497
|
+
return wrapResponse(result);
|
|
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
|
+
}
|
|
322
566
|
}
|
|
323
567
|
catch (error) {
|
|
324
568
|
return {
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -2,7 +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.
|
|
5
|
+
## [0.7.0]
|
|
6
|
+
|
|
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.
|
|
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.
|
|
14
|
+
|
|
15
|
+
## [0.4.0]
|
|
6
16
|
|
|
7
17
|
### Added
|
|
8
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.
|
|
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": {
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
19
|
+
"fast-xml-parser": "^5.5.1",
|
|
19
20
|
"zod": "^3.22.4"
|
|
20
21
|
},
|
|
21
22
|
"devDependencies": {
|
package/smoke-test.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { AndroidObserve } from "./src/android/observe.js";
|
|
2
|
+
import { AndroidInteract } from "./src/android/interact.js";
|
|
3
|
+
import { iOSObserve } from "./src/ios/observe.js";
|
|
4
|
+
import { iOSInteract } from "./src/ios/interact.js";
|
|
3
5
|
import fs from "fs/promises";
|
|
4
6
|
import path from "path";
|
|
5
7
|
|
|
8
|
+
const androidObserve = new AndroidObserve();
|
|
9
|
+
const androidInteract = new AndroidInteract();
|
|
10
|
+
const iosObserve = new iOSObserve();
|
|
11
|
+
const iosInteract = new iOSInteract();
|
|
12
|
+
|
|
6
13
|
async function main() {
|
|
7
14
|
const args = process.argv.slice(2);
|
|
8
15
|
const platform = args[0] as string; // Cast to string first
|
|
@@ -22,11 +29,11 @@ async function main() {
|
|
|
22
29
|
let launchTimeMs: number;
|
|
23
30
|
|
|
24
31
|
if (platform === "android") {
|
|
25
|
-
const result = await
|
|
32
|
+
const result = await androidInteract.startApp(appId);
|
|
26
33
|
startResult = result.appStarted;
|
|
27
34
|
launchTimeMs = result.launchTimeMs;
|
|
28
35
|
} else {
|
|
29
|
-
const result = await
|
|
36
|
+
const result = await iosInteract.startApp(appId);
|
|
30
37
|
startResult = result.appStarted;
|
|
31
38
|
launchTimeMs = result.launchTimeMs;
|
|
32
39
|
}
|
|
@@ -47,11 +54,11 @@ async function main() {
|
|
|
47
54
|
let resolution: { width: number; height: number };
|
|
48
55
|
|
|
49
56
|
if (platform === "android") {
|
|
50
|
-
const result = await
|
|
57
|
+
const result = await androidObserve.captureScreen();
|
|
51
58
|
screenshotBase64 = result.screenshot;
|
|
52
59
|
resolution = result.resolution;
|
|
53
60
|
} else {
|
|
54
|
-
const result = await
|
|
61
|
+
const result = await iosObserve.captureScreenshot();
|
|
55
62
|
screenshotBase64 = result.screenshot;
|
|
56
63
|
resolution = result.resolution;
|
|
57
64
|
}
|
|
@@ -70,11 +77,11 @@ async function main() {
|
|
|
70
77
|
let logs: string[] = [];
|
|
71
78
|
|
|
72
79
|
if (platform === "android") {
|
|
73
|
-
const result = await
|
|
80
|
+
const result = await androidObserve.getLogs(appId, 50);
|
|
74
81
|
logsCount = result.logCount;
|
|
75
82
|
logs = result.logs;
|
|
76
83
|
} else {
|
|
77
|
-
const result = await
|
|
84
|
+
const result = await iosObserve.getLogs(appId);
|
|
78
85
|
logsCount = result.logCount;
|
|
79
86
|
logs = result.logs;
|
|
80
87
|
}
|
|
@@ -90,10 +97,10 @@ async function main() {
|
|
|
90
97
|
let termResult: boolean;
|
|
91
98
|
|
|
92
99
|
if (platform === "android") {
|
|
93
|
-
const result = await
|
|
100
|
+
const result = await androidInteract.terminateApp(appId);
|
|
94
101
|
termResult = result.appTerminated;
|
|
95
102
|
} else {
|
|
96
|
-
const result = await
|
|
103
|
+
const result = await iosInteract.terminateApp(appId);
|
|
97
104
|
termResult = result.appTerminated;
|
|
98
105
|
}
|
|
99
106
|
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { StartAppResponse, TerminateAppResponse, RestartAppResponse, ResetAppDataResponse, WaitForElementResponse, TapResponse, SwipeResponse, TypeTextResponse, PressBackResponse } from "../types.js"
|
|
2
|
+
import { execAdb, getAndroidDeviceMetadata, getDeviceInfo } from "./utils.js"
|
|
3
|
+
import { AndroidObserve } from "./observe.js"
|
|
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
|
+
|
|
90
|
+
async startApp(appId: string, deviceId?: string): Promise<StartAppResponse> {
|
|
91
|
+
const metadata = await getAndroidDeviceMetadata(appId, deviceId)
|
|
92
|
+
const deviceInfo = getDeviceInfo(deviceId || 'default', metadata)
|
|
93
|
+
|
|
94
|
+
await execAdb(['shell', 'monkey', '-p', appId, '-c', 'android.intent.category.LAUNCHER', '1'], deviceId)
|
|
95
|
+
|
|
96
|
+
return { device: deviceInfo, appStarted: true, launchTimeMs: 1000 }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async terminateApp(appId: string, deviceId?: string): Promise<TerminateAppResponse> {
|
|
100
|
+
const metadata = await getAndroidDeviceMetadata(appId, deviceId)
|
|
101
|
+
const deviceInfo = getDeviceInfo(deviceId || 'default', metadata)
|
|
102
|
+
|
|
103
|
+
await execAdb(['shell', 'am', 'force-stop', appId], deviceId)
|
|
104
|
+
|
|
105
|
+
return { device: deviceInfo, appTerminated: true }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async restartApp(appId: string, deviceId?: string): Promise<RestartAppResponse> {
|
|
109
|
+
await this.terminateApp(appId, deviceId)
|
|
110
|
+
const startResult = await this.startApp(appId, deviceId)
|
|
111
|
+
return {
|
|
112
|
+
device: startResult.device,
|
|
113
|
+
appRestarted: startResult.appStarted,
|
|
114
|
+
launchTimeMs: startResult.launchTimeMs
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async resetAppData(appId: string, deviceId?: string): Promise<ResetAppDataResponse> {
|
|
119
|
+
const metadata = await getAndroidDeviceMetadata(appId, deviceId)
|
|
120
|
+
const deviceInfo = getDeviceInfo(deviceId || 'default', metadata)
|
|
121
|
+
|
|
122
|
+
const output = await execAdb(['shell', 'pm', 'clear', appId], deviceId)
|
|
123
|
+
|
|
124
|
+
return { device: deviceInfo, dataCleared: output === 'Success' }
|
|
125
|
+
}
|
|
126
|
+
}
|