mobile-debug-mcp 0.24.7 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/dist/interact/index.js +302 -6
- package/dist/observe/ios.js +56 -2
- package/dist/server/common.js +2 -1
- package/dist/server/tool-definitions.js +55 -0
- package/dist/server/tool-handlers.js +17 -0
- package/dist/server-core.js +1 -1
- package/dist/utils/android/utils.js +67 -1
- package/docs/CHANGELOG.md +6 -0
- package/docs/ROADMAP.md +388 -0
- package/docs/rfcs/001-state-verification.md +452 -0
- package/docs/specs/mcp-tooling-spec-v1.md +4 -0
- package/docs/tools/interact.md +25 -0
- package/docs/tools/observe.md +2 -1
- package/package.json +1 -1
- package/src/interact/index.ts +352 -7
- package/src/observe/ios.ts +62 -3
- package/src/server/common.ts +2 -1
- package/src/server/tool-definitions.ts +55 -0
- package/src/server/tool-handlers.ts +18 -0
- package/src/server-core.ts +1 -1
- package/src/types.ts +41 -0
- package/src/utils/android/utils.ts +78 -14
- package/test/unit/observe/find_element.test.ts +64 -5
- package/test/unit/observe/state_extraction.test.ts +43 -0
- package/test/unit/server/response_shapes.test.ts +40 -2
package/src/types.ts
CHANGED
|
@@ -79,6 +79,21 @@ export interface GetCrashResponse {
|
|
|
79
79
|
crashes: string[];
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
export interface UIElementState {
|
|
83
|
+
checked?: boolean | null;
|
|
84
|
+
selected?: boolean | string | { id: string; label?: string } | null;
|
|
85
|
+
focused?: boolean | null;
|
|
86
|
+
expanded?: boolean | null;
|
|
87
|
+
enabled?: boolean | null;
|
|
88
|
+
text_value?: string | null;
|
|
89
|
+
value?: number | string | null;
|
|
90
|
+
raw_value?: number | string | null;
|
|
91
|
+
value_range?: {
|
|
92
|
+
min: number;
|
|
93
|
+
max: number;
|
|
94
|
+
} | null;
|
|
95
|
+
}
|
|
96
|
+
|
|
82
97
|
export interface CaptureAndroidScreenResponse {
|
|
83
98
|
device: DeviceInfo;
|
|
84
99
|
screenshot: string; // base64 encoded string
|
|
@@ -116,6 +131,7 @@ export interface UIElement {
|
|
|
116
131
|
children?: number[];
|
|
117
132
|
center?: [number, number];
|
|
118
133
|
depth?: number;
|
|
134
|
+
state?: UIElementState | null;
|
|
119
135
|
}
|
|
120
136
|
|
|
121
137
|
export interface GetUITreeResponse {
|
|
@@ -198,6 +214,7 @@ export interface ActionTargetResolved {
|
|
|
198
214
|
class: string | null;
|
|
199
215
|
bounds: [number, number, number, number] | null;
|
|
200
216
|
index: number | null;
|
|
217
|
+
state?: UIElementState | null;
|
|
201
218
|
}
|
|
202
219
|
|
|
203
220
|
export interface ActionExecutionResult {
|
|
@@ -260,6 +277,30 @@ export interface ExpectElementVisibleResponse {
|
|
|
260
277
|
retryable?: boolean;
|
|
261
278
|
}
|
|
262
279
|
|
|
280
|
+
export interface ExpectStateResponse {
|
|
281
|
+
success: boolean;
|
|
282
|
+
selector?: {
|
|
283
|
+
text?: string;
|
|
284
|
+
resource_id?: string;
|
|
285
|
+
accessibility_id?: string;
|
|
286
|
+
contains?: boolean;
|
|
287
|
+
};
|
|
288
|
+
element_id: string | null;
|
|
289
|
+
expected_state: {
|
|
290
|
+
property: string;
|
|
291
|
+
expected: boolean | number | string | Record<string, unknown>;
|
|
292
|
+
};
|
|
293
|
+
element?: (ActionTargetResolved & { state?: UIElementState | null }) | null;
|
|
294
|
+
observed_state?: {
|
|
295
|
+
property: string;
|
|
296
|
+
value: boolean | number | string | Record<string, unknown> | null;
|
|
297
|
+
raw_value?: boolean | number | string | null;
|
|
298
|
+
};
|
|
299
|
+
reason?: string;
|
|
300
|
+
failure_code?: 'ELEMENT_NOT_FOUND' | 'UNKNOWN';
|
|
301
|
+
retryable?: boolean;
|
|
302
|
+
}
|
|
303
|
+
|
|
263
304
|
export interface SwipeResponse {
|
|
264
305
|
device: DeviceInfo;
|
|
265
306
|
success: boolean;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DeviceInfo, UIElement } from "../../types.js"
|
|
1
|
+
import { DeviceInfo, UIElement, UIElementState } from "../../types.js"
|
|
2
2
|
import { promises as fsPromises, existsSync } from 'fs'
|
|
3
3
|
import path from 'path'
|
|
4
4
|
import { detectJavaHome } from '../java.js'
|
|
@@ -323,6 +323,68 @@ export function getCenter(bounds: [number, number, number, number]): [number, nu
|
|
|
323
323
|
return [Math.floor((x1 + x2) / 2), Math.floor((y1 + y2) / 2)];
|
|
324
324
|
}
|
|
325
325
|
|
|
326
|
+
function parseBooleanAttr(value: unknown): boolean | null {
|
|
327
|
+
if (value === true || value === 'true') return true
|
|
328
|
+
if (value === false || value === 'false') return false
|
|
329
|
+
return null
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function parseNumberAttr(value: unknown): number | null {
|
|
333
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value
|
|
334
|
+
if (typeof value !== 'string') return null
|
|
335
|
+
const parsed = Number(value)
|
|
336
|
+
return Number.isFinite(parsed) ? parsed : null
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function isSliderLikeAndroid(node: any): boolean {
|
|
340
|
+
const className = String(node['@_class'] || '').toLowerCase()
|
|
341
|
+
return /seekbar|slider|range|progress/i.test(className)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function extractAndroidState(node: any): UIElementState | null {
|
|
345
|
+
const checked = parseBooleanAttr(node['@_checked'])
|
|
346
|
+
const selectedFlag = parseBooleanAttr(node['@_selected'])
|
|
347
|
+
const focused = parseBooleanAttr(node['@_focused'])
|
|
348
|
+
const expanded = parseBooleanAttr(node['@_expanded'])
|
|
349
|
+
const enabled = parseBooleanAttr(node['@_enabled'])
|
|
350
|
+
const textValue = typeof node['@_text'] === 'string' && node['@_text'].trim().length > 0 ? node['@_text'] : null
|
|
351
|
+
const state: UIElementState = {}
|
|
352
|
+
|
|
353
|
+
if (checked !== null) state.checked = checked
|
|
354
|
+
if (selectedFlag !== null) {
|
|
355
|
+
state.selected = textValue || node['@_content-desc'] || true
|
|
356
|
+
}
|
|
357
|
+
if (focused !== null) state.focused = focused
|
|
358
|
+
if (expanded !== null) state.expanded = expanded
|
|
359
|
+
if (enabled !== null) state.enabled = enabled
|
|
360
|
+
|
|
361
|
+
if (textValue && /edittext|textfield|search/i.test(String(node['@_class'] || ''))) {
|
|
362
|
+
state.text_value = textValue
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (isSliderLikeAndroid(node)) {
|
|
366
|
+
const rawProgress = parseNumberAttr(node['@_progress'])
|
|
367
|
+
const max = parseNumberAttr(node['@_max'])
|
|
368
|
+
const fallbackValue = rawProgress ?? parseNumberAttr(node['@_value']) ?? parseNumberAttr(node['@_content-desc'])
|
|
369
|
+
const numericValue = rawProgress ?? fallbackValue
|
|
370
|
+
if (numericValue !== null) {
|
|
371
|
+
state.raw_value = numericValue
|
|
372
|
+
state.value_range = max !== null && max > 0 ? { min: 0, max } : null
|
|
373
|
+
state.value = max !== null && max > 0 ? Math.round((numericValue / max) * 100) : numericValue
|
|
374
|
+
}
|
|
375
|
+
} else {
|
|
376
|
+
const numericValue = parseNumberAttr(node['@_value'])
|
|
377
|
+
if (numericValue !== null) {
|
|
378
|
+
state.value = numericValue
|
|
379
|
+
state.raw_value = numericValue
|
|
380
|
+
} else if (textValue) {
|
|
381
|
+
state.value = textValue
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return Object.keys(state).length > 0 ? state : null
|
|
386
|
+
}
|
|
387
|
+
|
|
326
388
|
export async function getScreenResolution(deviceId?: string): Promise<{ width: number; height: number }> {
|
|
327
389
|
try {
|
|
328
390
|
const output = await execAdb(['shell', 'wm', 'size'], deviceId);
|
|
@@ -339,27 +401,29 @@ export function traverseNode(node: any, elements: UIElement[], parentIndex: numb
|
|
|
339
401
|
|
|
340
402
|
let currentIndex = -1;
|
|
341
403
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
404
|
+
if (node['@_class']) {
|
|
405
|
+
const text = node['@_text'] || null;
|
|
406
|
+
const contentDescription = node['@_content-desc'] || null;
|
|
407
|
+
const clickable = node['@_clickable'] === 'true';
|
|
408
|
+
const bounds = parseBounds(node['@_bounds'] || '[0,0][0,0]');
|
|
409
|
+
const state = extractAndroidState(node);
|
|
347
410
|
|
|
348
|
-
|
|
411
|
+
const isUseful = clickable || (text && text.length > 0) || (contentDescription && contentDescription.length > 0);
|
|
349
412
|
|
|
350
|
-
|
|
351
|
-
|
|
413
|
+
if (isUseful) {
|
|
414
|
+
const element: UIElement = {
|
|
352
415
|
text,
|
|
353
416
|
contentDescription,
|
|
354
417
|
type: node['@_class'] || 'unknown',
|
|
355
418
|
resourceId: node['@_resource-id'] || null,
|
|
356
419
|
clickable,
|
|
357
420
|
enabled: node['@_enabled'] === 'true',
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
421
|
+
visible: true,
|
|
422
|
+
bounds,
|
|
423
|
+
center: getCenter(bounds),
|
|
424
|
+
depth,
|
|
425
|
+
state
|
|
426
|
+
};
|
|
363
427
|
|
|
364
428
|
if (parentIndex !== -1) {
|
|
365
429
|
element.parentId = parentIndex;
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import { traverseNode } from '../../../src/utils/android/utils.js'
|
|
3
|
+
import { traverseIDBNode } from '../../../src/observe/ios.js'
|
|
4
|
+
|
|
5
|
+
async function run() {
|
|
6
|
+
const androidElements: any[] = []
|
|
7
|
+
traverseNode({
|
|
8
|
+
'@_class': 'android.widget.SeekBar',
|
|
9
|
+
'@_text': '',
|
|
10
|
+
'@_content-desc': 'Duration',
|
|
11
|
+
'@_clickable': 'true',
|
|
12
|
+
'@_enabled': 'true',
|
|
13
|
+
'@_selected': 'true',
|
|
14
|
+
'@_progress': '7',
|
|
15
|
+
'@_max': '14',
|
|
16
|
+
'@_bounds': '[0,0][200,40]'
|
|
17
|
+
}, androidElements)
|
|
18
|
+
|
|
19
|
+
assert.strictEqual(androidElements.length, 1)
|
|
20
|
+
assert.deepStrictEqual(androidElements[0].state?.selected, 'Duration')
|
|
21
|
+
assert.strictEqual(androidElements[0].state?.raw_value, 7)
|
|
22
|
+
assert.strictEqual(androidElements[0].state?.value, 50)
|
|
23
|
+
assert.deepStrictEqual(androidElements[0].state?.value_range, { min: 0, max: 14 })
|
|
24
|
+
|
|
25
|
+
const iosElements: any[] = []
|
|
26
|
+
traverseIDBNode({
|
|
27
|
+
AXElementType: 'Slider',
|
|
28
|
+
AXLabel: 'Playback speed',
|
|
29
|
+
AXValue: '0.75',
|
|
30
|
+
AXTraits: ['UIAccessibilityTraitAdjustable']
|
|
31
|
+
}, iosElements)
|
|
32
|
+
|
|
33
|
+
assert.strictEqual(iosElements.length, 1)
|
|
34
|
+
assert.strictEqual(iosElements[0].state?.value, 75)
|
|
35
|
+
assert.strictEqual(iosElements[0].state?.raw_value, 0.75)
|
|
36
|
+
|
|
37
|
+
console.log('state extraction tests passed')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
run().catch((error) => {
|
|
41
|
+
console.error(error)
|
|
42
|
+
process.exit(1)
|
|
43
|
+
})
|
|
@@ -12,6 +12,7 @@ async function run() {
|
|
|
12
12
|
const originalTapHandler = (ToolsInteract as any).tapHandler
|
|
13
13
|
const originalExpectScreenHandler = (ToolsInteract as any).expectScreenHandler
|
|
14
14
|
const originalExpectElementVisibleHandler = (ToolsInteract as any).expectElementVisibleHandler
|
|
15
|
+
const originalExpectStateHandler = (ToolsInteract as any).expectStateHandler
|
|
15
16
|
const originalStartApp = AndroidManage.prototype.startApp
|
|
16
17
|
const originalCaptureScreenshotHandler = (ToolsObserve as any).captureScreenshotHandler
|
|
17
18
|
const originalGetUITreeHandler = (ToolsObserve as any).getUITreeHandler
|
|
@@ -130,8 +131,8 @@ async function run() {
|
|
|
130
131
|
selector: { text: 'Ready' },
|
|
131
132
|
element_id: 'el_ready',
|
|
132
133
|
expected_condition: 'visible',
|
|
133
|
-
element: { elementId: 'el_ready', text: 'Ready', resource_id: null, accessibility_id: null, class: 'TextView', bounds: [0, 0, 10, 10], index: 0 },
|
|
134
|
-
observed: { status: 'success', matched_count: 1, condition_satisfied: true, selected_index: 0, last_matched_element: { elementId: 'el_ready', text: 'Ready', resource_id: null, accessibility_id: null, class: 'TextView', bounds: [0, 0, 10, 10], index: 0 } },
|
|
134
|
+
element: { elementId: 'el_ready', text: 'Ready', resource_id: null, accessibility_id: null, class: 'TextView', bounds: [0, 0, 10, 10], index: 0, state: { enabled: true } },
|
|
135
|
+
observed: { status: 'success', matched_count: 1, condition_satisfied: true, selected_index: 0, last_matched_element: { elementId: 'el_ready', text: 'Ready', resource_id: null, accessibility_id: null, class: 'TextView', bounds: [0, 0, 10, 10], index: 0, state: { enabled: true } } },
|
|
135
136
|
reason: 'selector is visible'
|
|
136
137
|
})
|
|
137
138
|
|
|
@@ -141,6 +142,42 @@ async function run() {
|
|
|
141
142
|
assert.strictEqual(expectElementPayload.element_id, 'el_ready')
|
|
142
143
|
assert.strictEqual(expectElementPayload.expected_condition, 'visible')
|
|
143
144
|
|
|
145
|
+
;(ToolsObserve as any).getUITreeHandler = async () => ({
|
|
146
|
+
device: { platform: 'android', id: 'mock', osVersion: '14', model: 'Pixel', simulator: true },
|
|
147
|
+
resolution: { width: 1080, height: 2400 },
|
|
148
|
+
elements: [{
|
|
149
|
+
text: 'Notifications',
|
|
150
|
+
depth: 0,
|
|
151
|
+
center: { x: 50, y: 20 },
|
|
152
|
+
state: { checked: true, selected: 'Notifications' }
|
|
153
|
+
}]
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
;(ToolsInteract as any).expectStateHandler = async () => ({
|
|
157
|
+
success: true,
|
|
158
|
+
selector: { text: 'Notifications' },
|
|
159
|
+
element_id: 'el_notifications',
|
|
160
|
+
expected_state: { property: 'checked', expected: true },
|
|
161
|
+
element: {
|
|
162
|
+
elementId: 'el_notifications',
|
|
163
|
+
text: 'Notifications',
|
|
164
|
+
resource_id: null,
|
|
165
|
+
accessibility_id: null,
|
|
166
|
+
class: 'Switch',
|
|
167
|
+
bounds: [0, 0, 10, 10],
|
|
168
|
+
index: 0,
|
|
169
|
+
state: { checked: true, selected: 'Notifications' }
|
|
170
|
+
},
|
|
171
|
+
observed_state: { property: 'checked', value: true, raw_value: true },
|
|
172
|
+
reason: 'checked matches expected value'
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
const expectStateResponse = await handleToolCall('expect_state', { selector: { text: 'Notifications' }, property: 'checked', expected: true })
|
|
176
|
+
const expectStatePayload = JSON.parse((expectStateResponse as any).content[0].text)
|
|
177
|
+
assert.strictEqual(expectStatePayload.success, true)
|
|
178
|
+
assert.strictEqual(expectStatePayload.expected_state.property, 'checked')
|
|
179
|
+
assert.strictEqual(expectStatePayload.observed_state.value, true)
|
|
180
|
+
|
|
144
181
|
;(ToolsInteract as any).tapHandler = async () => {
|
|
145
182
|
throw new Error('boom')
|
|
146
183
|
}
|
|
@@ -234,6 +271,7 @@ async function run() {
|
|
|
234
271
|
;(ToolsInteract as any).tapHandler = originalTapHandler
|
|
235
272
|
;(ToolsInteract as any).expectScreenHandler = originalExpectScreenHandler
|
|
236
273
|
;(ToolsInteract as any).expectElementVisibleHandler = originalExpectElementVisibleHandler
|
|
274
|
+
;(ToolsInteract as any).expectStateHandler = originalExpectStateHandler
|
|
237
275
|
AndroidManage.prototype.startApp = originalStartApp
|
|
238
276
|
;(ToolsObserve as any).captureScreenshotHandler = originalCaptureScreenshotHandler
|
|
239
277
|
;(ToolsObserve as any).getUITreeHandler = originalGetUITreeHandler
|