mobile-debug-mcp 0.14.0 → 0.16.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/dist/android/interact.js +2 -2
- package/dist/android/observe.js +13 -0
- package/dist/cli/ios/run-ios-smoke.js +2 -2
- package/dist/cli/ios/run-ios-ui-tree-tap.js +2 -2
- package/dist/interact/android.js +91 -0
- package/dist/interact/index.js +76 -0
- package/dist/interact/ios.js +120 -0
- package/dist/interact/shared/fingerprint.js +1 -0
- package/dist/interact/shared/scroll_to_element.js +1 -0
- package/dist/ios/interact.js +2 -2
- package/dist/ios/observe.js +12 -0
- package/dist/manage/android.js +162 -0
- package/dist/manage/index.js +364 -0
- package/dist/manage/ios.js +353 -0
- package/dist/observe/android.js +351 -0
- package/dist/observe/fingerprint.js +1 -0
- package/dist/observe/index.js +85 -0
- package/dist/observe/ios.js +320 -0
- package/dist/observe/test/device/logstream-real.js +34 -0
- package/dist/observe/test/device/run-screen-fingerprint.js +29 -0
- package/dist/observe/test/device/run-scroll-test-android.js +22 -0
- package/dist/observe/test/device/test-ui-tree.js +67 -0
- package/dist/observe/test/device/wait_for_element_real.js +69 -0
- package/dist/observe/test/unit/get_screen_fingerprint.test.js +54 -0
- package/dist/observe/test/unit/logparse.test.js +39 -0
- package/dist/observe/test/unit/logstream.test.js +41 -0
- package/dist/observe/test/unit/scroll_to_element.test.js +113 -0
- package/dist/observe/test/unit/wait_for_element_mock.js +92 -0
- package/dist/server.js +41 -5
- package/dist/shared/fingerprint.js +72 -0
- package/dist/shared/scroll_to_element.js +98 -0
- package/dist/tools/interact.js +2 -2
- package/dist/tools/manage.js +2 -2
- package/dist/tools/observe.js +45 -43
- package/dist/utils/android/utils.js +373 -0
- package/dist/utils/cli/idb/check-idb.js +84 -0
- package/dist/utils/cli/idb/idb-helper.js +91 -0
- package/dist/utils/cli/idb/install-idb.js +82 -0
- package/dist/utils/cli/ios/preflight-ios.js +155 -0
- package/dist/utils/cli/ios/run-ios-smoke.js +28 -0
- package/dist/utils/cli/ios/run-ios-ui-tree-tap.js +29 -0
- package/dist/utils/diagnostics.js +1 -1
- package/dist/utils/exec.js +34 -0
- package/dist/utils/ios/utils.js +301 -0
- package/dist/utils/resolve-device.js +2 -2
- package/dist/utils/ui/index.js +169 -0
- package/docs/CHANGELOG.md +8 -0
- package/docs/tools/interact.md +29 -0
- package/docs/tools/observe.md +24 -0
- package/package.json +1 -1
- package/src/{android/interact.ts → interact/android.ts} +3 -3
- package/src/{tools/interact.ts → interact/index.ts} +47 -3
- package/src/{ios/interact.ts → interact/ios.ts} +3 -3
- package/src/{android/manage.ts → manage/android.ts} +2 -2
- package/src/{tools/manage.ts → manage/index.ts} +7 -4
- package/src/{ios/manage.ts → manage/ios.ts} +1 -1
- package/src/{android/observe.ts → observe/android.ts} +14 -26
- package/src/observe/index.ts +92 -0
- package/src/{ios/observe.ts → observe/ios.ts} +17 -35
- package/src/server.ts +45 -6
- package/src/types.ts +1 -0
- package/src/{android → utils/android}/utils.ts +12 -79
- package/src/{cli → utils/cli}/ios/run-ios-smoke.ts +2 -2
- package/src/{cli → utils/cli}/ios/run-ios-ui-tree-tap.ts +3 -3
- package/src/utils/diagnostics.ts +1 -1
- package/src/utils/exec.ts +33 -0
- package/src/{ios → utils/ios}/utils.ts +2 -2
- package/src/utils/resolve-device.ts +2 -2
- package/src/{tools/scroll_to_element.ts → utils/ui/index.ts} +73 -2
- package/test/{device/interact → interact/device}/smoke-test.ts +3 -4
- package/test/interact/unit/wait_for_screen_change.test.ts +32 -0
- package/test/{device/manage → manage/device}/run-install-android.ts +1 -1
- package/test/{device/manage → manage/device}/run-install-ios.ts +1 -1
- package/test/{device/manage → manage/device}/run-install-kmp.ts +1 -1
- package/test/{unit/manage → manage/unit}/build.test.ts +1 -1
- package/test/{unit/manage → manage/unit}/build_and_install.test.ts +1 -1
- package/test/{unit/manage → manage/unit}/detection.test.ts +1 -1
- package/test/{unit/manage → manage/unit}/diagnostics.test.ts +2 -2
- package/test/{unit/manage → manage/unit}/install.test.ts +1 -1
- package/test/{unit/manage → manage/unit}/mcp_disable_autodetect.test.ts +1 -1
- package/test/{device/observe → observe/device}/logstream-real.ts +1 -1
- package/test/observe/device/run-screen-fingerprint.ts +36 -0
- package/test/{device/observe → observe/device}/run-scroll-test-android.ts +2 -2
- package/test/{device/observe → observe/device}/test-ui-tree.ts +2 -2
- package/test/{device/observe → observe/device}/wait_for_element_real.ts +2 -2
- package/test/observe/unit/get_screen_fingerprint.test.ts +69 -0
- package/test/{unit/observe → observe/unit}/logparse.test.ts +1 -1
- package/test/{unit/observe → observe/unit}/logstream.test.ts +1 -1
- package/test/{unit/observe → observe/unit}/scroll_to_element.test.ts +3 -3
- package/test/{unit/observe → observe/unit}/wait_for_element_mock.ts +2 -2
- package/test/unit/index.ts +13 -11
- package/src/tools/observe.ts +0 -82
- package/test/device/README.md +0 -49
- package/test/device/index.ts +0 -27
- package/test/device/utils/test-dist.ts +0 -41
- package/test/unit/utils/detect-java.test.ts +0 -22
- /package/src/{cli → utils/cli}/idb/check-idb.ts +0 -0
- /package/src/{cli → utils/cli}/idb/idb-helper.ts +0 -0
- /package/src/{cli → utils/cli}/idb/install-idb.ts +0 -0
- /package/src/{cli → utils/cli}/ios/preflight-ios.ts +0 -0
- /package/test/{device/interact → interact/device}/run-real-test.ts +0 -0
- /package/test/{device/manage → manage/device}/install.integration.ts +0 -0
- /package/test/{device/manage → manage/device}/run-build-install-ios.ts +0 -0
package/dist/android/interact.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execAdb, getAndroidDeviceMetadata, getDeviceInfo } from "./utils.js";
|
|
2
|
-
import { AndroidObserve } from "
|
|
3
|
-
import { scrollToElementShared } from "../
|
|
2
|
+
import { AndroidObserve } from "../observe/index.js";
|
|
3
|
+
import { scrollToElementShared } from "../intera../interact/shared/scroll_to_element.js";
|
|
4
4
|
export class AndroidInteract {
|
|
5
5
|
observe = new AndroidObserve();
|
|
6
6
|
async waitForElement(text, timeout, deviceId) {
|
package/dist/android/observe.js
CHANGED
|
@@ -4,6 +4,7 @@ import { getAdbCmd, execAdb, getAndroidDeviceMetadata, getDeviceInfo, delay, get
|
|
|
4
4
|
import { createWriteStream } from "fs";
|
|
5
5
|
import { promises as fsPromises } from "fs";
|
|
6
6
|
import path from "path";
|
|
7
|
+
import { computeScreenFingerprint } from "../intera../interact/shared/fingerprint.js";
|
|
7
8
|
const activeLogStreams = new Map();
|
|
8
9
|
export class AndroidObserve {
|
|
9
10
|
async getDeviceMetadata(appId, deviceId) {
|
|
@@ -229,6 +230,18 @@ export class AndroidObserve {
|
|
|
229
230
|
};
|
|
230
231
|
}
|
|
231
232
|
}
|
|
233
|
+
async getScreenFingerprint(deviceId) {
|
|
234
|
+
try {
|
|
235
|
+
const tree = await this.getUITree(deviceId);
|
|
236
|
+
if (!tree || tree.error)
|
|
237
|
+
return { fingerprint: null, error: tree.error };
|
|
238
|
+
const current = await this.getCurrentScreen(deviceId).catch(() => null);
|
|
239
|
+
return computeScreenFingerprint(tree, current, 'android', 50);
|
|
240
|
+
}
|
|
241
|
+
catch (e) {
|
|
242
|
+
return { fingerprint: null, error: e instanceof Error ? e.message : String(e) };
|
|
243
|
+
}
|
|
244
|
+
}
|
|
232
245
|
async startLogStream(packageName, level = 'error', deviceId, sessionId = 'default') {
|
|
233
246
|
try {
|
|
234
247
|
const pidOutput = await execAdb(['shell', 'pidof', packageName], deviceId).catch(() => '');
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { iOSObserve } from '../../
|
|
2
|
-
import { iOSManage } from '../../
|
|
1
|
+
import { iOSObserve } from '../../observe/index.js';
|
|
2
|
+
import { iOSManage } from '../../manage/index.js';
|
|
3
3
|
async function main() {
|
|
4
4
|
const appId = process.argv[2] || 'com.apple.springboard';
|
|
5
5
|
const deviceId = 'booted';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { iOSObserve } from '../../
|
|
2
|
-
import { iOSInteract } from '../../
|
|
1
|
+
import { iOSObserve } from '../../observe/index.js';
|
|
2
|
+
import { iOSInteract } from '../../interact/index.js';
|
|
3
3
|
async function main() {
|
|
4
4
|
const deviceId = 'booted';
|
|
5
5
|
const obs = new iOSObserve();
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { execAdb, getAndroidDeviceMetadata, getDeviceInfo } from "../utils/android/utils.js";
|
|
2
|
+
import { AndroidObserve } from "../observe/index.js";
|
|
3
|
+
import { scrollToElementShared } from "../utils/ui/index.js";
|
|
4
|
+
export class AndroidInteract {
|
|
5
|
+
observe = new AndroidObserve();
|
|
6
|
+
async waitForElement(text, timeout, deviceId) {
|
|
7
|
+
const metadata = await getAndroidDeviceMetadata("", deviceId);
|
|
8
|
+
const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
|
|
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: deviceInfo, found: false, error: tree.error };
|
|
15
|
+
}
|
|
16
|
+
const element = tree.elements.find(e => e.text === text);
|
|
17
|
+
if (element) {
|
|
18
|
+
return { device: deviceInfo, 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: deviceInfo, found: false };
|
|
32
|
+
}
|
|
33
|
+
async tap(x, y, deviceId) {
|
|
34
|
+
const metadata = await getAndroidDeviceMetadata("", deviceId);
|
|
35
|
+
const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
|
|
36
|
+
try {
|
|
37
|
+
await execAdb(['shell', 'input', 'tap', x.toString(), y.toString()], deviceId);
|
|
38
|
+
return { device: deviceInfo, success: true, x, y };
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
return { device: deviceInfo, success: false, x, y, error: e instanceof Error ? e.message : String(e) };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async swipe(x1, y1, x2, y2, duration, deviceId) {
|
|
45
|
+
const metadata = await getAndroidDeviceMetadata("", deviceId);
|
|
46
|
+
const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
|
|
47
|
+
try {
|
|
48
|
+
await execAdb(['shell', 'input', 'swipe', x1.toString(), y1.toString(), x2.toString(), y2.toString(), duration.toString()], deviceId);
|
|
49
|
+
return { device: deviceInfo, success: true, start: [x1, y1], end: [x2, y2], duration };
|
|
50
|
+
}
|
|
51
|
+
catch (e) {
|
|
52
|
+
return { device: deviceInfo, success: false, start: [x1, y1], end: [x2, y2], duration, error: e instanceof Error ? e.message : String(e) };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async typeText(text, deviceId) {
|
|
56
|
+
const metadata = await getAndroidDeviceMetadata("", deviceId);
|
|
57
|
+
const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
|
|
58
|
+
try {
|
|
59
|
+
// Encode spaces as %s to ensure proper input handling by adb shell input text
|
|
60
|
+
const encodedText = text.replace(/\s/g, '%s');
|
|
61
|
+
// Note: 'input text' might fail with some characters or if keyboard isn't ready, but it's the standard ADB way.
|
|
62
|
+
await execAdb(['shell', 'input', 'text', encodedText], deviceId);
|
|
63
|
+
return { device: deviceInfo, success: true, text };
|
|
64
|
+
}
|
|
65
|
+
catch (e) {
|
|
66
|
+
return { device: deviceInfo, success: false, text, error: e instanceof Error ? e.message : String(e) };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async pressBack(deviceId) {
|
|
70
|
+
const metadata = await getAndroidDeviceMetadata("", deviceId);
|
|
71
|
+
const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
|
|
72
|
+
try {
|
|
73
|
+
await execAdb(['shell', 'input', 'keyevent', '4'], deviceId);
|
|
74
|
+
return { device: deviceInfo, success: true };
|
|
75
|
+
}
|
|
76
|
+
catch (e) {
|
|
77
|
+
return { device: deviceInfo, success: false, error: e instanceof Error ? e.message : String(e) };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async scrollToElement(selector, direction = 'down', maxScrolls = 10, scrollAmount = 0.7, deviceId) {
|
|
81
|
+
return await scrollToElementShared({
|
|
82
|
+
selector,
|
|
83
|
+
direction,
|
|
84
|
+
maxScrolls,
|
|
85
|
+
scrollAmount,
|
|
86
|
+
deviceId,
|
|
87
|
+
fetchTree: async () => await this.observe.getUITree(deviceId),
|
|
88
|
+
swipe: async (x1, y1, x2, y2, duration, devId) => await this.swipe(x1, y1, x2, y2, duration, devId)
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { AndroidInteract } from './android.js';
|
|
2
|
+
import { iOSInteract } from './ios.js';
|
|
3
|
+
export { AndroidInteract, iOSInteract };
|
|
4
|
+
import { resolveTargetDevice } from '../utils/resolve-device.js';
|
|
5
|
+
import { ToolsObserve } from '../observe/index.js';
|
|
6
|
+
export class ToolsInteract {
|
|
7
|
+
static async getInteractionService(platform, deviceId) {
|
|
8
|
+
const effectivePlatform = platform || 'android';
|
|
9
|
+
const resolved = await resolveTargetDevice({ platform: effectivePlatform, deviceId });
|
|
10
|
+
const interact = effectivePlatform === 'android' ? new AndroidInteract() : new iOSInteract();
|
|
11
|
+
return { interact: interact, resolved, platform: effectivePlatform };
|
|
12
|
+
}
|
|
13
|
+
static async waitForElementHandler({ platform, text, timeout, deviceId }) {
|
|
14
|
+
const effectiveTimeout = timeout ?? 10000;
|
|
15
|
+
const { interact, resolved } = await ToolsInteract.getInteractionService(platform, deviceId);
|
|
16
|
+
return await interact.waitForElement(text, effectiveTimeout, resolved.id);
|
|
17
|
+
}
|
|
18
|
+
static async tapHandler({ platform, x, y, deviceId }) {
|
|
19
|
+
const { interact, resolved } = await ToolsInteract.getInteractionService(platform, deviceId);
|
|
20
|
+
return await interact.tap(x, y, resolved.id);
|
|
21
|
+
}
|
|
22
|
+
static async swipeHandler({ platform = 'android', x1, y1, x2, y2, duration, deviceId }) {
|
|
23
|
+
const { interact, resolved } = await ToolsInteract.getInteractionService(platform, deviceId);
|
|
24
|
+
return await interact.swipe(x1, y1, x2, y2, duration, resolved.id);
|
|
25
|
+
}
|
|
26
|
+
static async typeTextHandler({ text, deviceId }) {
|
|
27
|
+
const resolved = await resolveTargetDevice({ platform: 'android', deviceId });
|
|
28
|
+
return await new AndroidInteract().typeText(text, resolved.id);
|
|
29
|
+
}
|
|
30
|
+
static async pressBackHandler({ deviceId }) {
|
|
31
|
+
const resolved = await resolveTargetDevice({ platform: 'android', deviceId });
|
|
32
|
+
return await new AndroidInteract().pressBack(resolved.id);
|
|
33
|
+
}
|
|
34
|
+
static async scrollToElementHandler({ platform, selector, direction = 'down', maxScrolls = 10, scrollAmount = 0.7, deviceId }) {
|
|
35
|
+
const { interact, resolved } = await ToolsInteract.getInteractionService(platform, deviceId);
|
|
36
|
+
return await interact.scrollToElement(selector, direction, maxScrolls, scrollAmount, resolved.id);
|
|
37
|
+
}
|
|
38
|
+
static async waitForScreenChangeHandler({ platform, previousFingerprint, timeoutMs = 5000, pollIntervalMs = 300, deviceId }) {
|
|
39
|
+
const start = Date.now();
|
|
40
|
+
let lastFingerprint = null;
|
|
41
|
+
while (Date.now() - start < timeoutMs) {
|
|
42
|
+
try {
|
|
43
|
+
const res = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId });
|
|
44
|
+
const fp = res?.fingerprint ?? null;
|
|
45
|
+
if (fp === null || fp === undefined) {
|
|
46
|
+
lastFingerprint = null;
|
|
47
|
+
await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
lastFingerprint = fp;
|
|
51
|
+
if (fp !== previousFingerprint) {
|
|
52
|
+
// Stability confirmation
|
|
53
|
+
await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
|
|
54
|
+
try {
|
|
55
|
+
const confirmRes = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId });
|
|
56
|
+
const confirmFp = confirmRes?.fingerprint ?? null;
|
|
57
|
+
if (confirmFp === fp) {
|
|
58
|
+
return { success: true, newFingerprint: fp, elapsedMs: Date.now() - start };
|
|
59
|
+
}
|
|
60
|
+
lastFingerprint = confirmFp;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// ignore and continue polling
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// ignore transient errors
|
|
71
|
+
}
|
|
72
|
+
await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
|
|
73
|
+
}
|
|
74
|
+
return { success: false, reason: 'timeout', lastFingerprint, elapsedMs: Date.now() - start };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import { getIOSDeviceMetadata, getIdbCmd, isIDBInstalled } from "../utils/ios/utils.js";
|
|
3
|
+
import { iOSObserve } from "../observe/index.js";
|
|
4
|
+
import { scrollToElementShared } from "../utils/ui/index.js";
|
|
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
|
+
// Use shared helper to detect idb
|
|
36
|
+
const idbExists = await isIDBInstalled();
|
|
37
|
+
if (!idbExists) {
|
|
38
|
+
return {
|
|
39
|
+
device,
|
|
40
|
+
success: false,
|
|
41
|
+
x,
|
|
42
|
+
y,
|
|
43
|
+
error: "iOS tap requires 'idb' (iOS Device Bridge)."
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const targetUdid = (device.id && device.id !== 'booted') ? device.id : undefined;
|
|
48
|
+
const args = ['ui', 'tap', x.toString(), y.toString()];
|
|
49
|
+
if (targetUdid) {
|
|
50
|
+
args.push('--udid', targetUdid);
|
|
51
|
+
}
|
|
52
|
+
await new Promise((resolve, reject) => {
|
|
53
|
+
const proc = spawn(getIdbCmd(), args);
|
|
54
|
+
let stderr = '';
|
|
55
|
+
proc.stderr.on('data', d => stderr += d.toString());
|
|
56
|
+
proc.on('close', code => {
|
|
57
|
+
if (code === 0)
|
|
58
|
+
resolve();
|
|
59
|
+
else
|
|
60
|
+
reject(new Error(`idb ui tap failed: ${stderr}`));
|
|
61
|
+
});
|
|
62
|
+
proc.on('error', err => reject(err));
|
|
63
|
+
});
|
|
64
|
+
return { device, success: true, x, y };
|
|
65
|
+
}
|
|
66
|
+
catch (e) {
|
|
67
|
+
return { device, success: false, x, y, error: e instanceof Error ? e.message : String(e) };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async swipe(x1, y1, x2, y2, duration, deviceId = "booted") {
|
|
71
|
+
const device = await getIOSDeviceMetadata(deviceId);
|
|
72
|
+
// Use shared helper to detect idb
|
|
73
|
+
const idbExists = await isIDBInstalled();
|
|
74
|
+
if (!idbExists) {
|
|
75
|
+
return {
|
|
76
|
+
device,
|
|
77
|
+
success: false,
|
|
78
|
+
start: [x1, y1],
|
|
79
|
+
end: [x2, y2],
|
|
80
|
+
duration,
|
|
81
|
+
error: "iOS swipe requires 'idb' (iOS Device Bridge)."
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
const targetUdid = (device.id && device.id !== 'booted') ? device.id : undefined;
|
|
86
|
+
// idb 'ui swipe' does not accept a duration parameter; use coordinates only
|
|
87
|
+
const args = ['ui', 'swipe', x1.toString(), y1.toString(), x2.toString(), y2.toString()];
|
|
88
|
+
if (targetUdid) {
|
|
89
|
+
args.push('--udid', targetUdid);
|
|
90
|
+
}
|
|
91
|
+
await new Promise((resolve, reject) => {
|
|
92
|
+
const proc = spawn(getIdbCmd(), args);
|
|
93
|
+
let stderr = '';
|
|
94
|
+
proc.stderr.on('data', d => stderr += d.toString());
|
|
95
|
+
proc.on('close', code => {
|
|
96
|
+
if (code === 0)
|
|
97
|
+
resolve();
|
|
98
|
+
else
|
|
99
|
+
reject(new Error(`idb ui swipe failed: ${stderr}`));
|
|
100
|
+
});
|
|
101
|
+
proc.on('error', err => reject(err));
|
|
102
|
+
});
|
|
103
|
+
return { device, success: true, start: [x1, y1], end: [x2, y2], duration };
|
|
104
|
+
}
|
|
105
|
+
catch (e) {
|
|
106
|
+
return { device, success: false, start: [x1, y1], end: [x2, y2], duration, error: e instanceof Error ? e.message : String(e) };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async scrollToElement(selector, direction = 'down', maxScrolls = 10, scrollAmount = 0.7, deviceId = 'booted') {
|
|
110
|
+
return await scrollToElementShared({
|
|
111
|
+
selector,
|
|
112
|
+
direction,
|
|
113
|
+
maxScrolls,
|
|
114
|
+
scrollAmount,
|
|
115
|
+
deviceId,
|
|
116
|
+
fetchTree: async () => await this.observe.getUITree(deviceId),
|
|
117
|
+
swipe: async (x1, y1, x2, y2, duration, devId) => await this.swipe(x1, y1, x2, y2, duration, devId)
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { computeScreenFingerprint } from '../../utils/ui/index.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { scrollToElementShared } from '../../utils/ui/index.js';
|
package/dist/ios/interact.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from "child_process";
|
|
2
2
|
import { getIOSDeviceMetadata, getIdbCmd, isIDBInstalled } from "./utils.js";
|
|
3
|
-
import { iOSObserve } from "
|
|
4
|
-
import { scrollToElementShared } from "../
|
|
3
|
+
import { iOSObserve } from "../observe/index.js";
|
|
4
|
+
import { scrollToElementShared } from "../intera../interact/shared/scroll_to_element.js";
|
|
5
5
|
export class iOSInteract {
|
|
6
6
|
observe = new iOSObserve();
|
|
7
7
|
async waitForElement(text, timeout, deviceId = "booted") {
|
package/dist/ios/observe.js
CHANGED
|
@@ -4,6 +4,7 @@ import { execCommand, getIOSDeviceMetadata, validateBundleId, getIdbCmd, getXcru
|
|
|
4
4
|
import { createWriteStream, promises as fsPromises } from 'fs';
|
|
5
5
|
import path from 'path';
|
|
6
6
|
import { parseLogLine } from '../android/utils.js';
|
|
7
|
+
import { computeScreenFingerprint } from '../intera../interact/shared/fingerprint.js';
|
|
7
8
|
// --- Helper Functions Specific to Observe ---
|
|
8
9
|
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
9
10
|
function parseIDBFrame(frame) {
|
|
@@ -237,6 +238,17 @@ export class iOSObserve {
|
|
|
237
238
|
};
|
|
238
239
|
}
|
|
239
240
|
}
|
|
241
|
+
async getScreenFingerprint(deviceId = 'booted') {
|
|
242
|
+
try {
|
|
243
|
+
const tree = await this.getUITree(deviceId);
|
|
244
|
+
if (!tree || tree.error)
|
|
245
|
+
return { fingerprint: null, error: tree && tree.error };
|
|
246
|
+
return computeScreenFingerprint(tree, null, 'ios', 50);
|
|
247
|
+
}
|
|
248
|
+
catch (e) {
|
|
249
|
+
return { fingerprint: null, error: e instanceof Error ? e.message : String(e) };
|
|
250
|
+
}
|
|
251
|
+
}
|
|
240
252
|
// --- Log stream methods ---
|
|
241
253
|
async startLogStream(bundleId, deviceId = 'booted', sessionId = 'default') {
|
|
242
254
|
try {
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { existsSync } from 'fs';
|
|
5
|
+
import { execAdb, spawnAdb, getAndroidDeviceMetadata, getDeviceInfo, findApk } from '../utils/android/utils.js';
|
|
6
|
+
import { execAdbWithDiagnostics } from '../utils/diagnostics.js';
|
|
7
|
+
import { detectJavaHome } from '../utils/java.js';
|
|
8
|
+
export class AndroidManage {
|
|
9
|
+
async build(projectPath, _variant) {
|
|
10
|
+
void _variant;
|
|
11
|
+
try {
|
|
12
|
+
// Always use the shared prepareGradle utility for consistent env/setup
|
|
13
|
+
const { execCmd, gradleArgs, spawnOpts } = await (await import('../utils/android/utils.js')).prepareGradle(projectPath);
|
|
14
|
+
await new Promise((resolve, reject) => {
|
|
15
|
+
const proc = spawn(execCmd, gradleArgs, spawnOpts);
|
|
16
|
+
let stderr = '';
|
|
17
|
+
proc.stderr?.on('data', d => stderr += d.toString());
|
|
18
|
+
proc.on('close', code => {
|
|
19
|
+
if (code === 0)
|
|
20
|
+
resolve();
|
|
21
|
+
else
|
|
22
|
+
reject(new Error(stderr || `Gradle failed with code ${code}`));
|
|
23
|
+
});
|
|
24
|
+
proc.on('error', err => reject(err));
|
|
25
|
+
});
|
|
26
|
+
const apk = await findApk(projectPath);
|
|
27
|
+
if (!apk)
|
|
28
|
+
return { error: 'Could not find APK after build' };
|
|
29
|
+
return { artifactPath: apk };
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
return { error: e instanceof Error ? e.message : String(e) };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async installApp(apkPath, deviceId) {
|
|
36
|
+
const metadata = await getAndroidDeviceMetadata('', deviceId);
|
|
37
|
+
const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
|
|
38
|
+
let apkToInstall = apkPath;
|
|
39
|
+
try {
|
|
40
|
+
const stat = await fs.stat(apkPath).catch(() => null);
|
|
41
|
+
if (stat && stat.isDirectory()) {
|
|
42
|
+
const detectedJavaHome = await detectJavaHome().catch(() => undefined);
|
|
43
|
+
const env = Object.assign({}, process.env);
|
|
44
|
+
if (detectedJavaHome) {
|
|
45
|
+
if (env.JAVA_HOME !== detectedJavaHome) {
|
|
46
|
+
env.JAVA_HOME = detectedJavaHome;
|
|
47
|
+
env.PATH = `${path.join(detectedJavaHome, 'bin')}${path.delimiter}${env.PATH || ''}`;
|
|
48
|
+
console.debug('[android-run] Overriding JAVA_HOME with detected path:', detectedJavaHome);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
delete env.SHELL;
|
|
53
|
+
}
|
|
54
|
+
catch { }
|
|
55
|
+
const gradleArgs = ['assembleDebug'];
|
|
56
|
+
if (detectedJavaHome) {
|
|
57
|
+
gradleArgs.push(`-Dorg.gradle.java.home=${detectedJavaHome}`);
|
|
58
|
+
gradleArgs.push('--no-daemon');
|
|
59
|
+
env.GRADLE_JAVA_HOME = detectedJavaHome;
|
|
60
|
+
}
|
|
61
|
+
const wrapperPath = path.join(apkPath, 'gradlew');
|
|
62
|
+
const useWrapper = existsSync(wrapperPath);
|
|
63
|
+
const execCmd = useWrapper ? wrapperPath : 'gradle';
|
|
64
|
+
const spawnOpts = { cwd: apkPath, env };
|
|
65
|
+
if (useWrapper) {
|
|
66
|
+
await fs.chmod(wrapperPath, 0o755).catch(() => { });
|
|
67
|
+
spawnOpts.shell = false;
|
|
68
|
+
}
|
|
69
|
+
else
|
|
70
|
+
spawnOpts.shell = true;
|
|
71
|
+
const proc = spawn(execCmd, gradleArgs, spawnOpts);
|
|
72
|
+
let stderr = '';
|
|
73
|
+
await new Promise((resolve, reject) => {
|
|
74
|
+
proc.stderr?.on('data', d => stderr += d.toString());
|
|
75
|
+
proc.on('close', code => {
|
|
76
|
+
if (code === 0)
|
|
77
|
+
resolve();
|
|
78
|
+
else
|
|
79
|
+
reject(new Error(stderr || `Gradle build failed with code ${code}`));
|
|
80
|
+
});
|
|
81
|
+
proc.on('error', err => reject(err));
|
|
82
|
+
});
|
|
83
|
+
const built = await findApk(apkPath);
|
|
84
|
+
if (!built)
|
|
85
|
+
throw new Error('Could not locate built APK after running Gradle');
|
|
86
|
+
apkToInstall = built;
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
const res = await spawnAdb(['install', '-r', apkToInstall], deviceId);
|
|
90
|
+
if (res.code === 0) {
|
|
91
|
+
return { device: deviceInfo, installed: true, output: res.stdout };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch (e) {
|
|
95
|
+
console.debug('[android-run] adb install failed, attempting push+pm fallback:', e instanceof Error ? e.message : String(e));
|
|
96
|
+
}
|
|
97
|
+
const basename = path.basename(apkToInstall);
|
|
98
|
+
const remotePath = `/data/local/tmp/${basename}`;
|
|
99
|
+
await execAdb(['push', apkToInstall, remotePath], deviceId);
|
|
100
|
+
const pmOut = await execAdb(['shell', 'pm', 'install', '-r', remotePath], deviceId);
|
|
101
|
+
try {
|
|
102
|
+
await execAdb(['shell', 'rm', remotePath], deviceId);
|
|
103
|
+
}
|
|
104
|
+
catch { }
|
|
105
|
+
return { device: deviceInfo, installed: true, output: pmOut };
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
// gather diagnostics for attempted adb operations
|
|
109
|
+
const basename = path.basename(apkToInstall);
|
|
110
|
+
const remotePath = `/data/local/tmp/${basename}`;
|
|
111
|
+
const installDiag = execAdbWithDiagnostics(['install', '-r', apkToInstall], deviceId);
|
|
112
|
+
const pushDiag = execAdbWithDiagnostics(['push', apkToInstall, remotePath], deviceId);
|
|
113
|
+
const pmDiag = execAdbWithDiagnostics(['shell', 'pm', 'install', '-r', remotePath], deviceId);
|
|
114
|
+
return { device: deviceInfo, installed: false, error: e instanceof Error ? e.message : String(e), diagnostics: { installDiag, pushDiag, pmDiag } };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async startApp(appId, deviceId) {
|
|
118
|
+
const metadata = await getAndroidDeviceMetadata(appId, deviceId);
|
|
119
|
+
const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
|
|
120
|
+
try {
|
|
121
|
+
await execAdb(['shell', 'monkey', '-p', appId, '-c', 'android.intent.category.LAUNCHER', '1'], deviceId);
|
|
122
|
+
return { device: deviceInfo, appStarted: true, launchTimeMs: 1000 };
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
const diag = execAdbWithDiagnostics(['shell', 'monkey', '-p', appId, '-c', 'android.intent.category.LAUNCHER', '1'], deviceId);
|
|
126
|
+
return { device: deviceInfo, appStarted: false, launchTimeMs: 0, error: e instanceof Error ? e.message : String(e), diagnostics: diag };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async terminateApp(appId, deviceId) {
|
|
130
|
+
const metadata = await getAndroidDeviceMetadata(appId, deviceId);
|
|
131
|
+
const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
|
|
132
|
+
try {
|
|
133
|
+
await execAdb(['shell', 'am', 'force-stop', appId], deviceId);
|
|
134
|
+
return { device: deviceInfo, appTerminated: true };
|
|
135
|
+
}
|
|
136
|
+
catch (e) {
|
|
137
|
+
const diag = execAdbWithDiagnostics(['shell', 'am', 'force-stop', appId], deviceId);
|
|
138
|
+
return { device: deviceInfo, appTerminated: false, error: e instanceof Error ? e.message : String(e), diagnostics: diag };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
async restartApp(appId, deviceId) {
|
|
142
|
+
await this.terminateApp(appId, deviceId);
|
|
143
|
+
const startResult = await this.startApp(appId, deviceId);
|
|
144
|
+
return {
|
|
145
|
+
device: startResult.device,
|
|
146
|
+
appRestarted: startResult.appStarted,
|
|
147
|
+
launchTimeMs: startResult.launchTimeMs
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
async resetAppData(appId, deviceId) {
|
|
151
|
+
const metadata = await getAndroidDeviceMetadata(appId, deviceId);
|
|
152
|
+
const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
|
|
153
|
+
try {
|
|
154
|
+
const output = await execAdb(['shell', 'pm', 'clear', appId], deviceId);
|
|
155
|
+
return { device: deviceInfo, dataCleared: output === 'Success' };
|
|
156
|
+
}
|
|
157
|
+
catch (e) {
|
|
158
|
+
const diag = execAdbWithDiagnostics(['shell', 'pm', 'clear', appId], deviceId);
|
|
159
|
+
return { device: deviceInfo, dataCleared: false, error: e instanceof Error ? e.message : String(e), diagnostics: diag };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|