mobile-debug-mcp 0.13.0 → 0.15.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.
Files changed (103) hide show
  1. package/README.md +2 -2
  2. package/dist/android/interact.js +13 -1
  3. package/dist/android/observe.js +13 -0
  4. package/dist/cli/ios/run-ios-smoke.js +2 -2
  5. package/dist/cli/ios/run-ios-ui-tree-tap.js +2 -2
  6. package/dist/interact/android.js +91 -0
  7. package/dist/interact/index.js +37 -0
  8. package/dist/interact/ios.js +120 -0
  9. package/dist/interact/shared/fingerprint.js +72 -0
  10. package/dist/interact/shared/scroll_to_element.js +98 -0
  11. package/dist/ios/interact.js +52 -1
  12. package/dist/ios/observe.js +12 -0
  13. package/dist/manage/android.js +162 -0
  14. package/dist/manage/index.js +364 -0
  15. package/dist/manage/ios.js +353 -0
  16. package/dist/observe/android.js +351 -0
  17. package/dist/observe/fingerprint.js +1 -0
  18. package/dist/observe/index.js +85 -0
  19. package/dist/observe/ios.js +320 -0
  20. package/dist/observe/test/device/logstream-real.js +34 -0
  21. package/dist/observe/test/device/run-screen-fingerprint.js +29 -0
  22. package/dist/observe/test/device/run-scroll-test-android.js +22 -0
  23. package/dist/observe/test/device/test-ui-tree.js +67 -0
  24. package/dist/observe/test/device/wait_for_element_real.js +69 -0
  25. package/dist/observe/test/unit/get_screen_fingerprint.test.js +54 -0
  26. package/dist/observe/test/unit/logparse.test.js +39 -0
  27. package/dist/observe/test/unit/logstream.test.js +41 -0
  28. package/dist/observe/test/unit/scroll_to_element.test.js +113 -0
  29. package/dist/observe/test/unit/wait_for_element_mock.js +92 -0
  30. package/dist/server.js +54 -9
  31. package/dist/shared/fingerprint.js +72 -0
  32. package/dist/shared/scroll_to_element.js +98 -0
  33. package/dist/tools/interact.js +19 -22
  34. package/dist/tools/manage.js +2 -2
  35. package/dist/tools/observe.js +45 -43
  36. package/dist/tools/scroll_to_element.js +98 -0
  37. package/dist/utils/android/utils.js +429 -0
  38. package/dist/utils/cli/idb/check-idb.js +84 -0
  39. package/dist/utils/cli/idb/idb-helper.js +91 -0
  40. package/dist/utils/cli/idb/install-idb.js +82 -0
  41. package/dist/utils/cli/ios/preflight-ios.js +155 -0
  42. package/dist/utils/cli/ios/run-ios-smoke.js +28 -0
  43. package/dist/utils/cli/ios/run-ios-ui-tree-tap.js +29 -0
  44. package/dist/utils/diagnostics.js +1 -1
  45. package/dist/utils/ios/utils.js +301 -0
  46. package/dist/utils/resolve-device.js +2 -2
  47. package/docs/CHANGELOG.md +11 -0
  48. package/docs/tools/TOOLS.md +3 -3
  49. package/docs/tools/interact.md +31 -0
  50. package/docs/tools/observe.md +24 -0
  51. package/package.json +1 -1
  52. package/src/{android/interact.ts → interact/android.ts} +15 -2
  53. package/src/interact/index.ts +47 -0
  54. package/src/{ios/interact.ts → interact/ios.ts} +58 -3
  55. package/src/interact/shared/fingerprint.ts +73 -0
  56. package/src/interact/shared/scroll_to_element.ts +110 -0
  57. package/src/{android/manage.ts → manage/android.ts} +2 -2
  58. package/src/{tools/manage.ts → manage/index.ts} +7 -4
  59. package/src/{ios/manage.ts → manage/ios.ts} +1 -1
  60. package/src/{android/observe.ts → observe/android.ts} +14 -26
  61. package/src/observe/index.ts +92 -0
  62. package/src/{ios/observe.ts → observe/ios.ts} +17 -35
  63. package/src/server.ts +57 -10
  64. package/src/{android → utils/android}/utils.ts +2 -2
  65. package/src/{cli → utils/cli}/ios/run-ios-smoke.ts +2 -2
  66. package/src/{cli → utils/cli}/ios/run-ios-ui-tree-tap.ts +3 -3
  67. package/src/utils/diagnostics.ts +1 -1
  68. package/src/{ios → utils/ios}/utils.ts +2 -2
  69. package/src/utils/resolve-device.ts +2 -2
  70. package/test/{device/interact → interact/device}/smoke-test.ts +3 -4
  71. package/test/{device/manage → manage/device}/run-install-android.ts +1 -1
  72. package/test/{device/manage → manage/device}/run-install-ios.ts +1 -1
  73. package/test/{device/manage → manage/device}/run-install-kmp.ts +1 -1
  74. package/test/{unit/manage → manage/unit}/build.test.ts +1 -1
  75. package/test/{unit/manage → manage/unit}/build_and_install.test.ts +1 -1
  76. package/test/{unit/manage → manage/unit}/detection.test.ts +1 -1
  77. package/test/{unit/manage → manage/unit}/diagnostics.test.ts +2 -2
  78. package/test/{unit/manage → manage/unit}/install.test.ts +1 -1
  79. package/test/{unit/manage → manage/unit}/mcp_disable_autodetect.test.ts +1 -1
  80. package/test/{device/observe → observe/device}/logstream-real.ts +1 -1
  81. package/test/observe/device/run-screen-fingerprint.ts +36 -0
  82. package/test/observe/device/run-scroll-test-android.ts +24 -0
  83. package/test/{device/observe → observe/device}/test-ui-tree.ts +2 -2
  84. package/test/{device/observe → observe/device}/wait_for_element_real.ts +2 -2
  85. package/test/observe/unit/get_screen_fingerprint.test.ts +69 -0
  86. package/test/{unit/observe → observe/unit}/logparse.test.ts +1 -1
  87. package/test/{unit/observe → observe/unit}/logstream.test.ts +1 -1
  88. package/test/observe/unit/scroll_to_element.test.ts +129 -0
  89. package/test/{unit/observe → observe/unit}/wait_for_element_mock.ts +3 -3
  90. package/test/unit/index.ts +12 -11
  91. package/src/tools/interact.ts +0 -45
  92. package/src/tools/observe.ts +0 -82
  93. package/test/device/README.md +0 -49
  94. package/test/device/index.ts +0 -27
  95. package/test/device/utils/test-dist.ts +0 -41
  96. package/test/unit/utils/detect-java.test.ts +0 -22
  97. /package/src/{cli → utils/cli}/idb/check-idb.ts +0 -0
  98. /package/src/{cli → utils/cli}/idb/idb-helper.ts +0 -0
  99. /package/src/{cli → utils/cli}/idb/install-idb.ts +0 -0
  100. /package/src/{cli → utils/cli}/ios/preflight-ios.ts +0 -0
  101. /package/test/{device/interact → interact/device}/run-real-test.ts +0 -0
  102. /package/test/{device/manage → manage/device}/install.integration.ts +0 -0
  103. /package/test/{device/manage → manage/device}/run-build-install-ios.ts +0 -0
@@ -0,0 +1,320 @@
1
+ import { spawn } from "child_process";
2
+ import { promises as fs } from "fs";
3
+ import { execCommand, getIOSDeviceMetadata, validateBundleId, getIdbCmd, getXcrunCmd, isIDBInstalled } from "../utils/ios/utils.js";
4
+ import { createWriteStream, promises as fsPromises } from 'fs';
5
+ import path from 'path';
6
+ import { parseLogLine } from '../utils/android/utils.js';
7
+ import { computeScreenFingerprint } from '../interact/shared/fingerprint.js';
8
+ const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
9
+ function parseIDBFrame(frame) {
10
+ if (!frame)
11
+ return [0, 0, 0, 0];
12
+ if (typeof frame === 'string') {
13
+ const nums = frame.match(/-?\d+(?:\.\d+)?/g);
14
+ if (!nums || nums.length < 4)
15
+ return [0, 0, 0, 0];
16
+ const x = Number(nums[0]);
17
+ const y = Number(nums[1]);
18
+ const w = Number(nums[2]);
19
+ const h = Number(nums[3]);
20
+ return [Math.round(x), Math.round(y), Math.round(x + w), Math.round(y + h)];
21
+ }
22
+ const x = Number(frame.x || 0);
23
+ const y = Number(frame.y || 0);
24
+ const w = Number(frame.width || frame.w || 0);
25
+ const h = Number(frame.height || frame.h || 0);
26
+ return [Math.round(x), Math.round(y), Math.round(x + w), Math.round(y + h)];
27
+ }
28
+ function getCenter(bounds) {
29
+ const [x1, y1, x2, y2] = bounds;
30
+ return [Math.floor((x1 + x2) / 2), Math.floor((y1 + y2) / 2)];
31
+ }
32
+ function traverseIDBNode(node, elements, parentIndex = -1, depth = 0) {
33
+ if (!node)
34
+ return -1;
35
+ let currentIndex = -1;
36
+ const type = node.AXElementType || node.type || "unknown";
37
+ const label = node.AXLabel || node.label || null;
38
+ const value = node.AXValue || null;
39
+ const frame = node.AXFrame || node.frame;
40
+ const traits = node.AXTraits || [];
41
+ const clickable = traits.includes("UIAccessibilityTraitButton") || type === "Button" || type === "Cell";
42
+ const isUseful = clickable || (label && label.length > 0) || (value && value.length > 0) || type === "Application" || type === "Window";
43
+ if (isUseful) {
44
+ const bounds = parseIDBFrame(frame);
45
+ const element = {
46
+ text: label,
47
+ contentDescription: value,
48
+ type: type,
49
+ resourceId: node.AXUniqueId || null,
50
+ clickable: clickable,
51
+ enabled: true,
52
+ visible: true,
53
+ bounds: bounds,
54
+ center: getCenter(bounds),
55
+ depth: depth
56
+ };
57
+ if (parentIndex !== -1) {
58
+ element.parentId = parentIndex;
59
+ }
60
+ elements.push(element);
61
+ currentIndex = elements.length - 1;
62
+ }
63
+ const nextParentIndex = currentIndex !== -1 ? currentIndex : parentIndex;
64
+ const nextDepth = currentIndex !== -1 ? depth + 1 : depth;
65
+ const childrenIndices = [];
66
+ if (node.children && Array.isArray(node.children)) {
67
+ for (const child of node.children) {
68
+ const childIndex = traverseIDBNode(child, elements, nextParentIndex, nextDepth);
69
+ if (childIndex !== -1) {
70
+ childrenIndices.push(childIndex);
71
+ }
72
+ }
73
+ }
74
+ if (currentIndex !== -1 && childrenIndices.length > 0) {
75
+ elements[currentIndex].children = childrenIndices;
76
+ }
77
+ return currentIndex;
78
+ }
79
+ const iosActiveLogStreams = new Map();
80
+ export function _setIOSActiveLogStream(sessionId, file) {
81
+ iosActiveLogStreams.set(sessionId, { proc: {}, file });
82
+ }
83
+ export function _clearIOSActiveLogStream(sessionId) {
84
+ iosActiveLogStreams.delete(sessionId);
85
+ }
86
+ export class iOSObserve {
87
+ async getDeviceMetadata(deviceId = "booted") {
88
+ return getIOSDeviceMetadata(deviceId);
89
+ }
90
+ async getLogs(appId, deviceId = "booted") {
91
+ const args = ['simctl', 'spawn', deviceId, 'log', 'show', '--style', 'syslog', '--last', '1m'];
92
+ if (appId) {
93
+ validateBundleId(appId);
94
+ args.push('--predicate', `subsystem contains "${appId}" or process == "${appId}"`);
95
+ }
96
+ const result = await execCommand(args, deviceId);
97
+ const device = await getIOSDeviceMetadata(deviceId);
98
+ const logs = result.output ? result.output.split('\n') : [];
99
+ return {
100
+ device,
101
+ logs,
102
+ logCount: logs.length,
103
+ };
104
+ }
105
+ async captureScreenshot(deviceId = "booted") {
106
+ const device = await getIOSDeviceMetadata(deviceId);
107
+ const tmpFile = `/tmp/mcp-ios-screenshot-${Date.now()}.png`;
108
+ try {
109
+ await execCommand(['simctl', 'io', deviceId, 'screenshot', tmpFile], deviceId);
110
+ const buffer = await fs.readFile(tmpFile);
111
+ const base64 = buffer.toString('base64');
112
+ await fs.rm(tmpFile).catch(() => { });
113
+ return {
114
+ device,
115
+ screenshot: base64,
116
+ resolution: { width: 0, height: 0 },
117
+ };
118
+ }
119
+ catch (e) {
120
+ await fs.rm(tmpFile).catch(() => { });
121
+ throw new Error(`Failed to capture screenshot: ${e instanceof Error ? e.message : String(e)}`);
122
+ }
123
+ }
124
+ async getUITree(deviceId = "booted") {
125
+ const device = await getIOSDeviceMetadata(deviceId);
126
+ const idbExists = await isIDBInstalled();
127
+ if (!idbExists) {
128
+ return {
129
+ device,
130
+ screen: "",
131
+ resolution: { width: 0, height: 0 },
132
+ elements: [],
133
+ error: "iOS UI tree retrieval requires 'idb' (iOS Device Bridge). Please install it via Homebrew: `brew tap facebook/fb && brew install idb-companion` and `pip3 install fb-idb`."
134
+ };
135
+ }
136
+ const targetUdid = (device.id && device.id !== 'booted') ? device.id : undefined;
137
+ let jsonContent = null;
138
+ let attempts = 0;
139
+ const maxAttempts = 3;
140
+ while (attempts < maxAttempts) {
141
+ attempts++;
142
+ try {
143
+ await delay(300 + (attempts * 100));
144
+ const args = ['ui', 'describe-all', '--json'];
145
+ if (targetUdid) {
146
+ args.push('--udid', targetUdid);
147
+ }
148
+ const output = await new Promise((resolve, reject) => {
149
+ const child = spawn(getIdbCmd(), args);
150
+ let stdout = '';
151
+ let stderr = '';
152
+ child.stdout.on('data', (data) => stdout += data.toString());
153
+ child.stderr.on('data', (data) => stderr += data.toString());
154
+ child.on('error', (err) => reject(new Error(`Failed to execute idb: ${err.message}`)));
155
+ child.on('close', (code) => {
156
+ if (code !== 0) {
157
+ reject(new Error(`idb failed (code ${code}): ${stderr.trim()}`));
158
+ }
159
+ else {
160
+ resolve(stdout);
161
+ }
162
+ });
163
+ });
164
+ if (output && output.trim().length > 0) {
165
+ jsonContent = JSON.parse(output);
166
+ break; // Success
167
+ }
168
+ }
169
+ catch (e) {
170
+ console.error(`Attempt ${attempts} failed: ${e}`);
171
+ }
172
+ if (attempts === maxAttempts) {
173
+ return {
174
+ device,
175
+ screen: "",
176
+ resolution: { width: 0, height: 0 },
177
+ elements: [],
178
+ error: `Failed to retrieve valid UI dump after ${maxAttempts} attempts.`
179
+ };
180
+ }
181
+ }
182
+ try {
183
+ const elements = [];
184
+ if (Array.isArray(jsonContent)) {
185
+ for (const node of jsonContent) {
186
+ traverseIDBNode(node, elements);
187
+ }
188
+ }
189
+ else {
190
+ traverseIDBNode(jsonContent, elements);
191
+ }
192
+ let width = 0;
193
+ let height = 0;
194
+ if (elements.length > 0) {
195
+ const rootBounds = elements[0].bounds;
196
+ width = rootBounds[2] - rootBounds[0];
197
+ height = rootBounds[3] - rootBounds[1];
198
+ }
199
+ return {
200
+ device,
201
+ screen: "",
202
+ resolution: { width, height },
203
+ elements
204
+ };
205
+ }
206
+ catch (e) {
207
+ return {
208
+ device,
209
+ screen: "",
210
+ resolution: { width: 0, height: 0 },
211
+ elements: [],
212
+ error: `Failed to parse idb output: ${e instanceof Error ? e.message : String(e)}`
213
+ };
214
+ }
215
+ }
216
+ async getScreenFingerprint(deviceId = 'booted') {
217
+ try {
218
+ const tree = await this.getUITree(deviceId);
219
+ if (!tree || tree.error)
220
+ return { fingerprint: null, error: tree && tree.error };
221
+ return computeScreenFingerprint(tree, null, 'ios', 50);
222
+ }
223
+ catch (e) {
224
+ return { fingerprint: null, error: e instanceof Error ? e.message : String(e) };
225
+ }
226
+ }
227
+ async startLogStream(bundleId, deviceId = 'booted', sessionId = 'default') {
228
+ try {
229
+ const predicate = `process == "${bundleId}" or subsystem contains "${bundleId}"`;
230
+ if (iosActiveLogStreams.has(sessionId)) {
231
+ try {
232
+ iosActiveLogStreams.get(sessionId).proc.kill();
233
+ }
234
+ catch { }
235
+ iosActiveLogStreams.delete(sessionId);
236
+ }
237
+ const args = ['simctl', 'spawn', deviceId, 'log', 'stream', '--style', 'syslog', '--predicate', predicate];
238
+ const proc = spawn(getXcrunCmd(), args);
239
+ const tmpDir = process.env.TMPDIR || '/tmp';
240
+ const file = path.join(tmpDir, `mobile-debug-ios-log-${sessionId}.ndjson`);
241
+ const stream = createWriteStream(file, { flags: 'a' });
242
+ proc.stdout.on('data', (chunk) => {
243
+ const text = chunk.toString();
244
+ const lines = text.split(/\r?\n/).filter(Boolean);
245
+ for (const l of lines) {
246
+ const entry = parseLogLine(l);
247
+ stream.write(JSON.stringify(entry) + '\n');
248
+ }
249
+ });
250
+ proc.stderr.on('data', (chunk) => {
251
+ const text = chunk.toString();
252
+ const lines = text.split(/\r?\n/).filter(Boolean);
253
+ for (const l of lines) {
254
+ const entry = { timestamp: '', level: 'E', tag: 'xcrun', message: l };
255
+ stream.write(JSON.stringify(entry) + '\n');
256
+ }
257
+ });
258
+ proc.on('close', () => {
259
+ stream.end();
260
+ iosActiveLogStreams.delete(sessionId);
261
+ });
262
+ iosActiveLogStreams.set(sessionId, { proc, file });
263
+ return { success: true, stream_started: true };
264
+ }
265
+ catch {
266
+ return { success: false, error: 'log_stream_start_failed' };
267
+ }
268
+ }
269
+ async stopLogStream(sessionId = 'default') {
270
+ const entry = iosActiveLogStreams.get(sessionId);
271
+ if (!entry)
272
+ return { success: true };
273
+ try {
274
+ entry.proc.kill();
275
+ }
276
+ catch { }
277
+ iosActiveLogStreams.delete(sessionId);
278
+ return { success: true };
279
+ }
280
+ async readLogStream(sessionId = 'default', limit = 100, since) {
281
+ const entry = iosActiveLogStreams.get(sessionId);
282
+ if (!entry)
283
+ return { entries: [] };
284
+ try {
285
+ const data = await fsPromises.readFile(entry.file, 'utf8').catch(() => '');
286
+ if (!data)
287
+ return { entries: [], crash_summary: { crash_detected: false } };
288
+ const lines = data.split(/\r?\n/).filter(Boolean);
289
+ const parsed = lines.map(l => {
290
+ try {
291
+ return JSON.parse(l);
292
+ }
293
+ catch {
294
+ return { message: l, _iso: null, crash: false };
295
+ }
296
+ });
297
+ let filtered = parsed;
298
+ if (since) {
299
+ let sinceMs = null;
300
+ if (/^\d+$/.test(since))
301
+ sinceMs = Number(since);
302
+ else {
303
+ const sDate = new Date(since);
304
+ if (!isNaN(sDate.getTime()))
305
+ sinceMs = sDate.getTime();
306
+ }
307
+ if (sinceMs !== null) {
308
+ filtered = parsed.filter(p => p._iso && (new Date(p._iso).getTime() >= sinceMs));
309
+ }
310
+ }
311
+ const entries = filtered.slice(-Math.max(0, limit));
312
+ const crashEntry = entries.find(e => e.crash);
313
+ const crash_summary = crashEntry ? { crash_detected: true, exception: crashEntry.exception, sample: crashEntry.message } : { crash_detected: false };
314
+ return { entries, crash_summary };
315
+ }
316
+ catch {
317
+ return { entries: [], crash_summary: { crash_detected: false } };
318
+ }
319
+ }
320
+ }
@@ -0,0 +1,34 @@
1
+ import { AndroidObserve } from '../../../../index.js';
2
+ async function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
3
+ async function main() {
4
+ const packageName = process.argv[2] || 'com.android.systemui';
5
+ const sessionId = 'real-logstream';
6
+ console.log('Starting log stream for', packageName);
7
+ const obs = new AndroidObserve();
8
+ const start = await obs.startLogStream(packageName, 'error', undefined, sessionId);
9
+ console.log('start result:', start);
10
+ if (!start.success) {
11
+ console.error('Failed to start log stream:', start.error);
12
+ process.exit(start.error === 'app_not_running' ? 2 : 1);
13
+ }
14
+ try {
15
+ for (let i = 0; i < 10; i++) {
16
+ console.log('\nPolling logs (iteration', i + 1, ')');
17
+ const { entries, crash_summary } = await obs.readLogStream(sessionId, 50);
18
+ console.log(`Entries: ${entries.length}`);
19
+ if (entries.length > 0)
20
+ console.log('Latest:', entries[entries.length - 1]);
21
+ console.log('Crash summary:', crash_summary);
22
+ if (crash_summary && crash_summary.crash_detected) {
23
+ console.log('Crash detected; stopping early');
24
+ break;
25
+ }
26
+ await sleep(1000);
27
+ }
28
+ }
29
+ finally {
30
+ console.log('Stopping log stream');
31
+ await obs.stopLogStream(sessionId);
32
+ }
33
+ }
34
+ main().catch(err => { console.error(err); process.exit(1); });
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Device E2E: get_screen_fingerprint
4
+ * Usage: RUN_DEVICE_TESTS=true npx tsx run-screen-fingerprint.ts [android|ios] [deviceId]
5
+ */
6
+ import { AndroidObserve, iOSObserve } from '../../../../../index.js';
7
+ async function main() {
8
+ const args = process.argv.slice(2);
9
+ const platform = (args[0] || 'android').toLowerCase();
10
+ const deviceId = args[1];
11
+ console.log(`Running screen fingerprint test for ${platform}${deviceId ? ` on ${deviceId}` : ''}`);
12
+ try {
13
+ const obs = platform === 'ios' ? new iOSObserve() : new AndroidObserve();
14
+ const id = platform === 'ios' ? (deviceId || 'booted') : deviceId;
15
+ const res = await obs.getScreenFingerprint(id);
16
+ if (res.error || !res.fingerprint) {
17
+ console.error('❌ Failed to compute fingerprint:', res.error);
18
+ process.exit(1);
19
+ }
20
+ console.log('Fingerprint:', res.fingerprint);
21
+ console.log('Activity:', res.activity || '<n/a>');
22
+ process.exit(0);
23
+ }
24
+ catch (err) {
25
+ console.error('❌ Test failed:', err instanceof Error ? err.message : String(err));
26
+ process.exit(1);
27
+ }
28
+ }
29
+ main();
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+ import { AndroidInteract } from '../../../dist/android/interact.js';
3
+ // Usage: tsx test/device/observe/run-scroll-test-android.ts <deviceId> <appId> <selectorText>
4
+ const args = process.argv.slice(2);
5
+ const DEVICE_ID = args[0] || process.env.DEVICE_ID || 'emulator-5554';
6
+ const SELECTOR = args[2] || process.env.SELECTOR || 'Generate Session';
7
+ async function main() {
8
+ console.log('Starting app if not running...');
9
+ // Best-effort tap to wake device/emulator
10
+ try {
11
+ const tmp = new AndroidInteract();
12
+ await tmp.tap(10, 10, DEVICE_ID).catch(() => { });
13
+ }
14
+ catch { }
15
+ await new Promise(r => setTimeout(r, 1000));
16
+ console.log('Running scroll_to_element for selector:', SELECTOR);
17
+ // Use ToolsInteract from dist to call the handler
18
+ const ToolsInteract = (await import('../../../dist/tools/interact.js')).ToolsInteract;
19
+ const res = await ToolsInteract.scrollToElementHandler({ platform: 'android', selector: { text: SELECTOR }, direction: 'down', maxScrolls: 10, scrollAmount: 0.7, deviceId: DEVICE_ID });
20
+ console.log('Result:', JSON.stringify(res, null, 2));
21
+ }
22
+ main().catch(console.error);
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Test script for verify UI Tree functionality.
3
+ *
4
+ * Usage:
5
+ * npx tsx test-ui-tree.ts [android|ios] [deviceId]
6
+ *
7
+ * Examples:
8
+ * npx tsx test-ui-tree.ts android
9
+ * npx tsx test-ui-tree.ts ios booted
10
+ */
11
+ import { AndroidObserve, iOSObserve } from '../../../../index.js';
12
+ async function main() {
13
+ const args = process.argv.slice(2);
14
+ const platform = (args[0] || 'android').toLowerCase();
15
+ const deviceId = args[1];
16
+ console.log(`Starting UI Tree Test for ${platform}...`);
17
+ if (deviceId)
18
+ console.log(`Targeting device: ${deviceId}`);
19
+ try {
20
+ let result;
21
+ if (platform === 'ios') {
22
+ const observer = new iOSObserve();
23
+ result = await observer.getUITree(deviceId || 'booted');
24
+ }
25
+ else {
26
+ const observer = new AndroidObserve();
27
+ result = await observer.getUITree(deviceId);
28
+ }
29
+ console.log("\nUI Tree Result Summary:");
30
+ console.log("-----------------------");
31
+ if (result.error) {
32
+ console.error("❌ Error:", result.error);
33
+ process.exit(1);
34
+ }
35
+ console.log(`Device: ${result.device.platform} (${result.device.model || 'Unknown Model'})`);
36
+ console.log(`Resolution: ${result.resolution.width}x${result.resolution.height}`);
37
+ console.log(`Elements Found: ${result.elements.length}`);
38
+ if (result.elements.length === 0) {
39
+ console.warn("⚠️ Warning: No elements found. Is the screen empty or locked?");
40
+ }
41
+ else {
42
+ // Print sample element to verify structure
43
+ const first = result.elements[0];
44
+ console.log("\nSample Element (First):");
45
+ console.log(JSON.stringify(first, null, 2));
46
+ // Check for new fields
47
+ if (first.center && first.depth !== undefined) {
48
+ console.log("\n✅ Verified 'center' and 'depth' fields exist.");
49
+ }
50
+ else {
51
+ console.error("\n❌ 'center' or 'depth' fields missing!");
52
+ process.exit(1);
53
+ }
54
+ // Check for filtering
55
+ const interactive = result.elements.filter(e => e.clickable).length;
56
+ const withText = result.elements.filter(e => e.text).length;
57
+ console.log(`\nStats:`);
58
+ console.log(`- Interactive elements: ${interactive}`);
59
+ console.log(`- Elements with text: ${withText}`);
60
+ }
61
+ }
62
+ catch {
63
+ console.error("\n❌ Test Failed:", error);
64
+ process.exit(1);
65
+ }
66
+ }
67
+ main();
@@ -0,0 +1,69 @@
1
+ import { AndroidInteract } from "../../src/android/interact.js";
2
+ import { AndroidObserve } from "../../../../index.js";
3
+ // Usage: npx tsx test/wait_for_element_real.ts <deviceId> <appId>
4
+ const args = process.argv.slice(2);
5
+ const DEVICE_ID = args[0] || process.env.DEVICE_ID;
6
+ const APP_ID = args[1] || process.env.APP_ID;
7
+ if (!DEVICE_ID || !APP_ID) {
8
+ console.error("Usage: npx tsx test/wait_for_element_real.ts <deviceId> <appId> or set DEVICE_ID and APP_ID env vars");
9
+ process.exit(1);
10
+ }
11
+ async function runRealTest() {
12
+ console.log(`Connecting to device ${DEVICE_ID}...`);
13
+ const interact = new AndroidInteract();
14
+ const observe = new AndroidObserve();
15
+ try {
16
+ console.log(`\nStarting app ${APP_ID}...`);
17
+ await interact.startApp(APP_ID, DEVICE_ID);
18
+ console.log("Waiting 3s for app to render...");
19
+ await new Promise(r => setTimeout(r, 3000));
20
+ console.log("\nFetching UI Tree to find a target text...");
21
+ const tree = await observe.getUITree(DEVICE_ID);
22
+ if (tree.error) {
23
+ console.error("Failed to get UI Tree:", tree.error);
24
+ return;
25
+ }
26
+ const targetElement = tree.elements.find(e => e.text && e.text.length > 0 && e.visible);
27
+ if (!targetElement || !targetElement.text) {
28
+ console.warn("No visible text elements found on screen to test with.");
29
+ console.log("Elements found:", tree.elements.length);
30
+ return;
31
+ }
32
+ const targetText = targetElement.text;
33
+ console.log(`Found target element: "${targetText}"`);
34
+ console.log(`\nTest 1: Waiting for existing element "${targetText}" (should succeed)...`);
35
+ const start1 = Date.now();
36
+ const result1 = await interact.waitForElement(targetText, 5000, DEVICE_ID);
37
+ const elapsed1 = Date.now() - start1;
38
+ console.log(`Result: ${result1.found ? "PASS" : "FAIL"}`);
39
+ console.log(`Found Element: ${result1.element?.text}`);
40
+ console.log(`Time taken: ${elapsed1}ms`);
41
+ const missingText = "THIS_TEXT_SHOULD_NOT_EXIST_XYZ_123";
42
+ console.log(`\nTest 2: Waiting for missing element "${missingText}" (should timeout)...`);
43
+ const start2 = Date.now();
44
+ const result2 = await interact.waitForElement(missingText, 2000, DEVICE_ID);
45
+ const elapsed2 = Date.now() - start2;
46
+ console.log(`Result: ${!result2.found ? "PASS" : "FAIL"}`);
47
+ console.log(`Found: ${result2.found}`);
48
+ console.log(`Time taken: ${elapsed2}ms (expected ~2000ms)`);
49
+ console.log(`\nTest 3: Found after polling`);
50
+ let calls = 0;
51
+ AndroidObserve.prototype.getUITree = async function () {
52
+ calls++;
53
+ if (calls < 3) {
54
+ return { device: { platform: "android", id: "mock", osVersion: "12", model: "Pixel", simulator: true }, screen: "", resolution: { width: 1080, height: 1920 }, elements: [] };
55
+ }
56
+ return { device: { platform: "android", id: "mock", osVersion: "12", model: "Pixel", simulator: true }, screen: "", resolution: { width: 1080, height: 1920 }, elements: [{ text: "Target", type: "Button", contentDescription: null, clickable: true, enabled: true, visible: true, bounds: [0, 0, 100, 100], resourceId: null }] };
57
+ };
58
+ const start3 = Date.now();
59
+ const result3 = await interact.waitForElement("Target", 2000, DEVICE_ID);
60
+ const elapsed3 = Date.now() - start3;
61
+ console.log(`Result: ${result3.found ? "PASS" : "FAIL"}`);
62
+ console.log(`Calls: ${calls} ${calls === 3 ? "PASS" : "FAIL"}`);
63
+ console.log(`Elapsed time (should be >= 1000ms): ${elapsed3} ${elapsed3 >= 1000 ? "PASS" : "FAIL"}`);
64
+ }
65
+ catch {
66
+ console.error("Test failed with error:", error);
67
+ }
68
+ }
69
+ runRealTest();
@@ -0,0 +1,54 @@
1
+ import { AndroidObserve } from '../../../../../index.js';
2
+ async function run() {
3
+ console.log('Starting get_screen_fingerprint unit tests...');
4
+ const origGet = AndroidObserve.prototype.getUITree;
5
+ const origCurrent = AndroidObserve.prototype.getCurrentScreen;
6
+ AndroidObserve.prototype.getUITree = async function () {
7
+ return {
8
+ device: { platform: 'android', id: 'mock', osVersion: '12', model: 'Pixel', simulator: true },
9
+ screen: '',
10
+ resolution: { width: 1080, height: 1920 },
11
+ elements: [
12
+ { text: 'Title', type: 'TextView', contentDescription: null, clickable: false, enabled: true, visible: true, bounds: [0, 0, 1080, 100], resourceId: 'id/title' },
13
+ { text: 'Sign in', type: 'Button', contentDescription: null, clickable: true, enabled: true, visible: true, bounds: [0, 200, 200, 260], resourceId: 'id/signin' }
14
+ ]
15
+ };
16
+ };
17
+ AndroidObserve.prototype.getCurrentScreen = async function () {
18
+ return { device: { platform: 'android', id: 'mock', osVersion: '12', model: 'Pixel', simulator: true }, package: 'com.example', activity: 'com.example.MainActivity', shortActivity: 'MainActivity' };
19
+ };
20
+ const ai = new AndroidObserve();
21
+ const a = await ai.getScreenFingerprint('mock');
22
+ const b = await ai.getScreenFingerprint('mock');
23
+ console.log('Test 1:', a.fingerprint === b.fingerprint ? 'PASS' : 'FAIL');
24
+ AndroidObserve.prototype.getUITree = async function () {
25
+ return {
26
+ device: { platform: 'android', id: 'mock', osVersion: '12', model: 'Pixel', simulator: true },
27
+ screen: '',
28
+ resolution: { width: 1080, height: 1920 },
29
+ elements: [
30
+ { text: 'Title', type: 'TextView', contentDescription: null, clickable: false, enabled: true, visible: true, bounds: [0, 0, 1080, 100], resourceId: 'id/title' },
31
+ { text: 'Profile', type: 'Button', contentDescription: null, clickable: true, enabled: true, visible: true, bounds: [0, 200, 200, 260], resourceId: 'id/signin' }
32
+ ]
33
+ };
34
+ };
35
+ const c = await ai.getScreenFingerprint('mock');
36
+ console.log('Test 2:', a.fingerprint !== c.fingerprint ? 'PASS' : 'FAIL');
37
+ AndroidObserve.prototype.getUITree = async function () {
38
+ return {
39
+ device: { platform: 'android', id: 'mock', osVersion: '12', model: 'Pixel', simulator: true },
40
+ screen: '',
41
+ resolution: { width: 1080, height: 1920 },
42
+ elements: [
43
+ { text: 'Title', type: 'TextView', contentDescription: null, clickable: false, enabled: true, visible: true, bounds: [0, 0, 1080, 100], resourceId: 'id/title' },
44
+ { text: 'Sign in', type: 'Button', contentDescription: null, clickable: true, enabled: true, visible: true, bounds: [0, 200, 200, 260], resourceId: 'id/signin' },
45
+ { text: '12:34', type: 'TextView', contentDescription: null, clickable: false, enabled: true, visible: true, bounds: [900, 10, 1080, 40], resourceId: null }
46
+ ]
47
+ };
48
+ };
49
+ const d = await ai.getScreenFingerprint('mock');
50
+ console.log('Test 3:', a.fingerprint === d.fingerprint ? 'PASS' : 'FAIL');
51
+ AndroidObserve.prototype.getUITree = origGet;
52
+ AndroidObserve.prototype.getCurrentScreen = origCurrent;
53
+ }
54
+ run().catch(console.error);
@@ -0,0 +1,39 @@
1
+ import { parseLogLine } from '../../../src/android/utils.js';
2
+ function assert(cond, msg) { if (!cond)
3
+ throw new Error(msg || 'Assertion failed'); }
4
+ function run() {
5
+ const samples = [
6
+ // Standard format
7
+ {
8
+ line: '03-13 15:08:25.257 2468 2578 E FromGoneTransitionInteractor: Ignoring startTransition: ...',
9
+ expect: { level: 'E', tag: 'FromGoneTransitionInteractor', crash: false }
10
+ },
11
+ // Full date format
12
+ {
13
+ line: '2026-03-13 15:08:25.257 2468 2578 E Something: Boom happened',
14
+ expect: { level: 'E', tag: 'Something', crash: false }
15
+ },
16
+ // Simple priority/tag
17
+ {
18
+ line: 'W/MyTag: Some warning here',
19
+ expect: { level: 'W', tag: 'MyTag', crash: false }
20
+ },
21
+ // Crash message
22
+ {
23
+ line: '03-13 15:09:01.123 9999 9999 E AndroidRuntime: FATAL EXCEPTION: main\njava.lang.NullPointerException: at ...',
24
+ expect: { level: 'E', tag: 'AndroidRuntime', crash: true, exceptionContains: 'NullPointerException' }
25
+ }
26
+ ];
27
+ for (const s of samples) {
28
+ const res = parseLogLine(s.line);
29
+ console.log('Parsed:', res);
30
+ assert(res.level === s.expect.level, `Expected level ${s.expect.level} got ${res.level}`);
31
+ assert(res.tag === s.expect.tag, `Expected tag ${s.expect.tag} got ${res.tag}`);
32
+ if (s.expect.crash)
33
+ assert(res.crash === true, 'Expected crash true');
34
+ if (s.expect.exceptionContains)
35
+ assert(res.exception && res.exception.indexOf(s.expect.exceptionContains) !== -1, 'Expected exception to contain ' + s.expect.exceptionContains);
36
+ }
37
+ console.log('Log parse tests passed');
38
+ }
39
+ run();