mobile-debug-mcp 0.24.7 → 0.24.8
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 +5 -5
- package/dist/interact/index.js +89 -3
- package/dist/server-core.js +1 -1
- package/docs/CHANGELOG.md +3 -0
- package/package.json +1 -1
- package/src/interact/index.ts +112 -4
- package/src/server-core.ts +1 -1
- package/test/unit/observe/find_element.test.ts +64 -5
package/README.md
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
A minimal, secure MCP server for AI-assisted mobile development. Build, install, and inspect Android/iOS apps from an MCP-compatible client.
|
|
4
4
|
|
|
5
5
|
> **Support:**
|
|
6
|
-
> *
|
|
7
|
-
> *
|
|
8
|
-
> *
|
|
9
|
-
> * Flutter
|
|
10
|
-
> * React native not tested
|
|
6
|
+
> * KMP
|
|
7
|
+
> * Android
|
|
8
|
+
> * iOS
|
|
9
|
+
> * Flutter - not tested
|
|
10
|
+
> * React native - not tested
|
|
11
11
|
|
|
12
12
|
## Requirements
|
|
13
13
|
|
package/dist/interact/index.js
CHANGED
|
@@ -7,6 +7,14 @@ import { ToolsObserve } from '../observe/index.js';
|
|
|
7
7
|
import { nextActionId } from '../server/common.js';
|
|
8
8
|
export class ToolsInteract {
|
|
9
9
|
static _maxResolvedUiElements = 256;
|
|
10
|
+
static _sliderSearchLookahead = 8;
|
|
11
|
+
static _sliderNegativeGapTolerancePx = 32;
|
|
12
|
+
static _sliderPositiveGapLimitPx = 640;
|
|
13
|
+
static _sliderTrackMinLengthPx = 220;
|
|
14
|
+
static _sliderTrackMaxThicknessPx = 180;
|
|
15
|
+
static _sliderTrackLengthRatio = 0.18;
|
|
16
|
+
static _sliderTrackThicknessRatio = 0.08;
|
|
17
|
+
static _sliderLabelWidthRatio = 1.5;
|
|
10
18
|
static _resolvedUiElements = new Map();
|
|
11
19
|
static _normalize(s) {
|
|
12
20
|
if (s === null || s === undefined)
|
|
@@ -174,6 +182,63 @@ export class ToolsInteract {
|
|
|
174
182
|
}
|
|
175
183
|
return best;
|
|
176
184
|
}
|
|
185
|
+
static _resolveNearbyActionableControl(elements, chosen, screen) {
|
|
186
|
+
if (!chosen)
|
|
187
|
+
return null;
|
|
188
|
+
const labelBounds = ToolsInteract._normalizeBounds(chosen.el.bounds);
|
|
189
|
+
if (!labelBounds)
|
|
190
|
+
return null;
|
|
191
|
+
const [labelLeft, labelTop, labelRight, labelBottom] = labelBounds;
|
|
192
|
+
const labelWidth = labelRight - labelLeft;
|
|
193
|
+
const labelHeight = labelBottom - labelTop;
|
|
194
|
+
const screenWidth = Number(screen?.width) > 0 ? Number(screen?.width) : 0;
|
|
195
|
+
const screenHeight = Number(screen?.height) > 0 ? Number(screen?.height) : 0;
|
|
196
|
+
const minTrackLengthPx = Math.max(ToolsInteract._sliderTrackMinLengthPx, screenWidth > 0 ? Math.floor(screenWidth * ToolsInteract._sliderTrackLengthRatio) : 0, screenHeight > 0 ? Math.floor(screenHeight * ToolsInteract._sliderTrackLengthRatio) : 0);
|
|
197
|
+
const maxTrackThicknessPx = Math.max(ToolsInteract._sliderTrackMaxThicknessPx, screenWidth > 0 ? Math.floor(screenWidth * ToolsInteract._sliderTrackThicknessRatio) : 0, screenHeight > 0 ? Math.floor(screenHeight * ToolsInteract._sliderTrackThicknessRatio) : 0);
|
|
198
|
+
let best = null;
|
|
199
|
+
let bestScore = Infinity;
|
|
200
|
+
for (let i = chosen.idx + 1; i < Math.min(elements.length, chosen.idx + ToolsInteract._sliderSearchLookahead); i++) {
|
|
201
|
+
const candidate = elements[i];
|
|
202
|
+
if (!candidate || !(candidate.clickable || candidate.focusable) || candidate.visible === false)
|
|
203
|
+
continue;
|
|
204
|
+
const candidateBounds = ToolsInteract._normalizeBounds(candidate.bounds);
|
|
205
|
+
if (!candidateBounds)
|
|
206
|
+
continue;
|
|
207
|
+
const [left, top, right] = candidateBounds;
|
|
208
|
+
const width = right - left;
|
|
209
|
+
const height = candidateBounds[3] - top;
|
|
210
|
+
const verticalGap = top - labelBottom;
|
|
211
|
+
if (verticalGap < -ToolsInteract._sliderNegativeGapTolerancePx || verticalGap > ToolsInteract._sliderPositiveGapLimitPx)
|
|
212
|
+
continue;
|
|
213
|
+
const horizontalOverlap = Math.min(labelRight, right) - Math.max(labelLeft, left);
|
|
214
|
+
if (horizontalOverlap < -ToolsInteract._sliderNegativeGapTolerancePx)
|
|
215
|
+
continue;
|
|
216
|
+
const candidateText = ToolsInteract._normalize(candidate.text ?? candidate.label ?? candidate.value ?? '');
|
|
217
|
+
const candidateContent = ToolsInteract._normalize(candidate.contentDescription ?? candidate.contentDesc ?? candidate.accessibilityLabel ?? '');
|
|
218
|
+
const candidateClass = ToolsInteract._normalize(candidate.type ?? candidate.class ?? '');
|
|
219
|
+
let score = verticalGap;
|
|
220
|
+
const horizontalTrackLike = width >= Math.max(minTrackLengthPx, Math.floor(labelWidth * ToolsInteract._sliderLabelWidthRatio)) &&
|
|
221
|
+
height <= maxTrackThicknessPx;
|
|
222
|
+
const verticalTrackLike = height >= Math.max(minTrackLengthPx, Math.floor(labelHeight * ToolsInteract._sliderLabelWidthRatio)) &&
|
|
223
|
+
width <= maxTrackThicknessPx;
|
|
224
|
+
const trackLike = /slider|seek|range/i.test(candidateClass) || horizontalTrackLike || verticalTrackLike;
|
|
225
|
+
if (!candidateText && !candidateContent)
|
|
226
|
+
score -= 18;
|
|
227
|
+
if (trackLike)
|
|
228
|
+
score -= 30;
|
|
229
|
+
if (/view|layout|group|frame/i.test(candidateClass))
|
|
230
|
+
score -= 10;
|
|
231
|
+
if (width > labelWidth * ToolsInteract._sliderLabelWidthRatio)
|
|
232
|
+
score -= 8;
|
|
233
|
+
if (candidateText || candidateContent)
|
|
234
|
+
score += 20;
|
|
235
|
+
if (score < bestScore) {
|
|
236
|
+
bestScore = score;
|
|
237
|
+
best = { el: candidate, idx: i, sliderLike: trackLike };
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return best;
|
|
241
|
+
}
|
|
177
242
|
static async getInteractionService(platform, deviceId) {
|
|
178
243
|
const effectivePlatform = platform || 'android';
|
|
179
244
|
const resolved = await resolveTargetDevice({ platform: effectivePlatform, deviceId });
|
|
@@ -262,6 +327,7 @@ export class ToolsInteract {
|
|
|
262
327
|
return { found: false, error: 'Empty query' };
|
|
263
328
|
let best = null;
|
|
264
329
|
let bestScore = 0;
|
|
330
|
+
let lastTree = null;
|
|
265
331
|
const scoreElement = (el) => {
|
|
266
332
|
if (!el || !el.visible)
|
|
267
333
|
return 0;
|
|
@@ -305,6 +371,7 @@ export class ToolsInteract {
|
|
|
305
371
|
while (Date.now() <= deadline) {
|
|
306
372
|
try {
|
|
307
373
|
const tree = await ToolsObserve.getUITreeHandler({ platform, deviceId });
|
|
374
|
+
lastTree = tree;
|
|
308
375
|
if (tree && Array.isArray(tree.elements)) {
|
|
309
376
|
const elements = tree.elements;
|
|
310
377
|
for (let i = 0; i < elements.length; i++) {
|
|
@@ -342,8 +409,8 @@ export class ToolsInteract {
|
|
|
342
409
|
return { found: false, error: 'Element not found' };
|
|
343
410
|
// If the best match is not interactable, try to resolve an actionable ancestor.
|
|
344
411
|
try {
|
|
345
|
-
const
|
|
346
|
-
const
|
|
412
|
+
const elements = (lastTree && Array.isArray(lastTree.elements)) ? lastTree.elements : [];
|
|
413
|
+
const screen = lastTree?.resolution && typeof lastTree.resolution === 'object' ? lastTree.resolution : null;
|
|
347
414
|
let chosen = best;
|
|
348
415
|
const childBounds = Array.isArray(chosen?.bounds) ? chosen.bounds : null;
|
|
349
416
|
// Strategy 1: if parentId references an index, climb that chain
|
|
@@ -412,6 +479,15 @@ export class ToolsInteract {
|
|
|
412
479
|
// small score bump to reflect actionability
|
|
413
480
|
bestScore = Math.min(1, bestScore + 0.02);
|
|
414
481
|
}
|
|
482
|
+
if (best && !(best.clickable || best.focusable)) {
|
|
483
|
+
const nearbyActionable = ToolsInteract._resolveNearbyActionableControl(elements, { el: best, idx: best._index ?? elements.indexOf(best) }, screen);
|
|
484
|
+
if (nearbyActionable) {
|
|
485
|
+
best = nearbyActionable.el;
|
|
486
|
+
best._index = nearbyActionable.idx;
|
|
487
|
+
best._interactable = true;
|
|
488
|
+
best._sliderLike = nearbyActionable.sliderLike;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
415
491
|
}
|
|
416
492
|
catch (e) {
|
|
417
493
|
console.error('Error resolving ancestor:', e);
|
|
@@ -431,9 +507,19 @@ export class ToolsInteract {
|
|
|
431
507
|
tapCoordinates,
|
|
432
508
|
telemetry: {
|
|
433
509
|
matchedIndex: best?._index ?? null,
|
|
434
|
-
matchedInteractable: !!best?._interactable
|
|
510
|
+
matchedInteractable: !!best?._interactable,
|
|
511
|
+
sliderLike: !!best?._sliderLike
|
|
435
512
|
}
|
|
436
513
|
};
|
|
514
|
+
if (best?._sliderLike) {
|
|
515
|
+
const isVertical = !!boundsObj && (boundsObj.bottom - boundsObj.top) > (boundsObj.right - boundsObj.left);
|
|
516
|
+
const interactionHint = {
|
|
517
|
+
kind: 'slider',
|
|
518
|
+
axis: isVertical ? 'vertical' : 'horizontal',
|
|
519
|
+
trackBounds: boundsObj
|
|
520
|
+
};
|
|
521
|
+
outEl.interactionHint = interactionHint;
|
|
522
|
+
}
|
|
437
523
|
const scoreVal = Math.min(1, Number(bestScore.toFixed(3)));
|
|
438
524
|
return { found: true, element: outEl, score: scoreVal, confidence: scoreVal };
|
|
439
525
|
}
|
package/dist/server-core.js
CHANGED
|
@@ -6,7 +6,7 @@ import { handleToolCall } from './server/tool-handlers.js';
|
|
|
6
6
|
export { wrapResponse, toolDefinitions, handleToolCall };
|
|
7
7
|
export const serverInfo = {
|
|
8
8
|
name: 'mobile-debug-mcp',
|
|
9
|
-
version: '0.24.
|
|
9
|
+
version: '0.24.8'
|
|
10
10
|
};
|
|
11
11
|
export function createServer() {
|
|
12
12
|
const server = new Server(serverInfo, {
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the **Mobile Debug MCP** project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.24.8]
|
|
6
|
+
- Improved slider interaction
|
|
7
|
+
|
|
5
8
|
## [0.24.7]
|
|
6
9
|
- Aligned runtime metadata with the published package version.
|
|
7
10
|
- Fixed stale CLI helper paths in npm scripts and the `idb` healthcheck helper.
|
package/package.json
CHANGED
package/src/interact/index.ts
CHANGED
|
@@ -36,6 +36,7 @@ interface UiElement {
|
|
|
36
36
|
parentId?: number | string | null
|
|
37
37
|
_index?: number
|
|
38
38
|
_interactable?: boolean
|
|
39
|
+
_sliderLike?: boolean
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
interface ResolvedUiElementContext {
|
|
@@ -46,9 +47,22 @@ interface ResolvedUiElementContext {
|
|
|
46
47
|
index: number
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
interface UiResolution {
|
|
51
|
+
width?: number
|
|
52
|
+
height?: number
|
|
53
|
+
}
|
|
54
|
+
|
|
49
55
|
|
|
50
56
|
export class ToolsInteract {
|
|
51
57
|
private static readonly _maxResolvedUiElements = 256
|
|
58
|
+
private static readonly _sliderSearchLookahead = 8
|
|
59
|
+
private static readonly _sliderNegativeGapTolerancePx = 32
|
|
60
|
+
private static readonly _sliderPositiveGapLimitPx = 640
|
|
61
|
+
private static readonly _sliderTrackMinLengthPx = 220
|
|
62
|
+
private static readonly _sliderTrackMaxThicknessPx = 180
|
|
63
|
+
private static readonly _sliderTrackLengthRatio = 0.18
|
|
64
|
+
private static readonly _sliderTrackThicknessRatio = 0.08
|
|
65
|
+
private static readonly _sliderLabelWidthRatio = 1.5
|
|
52
66
|
private static _resolvedUiElements = new Map<string, ResolvedUiElementContext>()
|
|
53
67
|
|
|
54
68
|
private static _normalize(s: any): string {
|
|
@@ -240,6 +254,78 @@ export class ToolsInteract {
|
|
|
240
254
|
return best
|
|
241
255
|
}
|
|
242
256
|
|
|
257
|
+
private static _resolveNearbyActionableControl(
|
|
258
|
+
elements: UiElement[],
|
|
259
|
+
chosen: { el: UiElement, idx: number } | null,
|
|
260
|
+
screen?: UiResolution | null
|
|
261
|
+
): { el: UiElement, idx: number, sliderLike?: boolean } | null {
|
|
262
|
+
if (!chosen) return null
|
|
263
|
+
|
|
264
|
+
const labelBounds = ToolsInteract._normalizeBounds(chosen.el.bounds)
|
|
265
|
+
if (!labelBounds) return null
|
|
266
|
+
|
|
267
|
+
const [labelLeft, labelTop, labelRight, labelBottom] = labelBounds
|
|
268
|
+
const labelWidth = labelRight - labelLeft
|
|
269
|
+
const labelHeight = labelBottom - labelTop
|
|
270
|
+
const screenWidth = Number(screen?.width) > 0 ? Number(screen?.width) : 0
|
|
271
|
+
const screenHeight = Number(screen?.height) > 0 ? Number(screen?.height) : 0
|
|
272
|
+
const minTrackLengthPx = Math.max(
|
|
273
|
+
ToolsInteract._sliderTrackMinLengthPx,
|
|
274
|
+
screenWidth > 0 ? Math.floor(screenWidth * ToolsInteract._sliderTrackLengthRatio) : 0,
|
|
275
|
+
screenHeight > 0 ? Math.floor(screenHeight * ToolsInteract._sliderTrackLengthRatio) : 0
|
|
276
|
+
)
|
|
277
|
+
const maxTrackThicknessPx = Math.max(
|
|
278
|
+
ToolsInteract._sliderTrackMaxThicknessPx,
|
|
279
|
+
screenWidth > 0 ? Math.floor(screenWidth * ToolsInteract._sliderTrackThicknessRatio) : 0,
|
|
280
|
+
screenHeight > 0 ? Math.floor(screenHeight * ToolsInteract._sliderTrackThicknessRatio) : 0
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
let best: { el: UiElement, idx: number, sliderLike?: boolean } | null = null
|
|
284
|
+
let bestScore = Infinity
|
|
285
|
+
|
|
286
|
+
for (let i = chosen.idx + 1; i < Math.min(elements.length, chosen.idx + ToolsInteract._sliderSearchLookahead); i++) {
|
|
287
|
+
const candidate = elements[i]
|
|
288
|
+
if (!candidate || !(candidate.clickable || candidate.focusable) || candidate.visible === false) continue
|
|
289
|
+
|
|
290
|
+
const candidateBounds = ToolsInteract._normalizeBounds(candidate.bounds)
|
|
291
|
+
if (!candidateBounds) continue
|
|
292
|
+
|
|
293
|
+
const [left, top, right] = candidateBounds
|
|
294
|
+
const width = right - left
|
|
295
|
+
const height = candidateBounds[3] - top
|
|
296
|
+
const verticalGap = top - labelBottom
|
|
297
|
+
if (verticalGap < -ToolsInteract._sliderNegativeGapTolerancePx || verticalGap > ToolsInteract._sliderPositiveGapLimitPx) continue
|
|
298
|
+
|
|
299
|
+
const horizontalOverlap = Math.min(labelRight, right) - Math.max(labelLeft, left)
|
|
300
|
+
if (horizontalOverlap < -ToolsInteract._sliderNegativeGapTolerancePx) continue
|
|
301
|
+
|
|
302
|
+
const candidateText = ToolsInteract._normalize(candidate.text ?? candidate.label ?? candidate.value ?? '')
|
|
303
|
+
const candidateContent = ToolsInteract._normalize(candidate.contentDescription ?? candidate.contentDesc ?? candidate.accessibilityLabel ?? '')
|
|
304
|
+
const candidateClass = ToolsInteract._normalize(candidate.type ?? candidate.class ?? '')
|
|
305
|
+
|
|
306
|
+
let score = verticalGap
|
|
307
|
+
const horizontalTrackLike =
|
|
308
|
+
width >= Math.max(minTrackLengthPx, Math.floor(labelWidth * ToolsInteract._sliderLabelWidthRatio)) &&
|
|
309
|
+
height <= maxTrackThicknessPx
|
|
310
|
+
const verticalTrackLike =
|
|
311
|
+
height >= Math.max(minTrackLengthPx, Math.floor(labelHeight * ToolsInteract._sliderLabelWidthRatio)) &&
|
|
312
|
+
width <= maxTrackThicknessPx
|
|
313
|
+
const trackLike = /slider|seek|range/i.test(candidateClass) || horizontalTrackLike || verticalTrackLike
|
|
314
|
+
if (!candidateText && !candidateContent) score -= 18
|
|
315
|
+
if (trackLike) score -= 30
|
|
316
|
+
if (/view|layout|group|frame/i.test(candidateClass)) score -= 10
|
|
317
|
+
if (width > labelWidth * ToolsInteract._sliderLabelWidthRatio) score -= 8
|
|
318
|
+
if (candidateText || candidateContent) score += 20
|
|
319
|
+
|
|
320
|
+
if (score < bestScore) {
|
|
321
|
+
bestScore = score
|
|
322
|
+
best = { el: candidate, idx: i, sliderLike: trackLike }
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return best
|
|
327
|
+
}
|
|
328
|
+
|
|
243
329
|
|
|
244
330
|
private static async getInteractionService(platform?: 'android' | 'ios', deviceId?: string) {
|
|
245
331
|
const effectivePlatform = platform || 'android'
|
|
@@ -347,6 +433,7 @@ export class ToolsInteract {
|
|
|
347
433
|
|
|
348
434
|
let best: UiElement | null = null
|
|
349
435
|
let bestScore = 0
|
|
436
|
+
let lastTree: any = null
|
|
350
437
|
|
|
351
438
|
const scoreElement = (el: UiElement | null) => {
|
|
352
439
|
if (!el || !el.visible) return 0
|
|
@@ -380,7 +467,8 @@ export class ToolsInteract {
|
|
|
380
467
|
|
|
381
468
|
while (Date.now() <= deadline) {
|
|
382
469
|
try {
|
|
383
|
-
|
|
470
|
+
const tree = await ToolsObserve.getUITreeHandler({ platform, deviceId })
|
|
471
|
+
lastTree = tree
|
|
384
472
|
if (tree && Array.isArray((tree as any).elements)) {
|
|
385
473
|
const elements = ((tree as any).elements as UiElement[])
|
|
386
474
|
for (let i = 0; i < elements.length; i++) {
|
|
@@ -407,8 +495,8 @@ export class ToolsInteract {
|
|
|
407
495
|
|
|
408
496
|
// If the best match is not interactable, try to resolve an actionable ancestor.
|
|
409
497
|
try {
|
|
410
|
-
const
|
|
411
|
-
const
|
|
498
|
+
const elements = (lastTree && Array.isArray(lastTree.elements)) ? (lastTree.elements as UiElement[]) : []
|
|
499
|
+
const screen = lastTree?.resolution && typeof lastTree.resolution === 'object' ? lastTree.resolution as UiResolution : null
|
|
412
500
|
let chosen = best as any
|
|
413
501
|
const childBounds = Array.isArray(chosen?.bounds) ? chosen.bounds : null
|
|
414
502
|
|
|
@@ -465,6 +553,16 @@ export class ToolsInteract {
|
|
|
465
553
|
// small score bump to reflect actionability
|
|
466
554
|
bestScore = Math.min(1, bestScore + 0.02)
|
|
467
555
|
}
|
|
556
|
+
|
|
557
|
+
if (best && !(best.clickable || best.focusable)) {
|
|
558
|
+
const nearbyActionable = ToolsInteract._resolveNearbyActionableControl(elements, { el: best, idx: best._index ?? elements.indexOf(best) }, screen)
|
|
559
|
+
if (nearbyActionable) {
|
|
560
|
+
best = nearbyActionable.el
|
|
561
|
+
best._index = nearbyActionable.idx
|
|
562
|
+
best._interactable = true
|
|
563
|
+
best._sliderLike = nearbyActionable.sliderLike
|
|
564
|
+
}
|
|
565
|
+
}
|
|
468
566
|
} catch (e) { console.error('Error resolving ancestor:', e) }
|
|
469
567
|
|
|
470
568
|
if (!best) return { found: false, error: 'Element not found' }
|
|
@@ -483,8 +581,18 @@ export class ToolsInteract {
|
|
|
483
581
|
tapCoordinates,
|
|
484
582
|
telemetry: {
|
|
485
583
|
matchedIndex: best?._index ?? null,
|
|
486
|
-
matchedInteractable: !!best?._interactable
|
|
584
|
+
matchedInteractable: !!best?._interactable,
|
|
585
|
+
sliderLike: !!best?._sliderLike
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
if (best?._sliderLike) {
|
|
589
|
+
const isVertical = !!boundsObj && (boundsObj.bottom - boundsObj.top) > (boundsObj.right - boundsObj.left)
|
|
590
|
+
const interactionHint = {
|
|
591
|
+
kind: 'slider',
|
|
592
|
+
axis: isVertical ? 'vertical' : 'horizontal',
|
|
593
|
+
trackBounds: boundsObj
|
|
487
594
|
}
|
|
595
|
+
;(outEl as any).interactionHint = interactionHint
|
|
488
596
|
}
|
|
489
597
|
const scoreVal = Math.min(1, Number(bestScore.toFixed(3)))
|
|
490
598
|
return { found: true, element: outEl, score: scoreVal, confidence: scoreVal }
|
package/src/server-core.ts
CHANGED
|
@@ -75,14 +75,73 @@ async function run() {
|
|
|
75
75
|
assert.ok(pass4, 'Child text should resolve to a clickable parent ancestor')
|
|
76
76
|
process.stdout.write('Test 4: ' + (pass4 ? 'PASS' : 'FAIL') + '\n');
|
|
77
77
|
|
|
78
|
-
// Test 5:
|
|
79
|
-
;(ToolsObserve as any).getUITreeHandler = async () => ({
|
|
80
|
-
|
|
78
|
+
// Test 5: duration label should resolve to the nearby slider control
|
|
79
|
+
;(ToolsObserve as any).getUITreeHandler = async () => ({
|
|
80
|
+
device: { platform: 'android', id: 'mock' },
|
|
81
|
+
screen: '',
|
|
82
|
+
resolution: { width: 1080, height: 1920 },
|
|
83
|
+
elements: [
|
|
84
|
+
{ text: 'Duration: 5 min', type: 'android.widget.TextView', contentDescription: null, clickable: false, enabled: true, visible: true, bounds: [10,10,260,50], resourceId: null },
|
|
85
|
+
{ text: null, type: 'android.view.View', contentDescription: null, clickable: true, enabled: true, visible: true, bounds: [10,60,1040,140], resourceId: null }
|
|
86
|
+
]
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
const res5: any = await ToolsInteract.findElementHandler({ query: 'duration', exact: false, platform: 'android', timeoutMs: 300 })
|
|
81
90
|
process.stdout.write('res5 ' + JSON.stringify(res5, null, 2) + '\n');
|
|
82
|
-
const pass5 = res5.found ===
|
|
83
|
-
assert.ok(pass5, '
|
|
91
|
+
const pass5 = res5.found === true && res5.element && res5.element.clickable === true && res5.element.bounds && res5.element.bounds.top === 60 && res5.element.bounds.bottom === 140
|
|
92
|
+
assert.ok(pass5, 'Duration label should resolve to the slider control below it')
|
|
84
93
|
process.stdout.write('Test 5: ' + (pass5 ? 'PASS' : 'FAIL') + '\n');
|
|
85
94
|
|
|
95
|
+
// Test 6: prefer track-like control over a closer texty sibling
|
|
96
|
+
;(ToolsObserve as any).getUITreeHandler = async () => ({
|
|
97
|
+
device: { platform: 'android', id: 'mock' },
|
|
98
|
+
screen: '',
|
|
99
|
+
resolution: { width: 1080, height: 1920 },
|
|
100
|
+
elements: [
|
|
101
|
+
{ text: 'Duration: 5 min', type: 'android.widget.TextView', contentDescription: null, clickable: false, enabled: true, visible: true, bounds: [10,10,260,50], resourceId: null },
|
|
102
|
+
{ text: 'Reset', type: 'android.widget.Button', contentDescription: null, clickable: true, enabled: true, visible: true, bounds: [10,60,150,120], resourceId: 'btn_reset' },
|
|
103
|
+
{ text: null, type: 'android.view.View', contentDescription: null, clickable: true, enabled: true, visible: true, bounds: [10,130,1040,210], resourceId: null }
|
|
104
|
+
]
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
const res6: any = await ToolsInteract.findElementHandler({ query: 'duration', exact: false, platform: 'android', timeoutMs: 300 })
|
|
108
|
+
process.stdout.write('res6 ' + JSON.stringify(res6, null, 2) + '\n');
|
|
109
|
+
const pass6 = res6.found === true && res6.element && res6.element.clickable === true && res6.element.bounds && res6.element.bounds.top === 130 && res6.element.bounds.bottom === 210
|
|
110
|
+
assert.ok(pass6, 'Duration lookup should prefer the track-like control over a closer text button')
|
|
111
|
+
process.stdout.write('Test 6: ' + (pass6 ? 'PASS' : 'FAIL') + '\n');
|
|
112
|
+
const pass6b = res6.element && res6.element.telemetry && res6.element.telemetry.sliderLike === true && res6.element.interactionHint && res6.element.interactionHint.kind === 'slider'
|
|
113
|
+
assert.ok(pass6b, 'Duration lookup should include slider-specific telemetry')
|
|
114
|
+
process.stdout.write('Test 6b: ' + (pass6b ? 'PASS' : 'FAIL') + '\n');
|
|
115
|
+
|
|
116
|
+
// Test 7: prefer vertical track-like control over a closer text button
|
|
117
|
+
;(ToolsObserve as any).getUITreeHandler = async () => ({
|
|
118
|
+
device: { platform: 'android', id: 'mock' },
|
|
119
|
+
screen: '',
|
|
120
|
+
resolution: { width: 1080, height: 2400 },
|
|
121
|
+
elements: [
|
|
122
|
+
{ text: 'Duration: 5 min', type: 'android.widget.TextView', contentDescription: null, clickable: false, enabled: true, visible: true, bounds: [10,10,260,50], resourceId: null },
|
|
123
|
+
{ text: 'Reset', type: 'android.widget.Button', contentDescription: null, clickable: true, enabled: true, visible: true, bounds: [10,60,150,120], resourceId: 'btn_reset' },
|
|
124
|
+
{ text: null, type: 'android.view.View', contentDescription: null, clickable: true, enabled: true, visible: true, bounds: [270,20,350,1040], resourceId: null }
|
|
125
|
+
]
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
const res7: any = await ToolsInteract.findElementHandler({ query: 'duration', exact: false, platform: 'android', timeoutMs: 300 })
|
|
129
|
+
process.stdout.write('res7 ' + JSON.stringify(res7, null, 2) + '\n');
|
|
130
|
+
const pass7 = res7.found === true && res7.element && res7.element.clickable === true && res7.element.bounds && res7.element.bounds.left === 270 && res7.element.bounds.right === 350
|
|
131
|
+
assert.ok(pass7, 'Duration lookup should prefer a vertical track-like control')
|
|
132
|
+
process.stdout.write('Test 7: ' + (pass7 ? 'PASS' : 'FAIL') + '\n');
|
|
133
|
+
const pass7b = res7.element && res7.element.interactionHint && res7.element.interactionHint.axis === 'vertical'
|
|
134
|
+
assert.ok(pass7b, 'Vertical sliders should report a vertical interaction axis')
|
|
135
|
+
process.stdout.write('Test 7b: ' + (pass7b ? 'PASS' : 'FAIL') + '\n');
|
|
136
|
+
|
|
137
|
+
// Test 8: not found
|
|
138
|
+
;(ToolsObserve as any).getUITreeHandler = async () => ({ device: { platform: 'android', id: 'mock' }, screen: '', resolution: { width: 1080, height: 1920 }, elements: [] })
|
|
139
|
+
const res8: any = await ToolsInteract.findElementHandler({ query: 'nope', exact: false, platform: 'android', timeoutMs: 300 })
|
|
140
|
+
process.stdout.write('res8 ' + JSON.stringify(res8, null, 2) + '\n');
|
|
141
|
+
const pass8 = res8.found === false
|
|
142
|
+
assert.ok(pass8, 'Missing elements should return found=false')
|
|
143
|
+
process.stdout.write('Test 8: ' + (pass8 ? 'PASS' : 'FAIL') + '\n');
|
|
144
|
+
|
|
86
145
|
} finally {
|
|
87
146
|
;(ToolsObserve as any).getUITreeHandler = origGetTree
|
|
88
147
|
}
|