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 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 |
@@ -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":"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"}
1
+ {"version":3,"file":"android.d.ts","sourceRoot":"","sources":["../../src/core/android.ts"],"names":[],"mappings":"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"}
@@ -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