mobile-debug-mcp 0.21.4 → 0.21.5
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/interact/index.js +43 -28
- package/docs/CHANGELOG.md +3 -0
- package/package.json +1 -1
- package/src/interact/index.ts +36 -25
package/dist/interact/index.js
CHANGED
|
@@ -4,6 +4,16 @@ export { AndroidInteract, iOSInteract };
|
|
|
4
4
|
import { resolveTargetDevice } from '../utils/resolve-device.js';
|
|
5
5
|
import { ToolsObserve } from '../observe/index.js';
|
|
6
6
|
export class ToolsInteract {
|
|
7
|
+
static _normalize(s) {
|
|
8
|
+
if (s === null || s === undefined)
|
|
9
|
+
return '';
|
|
10
|
+
try {
|
|
11
|
+
return String(s).toLowerCase().trim();
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return '';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
7
17
|
static async getInteractionService(platform, deviceId) {
|
|
8
18
|
const effectivePlatform = platform || 'android';
|
|
9
19
|
const resolved = await resolveTargetDevice({ platform: effectivePlatform, deviceId });
|
|
@@ -34,7 +44,7 @@ export class ToolsInteract {
|
|
|
34
44
|
// Try to use observe layer to fetch the current UI tree and perform a fast semantic search
|
|
35
45
|
const start = Date.now();
|
|
36
46
|
const deadline = start + timeoutMs;
|
|
37
|
-
const normalize =
|
|
47
|
+
const normalize = ToolsInteract._normalize;
|
|
38
48
|
const q = normalize(query);
|
|
39
49
|
if (!q)
|
|
40
50
|
return { found: false, error: 'Empty query' };
|
|
@@ -217,15 +227,19 @@ export class ToolsInteract {
|
|
|
217
227
|
}
|
|
218
228
|
static async waitForUIHandler({ selector, condition = 'exists', timeout_ms = 60000, poll_interval_ms = 300, match, retry = { max_attempts: 1, backoff_ms: 0 }, platform, deviceId }) {
|
|
219
229
|
const overallStart = Date.now();
|
|
220
|
-
// Validate selector: require at least one
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const hasText = selector && typeof selector.text === 'string' && selector.text.trim().length > 0;
|
|
225
|
-
const hasResId = selector && typeof selector.resource_id === 'string' && selector.resource_id.trim().length > 0;
|
|
226
|
-
const hasAccId = selector && typeof selector.accessibility_id === 'string' && selector.accessibility_id.trim().length > 0;
|
|
230
|
+
// Validate selector: require at least one non-empty field (text, resource_id, or accessibility_id)
|
|
231
|
+
const hasText = typeof selector?.text === 'string' && selector.text.trim().length > 0;
|
|
232
|
+
const hasResId = typeof selector?.resource_id === 'string' && selector.resource_id.trim().length > 0;
|
|
233
|
+
const hasAccId = typeof selector?.accessibility_id === 'string' && selector.accessibility_id.trim().length > 0;
|
|
227
234
|
if (!hasText && !hasResId && !hasAccId) {
|
|
228
|
-
return {
|
|
235
|
+
return {
|
|
236
|
+
status: 'timeout',
|
|
237
|
+
error: {
|
|
238
|
+
code: 'INVALID_SELECTOR',
|
|
239
|
+
message: 'Selector must include at least one non-empty field: text, resource_id, or accessibility_id'
|
|
240
|
+
},
|
|
241
|
+
metrics: { latency_ms: Date.now() - overallStart, poll_count: 0, attempts: 0 }
|
|
242
|
+
};
|
|
229
243
|
}
|
|
230
244
|
// Validate condition
|
|
231
245
|
if (!['exists', 'not_exists', 'visible', 'clickable'].includes(condition)) {
|
|
@@ -241,11 +255,11 @@ export class ToolsInteract {
|
|
|
241
255
|
let attempts = 0;
|
|
242
256
|
let totalPollCount = 0;
|
|
243
257
|
// Precompute normalized selector values and helpers (constant across polls)
|
|
244
|
-
const normalize =
|
|
245
|
-
const containsFlag = !!selector
|
|
246
|
-
const selText = normalize(selector
|
|
247
|
-
const selRid = normalize(selector
|
|
248
|
-
const selAid = normalize(selector
|
|
258
|
+
const normalize = ToolsInteract._normalize;
|
|
259
|
+
const containsFlag = !!selector?.contains;
|
|
260
|
+
const selText = normalize(selector?.text);
|
|
261
|
+
const selRid = normalize(selector?.resource_id);
|
|
262
|
+
const selAid = normalize(selector?.accessibility_id);
|
|
249
263
|
try {
|
|
250
264
|
while (attempts < maxAttempts) {
|
|
251
265
|
attempts++;
|
|
@@ -301,30 +315,31 @@ export class ToolsInteract {
|
|
|
301
315
|
}
|
|
302
316
|
// Evaluate condition
|
|
303
317
|
const matchedCount = matches.length;
|
|
304
|
-
const
|
|
305
|
-
const pickIndex = pickIndexProvided ? Number(match.index) : 0;
|
|
318
|
+
const pickIndex = (typeof match?.index === 'number') ? match.index : undefined;
|
|
306
319
|
let chosen = null;
|
|
307
|
-
if (matches.length
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
320
|
+
if (matches.length > 0) {
|
|
321
|
+
if (pickIndex !== undefined) {
|
|
322
|
+
// If a specific index is requested but out of bounds, treat as not matched for this poll (deterministic)
|
|
323
|
+
if (pickIndex >= 0 && pickIndex < matches.length)
|
|
324
|
+
chosen = matches[pickIndex];
|
|
325
|
+
else
|
|
326
|
+
chosen = null;
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
chosen = matches[0];
|
|
330
|
+
}
|
|
316
331
|
}
|
|
317
332
|
else {
|
|
318
|
-
chosen =
|
|
333
|
+
chosen = null;
|
|
319
334
|
}
|
|
320
335
|
let conditionMet = false;
|
|
321
336
|
if (condition === 'exists') {
|
|
322
337
|
// when an index is specified, existence requires that specific index be present
|
|
323
|
-
conditionMet =
|
|
338
|
+
conditionMet = (pickIndex !== undefined) ? (chosen !== null) : (matchedCount >= 1);
|
|
324
339
|
}
|
|
325
340
|
else if (condition === 'not_exists') {
|
|
326
341
|
// when an index is specified, not_exists is true if that index is absent
|
|
327
|
-
conditionMet =
|
|
342
|
+
conditionMet = (pickIndex !== undefined) ? (chosen === null) : (matchedCount === 0);
|
|
328
343
|
}
|
|
329
344
|
else if (condition === 'visible') {
|
|
330
345
|
if (chosen) {
|
package/docs/CHANGELOG.md
CHANGED
package/package.json
CHANGED
package/src/interact/index.ts
CHANGED
|
@@ -32,6 +32,12 @@ interface UiElement {
|
|
|
32
32
|
|
|
33
33
|
export class ToolsInteract {
|
|
34
34
|
|
|
35
|
+
private static _normalize(s: any): string {
|
|
36
|
+
if (s === null || s === undefined) return ''
|
|
37
|
+
try { return String(s).toLowerCase().trim() } catch { return '' }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
35
41
|
private static async getInteractionService(platform?: 'android' | 'ios', deviceId?: string) {
|
|
36
42
|
const effectivePlatform = platform || 'android'
|
|
37
43
|
const resolved = await resolveTargetDevice({ platform: effectivePlatform as 'android' | 'ios', deviceId })
|
|
@@ -68,7 +74,7 @@ export class ToolsInteract {
|
|
|
68
74
|
// Try to use observe layer to fetch the current UI tree and perform a fast semantic search
|
|
69
75
|
const start = Date.now()
|
|
70
76
|
const deadline = start + timeoutMs
|
|
71
|
-
const normalize =
|
|
77
|
+
const normalize = ToolsInteract._normalize
|
|
72
78
|
|
|
73
79
|
const q = normalize(query)
|
|
74
80
|
if (!q) return { found: false, error: 'Empty query' }
|
|
@@ -221,16 +227,20 @@ export class ToolsInteract {
|
|
|
221
227
|
static async waitForUIHandler({ selector, condition = 'exists', timeout_ms = 60000, poll_interval_ms = 300, match, retry = { max_attempts: 1, backoff_ms: 0 }, platform, deviceId }: { selector?: { text?: string, resource_id?: string, accessibility_id?: string, contains?: boolean }, condition?: 'exists'|'not_exists'|'visible'|'clickable', timeout_ms?: number, poll_interval_ms?: number, match?: { index?: number }, retry?: { max_attempts?: number, backoff_ms?: number }, platform?: 'android'|'ios', deviceId?: string }) {
|
|
222
228
|
const overallStart = Date.now()
|
|
223
229
|
|
|
224
|
-
// Validate selector: require at least one
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
230
|
+
// Validate selector: require at least one non-empty field (text, resource_id, or accessibility_id)
|
|
231
|
+
const hasText = typeof selector?.text === 'string' && selector.text.trim().length > 0;
|
|
232
|
+
const hasResId = typeof selector?.resource_id === 'string' && selector.resource_id.trim().length > 0;
|
|
233
|
+
const hasAccId = typeof selector?.accessibility_id === 'string' && selector.accessibility_id.trim().length > 0;
|
|
228
234
|
|
|
229
|
-
const hasText = selector && typeof (selector as any).text === 'string' && (selector as any).text.trim().length > 0
|
|
230
|
-
const hasResId = selector && typeof (selector as any).resource_id === 'string' && (selector as any).resource_id.trim().length > 0
|
|
231
|
-
const hasAccId = selector && typeof (selector as any).accessibility_id === 'string' && (selector as any).accessibility_id.trim().length > 0
|
|
232
235
|
if (!hasText && !hasResId && !hasAccId) {
|
|
233
|
-
return {
|
|
236
|
+
return {
|
|
237
|
+
status: 'timeout',
|
|
238
|
+
error: {
|
|
239
|
+
code: 'INVALID_SELECTOR',
|
|
240
|
+
message: 'Selector must include at least one non-empty field: text, resource_id, or accessibility_id'
|
|
241
|
+
},
|
|
242
|
+
metrics: { latency_ms: Date.now() - overallStart, poll_count: 0, attempts: 0 }
|
|
243
|
+
};
|
|
234
244
|
}
|
|
235
245
|
|
|
236
246
|
// Validate condition
|
|
@@ -251,11 +261,11 @@ export class ToolsInteract {
|
|
|
251
261
|
let totalPollCount = 0
|
|
252
262
|
|
|
253
263
|
// Precompute normalized selector values and helpers (constant across polls)
|
|
254
|
-
const normalize =
|
|
255
|
-
const containsFlag = !!selector
|
|
256
|
-
const selText = normalize(
|
|
257
|
-
const selRid = normalize(
|
|
258
|
-
const selAid = normalize(
|
|
264
|
+
const normalize = ToolsInteract._normalize
|
|
265
|
+
const containsFlag = !!selector?.contains
|
|
266
|
+
const selText = normalize(selector?.text)
|
|
267
|
+
const selRid = normalize(selector?.resource_id)
|
|
268
|
+
const selAid = normalize(selector?.accessibility_id)
|
|
259
269
|
|
|
260
270
|
try {
|
|
261
271
|
while (attempts < maxAttempts) {
|
|
@@ -310,26 +320,27 @@ export class ToolsInteract {
|
|
|
310
320
|
|
|
311
321
|
// Evaluate condition
|
|
312
322
|
const matchedCount = matches.length
|
|
313
|
-
const
|
|
314
|
-
const pickIndex: number = pickIndexProvided ? Number((match as any).index) : 0
|
|
323
|
+
const pickIndex = (typeof match?.index === 'number') ? match!.index as number : undefined
|
|
315
324
|
let chosen: { el: any, idx: number } | null = null
|
|
316
|
-
if (matches.length
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
else
|
|
325
|
+
if (matches.length > 0) {
|
|
326
|
+
if (pickIndex !== undefined) {
|
|
327
|
+
// If a specific index is requested but out of bounds, treat as not matched for this poll (deterministic)
|
|
328
|
+
if (pickIndex >= 0 && pickIndex < matches.length) chosen = matches[pickIndex]
|
|
329
|
+
else chosen = null
|
|
330
|
+
} else {
|
|
331
|
+
chosen = matches[0]
|
|
332
|
+
}
|
|
322
333
|
} else {
|
|
323
|
-
chosen =
|
|
334
|
+
chosen = null
|
|
324
335
|
}
|
|
325
336
|
|
|
326
337
|
let conditionMet = false
|
|
327
338
|
if (condition === 'exists') {
|
|
328
339
|
// when an index is specified, existence requires that specific index be present
|
|
329
|
-
conditionMet =
|
|
340
|
+
conditionMet = (pickIndex !== undefined) ? (chosen !== null) : (matchedCount >= 1)
|
|
330
341
|
} else if (condition === 'not_exists') {
|
|
331
342
|
// when an index is specified, not_exists is true if that index is absent
|
|
332
|
-
conditionMet =
|
|
343
|
+
conditionMet = (pickIndex !== undefined) ? (chosen === null) : (matchedCount === 0)
|
|
333
344
|
} else if (condition === 'visible') {
|
|
334
345
|
if (chosen) {
|
|
335
346
|
const b = chosen.el.bounds
|