mobile-debug-mcp 0.25.0 → 0.25.1
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 +30 -4
- package/dist/observe/ios.js +71 -2
- package/dist/server-core.js +1 -1
- package/dist/utils/android/utils.js +68 -3
- package/docs/CHANGELOG.md +6 -0
- package/docs/ROADMAP.md +19 -1
- package/docs/rfcs/002-richer-element-identity +400 -0
- package/docs/rfcs/003-wait-and-synchronization-reliability +232 -0
- package/docs/specs/mcp-tooling-spec-v1.md +1 -0
- package/docs/tools/observe.md +2 -1
- package/package.json +1 -1
- package/src/interact/index.ts +35 -4
- package/src/observe/index.ts +6 -0
- package/src/observe/ios.ts +69 -3
- package/src/server-core.ts +1 -1
- package/src/types.ts +26 -1
- package/src/utils/android/utils.ts +78 -20
- package/test/unit/observe/state_extraction.test.ts +47 -0
package/src/interact/index.ts
CHANGED
|
@@ -40,6 +40,11 @@ interface UiElement {
|
|
|
40
40
|
_interactable?: boolean
|
|
41
41
|
_sliderLike?: boolean
|
|
42
42
|
state?: UIElementState | null
|
|
43
|
+
stable_id?: string | null
|
|
44
|
+
role?: string | null
|
|
45
|
+
test_tag?: string | null
|
|
46
|
+
selector?: { value: string | null, confidence: { score: number, reason: string } | null } | null
|
|
47
|
+
semantic?: { is_clickable: boolean, is_container: boolean } | null
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
interface ResolvedUiElementContext {
|
|
@@ -157,7 +162,13 @@ export class ToolsInteract {
|
|
|
157
162
|
class: el.type ?? el.class ?? null,
|
|
158
163
|
bounds,
|
|
159
164
|
index,
|
|
160
|
-
elementId
|
|
165
|
+
elementId,
|
|
166
|
+
state: el.state ?? null,
|
|
167
|
+
stable_id: el.stable_id ?? null,
|
|
168
|
+
role: el.role ?? null,
|
|
169
|
+
test_tag: el.test_tag ?? null,
|
|
170
|
+
selector: el.selector ?? null,
|
|
171
|
+
semantic: el.semantic ?? null
|
|
161
172
|
}
|
|
162
173
|
}
|
|
163
174
|
|
|
@@ -197,7 +208,12 @@ export class ToolsInteract {
|
|
|
197
208
|
class: element.type ?? element.class ?? null,
|
|
198
209
|
bounds: ToolsInteract._normalizeBounds(element.bounds),
|
|
199
210
|
index,
|
|
200
|
-
state: element.state ?? null
|
|
211
|
+
state: element.state ?? null,
|
|
212
|
+
stable_id: element.stable_id ?? null,
|
|
213
|
+
role: element.role ?? null,
|
|
214
|
+
test_tag: element.test_tag ?? null,
|
|
215
|
+
selector: element.selector ?? null,
|
|
216
|
+
semantic: element.semantic ?? null
|
|
201
217
|
}
|
|
202
218
|
}
|
|
203
219
|
|
|
@@ -621,6 +637,11 @@ export class ToolsInteract {
|
|
|
621
637
|
bounds: boundsObj,
|
|
622
638
|
clickable: !!best.clickable,
|
|
623
639
|
enabled: !!best.enabled,
|
|
640
|
+
stable_id: best.stable_id ?? null,
|
|
641
|
+
role: best.role ?? null,
|
|
642
|
+
test_tag: best.test_tag ?? null,
|
|
643
|
+
selector: best.selector ?? null,
|
|
644
|
+
semantic: best.semantic ?? null,
|
|
624
645
|
tapCoordinates,
|
|
625
646
|
telemetry: {
|
|
626
647
|
matchedIndex: best?._index ?? null,
|
|
@@ -1040,7 +1061,12 @@ export class ToolsInteract {
|
|
|
1040
1061
|
class: result.element.class ?? null,
|
|
1041
1062
|
bounds: result.element.bounds ?? null,
|
|
1042
1063
|
index: typeof result.element.index === 'number' ? result.element.index : null,
|
|
1043
|
-
state: (result.element as any).state ?? null
|
|
1064
|
+
state: (result.element as any).state ?? null,
|
|
1065
|
+
stable_id: (result.element as any).stable_id ?? null,
|
|
1066
|
+
role: (result.element as any).role ?? null,
|
|
1067
|
+
test_tag: (result.element as any).test_tag ?? null,
|
|
1068
|
+
selector: (result.element as any).selector ?? null,
|
|
1069
|
+
semantic: (result.element as any).semantic ?? null
|
|
1044
1070
|
},
|
|
1045
1071
|
observed: {
|
|
1046
1072
|
status: result.status,
|
|
@@ -1055,7 +1081,12 @@ export class ToolsInteract {
|
|
|
1055
1081
|
class: result.element.class ?? null,
|
|
1056
1082
|
bounds: result.element.bounds ?? null,
|
|
1057
1083
|
index: typeof result.element.index === 'number' ? result.element.index : null,
|
|
1058
|
-
state: (result.element as any).state ?? null
|
|
1084
|
+
state: (result.element as any).state ?? null,
|
|
1085
|
+
stable_id: (result.element as any).stable_id ?? null,
|
|
1086
|
+
role: (result.element as any).role ?? null,
|
|
1087
|
+
test_tag: (result.element as any).test_tag ?? null,
|
|
1088
|
+
selector: (result.element as any).selector ?? null,
|
|
1089
|
+
semantic: (result.element as any).semantic ?? null
|
|
1059
1090
|
}
|
|
1060
1091
|
},
|
|
1061
1092
|
reason: 'selector is visible'
|
package/src/observe/index.ts
CHANGED
|
@@ -21,6 +21,12 @@ interface SnapshotTreeElementLike {
|
|
|
21
21
|
clickable?: boolean
|
|
22
22
|
enabled?: boolean
|
|
23
23
|
visible?: boolean
|
|
24
|
+
state?: unknown
|
|
25
|
+
stable_id?: string | null
|
|
26
|
+
role?: string | null
|
|
27
|
+
test_tag?: string | null
|
|
28
|
+
selector?: unknown
|
|
29
|
+
semantic?: unknown
|
|
24
30
|
}
|
|
25
31
|
|
|
26
32
|
interface SnapshotTreeLike {
|
package/src/observe/ios.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from "child_process"
|
|
2
2
|
import { promises as fs } from "fs"
|
|
3
|
-
import { GetLogsResponse, CaptureIOSScreenshotResponse, GetUITreeResponse, UIElement, DeviceInfo, UIElementState } from "../types.js"
|
|
3
|
+
import { GetLogsResponse, CaptureIOSScreenshotResponse, GetUITreeResponse, UIElement, DeviceInfo, UIElementSemanticMetadata, UIElementState, UIResolutionSelector, SelectorConfidence } from "../types.js"
|
|
4
4
|
import { execCommand, getIOSDeviceMetadata, validateBundleId, getIdbCmd, getXcrunCmd, isIDBInstalled } from "../utils/ios/utils.js"
|
|
5
5
|
import { createWriteStream, promises as fsPromises } from 'fs'
|
|
6
6
|
import path from 'path'
|
|
@@ -22,6 +22,9 @@ export function _resetIOSExecCommandForTests() {
|
|
|
22
22
|
interface IDBElement {
|
|
23
23
|
AXFrame?: { x: number | string, y: number | string, width: number | string, height: number | string, w?: number | string, h?: number | string };
|
|
24
24
|
frame?: { x: number | string, y: number | string, width: number | string, height: number | string, w?: number | string, h?: number | string };
|
|
25
|
+
AXIdentifier?: string;
|
|
26
|
+
accessibilityIdentifier?: string;
|
|
27
|
+
identifier?: string;
|
|
25
28
|
AXUniqueId?: string;
|
|
26
29
|
AXLabel?: string;
|
|
27
30
|
AXValue?: string;
|
|
@@ -63,6 +66,59 @@ function parseIOSNumber(value: unknown): number | null {
|
|
|
63
66
|
return Number.isFinite(parsed) ? parsed : null
|
|
64
67
|
}
|
|
65
68
|
|
|
69
|
+
function normalizeIOSType(value: unknown): string {
|
|
70
|
+
return typeof value === 'string' ? value.trim().toLowerCase() : ''
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function inferIOSRole(type: string, traits: string[]): string | null {
|
|
74
|
+
if (/slider|adjustable/.test(type) || traits.some((trait) => /adjustable|slider/.test(trait))) return 'slider'
|
|
75
|
+
if (/button/.test(type) || traits.some((trait) => /button/.test(trait))) return 'button'
|
|
76
|
+
if (/cell/.test(type)) return 'cell'
|
|
77
|
+
if (/switch/.test(type)) return 'switch'
|
|
78
|
+
if (/text field|textfield|search field/.test(type)) return 'text_field'
|
|
79
|
+
if (/image/.test(type)) return 'image'
|
|
80
|
+
if (/window|application|group|scroll view|collection view/.test(type)) return 'container'
|
|
81
|
+
return null
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getIOSStableId(node: IDBElement): string | null {
|
|
85
|
+
const candidates = [node.AXIdentifier, node.accessibilityIdentifier, node.identifier, node.AXUniqueId]
|
|
86
|
+
for (const candidate of candidates) {
|
|
87
|
+
if (typeof candidate === 'string' && candidate.trim().length > 0) return candidate
|
|
88
|
+
}
|
|
89
|
+
return null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function buildIOSSelectorConfidence(source: 'identifier' | 'label' | 'value' | 'type' | 'none'): SelectorConfidence | null {
|
|
93
|
+
switch (source) {
|
|
94
|
+
case 'identifier':
|
|
95
|
+
return { score: 1, reason: 'accessibility_identifier' }
|
|
96
|
+
case 'label':
|
|
97
|
+
return { score: 0.9, reason: 'label_match' }
|
|
98
|
+
case 'value':
|
|
99
|
+
return { score: 0.75, reason: 'value_match' }
|
|
100
|
+
case 'type':
|
|
101
|
+
return { score: 0.35, reason: 'type_match' }
|
|
102
|
+
default:
|
|
103
|
+
return null
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function buildIOSSelector(type: string, label: string | null, value: string | null, stableId: string | null): UIResolutionSelector | null {
|
|
108
|
+
if (stableId) return { value: stableId, confidence: buildIOSSelectorConfidence('identifier') }
|
|
109
|
+
if (label) return { value: label, confidence: buildIOSSelectorConfidence('label') }
|
|
110
|
+
if (value) return { value: value, confidence: buildIOSSelectorConfidence('value') }
|
|
111
|
+
if (type) return { value: type, confidence: buildIOSSelectorConfidence('type') }
|
|
112
|
+
return null
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function buildIOSSemantic(type: string, traits: string[]): UIElementSemanticMetadata {
|
|
116
|
+
return {
|
|
117
|
+
is_clickable: traits.includes("UIAccessibilityTraitButton") || /adjustable|slider/.test(type) || type === "Button" || type === "Cell",
|
|
118
|
+
is_container: /window|application|group|scroll view|collection view/.test(type)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
66
122
|
function isIOSAdjustable(node: IDBElement, type: string, traits: string[]): boolean {
|
|
67
123
|
return /slider|adjustable|stepper|progress/i.test(type) || traits.some((trait) => /adjustable|slider|progress/i.test(trait))
|
|
68
124
|
}
|
|
@@ -124,6 +180,11 @@ export function traverseIDBNode(node: IDBElement, elements: UIElement[], parentI
|
|
|
124
180
|
const frame = node.AXFrame || node.frame;
|
|
125
181
|
const traits = node.AXTraits || [];
|
|
126
182
|
const state = extractIOSState(node, type, label, value, traits);
|
|
183
|
+
const normalizedType = normalizeIOSType(type)
|
|
184
|
+
const stableId = getIOSStableId(node)
|
|
185
|
+
const selector = buildIOSSelector(type, label, value, stableId)
|
|
186
|
+
const semantic = buildIOSSemantic(normalizedType, traits)
|
|
187
|
+
const role = inferIOSRole(normalizedType, traits)
|
|
127
188
|
|
|
128
189
|
const clickable = traits.includes("UIAccessibilityTraitButton") || type === "Button" || type === "Cell";
|
|
129
190
|
|
|
@@ -135,14 +196,19 @@ export function traverseIDBNode(node: IDBElement, elements: UIElement[], parentI
|
|
|
135
196
|
text: label,
|
|
136
197
|
contentDescription: value,
|
|
137
198
|
type: type,
|
|
138
|
-
resourceId:
|
|
199
|
+
resourceId: stableId,
|
|
139
200
|
clickable: clickable,
|
|
140
201
|
enabled: true,
|
|
141
202
|
visible: true,
|
|
142
203
|
bounds: bounds,
|
|
143
204
|
center: getCenter(bounds),
|
|
144
205
|
depth: depth,
|
|
145
|
-
state
|
|
206
|
+
state,
|
|
207
|
+
stable_id: stableId,
|
|
208
|
+
role,
|
|
209
|
+
test_tag: stableId,
|
|
210
|
+
selector,
|
|
211
|
+
semantic
|
|
146
212
|
};
|
|
147
213
|
|
|
148
214
|
if (parentIndex !== -1) {
|
package/src/server-core.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -94,6 +94,21 @@ export interface UIElementState {
|
|
|
94
94
|
} | null;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
export interface SelectorConfidence {
|
|
98
|
+
score: number;
|
|
99
|
+
reason: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface UIResolutionSelector {
|
|
103
|
+
value: string | null;
|
|
104
|
+
confidence: SelectorConfidence | null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface UIElementSemanticMetadata {
|
|
108
|
+
is_clickable: boolean;
|
|
109
|
+
is_container: boolean;
|
|
110
|
+
}
|
|
111
|
+
|
|
97
112
|
export interface CaptureAndroidScreenResponse {
|
|
98
113
|
device: DeviceInfo;
|
|
99
114
|
screenshot: string; // base64 encoded string
|
|
@@ -132,6 +147,11 @@ export interface UIElement {
|
|
|
132
147
|
center?: [number, number];
|
|
133
148
|
depth?: number;
|
|
134
149
|
state?: UIElementState | null;
|
|
150
|
+
stable_id?: string | null;
|
|
151
|
+
role?: string | null;
|
|
152
|
+
test_tag?: string | null;
|
|
153
|
+
selector?: UIResolutionSelector | null;
|
|
154
|
+
semantic?: UIElementSemanticMetadata | null;
|
|
135
155
|
}
|
|
136
156
|
|
|
137
157
|
export interface GetUITreeResponse {
|
|
@@ -167,7 +187,7 @@ export interface CaptureDebugSnapshotRawResponse {
|
|
|
167
187
|
activity: string | null;
|
|
168
188
|
fingerprint: string | null;
|
|
169
189
|
screenshot: string | null;
|
|
170
|
-
ui_tree:
|
|
190
|
+
ui_tree: GetUITreeResponse | null;
|
|
171
191
|
logs: StructuredLogEntry[];
|
|
172
192
|
device?: DeviceInfo;
|
|
173
193
|
screenshot_error?: string;
|
|
@@ -215,6 +235,11 @@ export interface ActionTargetResolved {
|
|
|
215
235
|
bounds: [number, number, number, number] | null;
|
|
216
236
|
index: number | null;
|
|
217
237
|
state?: UIElementState | null;
|
|
238
|
+
stable_id?: string | null;
|
|
239
|
+
role?: string | null;
|
|
240
|
+
test_tag?: string | null;
|
|
241
|
+
selector?: UIResolutionSelector | null;
|
|
242
|
+
semantic?: UIElementSemanticMetadata | null;
|
|
218
243
|
}
|
|
219
244
|
|
|
220
245
|
export interface ActionExecutionResult {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DeviceInfo, UIElement, UIElementState } from "../../types.js"
|
|
1
|
+
import { DeviceInfo, UIElement, UIElementSemanticMetadata, UIElementState, UIResolutionSelector, SelectorConfidence } 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'
|
|
@@ -336,6 +336,52 @@ function parseNumberAttr(value: unknown): number | null {
|
|
|
336
336
|
return Number.isFinite(parsed) ? parsed : null
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
+
function normalizeClassName(value: unknown): string {
|
|
340
|
+
return typeof value === 'string' ? value.trim().toLowerCase() : ''
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function inferAndroidRole(className: string): string | null {
|
|
344
|
+
if (/seekbar|slider|progress/.test(className)) return 'slider'
|
|
345
|
+
if (/switch|toggle/.test(className)) return 'switch'
|
|
346
|
+
if (/checkbox/.test(className)) return 'checkbox'
|
|
347
|
+
if (/radiobutton|radio/.test(className)) return 'radio'
|
|
348
|
+
if (/edittext|textfield|search/.test(className)) return 'text_field'
|
|
349
|
+
if (/button|fab/.test(className)) return 'button'
|
|
350
|
+
if (/imageview|icon/.test(className)) return 'image'
|
|
351
|
+
if (/recyclerview|scroll|layout|viewgroup|frame/.test(className)) return 'container'
|
|
352
|
+
return null
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function buildAndroidSelectorConfidence(source: 'resource_id' | 'content_desc' | 'text' | 'class' | 'none'): SelectorConfidence | null {
|
|
356
|
+
switch (source) {
|
|
357
|
+
case 'resource_id':
|
|
358
|
+
return { score: 1, reason: 'resource_id' }
|
|
359
|
+
case 'content_desc':
|
|
360
|
+
return { score: 0.9, reason: 'content_description' }
|
|
361
|
+
case 'text':
|
|
362
|
+
return { score: 0.6, reason: 'text_match' }
|
|
363
|
+
case 'class':
|
|
364
|
+
return { score: 0.35, reason: 'class_match' }
|
|
365
|
+
default:
|
|
366
|
+
return null
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function buildAndroidSelector(text: string | null, contentDescription: string | null, resourceId: string | null, className: string): UIResolutionSelector | null {
|
|
371
|
+
if (resourceId) return { value: resourceId, confidence: buildAndroidSelectorConfidence('resource_id') }
|
|
372
|
+
if (contentDescription) return { value: contentDescription, confidence: buildAndroidSelectorConfidence('content_desc') }
|
|
373
|
+
if (text) return { value: text, confidence: buildAndroidSelectorConfidence('text') }
|
|
374
|
+
if (className) return { value: className, confidence: buildAndroidSelectorConfidence('class') }
|
|
375
|
+
return null
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function buildAndroidSemantic(clickable: boolean, className: string): UIElementSemanticMetadata {
|
|
379
|
+
return {
|
|
380
|
+
is_clickable: clickable,
|
|
381
|
+
is_container: /recyclerview|scroll|layout|viewgroup|frame/.test(className)
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
339
385
|
function isSliderLikeAndroid(node: any): boolean {
|
|
340
386
|
const className = String(node['@_class'] || '').toLowerCase()
|
|
341
387
|
return /seekbar|slider|range|progress/i.test(className)
|
|
@@ -401,29 +447,41 @@ export function traverseNode(node: any, elements: UIElement[], parentIndex: numb
|
|
|
401
447
|
|
|
402
448
|
let currentIndex = -1;
|
|
403
449
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
450
|
+
if (node['@_class']) {
|
|
451
|
+
const text = node['@_text'] || null;
|
|
452
|
+
const contentDescription = node['@_content-desc'] || null;
|
|
453
|
+
const clickable = node['@_clickable'] === 'true';
|
|
454
|
+
const className = String(node['@_class'] || 'unknown');
|
|
455
|
+
const bounds = parseBounds(node['@_bounds'] || '[0,0][0,0]');
|
|
456
|
+
const state = extractAndroidState(node);
|
|
457
|
+
const role = inferAndroidRole(normalizeClassName(className));
|
|
458
|
+
const resourceId = typeof node['@_resource-id'] === 'string' && node['@_resource-id'].trim().length > 0 ? node['@_resource-id'] : null
|
|
459
|
+
const stableId = resourceId ?? (typeof contentDescription === 'string' && contentDescription.trim().length > 0 ? contentDescription : null)
|
|
460
|
+
const testTag = stableId
|
|
461
|
+
const selector = buildAndroidSelector(text, contentDescription, resourceId, normalizeClassName(className))
|
|
462
|
+
const semantic = buildAndroidSemantic(clickable, normalizeClassName(className))
|
|
463
|
+
|
|
464
|
+
const isUseful = clickable || (text && text.length > 0) || (contentDescription && contentDescription.length > 0);
|
|
465
|
+
|
|
466
|
+
if (isUseful) {
|
|
467
|
+
const element: UIElement = {
|
|
415
468
|
text,
|
|
416
469
|
contentDescription,
|
|
417
|
-
type:
|
|
418
|
-
resourceId
|
|
470
|
+
type: className,
|
|
471
|
+
resourceId,
|
|
419
472
|
clickable,
|
|
420
473
|
enabled: node['@_enabled'] === 'true',
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
474
|
+
visible: true,
|
|
475
|
+
bounds,
|
|
476
|
+
center: getCenter(bounds),
|
|
477
|
+
depth,
|
|
478
|
+
state,
|
|
479
|
+
stable_id: stableId,
|
|
480
|
+
role,
|
|
481
|
+
test_tag: testTag,
|
|
482
|
+
selector,
|
|
483
|
+
semantic
|
|
484
|
+
};
|
|
427
485
|
|
|
428
486
|
if (parentIndex !== -1) {
|
|
429
487
|
element.parentId = parentIndex;
|
|
@@ -8,6 +8,7 @@ async function run() {
|
|
|
8
8
|
'@_class': 'android.widget.SeekBar',
|
|
9
9
|
'@_text': '',
|
|
10
10
|
'@_content-desc': 'Duration',
|
|
11
|
+
'@_resource-id': 'com.example:id/duration',
|
|
11
12
|
'@_clickable': 'true',
|
|
12
13
|
'@_enabled': 'true',
|
|
13
14
|
'@_selected': 'true',
|
|
@@ -21,18 +22,64 @@ async function run() {
|
|
|
21
22
|
assert.strictEqual(androidElements[0].state?.raw_value, 7)
|
|
22
23
|
assert.strictEqual(androidElements[0].state?.value, 50)
|
|
23
24
|
assert.deepStrictEqual(androidElements[0].state?.value_range, { min: 0, max: 14 })
|
|
25
|
+
assert.strictEqual(androidElements[0].stable_id, 'com.example:id/duration')
|
|
26
|
+
assert.strictEqual(androidElements[0].role, 'slider')
|
|
27
|
+
assert.strictEqual(androidElements[0].test_tag, 'com.example:id/duration')
|
|
28
|
+
assert.deepStrictEqual(androidElements[0].selector, {
|
|
29
|
+
value: 'com.example:id/duration',
|
|
30
|
+
confidence: { score: 1, reason: 'resource_id' }
|
|
31
|
+
})
|
|
32
|
+
assert.deepStrictEqual(androidElements[0].semantic, { is_clickable: true, is_container: false })
|
|
33
|
+
|
|
34
|
+
const androidFallbackElements: any[] = []
|
|
35
|
+
traverseNode({
|
|
36
|
+
'@_class': 'android.widget.Button',
|
|
37
|
+
'@_text': '',
|
|
38
|
+
'@_content-desc': 'Save',
|
|
39
|
+
'@_clickable': 'true',
|
|
40
|
+
'@_enabled': 'true',
|
|
41
|
+
'@_bounds': '[0,0][100,50]'
|
|
42
|
+
}, androidFallbackElements)
|
|
43
|
+
|
|
44
|
+
assert.strictEqual(androidFallbackElements.length, 1)
|
|
45
|
+
assert.strictEqual(androidFallbackElements[0].resourceId, null)
|
|
46
|
+
assert.strictEqual(androidFallbackElements[0].stable_id, 'Save')
|
|
47
|
+
assert.deepStrictEqual(androidFallbackElements[0].selector, {
|
|
48
|
+
value: 'Save',
|
|
49
|
+
confidence: { score: 0.9, reason: 'content_description' }
|
|
50
|
+
})
|
|
24
51
|
|
|
25
52
|
const iosElements: any[] = []
|
|
26
53
|
traverseIDBNode({
|
|
27
54
|
AXElementType: 'Slider',
|
|
28
55
|
AXLabel: 'Playback speed',
|
|
29
56
|
AXValue: '0.75',
|
|
57
|
+
AXUniqueId: 'playback_speed_slider',
|
|
30
58
|
AXTraits: ['UIAccessibilityTraitAdjustable']
|
|
31
59
|
}, iosElements)
|
|
32
60
|
|
|
33
61
|
assert.strictEqual(iosElements.length, 1)
|
|
34
62
|
assert.strictEqual(iosElements[0].state?.value, 75)
|
|
35
63
|
assert.strictEqual(iosElements[0].state?.raw_value, 0.75)
|
|
64
|
+
assert.strictEqual(iosElements[0].stable_id, 'playback_speed_slider')
|
|
65
|
+
assert.strictEqual(iosElements[0].role, 'slider')
|
|
66
|
+
assert.strictEqual(iosElements[0].test_tag, 'playback_speed_slider')
|
|
67
|
+
assert.deepStrictEqual(iosElements[0].selector, {
|
|
68
|
+
value: 'playback_speed_slider',
|
|
69
|
+
confidence: { score: 1, reason: 'accessibility_identifier' }
|
|
70
|
+
})
|
|
71
|
+
assert.deepStrictEqual(iosElements[0].semantic, { is_clickable: true, is_container: false })
|
|
72
|
+
|
|
73
|
+
const iosFallbackElements: any[] = []
|
|
74
|
+
traverseIDBNode({
|
|
75
|
+
AXElementType: 'Button',
|
|
76
|
+
AXLabel: 'Save',
|
|
77
|
+
AXTraits: ['UIAccessibilityTraitButton'],
|
|
78
|
+
AXUniqueId: 'fallback_unique_id'
|
|
79
|
+
}, iosFallbackElements)
|
|
80
|
+
|
|
81
|
+
assert.strictEqual(iosFallbackElements.length, 1)
|
|
82
|
+
assert.strictEqual(iosFallbackElements[0].stable_id, 'fallback_unique_id')
|
|
36
83
|
|
|
37
84
|
console.log('state extraction tests passed')
|
|
38
85
|
}
|