react-native-ai-debugger 1.0.5 → 1.0.7
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 +161 -0
- package/build/core/android.d.ts +63 -0
- package/build/core/android.d.ts.map +1 -1
- package/build/core/android.js +391 -0
- package/build/core/android.js.map +1 -1
- package/build/core/index.d.ts +4 -2
- package/build/core/index.d.ts.map +1 -1
- package/build/core/index.js +9 -3
- package/build/core/index.js.map +1 -1
- package/build/core/ios.d.ts +80 -0
- package/build/core/ios.d.ts.map +1 -1
- package/build/core/ios.js +591 -1
- package/build/core/ios.js.map +1 -1
- package/build/core/telemetry.d.ts +4 -0
- package/build/core/telemetry.d.ts.map +1 -0
- package/build/core/telemetry.js +225 -0
- package/build/core/telemetry.js.map +1 -0
- package/build/index.js +348 -5
- package/build/index.js.map +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -16,11 +16,14 @@ An MCP (Model Context Protocol) server for AI-powered React Native debugging. En
|
|
|
16
16
|
- **Discover debug globals** available in the app
|
|
17
17
|
- **Android device control** - screenshots, tap, swipe, text input, key events via ADB
|
|
18
18
|
- **iOS simulator control** - screenshots, app management, URL handling via simctl
|
|
19
|
+
- **iOS UI automation** - tap, swipe, text input, button presses via IDB (optional)
|
|
20
|
+
- **iOS accessibility inspection** - get UI element tree and element info at coordinates via IDB
|
|
19
21
|
|
|
20
22
|
## Requirements
|
|
21
23
|
|
|
22
24
|
- Node.js 18+
|
|
23
25
|
- React Native app running with Metro bundler
|
|
26
|
+
- **Optional for iOS UI automation**: [Facebook IDB](https://fbidb.io/) - `brew install idb-companion`
|
|
24
27
|
|
|
25
28
|
## Claude Code Setup
|
|
26
29
|
|
|
@@ -144,6 +147,14 @@ Requires VS Code 1.102+ with Copilot ([docs](https://code.visualstudio.com/docs/
|
|
|
144
147
|
| `android_key_event` | Send key events (HOME, BACK, ENTER, etc.) |
|
|
145
148
|
| `android_get_screen_size` | Get device screen resolution |
|
|
146
149
|
|
|
150
|
+
### Android Accessibility (UI Hierarchy)
|
|
151
|
+
|
|
152
|
+
| Tool | Description |
|
|
153
|
+
| ----------------------- | ----------------------------------------------------- |
|
|
154
|
+
| `android_describe_all` | Get full UI hierarchy tree using uiautomator |
|
|
155
|
+
| `android_describe_point`| Get UI element info at specific coordinates |
|
|
156
|
+
| `android_tap_element` | Tap element by text, content-desc, or resource-id |
|
|
157
|
+
|
|
147
158
|
### iOS (Simulator)
|
|
148
159
|
|
|
149
160
|
| Tool | Description |
|
|
@@ -156,6 +167,22 @@ Requires VS Code 1.102+ with Copilot ([docs](https://code.visualstudio.com/docs/
|
|
|
156
167
|
| `ios_terminate_app` | Terminate a running app |
|
|
157
168
|
| `ios_boot_simulator` | Boot a simulator by UDID |
|
|
158
169
|
|
|
170
|
+
### iOS UI Interaction (requires IDB)
|
|
171
|
+
|
|
172
|
+
These tools require [Facebook IDB](https://fbidb.io/) to be installed: `brew install idb-companion`
|
|
173
|
+
|
|
174
|
+
| Tool | Description |
|
|
175
|
+
| ------------------- | ----------------------------------------------------- |
|
|
176
|
+
| `ios_tap` | Tap at specific coordinates on screen |
|
|
177
|
+
| `ios_tap_element` | Tap an element by its accessibility label |
|
|
178
|
+
| `ios_swipe` | Swipe from one point to another |
|
|
179
|
+
| `ios_input_text` | Type text into the active input field |
|
|
180
|
+
| `ios_button` | Press hardware buttons (HOME, LOCK, SIRI, etc.) |
|
|
181
|
+
| `ios_key_event` | Send a key event by keycode |
|
|
182
|
+
| `ios_key_sequence` | Send multiple key events in sequence |
|
|
183
|
+
| `ios_describe_all` | Get accessibility tree for entire screen |
|
|
184
|
+
| `ios_describe_point`| Get accessibility info for element at specific point |
|
|
185
|
+
|
|
159
186
|
## Usage
|
|
160
187
|
|
|
161
188
|
1. Start your React Native app:
|
|
@@ -384,6 +411,20 @@ Set `awaitPromise=false` for synchronous execution only.
|
|
|
384
411
|
|
|
385
412
|
## Device Interaction
|
|
386
413
|
|
|
414
|
+
> **💡 Best Practice: Use Element-Based Tapping**
|
|
415
|
+
>
|
|
416
|
+
> Prefer `android_tap_element` and `ios_tap_element` over coordinate-based tapping (`android_tap`, `ios_tap`).
|
|
417
|
+
>
|
|
418
|
+
> **Why?**
|
|
419
|
+
> - More reliable - elements are found by text/label, not fragile coordinates
|
|
420
|
+
> - Self-documenting - `tap_element(text="Settings")` is clearer than `tap(x=540, y=1200)`
|
|
421
|
+
> - Resolution-independent - works across different screen sizes
|
|
422
|
+
>
|
|
423
|
+
> **Workflow:**
|
|
424
|
+
> 1. Use `android_describe_all` or `ios_describe_all` to see available elements
|
|
425
|
+
> 2. Use `android_tap_element` or `ios_tap_element` to interact by text/label
|
|
426
|
+
> 3. Fall back to coordinate-based tapping only when elements lack accessible text
|
|
427
|
+
|
|
387
428
|
### Android (requires ADB)
|
|
388
429
|
|
|
389
430
|
List connected devices:
|
|
@@ -425,6 +466,53 @@ android_key_event with key="HOME"
|
|
|
425
466
|
android_key_event with key="ENTER"
|
|
426
467
|
```
|
|
427
468
|
|
|
469
|
+
### Android UI Automation (Accessibility)
|
|
470
|
+
|
|
471
|
+
Get the full UI hierarchy:
|
|
472
|
+
|
|
473
|
+
```
|
|
474
|
+
android_describe_all
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
Example output:
|
|
478
|
+
```
|
|
479
|
+
[FrameLayout] frame=(0, 0, 1080x2340) tap=(540, 1170)
|
|
480
|
+
[LinearLayout] frame=(0, 63, 1080x147) tap=(540, 136)
|
|
481
|
+
[TextView] "Settings" frame=(48, 77, 200x63) tap=(148, 108)
|
|
482
|
+
[RecyclerView] frame=(0, 210, 1080x2130) tap=(540, 1275)
|
|
483
|
+
[Button] "Save" frame=(800, 2200, 200x80) tap=(900, 2240)
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
Get element info at coordinates:
|
|
487
|
+
|
|
488
|
+
```
|
|
489
|
+
android_describe_point with x=540 y=1170
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
Tap an element by text:
|
|
493
|
+
|
|
494
|
+
```
|
|
495
|
+
android_tap_element with text="Settings"
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
Tap using partial text match:
|
|
499
|
+
|
|
500
|
+
```
|
|
501
|
+
android_tap_element with textContains="Save"
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
Tap by resource ID:
|
|
505
|
+
|
|
506
|
+
```
|
|
507
|
+
android_tap_element with resourceId="save_button"
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
Tap by content description:
|
|
511
|
+
|
|
512
|
+
```
|
|
513
|
+
android_tap_element with contentDesc="Navigate up"
|
|
514
|
+
```
|
|
515
|
+
|
|
428
516
|
### iOS Simulator (requires Xcode)
|
|
429
517
|
|
|
430
518
|
List available simulators:
|
|
@@ -457,6 +545,79 @@ Open a deep link:
|
|
|
457
545
|
ios_open_url with url="myapp://settings"
|
|
458
546
|
```
|
|
459
547
|
|
|
548
|
+
### iOS UI Automation (requires IDB)
|
|
549
|
+
|
|
550
|
+
Install IDB first: `brew install idb-companion`
|
|
551
|
+
|
|
552
|
+
**Important: Coordinate System**
|
|
553
|
+
- iOS IDB uses **points** (logical coordinates), not pixels
|
|
554
|
+
- For 2x Retina displays: 1 point = 2 pixels
|
|
555
|
+
- Example: 1640x2360 pixel screenshot = 820x1180 points
|
|
556
|
+
- Use `ios_describe_all` to get exact element coordinates in points
|
|
557
|
+
|
|
558
|
+
Tap on screen (coordinates in points):
|
|
559
|
+
|
|
560
|
+
```
|
|
561
|
+
ios_tap with x=200 y=400
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
Long press (hold for 2 seconds):
|
|
565
|
+
|
|
566
|
+
```
|
|
567
|
+
ios_tap with x=200 y=400 duration=2
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
Swipe gesture:
|
|
571
|
+
|
|
572
|
+
```
|
|
573
|
+
ios_swipe with startX=200 startY=600 endX=200 endY=200
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
Type text (tap input field first):
|
|
577
|
+
|
|
578
|
+
```
|
|
579
|
+
ios_tap with x=200 y=300
|
|
580
|
+
ios_input_text with text="hello@example.com"
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
Press hardware buttons:
|
|
584
|
+
|
|
585
|
+
```
|
|
586
|
+
ios_button with button="HOME"
|
|
587
|
+
ios_button with button="LOCK"
|
|
588
|
+
ios_button with button="SIRI"
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
Get accessibility info for the screen:
|
|
592
|
+
|
|
593
|
+
```
|
|
594
|
+
ios_describe_all
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
Get accessibility info at a specific point:
|
|
598
|
+
|
|
599
|
+
```
|
|
600
|
+
ios_describe_point with x=200 y=400
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
Tap an element by accessibility label:
|
|
604
|
+
|
|
605
|
+
```
|
|
606
|
+
ios_tap_element with label="Settings"
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
Tap using partial label match:
|
|
610
|
+
|
|
611
|
+
```
|
|
612
|
+
ios_tap_element with labelContains="Sign"
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
When multiple elements match, use index (0-based):
|
|
616
|
+
|
|
617
|
+
```
|
|
618
|
+
ios_tap_element with labelContains="Button" index=1
|
|
619
|
+
```
|
|
620
|
+
|
|
460
621
|
## Supported React Native Versions
|
|
461
622
|
|
|
462
623
|
| Version | Runtime | Status |
|
package/build/core/android.d.ts
CHANGED
|
@@ -105,4 +105,67 @@ export declare function androidGetScreenSize(deviceId?: string): Promise<{
|
|
|
105
105
|
height?: number;
|
|
106
106
|
error?: string;
|
|
107
107
|
}>;
|
|
108
|
+
/**
|
|
109
|
+
* Android UI element from uiautomator dump
|
|
110
|
+
*/
|
|
111
|
+
export interface AndroidAccessibilityElement {
|
|
112
|
+
class: string;
|
|
113
|
+
text?: string;
|
|
114
|
+
contentDesc?: string;
|
|
115
|
+
resourceId?: string;
|
|
116
|
+
bounds: {
|
|
117
|
+
left: number;
|
|
118
|
+
top: number;
|
|
119
|
+
right: number;
|
|
120
|
+
bottom: number;
|
|
121
|
+
};
|
|
122
|
+
frame: {
|
|
123
|
+
x: number;
|
|
124
|
+
y: number;
|
|
125
|
+
width: number;
|
|
126
|
+
height: number;
|
|
127
|
+
};
|
|
128
|
+
tap: {
|
|
129
|
+
x: number;
|
|
130
|
+
y: number;
|
|
131
|
+
};
|
|
132
|
+
children: AndroidAccessibilityElement[];
|
|
133
|
+
checkable?: boolean;
|
|
134
|
+
checked?: boolean;
|
|
135
|
+
clickable?: boolean;
|
|
136
|
+
enabled?: boolean;
|
|
137
|
+
focusable?: boolean;
|
|
138
|
+
focused?: boolean;
|
|
139
|
+
scrollable?: boolean;
|
|
140
|
+
selected?: boolean;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Result type for accessibility operations
|
|
144
|
+
*/
|
|
145
|
+
export interface AndroidDescribeResult {
|
|
146
|
+
success: boolean;
|
|
147
|
+
elements?: AndroidAccessibilityElement[];
|
|
148
|
+
formatted?: string;
|
|
149
|
+
error?: string;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Get the UI hierarchy from the connected Android device using uiautomator dump
|
|
153
|
+
*/
|
|
154
|
+
export declare function androidDescribeAll(deviceId?: string): Promise<AndroidDescribeResult>;
|
|
155
|
+
/**
|
|
156
|
+
* Get accessibility info for the UI element at specific coordinates
|
|
157
|
+
*/
|
|
158
|
+
export declare function androidDescribePoint(x: number, y: number, deviceId?: string): Promise<AndroidDescribeResult>;
|
|
159
|
+
/**
|
|
160
|
+
* Tap an element by its text, content-description, or resource-id
|
|
161
|
+
*/
|
|
162
|
+
export declare function androidTapElement(options: {
|
|
163
|
+
text?: string;
|
|
164
|
+
textContains?: string;
|
|
165
|
+
contentDesc?: string;
|
|
166
|
+
contentDescContains?: string;
|
|
167
|
+
resourceId?: string;
|
|
168
|
+
index?: number;
|
|
169
|
+
deviceId?: string;
|
|
170
|
+
}): Promise<AdbResult>;
|
|
108
171
|
//# sourceMappingURL=android.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"android.d.ts","sourceRoot":"","sources":["../../src/core/android.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"android.d.ts","sourceRoot":"","sources":["../../src/core/android.ts"],"names":[],"mappings":"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"}
|
package/build/core/android.js
CHANGED
|
@@ -5,6 +5,8 @@ import path from "path";
|
|
|
5
5
|
import os from "os";
|
|
6
6
|
import sharp from "sharp";
|
|
7
7
|
const execAsync = promisify(exec);
|
|
8
|
+
// XML parsing for uiautomator dump
|
|
9
|
+
import { XMLParser } from "fast-xml-parser";
|
|
8
10
|
// ADB command timeout in milliseconds
|
|
9
11
|
const ADB_TIMEOUT = 30000;
|
|
10
12
|
/**
|
|
@@ -683,4 +685,393 @@ export async function androidGetScreenSize(deviceId) {
|
|
|
683
685
|
};
|
|
684
686
|
}
|
|
685
687
|
}
|
|
688
|
+
/**
|
|
689
|
+
* Simplify Android class name for display
|
|
690
|
+
* android.widget.Button -> Button
|
|
691
|
+
* android.widget.TextView -> TextView
|
|
692
|
+
*/
|
|
693
|
+
function simplifyClassName(className) {
|
|
694
|
+
if (!className)
|
|
695
|
+
return "Unknown";
|
|
696
|
+
const parts = className.split(".");
|
|
697
|
+
return parts[parts.length - 1];
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Parse bounds string "[left,top][right,bottom]" to object
|
|
701
|
+
*/
|
|
702
|
+
function parseBounds(boundsStr) {
|
|
703
|
+
const match = boundsStr?.match(/\[(\d+),(\d+)\]\[(\d+),(\d+)\]/);
|
|
704
|
+
if (!match)
|
|
705
|
+
return null;
|
|
706
|
+
return {
|
|
707
|
+
left: parseInt(match[1], 10),
|
|
708
|
+
top: parseInt(match[2], 10),
|
|
709
|
+
right: parseInt(match[3], 10),
|
|
710
|
+
bottom: parseInt(match[4], 10)
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Parse a single node from uiautomator XML
|
|
715
|
+
*/
|
|
716
|
+
function parseUiNode(node) {
|
|
717
|
+
const attrs = node["@_bounds"]
|
|
718
|
+
? node
|
|
719
|
+
: node.node
|
|
720
|
+
? (Array.isArray(node.node) ? node.node[0] : node.node)
|
|
721
|
+
: null;
|
|
722
|
+
if (!attrs)
|
|
723
|
+
return null;
|
|
724
|
+
const boundsStr = attrs["@_bounds"];
|
|
725
|
+
const bounds = parseBounds(boundsStr);
|
|
726
|
+
if (!bounds)
|
|
727
|
+
return null;
|
|
728
|
+
const width = bounds.right - bounds.left;
|
|
729
|
+
const height = bounds.bottom - bounds.top;
|
|
730
|
+
const centerX = Math.round(bounds.left + width / 2);
|
|
731
|
+
const centerY = Math.round(bounds.top + height / 2);
|
|
732
|
+
const element = {
|
|
733
|
+
class: simplifyClassName(attrs["@_class"] || ""),
|
|
734
|
+
bounds,
|
|
735
|
+
frame: {
|
|
736
|
+
x: bounds.left,
|
|
737
|
+
y: bounds.top,
|
|
738
|
+
width,
|
|
739
|
+
height
|
|
740
|
+
},
|
|
741
|
+
tap: {
|
|
742
|
+
x: centerX,
|
|
743
|
+
y: centerY
|
|
744
|
+
},
|
|
745
|
+
children: []
|
|
746
|
+
};
|
|
747
|
+
// Add optional attributes
|
|
748
|
+
if (attrs["@_text"])
|
|
749
|
+
element.text = attrs["@_text"];
|
|
750
|
+
if (attrs["@_content-desc"])
|
|
751
|
+
element.contentDesc = attrs["@_content-desc"];
|
|
752
|
+
if (attrs["@_resource-id"])
|
|
753
|
+
element.resourceId = attrs["@_resource-id"];
|
|
754
|
+
if (attrs["@_checkable"] === "true")
|
|
755
|
+
element.checkable = true;
|
|
756
|
+
if (attrs["@_checked"] === "true")
|
|
757
|
+
element.checked = true;
|
|
758
|
+
if (attrs["@_clickable"] === "true")
|
|
759
|
+
element.clickable = true;
|
|
760
|
+
if (attrs["@_enabled"] === "true")
|
|
761
|
+
element.enabled = true;
|
|
762
|
+
if (attrs["@_focusable"] === "true")
|
|
763
|
+
element.focusable = true;
|
|
764
|
+
if (attrs["@_focused"] === "true")
|
|
765
|
+
element.focused = true;
|
|
766
|
+
if (attrs["@_scrollable"] === "true")
|
|
767
|
+
element.scrollable = true;
|
|
768
|
+
if (attrs["@_selected"] === "true")
|
|
769
|
+
element.selected = true;
|
|
770
|
+
return element;
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* Recursively parse UI hierarchy from XML node
|
|
774
|
+
*/
|
|
775
|
+
function parseHierarchy(node) {
|
|
776
|
+
const results = [];
|
|
777
|
+
// Handle the node itself
|
|
778
|
+
if (node["@_bounds"]) {
|
|
779
|
+
const element = parseUiNode(node);
|
|
780
|
+
if (element) {
|
|
781
|
+
// Parse children
|
|
782
|
+
if (node.node) {
|
|
783
|
+
const children = Array.isArray(node.node) ? node.node : [node.node];
|
|
784
|
+
for (const child of children) {
|
|
785
|
+
element.children.push(...parseHierarchy(child));
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
results.push(element);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
else if (node.node) {
|
|
792
|
+
// This is a container without bounds (like hierarchy root)
|
|
793
|
+
const children = Array.isArray(node.node) ? node.node : [node.node];
|
|
794
|
+
for (const child of children) {
|
|
795
|
+
results.push(...parseHierarchy(child));
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
return results;
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* Format accessibility tree for display (similar to iOS format)
|
|
802
|
+
*/
|
|
803
|
+
function formatAndroidAccessibilityTree(elements, indent = 0) {
|
|
804
|
+
const lines = [];
|
|
805
|
+
const prefix = " ".repeat(indent);
|
|
806
|
+
for (const element of elements) {
|
|
807
|
+
const parts = [];
|
|
808
|
+
// [ClassName] "text" or "content-desc"
|
|
809
|
+
parts.push(`[${element.class}]`);
|
|
810
|
+
// Add label (text or content-desc)
|
|
811
|
+
const label = element.text || element.contentDesc;
|
|
812
|
+
if (label) {
|
|
813
|
+
parts.push(`"${label}"`);
|
|
814
|
+
}
|
|
815
|
+
// Add frame and tap coordinates
|
|
816
|
+
const f = element.frame;
|
|
817
|
+
parts.push(`frame=(${f.x}, ${f.y}, ${f.width}x${f.height}) tap=(${element.tap.x}, ${element.tap.y})`);
|
|
818
|
+
lines.push(`${prefix}${parts.join(" ")}`);
|
|
819
|
+
// Recurse into children
|
|
820
|
+
if (element.children.length > 0) {
|
|
821
|
+
lines.push(formatAndroidAccessibilityTree(element.children, indent + 1));
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
return lines.join("\n");
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* Flatten element tree to array for searching
|
|
828
|
+
*/
|
|
829
|
+
function flattenElements(elements) {
|
|
830
|
+
const result = [];
|
|
831
|
+
for (const element of elements) {
|
|
832
|
+
result.push(element);
|
|
833
|
+
if (element.children.length > 0) {
|
|
834
|
+
result.push(...flattenElements(element.children));
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
return result;
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Get the UI hierarchy from the connected Android device using uiautomator dump
|
|
841
|
+
*/
|
|
842
|
+
export async function androidDescribeAll(deviceId) {
|
|
843
|
+
try {
|
|
844
|
+
const adbAvailable = await isAdbAvailable();
|
|
845
|
+
if (!adbAvailable) {
|
|
846
|
+
return {
|
|
847
|
+
success: false,
|
|
848
|
+
error: "ADB is not installed or not in PATH. Install Android SDK Platform Tools."
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
const deviceArg = buildDeviceArg(deviceId);
|
|
852
|
+
const device = deviceId || (await getDefaultAndroidDevice());
|
|
853
|
+
if (!device) {
|
|
854
|
+
return {
|
|
855
|
+
success: false,
|
|
856
|
+
error: "No Android device connected. Connect a device or start an emulator."
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
// Use file-based approach (most reliable across devices)
|
|
860
|
+
// /dev/tty doesn't work on most emulators/devices
|
|
861
|
+
const remotePath = "/sdcard/ui_dump.xml";
|
|
862
|
+
await execAsync(`adb ${deviceArg} shell uiautomator dump ${remotePath}`, {
|
|
863
|
+
timeout: ADB_TIMEOUT
|
|
864
|
+
});
|
|
865
|
+
const { stdout } = await execAsync(`adb ${deviceArg} shell cat ${remotePath}`, {
|
|
866
|
+
timeout: ADB_TIMEOUT,
|
|
867
|
+
maxBuffer: 10 * 1024 * 1024
|
|
868
|
+
});
|
|
869
|
+
const xmlContent = stdout.trim();
|
|
870
|
+
// Clean up
|
|
871
|
+
await execAsync(`adb ${deviceArg} shell rm ${remotePath}`, {
|
|
872
|
+
timeout: 5000
|
|
873
|
+
}).catch(() => { });
|
|
874
|
+
if (!xmlContent || !xmlContent.includes("<hierarchy")) {
|
|
875
|
+
return {
|
|
876
|
+
success: false,
|
|
877
|
+
error: "Failed to get UI hierarchy. Make sure the device screen is unlocked and the app is in foreground."
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
// Parse XML
|
|
881
|
+
const parser = new XMLParser({
|
|
882
|
+
ignoreAttributes: false,
|
|
883
|
+
attributeNamePrefix: "@_"
|
|
884
|
+
});
|
|
885
|
+
const parsed = parser.parse(xmlContent);
|
|
886
|
+
if (!parsed.hierarchy) {
|
|
887
|
+
return {
|
|
888
|
+
success: false,
|
|
889
|
+
error: "Invalid UI hierarchy XML structure"
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
const elements = parseHierarchy(parsed.hierarchy);
|
|
893
|
+
const formatted = formatAndroidAccessibilityTree(elements);
|
|
894
|
+
return {
|
|
895
|
+
success: true,
|
|
896
|
+
elements,
|
|
897
|
+
formatted
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
catch (error) {
|
|
901
|
+
return {
|
|
902
|
+
success: false,
|
|
903
|
+
error: `Failed to get UI hierarchy: ${error instanceof Error ? error.message : String(error)}`
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* Get accessibility info for the UI element at specific coordinates
|
|
909
|
+
*/
|
|
910
|
+
export async function androidDescribePoint(x, y, deviceId) {
|
|
911
|
+
try {
|
|
912
|
+
// First get the full hierarchy
|
|
913
|
+
const result = await androidDescribeAll(deviceId);
|
|
914
|
+
if (!result.success || !result.elements) {
|
|
915
|
+
return result;
|
|
916
|
+
}
|
|
917
|
+
// Flatten and find elements containing the point
|
|
918
|
+
const allElements = flattenElements(result.elements);
|
|
919
|
+
// Find all elements whose bounds contain the point
|
|
920
|
+
const matchingElements = allElements.filter((el) => {
|
|
921
|
+
const b = el.bounds;
|
|
922
|
+
return x >= b.left && x <= b.right && y >= b.top && y <= b.bottom;
|
|
923
|
+
});
|
|
924
|
+
if (matchingElements.length === 0) {
|
|
925
|
+
return {
|
|
926
|
+
success: true,
|
|
927
|
+
formatted: `No element found at (${x}, ${y})`
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
// Return the deepest (smallest) element that contains the point
|
|
931
|
+
// Sort by area (smallest first) to get the most specific element
|
|
932
|
+
matchingElements.sort((a, b) => {
|
|
933
|
+
const areaA = a.frame.width * a.frame.height;
|
|
934
|
+
const areaB = b.frame.width * b.frame.height;
|
|
935
|
+
return areaA - areaB;
|
|
936
|
+
});
|
|
937
|
+
const element = matchingElements[0];
|
|
938
|
+
// Format detailed output
|
|
939
|
+
const lines = [];
|
|
940
|
+
const label = element.text || element.contentDesc;
|
|
941
|
+
lines.push(`[${element.class}]${label ? ` "${label}"` : ""} frame=(${element.frame.x}, ${element.frame.y}, ${element.frame.width}x${element.frame.height}) tap=(${element.tap.x}, ${element.tap.y})`);
|
|
942
|
+
if (element.resourceId) {
|
|
943
|
+
lines.push(` resource-id: ${element.resourceId}`);
|
|
944
|
+
}
|
|
945
|
+
if (element.contentDesc && element.text) {
|
|
946
|
+
// Show content-desc separately if we showed text as label
|
|
947
|
+
lines.push(` content-desc: ${element.contentDesc}`);
|
|
948
|
+
}
|
|
949
|
+
if (element.text && element.contentDesc) {
|
|
950
|
+
// Show text separately if we showed content-desc as label
|
|
951
|
+
lines.push(` text: ${element.text}`);
|
|
952
|
+
}
|
|
953
|
+
// Show state flags
|
|
954
|
+
const flags = [];
|
|
955
|
+
if (element.clickable)
|
|
956
|
+
flags.push("clickable");
|
|
957
|
+
if (element.enabled)
|
|
958
|
+
flags.push("enabled");
|
|
959
|
+
if (element.focusable)
|
|
960
|
+
flags.push("focusable");
|
|
961
|
+
if (element.focused)
|
|
962
|
+
flags.push("focused");
|
|
963
|
+
if (element.scrollable)
|
|
964
|
+
flags.push("scrollable");
|
|
965
|
+
if (element.selected)
|
|
966
|
+
flags.push("selected");
|
|
967
|
+
if (element.checked)
|
|
968
|
+
flags.push("checked");
|
|
969
|
+
if (flags.length > 0) {
|
|
970
|
+
lines.push(` state: ${flags.join(", ")}`);
|
|
971
|
+
}
|
|
972
|
+
return {
|
|
973
|
+
success: true,
|
|
974
|
+
elements: [element],
|
|
975
|
+
formatted: lines.join("\n")
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
catch (error) {
|
|
979
|
+
return {
|
|
980
|
+
success: false,
|
|
981
|
+
error: `Failed to describe point: ${error instanceof Error ? error.message : String(error)}`
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Tap an element by its text, content-description, or resource-id
|
|
987
|
+
*/
|
|
988
|
+
export async function androidTapElement(options) {
|
|
989
|
+
try {
|
|
990
|
+
const { text, textContains, contentDesc, contentDescContains, resourceId, index = 0, deviceId } = options;
|
|
991
|
+
// Validate that at least one search criterion is provided
|
|
992
|
+
if (!text && !textContains && !contentDesc && !contentDescContains && !resourceId) {
|
|
993
|
+
return {
|
|
994
|
+
success: false,
|
|
995
|
+
error: "At least one of text, textContains, contentDesc, contentDescContains, or resourceId must be provided"
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
// Get the UI hierarchy
|
|
999
|
+
const result = await androidDescribeAll(deviceId);
|
|
1000
|
+
if (!result.success || !result.elements) {
|
|
1001
|
+
return {
|
|
1002
|
+
success: false,
|
|
1003
|
+
error: result.error || "Failed to get UI hierarchy"
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
// Flatten and search
|
|
1007
|
+
const allElements = flattenElements(result.elements);
|
|
1008
|
+
// Filter elements based on search criteria
|
|
1009
|
+
const matchingElements = allElements.filter((el) => {
|
|
1010
|
+
if (text && el.text !== text)
|
|
1011
|
+
return false;
|
|
1012
|
+
if (textContains && (!el.text || !el.text.toLowerCase().includes(textContains.toLowerCase())))
|
|
1013
|
+
return false;
|
|
1014
|
+
if (contentDesc && el.contentDesc !== contentDesc)
|
|
1015
|
+
return false;
|
|
1016
|
+
if (contentDescContains && (!el.contentDesc || !el.contentDesc.toLowerCase().includes(contentDescContains.toLowerCase())))
|
|
1017
|
+
return false;
|
|
1018
|
+
if (resourceId) {
|
|
1019
|
+
// Support both full resource-id and short form
|
|
1020
|
+
if (!el.resourceId)
|
|
1021
|
+
return false;
|
|
1022
|
+
if (el.resourceId !== resourceId && !el.resourceId.endsWith(`:id/${resourceId}`))
|
|
1023
|
+
return false;
|
|
1024
|
+
}
|
|
1025
|
+
return true;
|
|
1026
|
+
});
|
|
1027
|
+
if (matchingElements.length === 0) {
|
|
1028
|
+
const criteria = [];
|
|
1029
|
+
if (text)
|
|
1030
|
+
criteria.push(`text="${text}"`);
|
|
1031
|
+
if (textContains)
|
|
1032
|
+
criteria.push(`textContains="${textContains}"`);
|
|
1033
|
+
if (contentDesc)
|
|
1034
|
+
criteria.push(`contentDesc="${contentDesc}"`);
|
|
1035
|
+
if (contentDescContains)
|
|
1036
|
+
criteria.push(`contentDescContains="${contentDescContains}"`);
|
|
1037
|
+
if (resourceId)
|
|
1038
|
+
criteria.push(`resourceId="${resourceId}"`);
|
|
1039
|
+
return {
|
|
1040
|
+
success: false,
|
|
1041
|
+
error: `Element not found: ${criteria.join(", ")}`
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
if (index >= matchingElements.length) {
|
|
1045
|
+
return {
|
|
1046
|
+
success: false,
|
|
1047
|
+
error: `Index ${index} out of range. Found ${matchingElements.length} matching element(s).`
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
const element = matchingElements[index];
|
|
1051
|
+
const label = element.text || element.contentDesc || element.resourceId || element.class;
|
|
1052
|
+
// Log if multiple matches
|
|
1053
|
+
let resultMessage;
|
|
1054
|
+
if (matchingElements.length > 1) {
|
|
1055
|
+
resultMessage = `Found ${matchingElements.length} elements, tapping "${label}" (index ${index}) at (${element.tap.x}, ${element.tap.y})`;
|
|
1056
|
+
}
|
|
1057
|
+
else {
|
|
1058
|
+
resultMessage = `Tapped "${label}" at (${element.tap.x}, ${element.tap.y})`;
|
|
1059
|
+
}
|
|
1060
|
+
// Perform the tap
|
|
1061
|
+
const tapResult = await androidTap(element.tap.x, element.tap.y, deviceId);
|
|
1062
|
+
if (!tapResult.success) {
|
|
1063
|
+
return tapResult;
|
|
1064
|
+
}
|
|
1065
|
+
return {
|
|
1066
|
+
success: true,
|
|
1067
|
+
result: resultMessage
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
catch (error) {
|
|
1071
|
+
return {
|
|
1072
|
+
success: false,
|
|
1073
|
+
error: `Failed to tap element: ${error instanceof Error ? error.message : String(error)}`
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
686
1077
|
//# sourceMappingURL=android.js.map
|