mobile-debug-mcp 0.26.3 → 0.26.4
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 +456 -0
- package/dist/observe/ios.js +1 -1
- package/dist/server/tool-definitions.js +56 -0
- package/dist/server/tool-handlers.js +25 -0
- package/dist/server-core.js +1 -1
- package/dist/utils/android/utils.js +2 -2
- package/docs/CHANGELOG.md +3 -0
- package/docs/ROADMAP.md +64 -9
- package/docs/rfcs/008-adjustable-control-support-and-semantic-value-manipulation.md +273 -0
- package/docs/specs/mcp-tooling-spec-v1.md +1 -1
- package/docs/tools/interact.md +21 -0
- package/package.json +1 -1
- package/src/interact/index.ts +585 -0
- package/src/observe/ios.ts +1 -1
- package/src/server/tool-definitions.ts +56 -0
- package/src/server/tool-handlers.ts +26 -0
- package/src/server-core.ts +1 -1
- package/src/types.ts +17 -0
- package/src/utils/android/utils.ts +2 -2
- package/test/unit/interact/adjust_control.test.ts +365 -0
- package/test/unit/observe/state_extraction.test.ts +24 -0
- package/test/unit/server/contract.test.ts +8 -0
- package/test/unit/server/response_shapes.test.ts +39 -0
|
@@ -277,6 +277,31 @@ async function handleExpectState(args: ToolCallArgs) {
|
|
|
277
277
|
return wrapResponse(res)
|
|
278
278
|
}
|
|
279
279
|
|
|
280
|
+
async function handleAdjustControl(args: ToolCallArgs) {
|
|
281
|
+
const selector = getObjectArg<ExpectElementSelectorArg>(args, 'selector')
|
|
282
|
+
const element_id = getStringArg(args, 'element_id')
|
|
283
|
+
const property = getStringArg(args, 'property') ?? 'value'
|
|
284
|
+
const targetValue = requireNumberArg(args, 'targetValue')
|
|
285
|
+
const tolerance = getNumberArg(args, 'tolerance') ?? 0
|
|
286
|
+
const maxAttempts = getNumberArg(args, 'maxAttempts') ?? 3
|
|
287
|
+
const platform = getStringArg(args, 'platform') as PlatformArg | undefined
|
|
288
|
+
const deviceId = getStringArg(args, 'deviceId')
|
|
289
|
+
if (!selector && !element_id) {
|
|
290
|
+
throw new Error('Missing selector or element_id argument')
|
|
291
|
+
}
|
|
292
|
+
const res = await ToolsInteract.adjustControlHandler({
|
|
293
|
+
selector: selector ?? undefined,
|
|
294
|
+
element_id: element_id ?? undefined,
|
|
295
|
+
property,
|
|
296
|
+
targetValue,
|
|
297
|
+
tolerance,
|
|
298
|
+
maxAttempts,
|
|
299
|
+
platform,
|
|
300
|
+
deviceId
|
|
301
|
+
})
|
|
302
|
+
return wrapResponse(res)
|
|
303
|
+
}
|
|
304
|
+
|
|
280
305
|
async function handleWaitForUI(args: ToolCallArgs) {
|
|
281
306
|
const selector = getObjectArg<ExpectElementSelectorArg>(args, 'selector')
|
|
282
307
|
const condition = (getStringArg(args, 'condition') as 'exists' | 'not_exists' | 'visible' | 'clickable' | undefined) ?? 'exists'
|
|
@@ -496,6 +521,7 @@ export const toolHandlers: Record<string, ToolHandler> = {
|
|
|
496
521
|
expect_screen: handleExpectScreen,
|
|
497
522
|
expect_element_visible: handleExpectElementVisible,
|
|
498
523
|
expect_state: handleExpectState,
|
|
524
|
+
adjust_control: handleAdjustControl,
|
|
499
525
|
wait_for_ui: handleWaitForUI,
|
|
500
526
|
find_element: handleFindElement,
|
|
501
527
|
tap: handleTap,
|
package/src/server-core.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -413,6 +413,23 @@ export interface ExpectStateResponse {
|
|
|
413
413
|
retryable?: boolean;
|
|
414
414
|
}
|
|
415
415
|
|
|
416
|
+
export interface AdjustControlResponse extends ActionExecutionResult {
|
|
417
|
+
target_state: {
|
|
418
|
+
property: string;
|
|
419
|
+
target_value: number;
|
|
420
|
+
tolerance: number;
|
|
421
|
+
};
|
|
422
|
+
actual_state: {
|
|
423
|
+
property: string;
|
|
424
|
+
value: number | null;
|
|
425
|
+
raw_value?: number | null;
|
|
426
|
+
} | null;
|
|
427
|
+
within_tolerance: boolean;
|
|
428
|
+
converged: boolean;
|
|
429
|
+
attempts: number;
|
|
430
|
+
adjustment_mode: 'semantic' | 'gesture' | 'coordinate';
|
|
431
|
+
}
|
|
432
|
+
|
|
416
433
|
export interface WaitForUIChangeResponse {
|
|
417
434
|
success: boolean;
|
|
418
435
|
observed_change: 'hierarchy_diff' | 'text_change' | 'state_change' | null;
|
|
@@ -341,7 +341,7 @@ function normalizeClassName(value: unknown): string {
|
|
|
341
341
|
}
|
|
342
342
|
|
|
343
343
|
function inferAndroidRole(className: string): string | null {
|
|
344
|
-
if (/seekbar|slider
|
|
344
|
+
if (/seekbar|slider/.test(className)) return 'slider'
|
|
345
345
|
if (/switch|toggle/.test(className)) return 'switch'
|
|
346
346
|
if (/checkbox/.test(className)) return 'checkbox'
|
|
347
347
|
if (/radiobutton|radio/.test(className)) return 'radio'
|
|
@@ -384,7 +384,7 @@ function buildAndroidSemantic(clickable: boolean, className: string): UIElementS
|
|
|
384
384
|
|
|
385
385
|
function isSliderLikeAndroid(node: any): boolean {
|
|
386
386
|
const className = String(node['@_class'] || '').toLowerCase()
|
|
387
|
-
return /seekbar|slider|range
|
|
387
|
+
return /seekbar|slider|range/i.test(className)
|
|
388
388
|
}
|
|
389
389
|
|
|
390
390
|
function extractAndroidState(node: any): UIElementState | null {
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import { ToolsInteract } from '../../../src/interact/index.js'
|
|
3
|
+
import * as Observe from '../../../src/observe/index.js'
|
|
4
|
+
|
|
5
|
+
async function run() {
|
|
6
|
+
console.log('Starting adjust_control unit tests...')
|
|
7
|
+
|
|
8
|
+
const originalGetUITreeHandler = (Observe as any).ToolsObserve.getUITreeHandler
|
|
9
|
+
const originalGetScreenFingerprintHandler = (Observe as any).ToolsObserve.getScreenFingerprintHandler
|
|
10
|
+
const originalTapHandler = (ToolsInteract as any).tapHandler
|
|
11
|
+
const originalSwipeHandler = (ToolsInteract as any).swipeHandler
|
|
12
|
+
const originalExpectStateHandler = (ToolsInteract as any).expectStateHandler
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
;(Observe as any).ToolsObserve.getUITreeHandler = async () => ({
|
|
16
|
+
device: { platform: 'android', id: 'mock-device', osVersion: '14', model: 'Pixel', simulator: true },
|
|
17
|
+
screen: '',
|
|
18
|
+
resolution: { width: 1080, height: 2400 },
|
|
19
|
+
elements: [
|
|
20
|
+
{
|
|
21
|
+
text: 'Duration',
|
|
22
|
+
type: 'android.widget.SeekBar',
|
|
23
|
+
contentDescription: null,
|
|
24
|
+
clickable: true,
|
|
25
|
+
enabled: true,
|
|
26
|
+
visible: true,
|
|
27
|
+
bounds: [0, 0, 200, 40],
|
|
28
|
+
resourceId: 'seek_duration',
|
|
29
|
+
state: {
|
|
30
|
+
value: 10,
|
|
31
|
+
raw_value: 10,
|
|
32
|
+
value_range: { min: 0, max: 100 }
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
;(Observe as any).ToolsObserve.getScreenFingerprintHandler = async () => ({ fingerprint: 'fp_slider', activity: 'MainActivity' })
|
|
39
|
+
|
|
40
|
+
const wait = await ToolsInteract.waitForUIHandler({
|
|
41
|
+
selector: { text: 'Duration' },
|
|
42
|
+
condition: 'clickable',
|
|
43
|
+
timeout_ms: 200,
|
|
44
|
+
poll_interval_ms: 50,
|
|
45
|
+
platform: 'android'
|
|
46
|
+
})
|
|
47
|
+
assert.strictEqual(wait.status, 'success')
|
|
48
|
+
assert.ok(wait.element?.elementId)
|
|
49
|
+
|
|
50
|
+
const tapCalls: Array<{ platform?: string, x: number, y: number, deviceId?: string }> = []
|
|
51
|
+
const swipeCalls: Array<{ platform?: string, x1: number, y1: number, x2: number, y2: number, duration: number, deviceId?: string }> = []
|
|
52
|
+
;(ToolsInteract as any).tapHandler = async ({ platform, x, y, deviceId }: any) => {
|
|
53
|
+
tapCalls.push({ platform, x, y, deviceId })
|
|
54
|
+
return {
|
|
55
|
+
device: { platform: platform || 'android', id: deviceId || 'mock-device', osVersion: '14', model: 'Pixel', simulator: true },
|
|
56
|
+
success: true,
|
|
57
|
+
x,
|
|
58
|
+
y
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
;(ToolsInteract as any).swipeHandler = async ({ platform, x1, y1, x2, y2, duration, deviceId }: any) => {
|
|
62
|
+
swipeCalls.push({ platform, x1, y1, x2, y2, duration, deviceId })
|
|
63
|
+
return {
|
|
64
|
+
device: { platform: platform || 'android', id: deviceId || 'mock-device', osVersion: '14', model: 'Pixel', simulator: true },
|
|
65
|
+
success: true,
|
|
66
|
+
start: [x1, y1],
|
|
67
|
+
end: [x2, y2],
|
|
68
|
+
duration
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
;(ToolsInteract as any).expectStateHandler = async () => ({
|
|
73
|
+
success: true,
|
|
74
|
+
selector: { text: 'Duration' },
|
|
75
|
+
element_id: wait.element.elementId,
|
|
76
|
+
expected_state: { property: 'value', expected: 30 },
|
|
77
|
+
element: {
|
|
78
|
+
elementId: wait.element.elementId,
|
|
79
|
+
text: 'Duration',
|
|
80
|
+
resource_id: 'seek_duration',
|
|
81
|
+
accessibility_id: null,
|
|
82
|
+
class: 'android.widget.SeekBar',
|
|
83
|
+
bounds: [0, 0, 200, 40],
|
|
84
|
+
index: 0,
|
|
85
|
+
state: { value: 30, raw_value: 30, value_range: { min: 0, max: 100 } }
|
|
86
|
+
},
|
|
87
|
+
observed_state: { property: 'value', value: 30, raw_value: 30 },
|
|
88
|
+
reason: 'value matches expected value'
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
const adjust = await ToolsInteract.adjustControlHandler({
|
|
92
|
+
element_id: wait.element.elementId,
|
|
93
|
+
property: 'value',
|
|
94
|
+
targetValue: 30,
|
|
95
|
+
tolerance: 0.5,
|
|
96
|
+
maxAttempts: 2,
|
|
97
|
+
platform: 'android'
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
assert.strictEqual(adjust.success, true)
|
|
101
|
+
assert.strictEqual(adjust.converged, true)
|
|
102
|
+
assert.strictEqual(adjust.within_tolerance, true)
|
|
103
|
+
assert.strictEqual(adjust.adjustment_mode, 'coordinate')
|
|
104
|
+
assert.strictEqual(adjust.target_state.target_value, 30)
|
|
105
|
+
assert.strictEqual(adjust.attempts, 1)
|
|
106
|
+
assert.strictEqual(tapCalls.length, 1)
|
|
107
|
+
assert.strictEqual(swipeCalls.length, 0)
|
|
108
|
+
assert.ok(tapCalls[0].x <= 66, 'tap should bias inward from the exact target point')
|
|
109
|
+
assert.strictEqual(adjust.action_type, 'adjust_control')
|
|
110
|
+
assert.strictEqual(adjust.target.selector.elementId, wait.element.elementId)
|
|
111
|
+
|
|
112
|
+
;(ToolsInteract as any).expectStateHandler = async () => ({
|
|
113
|
+
success: true,
|
|
114
|
+
selector: { text: 'Duration' },
|
|
115
|
+
element_id: wait.element.elementId,
|
|
116
|
+
expected_state: { property: 'value', expected: 2 },
|
|
117
|
+
element: {
|
|
118
|
+
elementId: wait.element.elementId,
|
|
119
|
+
text: 'Duration',
|
|
120
|
+
resource_id: 'seek_duration',
|
|
121
|
+
accessibility_id: null,
|
|
122
|
+
class: 'android.widget.SeekBar',
|
|
123
|
+
bounds: [0, 0, 200, 40],
|
|
124
|
+
index: 0,
|
|
125
|
+
state: { value: 2, raw_value: 2, value_range: { min: 0, max: 100 } }
|
|
126
|
+
},
|
|
127
|
+
observed_state: { property: 'value', value: 2, raw_value: 2 },
|
|
128
|
+
reason: 'value matches expected value'
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
const lowEndAdjust = await ToolsInteract.adjustControlHandler({
|
|
132
|
+
element_id: wait.element.elementId,
|
|
133
|
+
property: 'value',
|
|
134
|
+
targetValue: 2,
|
|
135
|
+
tolerance: 0.5,
|
|
136
|
+
maxAttempts: 2,
|
|
137
|
+
platform: 'android'
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
assert.strictEqual(lowEndAdjust.success, true)
|
|
141
|
+
assert.strictEqual(lowEndAdjust.converged, true)
|
|
142
|
+
assert.strictEqual(lowEndAdjust.within_tolerance, true)
|
|
143
|
+
assert.strictEqual(lowEndAdjust.attempts, 1)
|
|
144
|
+
assert.strictEqual(tapCalls.length, 2)
|
|
145
|
+
assert.strictEqual(swipeCalls.length, 0)
|
|
146
|
+
assert.ok(tapCalls[1].x >= 22, 'low-end tap should stay inside the first step instead of hugging the edge')
|
|
147
|
+
|
|
148
|
+
;(Observe as any).ToolsObserve.getUITreeHandler = async () => ({
|
|
149
|
+
device: { platform: 'android', id: 'mock-device', osVersion: '14', model: 'Pixel', simulator: true },
|
|
150
|
+
screen: '',
|
|
151
|
+
resolution: { width: 1080, height: 2400 },
|
|
152
|
+
elements: [
|
|
153
|
+
{
|
|
154
|
+
text: 'Duration',
|
|
155
|
+
type: 'android.widget.SeekBar',
|
|
156
|
+
contentDescription: null,
|
|
157
|
+
clickable: true,
|
|
158
|
+
enabled: true,
|
|
159
|
+
visible: true,
|
|
160
|
+
bounds: [0, 0, 200, 40],
|
|
161
|
+
resourceId: 'seek_duration',
|
|
162
|
+
state: {
|
|
163
|
+
value: 18,
|
|
164
|
+
raw_value: 18,
|
|
165
|
+
value_range: { min: 0, max: 20 }
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
]
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
;(ToolsInteract as any).expectStateHandler = async () => ({
|
|
172
|
+
success: true,
|
|
173
|
+
selector: { text: 'Duration' },
|
|
174
|
+
element_id: wait.element.elementId,
|
|
175
|
+
expected_state: { property: 'value', expected: 20 },
|
|
176
|
+
element: {
|
|
177
|
+
elementId: wait.element.elementId,
|
|
178
|
+
text: 'Duration',
|
|
179
|
+
resource_id: 'seek_duration',
|
|
180
|
+
accessibility_id: null,
|
|
181
|
+
class: 'android.widget.SeekBar',
|
|
182
|
+
bounds: [0, 0, 200, 40],
|
|
183
|
+
index: 0,
|
|
184
|
+
state: { value: 20, raw_value: 20, value_range: { min: 0, max: 20 } }
|
|
185
|
+
},
|
|
186
|
+
observed_state: { property: 'value', value: 20, raw_value: 20 },
|
|
187
|
+
reason: 'value matches expected value'
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
const highEndAdjust = await ToolsInteract.adjustControlHandler({
|
|
191
|
+
element_id: wait.element.elementId,
|
|
192
|
+
property: 'value',
|
|
193
|
+
targetValue: 20,
|
|
194
|
+
tolerance: 0.5,
|
|
195
|
+
maxAttempts: 2,
|
|
196
|
+
platform: 'android'
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
assert.strictEqual(highEndAdjust.success, true)
|
|
200
|
+
assert.strictEqual(highEndAdjust.converged, true)
|
|
201
|
+
assert.strictEqual(highEndAdjust.within_tolerance, true)
|
|
202
|
+
assert.strictEqual(highEndAdjust.attempts, 1)
|
|
203
|
+
assert.strictEqual(tapCalls.length, 3)
|
|
204
|
+
assert.strictEqual(swipeCalls.length, 0)
|
|
205
|
+
assert.ok(tapCalls[2].x >= 180, 'high-end tap should bias into the last step without hitting the edge')
|
|
206
|
+
|
|
207
|
+
;(Observe as any).ToolsObserve.getUITreeHandler = async () => ({
|
|
208
|
+
device: { platform: 'android', id: 'mock-device', osVersion: '14', model: 'Pixel', simulator: true },
|
|
209
|
+
screen: '',
|
|
210
|
+
resolution: { width: 1440, height: 3200 },
|
|
211
|
+
elements: [
|
|
212
|
+
{
|
|
213
|
+
text: 'Precision',
|
|
214
|
+
type: 'android.widget.SeekBar',
|
|
215
|
+
contentDescription: null,
|
|
216
|
+
clickable: true,
|
|
217
|
+
enabled: true,
|
|
218
|
+
visible: true,
|
|
219
|
+
bounds: [0, 0, 3000, 40],
|
|
220
|
+
resourceId: 'seek_precision',
|
|
221
|
+
state: {
|
|
222
|
+
value: 9000,
|
|
223
|
+
raw_value: 9000,
|
|
224
|
+
value_range: { min: 0, max: 10000 }
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
]
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
;(ToolsInteract as any).expectStateHandler = async () => ({
|
|
231
|
+
success: true,
|
|
232
|
+
selector: { text: 'Precision' },
|
|
233
|
+
element_id: wait.element.elementId,
|
|
234
|
+
expected_state: { property: 'value', expected: 9999 },
|
|
235
|
+
element: {
|
|
236
|
+
elementId: wait.element.elementId,
|
|
237
|
+
text: 'Precision',
|
|
238
|
+
resource_id: 'seek_precision',
|
|
239
|
+
accessibility_id: null,
|
|
240
|
+
class: 'android.widget.SeekBar',
|
|
241
|
+
bounds: [0, 0, 3000, 40],
|
|
242
|
+
index: 0,
|
|
243
|
+
state: { value: 9999, raw_value: 9999, value_range: { min: 0, max: 10000 } }
|
|
244
|
+
},
|
|
245
|
+
observed_state: { property: 'value', value: 9999, raw_value: 9999 },
|
|
246
|
+
reason: 'value matches expected value'
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
const precisionAdjust = await ToolsInteract.adjustControlHandler({
|
|
250
|
+
selector: { text: 'Precision' },
|
|
251
|
+
property: 'value',
|
|
252
|
+
targetValue: 9999,
|
|
253
|
+
tolerance: 0.5,
|
|
254
|
+
maxAttempts: 2,
|
|
255
|
+
platform: 'android'
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
assert.strictEqual(precisionAdjust.success, true)
|
|
259
|
+
assert.strictEqual(precisionAdjust.converged, true)
|
|
260
|
+
assert.strictEqual(precisionAdjust.within_tolerance, true)
|
|
261
|
+
assert.strictEqual(precisionAdjust.attempts, 1)
|
|
262
|
+
assert.strictEqual(tapCalls.length, 4)
|
|
263
|
+
assert.strictEqual(swipeCalls.length, 0)
|
|
264
|
+
assert.ok(tapCalls[3].x > 2750, 'wide, high-range control should not be clamped to a 3% endpoint margin')
|
|
265
|
+
|
|
266
|
+
let treeFetches = 0
|
|
267
|
+
let retryVerificationCount = 0
|
|
268
|
+
;(Observe as any).ToolsObserve.getUITreeHandler = async () => {
|
|
269
|
+
treeFetches++
|
|
270
|
+
return {
|
|
271
|
+
device: { platform: 'android', id: 'mock-device', osVersion: '14', model: 'Pixel', simulator: true },
|
|
272
|
+
screen: '',
|
|
273
|
+
resolution: { width: 1080, height: 2400 },
|
|
274
|
+
elements: [
|
|
275
|
+
{
|
|
276
|
+
text: 'Duration',
|
|
277
|
+
type: 'android.widget.SeekBar',
|
|
278
|
+
contentDescription: null,
|
|
279
|
+
clickable: true,
|
|
280
|
+
enabled: true,
|
|
281
|
+
visible: true,
|
|
282
|
+
bounds: [0, 0, 200, 40],
|
|
283
|
+
resourceId: 'seek_duration',
|
|
284
|
+
state: {
|
|
285
|
+
value: 10,
|
|
286
|
+
raw_value: 10,
|
|
287
|
+
value_range: { min: 0, max: 20 }
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
]
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
;(ToolsInteract as any).tapHandler = async ({ platform, x, y, deviceId }: any) => {
|
|
295
|
+
tapCalls.push({ platform, x, y, deviceId })
|
|
296
|
+
return {
|
|
297
|
+
device: { platform: platform || 'android', id: deviceId || 'mock-device', osVersion: '14', model: 'Pixel', simulator: true },
|
|
298
|
+
success: false,
|
|
299
|
+
error: 'tap failed'
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
;(ToolsInteract as any).swipeHandler = async ({ platform, x1, y1, x2, y2, duration, deviceId }: any) => {
|
|
304
|
+
swipeCalls.push({ platform, x1, y1, x2, y2, duration, deviceId })
|
|
305
|
+
return {
|
|
306
|
+
device: { platform: platform || 'android', id: deviceId || 'mock-device', osVersion: '14', model: 'Pixel', simulator: true },
|
|
307
|
+
success: true,
|
|
308
|
+
start: [x1, y1],
|
|
309
|
+
end: [x2, y2],
|
|
310
|
+
duration
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
;(ToolsInteract as any).expectStateHandler = async () => {
|
|
315
|
+
retryVerificationCount++
|
|
316
|
+
const value = retryVerificationCount === 1 ? 11 : 12
|
|
317
|
+
return {
|
|
318
|
+
success: true,
|
|
319
|
+
selector: { text: 'Duration' },
|
|
320
|
+
element_id: wait.element.elementId,
|
|
321
|
+
expected_state: { property: 'value', expected: 12 },
|
|
322
|
+
element: {
|
|
323
|
+
elementId: wait.element.elementId,
|
|
324
|
+
text: 'Duration',
|
|
325
|
+
resource_id: 'seek_duration',
|
|
326
|
+
accessibility_id: null,
|
|
327
|
+
class: 'android.widget.SeekBar',
|
|
328
|
+
bounds: [0, 0, 200, 40],
|
|
329
|
+
index: 0,
|
|
330
|
+
state: { value, raw_value: value, value_range: { min: 0, max: 20 } }
|
|
331
|
+
},
|
|
332
|
+
observed_state: { property: 'value', value, raw_value: value },
|
|
333
|
+
reason: value === 12 ? 'value matches expected value' : 'value still below target'
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const cachedResolveAdjust = await ToolsInteract.adjustControlHandler({
|
|
338
|
+
element_id: wait.element.elementId,
|
|
339
|
+
property: 'value',
|
|
340
|
+
targetValue: 12,
|
|
341
|
+
tolerance: 0.5,
|
|
342
|
+
maxAttempts: 2,
|
|
343
|
+
platform: 'android'
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
assert.strictEqual(cachedResolveAdjust.success, true)
|
|
347
|
+
assert.strictEqual(cachedResolveAdjust.converged, true)
|
|
348
|
+
assert.strictEqual(cachedResolveAdjust.within_tolerance, true)
|
|
349
|
+
assert.strictEqual(cachedResolveAdjust.attempts, 3)
|
|
350
|
+
assert.strictEqual(treeFetches, 1, 'second attempt should reuse the resolved element instead of refetching the UI tree')
|
|
351
|
+
|
|
352
|
+
console.log('adjust_control unit tests passed')
|
|
353
|
+
} finally {
|
|
354
|
+
;(Observe as any).ToolsObserve.getUITreeHandler = originalGetUITreeHandler
|
|
355
|
+
;(Observe as any).ToolsObserve.getScreenFingerprintHandler = originalGetScreenFingerprintHandler
|
|
356
|
+
;(ToolsInteract as any).tapHandler = originalTapHandler
|
|
357
|
+
;(ToolsInteract as any).swipeHandler = originalSwipeHandler
|
|
358
|
+
;(ToolsInteract as any).expectStateHandler = originalExpectStateHandler
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
run().catch((error) => {
|
|
363
|
+
console.error(error)
|
|
364
|
+
process.exit(1)
|
|
365
|
+
})
|
|
@@ -31,6 +31,20 @@ async function run() {
|
|
|
31
31
|
})
|
|
32
32
|
assert.deepStrictEqual(androidElements[0].semantic, { is_clickable: true, is_container: false })
|
|
33
33
|
|
|
34
|
+
const androidProgressElements: any[] = []
|
|
35
|
+
traverseNode({
|
|
36
|
+
'@_class': 'android.widget.ProgressBar',
|
|
37
|
+
'@_text': 'Loading progress',
|
|
38
|
+
'@_content-desc': 'Loading progress',
|
|
39
|
+
'@_enabled': 'true',
|
|
40
|
+
'@_progress': '40',
|
|
41
|
+
'@_max': '100',
|
|
42
|
+
'@_bounds': '[0,0][200,40]'
|
|
43
|
+
}, androidProgressElements)
|
|
44
|
+
|
|
45
|
+
assert.notStrictEqual(androidProgressElements[0]?.role, 'slider')
|
|
46
|
+
assert.notStrictEqual(androidProgressElements[0]?.state?.value, 40)
|
|
47
|
+
|
|
34
48
|
const androidFallbackElements: any[] = []
|
|
35
49
|
traverseNode({
|
|
36
50
|
'@_class': 'android.widget.Button',
|
|
@@ -70,6 +84,16 @@ async function run() {
|
|
|
70
84
|
})
|
|
71
85
|
assert.deepStrictEqual(iosElements[0].semantic, { is_clickable: true, is_container: false })
|
|
72
86
|
|
|
87
|
+
const iosProgressElements: any[] = []
|
|
88
|
+
traverseIDBNode({
|
|
89
|
+
AXElementType: 'ProgressIndicator',
|
|
90
|
+
AXLabel: 'Loading progress',
|
|
91
|
+
AXValue: '0.4',
|
|
92
|
+
AXTraits: ['UIAccessibilityTraitUpdatesFrequently']
|
|
93
|
+
}, iosProgressElements)
|
|
94
|
+
|
|
95
|
+
assert.notStrictEqual(iosProgressElements[0]?.role, 'slider')
|
|
96
|
+
|
|
73
97
|
const iosFallbackElements: any[] = []
|
|
74
98
|
traverseIDBNode({
|
|
75
99
|
AXElementType: 'Button',
|
|
@@ -16,6 +16,7 @@ async function run() {
|
|
|
16
16
|
assert(names.includes('capture_screenshot'))
|
|
17
17
|
assert(names.includes('get_ui_tree'))
|
|
18
18
|
assert(names.includes('tap_element'))
|
|
19
|
+
assert(names.includes('adjust_control'))
|
|
19
20
|
|
|
20
21
|
const waitForUI = toolDefinitions.find((tool) => tool.name === 'wait_for_ui')
|
|
21
22
|
assert(waitForUI, 'wait_for_ui should be registered')
|
|
@@ -66,6 +67,13 @@ async function run() {
|
|
|
66
67
|
assert.match((expectElementVisible as any).description, /selector is the primary input/i)
|
|
67
68
|
assert.match((expectElementVisible as any).description, /Returns structured binary success\/failure only/i)
|
|
68
69
|
|
|
70
|
+
const adjustControl = toolDefinitions.find((tool) => tool.name === 'adjust_control')
|
|
71
|
+
assert(adjustControl, 'adjust_control should be registered')
|
|
72
|
+
assert.deepStrictEqual((adjustControl as any).inputSchema.required, ['targetValue'])
|
|
73
|
+
assert.strictEqual((adjustControl as any).inputSchema.properties.targetValue.type, 'number')
|
|
74
|
+
assert.match((adjustControl as any).description, /numeric control value/i)
|
|
75
|
+
assert.match((adjustControl as any).description, /expect_state/i)
|
|
76
|
+
|
|
69
77
|
const classifyActionOutcome = toolDefinitions.find((tool) => tool.name === 'classify_action_outcome')
|
|
70
78
|
assert(classifyActionOutcome, 'classify_action_outcome should be registered')
|
|
71
79
|
assert.match((classifyActionOutcome as any).description, /action_type/i)
|
|
@@ -14,6 +14,7 @@ async function run() {
|
|
|
14
14
|
const originalExpectScreenHandler = (ToolsInteract as any).expectScreenHandler
|
|
15
15
|
const originalExpectElementVisibleHandler = (ToolsInteract as any).expectElementVisibleHandler
|
|
16
16
|
const originalExpectStateHandler = (ToolsInteract as any).expectStateHandler
|
|
17
|
+
const originalAdjustControlHandler = (ToolsInteract as any).adjustControlHandler
|
|
17
18
|
const originalStartApp = AndroidManage.prototype.startApp
|
|
18
19
|
const originalCaptureScreenshotHandler = (ToolsObserve as any).captureScreenshotHandler
|
|
19
20
|
const originalGetUITreeHandler = (ToolsObserve as any).getUITreeHandler
|
|
@@ -191,6 +192,43 @@ async function run() {
|
|
|
191
192
|
assert.strictEqual(expectStatePayload.expected_state.property, 'checked')
|
|
192
193
|
assert.strictEqual(expectStatePayload.observed_state.value, true)
|
|
193
194
|
|
|
195
|
+
;(ToolsInteract as any).adjustControlHandler = async () => ({
|
|
196
|
+
action_id: 'adjust_control_1',
|
|
197
|
+
timestamp: '2026-04-29T08:00:00.000Z',
|
|
198
|
+
action_type: 'adjust_control',
|
|
199
|
+
lifecycle_state: 'pending_verification',
|
|
200
|
+
source_module: 'interact',
|
|
201
|
+
target: {
|
|
202
|
+
selector: { elementId: 'el_duration' },
|
|
203
|
+
resolved: {
|
|
204
|
+
elementId: 'el_duration',
|
|
205
|
+
text: 'Duration',
|
|
206
|
+
resource_id: null,
|
|
207
|
+
accessibility_id: null,
|
|
208
|
+
class: 'android.view.View',
|
|
209
|
+
bounds: [0, 0, 100, 20],
|
|
210
|
+
index: 0
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
success: true,
|
|
214
|
+
ui_fingerprint_before: 'fp_before',
|
|
215
|
+
ui_fingerprint_after: 'fp_after',
|
|
216
|
+
target_state: { property: 'value', target_value: 30, tolerance: 0.5 },
|
|
217
|
+
actual_state: { property: 'value', value: 30, raw_value: 30 },
|
|
218
|
+
within_tolerance: true,
|
|
219
|
+
converged: true,
|
|
220
|
+
attempts: 1,
|
|
221
|
+
adjustment_mode: 'semantic'
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
const adjustControlResponse = await handleToolCall('adjust_control', { element_id: 'el_duration', targetValue: 30, tolerance: 0.5, property: 'value' })
|
|
225
|
+
const adjustControlPayload = JSON.parse((adjustControlResponse as any).content[0].text)
|
|
226
|
+
assert.strictEqual(adjustControlPayload.success, true)
|
|
227
|
+
assert.strictEqual(adjustControlPayload.action_type, 'adjust_control')
|
|
228
|
+
assert.strictEqual(adjustControlPayload.target_state.target_value, 30)
|
|
229
|
+
assert.strictEqual(adjustControlPayload.within_tolerance, true)
|
|
230
|
+
assert.strictEqual(adjustControlPayload.converged, true)
|
|
231
|
+
|
|
194
232
|
;(ToolsInteract as any).tapHandler = async () => {
|
|
195
233
|
throw new Error('boom')
|
|
196
234
|
}
|
|
@@ -319,6 +357,7 @@ async function run() {
|
|
|
319
357
|
;(ToolsObserve as any).getUITreeHandler = originalGetUITreeHandler
|
|
320
358
|
;(ToolsObserve as any).getScreenFingerprintHandler = originalGetScreenFingerprintHandler
|
|
321
359
|
;(ToolsObserve as any).captureDebugSnapshotHandler = originalCaptureDebugSnapshotHandler
|
|
360
|
+
;(ToolsInteract as any).adjustControlHandler = originalAdjustControlHandler
|
|
322
361
|
}
|
|
323
362
|
}
|
|
324
363
|
|