react-native-ai-debugger 1.0.6 → 1.0.8

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.
@@ -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
  /**
@@ -639,6 +641,245 @@ export async function androidKeyEvent(keyCode, deviceId) {
639
641
  };
640
642
  }
641
643
  }
644
+ /**
645
+ * Parse bounds string like "[0,0][1080,1920]" to AndroidUIElement bounds
646
+ */
647
+ function parseBoundsForUIElement(boundsStr) {
648
+ const match = boundsStr.match(/\[(\d+),(\d+)\]\[(\d+),(\d+)\]/);
649
+ if (!match)
650
+ return null;
651
+ const left = parseInt(match[1], 10);
652
+ const top = parseInt(match[2], 10);
653
+ const right = parseInt(match[3], 10);
654
+ const bottom = parseInt(match[4], 10);
655
+ return {
656
+ left,
657
+ top,
658
+ right,
659
+ bottom,
660
+ width: right - left,
661
+ height: bottom - top
662
+ };
663
+ }
664
+ /**
665
+ * Parse uiautomator XML dump into element array
666
+ */
667
+ function parseUIAutomatorXML(xml) {
668
+ const elements = [];
669
+ // Match all node elements with their attributes
670
+ const nodeRegex = /<node\s+([^>]+)\/?>|<node\s+([^>]+)>/g;
671
+ let match;
672
+ while ((match = nodeRegex.exec(xml)) !== null) {
673
+ const attrStr = match[1] || match[2];
674
+ if (!attrStr)
675
+ continue;
676
+ // Extract attributes
677
+ const getAttr = (name) => {
678
+ const attrMatch = attrStr.match(new RegExp(`${name}="([^"]*)"`));
679
+ return attrMatch ? attrMatch[1] : "";
680
+ };
681
+ const boundsStr = getAttr("bounds");
682
+ const bounds = parseBoundsForUIElement(boundsStr);
683
+ if (!bounds)
684
+ continue;
685
+ const element = {
686
+ text: getAttr("text"),
687
+ contentDesc: getAttr("content-desc"),
688
+ resourceId: getAttr("resource-id"),
689
+ className: getAttr("class"),
690
+ bounds,
691
+ center: {
692
+ x: Math.round((bounds.left + bounds.right) / 2),
693
+ y: Math.round((bounds.top + bounds.bottom) / 2)
694
+ },
695
+ clickable: getAttr("clickable") === "true",
696
+ enabled: getAttr("enabled") === "true",
697
+ focused: getAttr("focused") === "true",
698
+ scrollable: getAttr("scrollable") === "true",
699
+ selected: getAttr("selected") === "true"
700
+ };
701
+ elements.push(element);
702
+ }
703
+ return elements;
704
+ }
705
+ /**
706
+ * Match element against find options
707
+ */
708
+ function matchesElement(element, options) {
709
+ if (options.text !== undefined) {
710
+ if (element.text !== options.text)
711
+ return false;
712
+ }
713
+ if (options.textContains !== undefined) {
714
+ if (!element.text.toLowerCase().includes(options.textContains.toLowerCase()))
715
+ return false;
716
+ }
717
+ if (options.contentDesc !== undefined) {
718
+ if (element.contentDesc !== options.contentDesc)
719
+ return false;
720
+ }
721
+ if (options.contentDescContains !== undefined) {
722
+ if (!element.contentDesc.toLowerCase().includes(options.contentDescContains.toLowerCase()))
723
+ return false;
724
+ }
725
+ if (options.resourceId !== undefined) {
726
+ // Support both full "com.app:id/button" and short "button" forms
727
+ const shortId = element.resourceId.split("/").pop() || "";
728
+ if (element.resourceId !== options.resourceId && shortId !== options.resourceId)
729
+ return false;
730
+ }
731
+ return true;
732
+ }
733
+ /**
734
+ * Get UI accessibility tree from Android device using uiautomator
735
+ */
736
+ export async function androidGetUITree(deviceId) {
737
+ try {
738
+ const adbAvailable = await isAdbAvailable();
739
+ if (!adbAvailable) {
740
+ return {
741
+ success: false,
742
+ error: "ADB is not installed or not in PATH. Install Android SDK Platform Tools."
743
+ };
744
+ }
745
+ const deviceArg = buildDeviceArg(deviceId);
746
+ const device = deviceId || (await getDefaultAndroidDevice());
747
+ if (!device) {
748
+ return {
749
+ success: false,
750
+ error: "No Android device connected. Connect a device or start an emulator."
751
+ };
752
+ }
753
+ // Dump UI hierarchy to device
754
+ const remotePath = "/sdcard/ui_dump.xml";
755
+ await execAsync(`adb ${deviceArg} shell uiautomator dump ${remotePath}`, {
756
+ timeout: ADB_TIMEOUT
757
+ });
758
+ // Read the XML content
759
+ const { stdout } = await execAsync(`adb ${deviceArg} shell cat ${remotePath}`, {
760
+ timeout: ADB_TIMEOUT
761
+ });
762
+ // Clean up remote file
763
+ await execAsync(`adb ${deviceArg} shell rm ${remotePath}`, {
764
+ timeout: ADB_TIMEOUT
765
+ }).catch(() => { });
766
+ const elements = parseUIAutomatorXML(stdout);
767
+ return {
768
+ success: true,
769
+ elements,
770
+ rawXml: stdout
771
+ };
772
+ }
773
+ catch (error) {
774
+ return {
775
+ success: false,
776
+ error: `Failed to get UI tree: ${error instanceof Error ? error.message : String(error)}`
777
+ };
778
+ }
779
+ }
780
+ /**
781
+ * Find element(s) in the UI tree matching the given criteria
782
+ */
783
+ export async function androidFindElement(options, deviceId) {
784
+ try {
785
+ // Validate that at least one search criteria is provided
786
+ if (!options.text && !options.textContains && !options.contentDesc &&
787
+ !options.contentDescContains && !options.resourceId) {
788
+ return {
789
+ success: false,
790
+ found: false,
791
+ error: "At least one search criteria (text, textContains, contentDesc, contentDescContains, or resourceId) must be provided"
792
+ };
793
+ }
794
+ const treeResult = await androidGetUITree(deviceId);
795
+ if (!treeResult.success || !treeResult.elements) {
796
+ return {
797
+ success: false,
798
+ found: false,
799
+ error: treeResult.error
800
+ };
801
+ }
802
+ // Find matching elements
803
+ const matches = treeResult.elements.filter(el => matchesElement(el, options));
804
+ if (matches.length === 0) {
805
+ return {
806
+ success: true,
807
+ found: false,
808
+ matchCount: 0
809
+ };
810
+ }
811
+ // Select the element at the specified index (default 0)
812
+ const index = options.index ?? 0;
813
+ const selectedElement = matches[index];
814
+ if (!selectedElement) {
815
+ return {
816
+ success: true,
817
+ found: false,
818
+ matchCount: matches.length,
819
+ error: `Index ${index} out of bounds. Found ${matches.length} matching element(s).`
820
+ };
821
+ }
822
+ return {
823
+ success: true,
824
+ found: true,
825
+ element: selectedElement,
826
+ allMatches: matches,
827
+ matchCount: matches.length
828
+ };
829
+ }
830
+ catch (error) {
831
+ return {
832
+ success: false,
833
+ found: false,
834
+ error: `Failed to find element: ${error instanceof Error ? error.message : String(error)}`
835
+ };
836
+ }
837
+ }
838
+ /**
839
+ * Wait for element to appear on screen with polling
840
+ */
841
+ export async function androidWaitForElement(options, deviceId) {
842
+ const timeoutMs = options.timeoutMs ?? 10000;
843
+ const pollIntervalMs = options.pollIntervalMs ?? 500;
844
+ const startTime = Date.now();
845
+ // Validate that at least one search criteria is provided
846
+ if (!options.text && !options.textContains && !options.contentDesc &&
847
+ !options.contentDescContains && !options.resourceId) {
848
+ return {
849
+ success: false,
850
+ found: false,
851
+ timedOut: false,
852
+ error: "At least one search criteria (text, textContains, contentDesc, contentDescContains, or resourceId) must be provided"
853
+ };
854
+ }
855
+ while (Date.now() - startTime < timeoutMs) {
856
+ const result = await androidFindElement(options, deviceId);
857
+ if (result.found && result.element) {
858
+ return {
859
+ ...result,
860
+ elapsedMs: Date.now() - startTime,
861
+ timedOut: false
862
+ };
863
+ }
864
+ // If there was an error (not just "not found"), return it
865
+ if (!result.success) {
866
+ return {
867
+ ...result,
868
+ elapsedMs: Date.now() - startTime,
869
+ timedOut: false
870
+ };
871
+ }
872
+ // Wait before next poll
873
+ await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
874
+ }
875
+ return {
876
+ success: true,
877
+ found: false,
878
+ elapsedMs: Date.now() - startTime,
879
+ timedOut: true,
880
+ error: `Timed out after ${timeoutMs}ms waiting for element`
881
+ };
882
+ }
642
883
  /**
643
884
  * Get device screen size
644
885
  */
@@ -683,4 +924,393 @@ export async function androidGetScreenSize(deviceId) {
683
924
  };
684
925
  }
685
926
  }
927
+ /**
928
+ * Simplify Android class name for display
929
+ * android.widget.Button -> Button
930
+ * android.widget.TextView -> TextView
931
+ */
932
+ function simplifyClassName(className) {
933
+ if (!className)
934
+ return "Unknown";
935
+ const parts = className.split(".");
936
+ return parts[parts.length - 1];
937
+ }
938
+ /**
939
+ * Parse bounds string "[left,top][right,bottom]" to object
940
+ */
941
+ function parseBounds(boundsStr) {
942
+ const match = boundsStr?.match(/\[(\d+),(\d+)\]\[(\d+),(\d+)\]/);
943
+ if (!match)
944
+ return null;
945
+ return {
946
+ left: parseInt(match[1], 10),
947
+ top: parseInt(match[2], 10),
948
+ right: parseInt(match[3], 10),
949
+ bottom: parseInt(match[4], 10)
950
+ };
951
+ }
952
+ /**
953
+ * Parse a single node from uiautomator XML
954
+ */
955
+ function parseUiNode(node) {
956
+ const attrs = node["@_bounds"]
957
+ ? node
958
+ : node.node
959
+ ? (Array.isArray(node.node) ? node.node[0] : node.node)
960
+ : null;
961
+ if (!attrs)
962
+ return null;
963
+ const boundsStr = attrs["@_bounds"];
964
+ const bounds = parseBounds(boundsStr);
965
+ if (!bounds)
966
+ return null;
967
+ const width = bounds.right - bounds.left;
968
+ const height = bounds.bottom - bounds.top;
969
+ const centerX = Math.round(bounds.left + width / 2);
970
+ const centerY = Math.round(bounds.top + height / 2);
971
+ const element = {
972
+ class: simplifyClassName(attrs["@_class"] || ""),
973
+ bounds,
974
+ frame: {
975
+ x: bounds.left,
976
+ y: bounds.top,
977
+ width,
978
+ height
979
+ },
980
+ tap: {
981
+ x: centerX,
982
+ y: centerY
983
+ },
984
+ children: []
985
+ };
986
+ // Add optional attributes
987
+ if (attrs["@_text"])
988
+ element.text = attrs["@_text"];
989
+ if (attrs["@_content-desc"])
990
+ element.contentDesc = attrs["@_content-desc"];
991
+ if (attrs["@_resource-id"])
992
+ element.resourceId = attrs["@_resource-id"];
993
+ if (attrs["@_checkable"] === "true")
994
+ element.checkable = true;
995
+ if (attrs["@_checked"] === "true")
996
+ element.checked = true;
997
+ if (attrs["@_clickable"] === "true")
998
+ element.clickable = true;
999
+ if (attrs["@_enabled"] === "true")
1000
+ element.enabled = true;
1001
+ if (attrs["@_focusable"] === "true")
1002
+ element.focusable = true;
1003
+ if (attrs["@_focused"] === "true")
1004
+ element.focused = true;
1005
+ if (attrs["@_scrollable"] === "true")
1006
+ element.scrollable = true;
1007
+ if (attrs["@_selected"] === "true")
1008
+ element.selected = true;
1009
+ return element;
1010
+ }
1011
+ /**
1012
+ * Recursively parse UI hierarchy from XML node
1013
+ */
1014
+ function parseHierarchy(node) {
1015
+ const results = [];
1016
+ // Handle the node itself
1017
+ if (node["@_bounds"]) {
1018
+ const element = parseUiNode(node);
1019
+ if (element) {
1020
+ // Parse children
1021
+ if (node.node) {
1022
+ const children = Array.isArray(node.node) ? node.node : [node.node];
1023
+ for (const child of children) {
1024
+ element.children.push(...parseHierarchy(child));
1025
+ }
1026
+ }
1027
+ results.push(element);
1028
+ }
1029
+ }
1030
+ else if (node.node) {
1031
+ // This is a container without bounds (like hierarchy root)
1032
+ const children = Array.isArray(node.node) ? node.node : [node.node];
1033
+ for (const child of children) {
1034
+ results.push(...parseHierarchy(child));
1035
+ }
1036
+ }
1037
+ return results;
1038
+ }
1039
+ /**
1040
+ * Format accessibility tree for display (similar to iOS format)
1041
+ */
1042
+ function formatAndroidAccessibilityTree(elements, indent = 0) {
1043
+ const lines = [];
1044
+ const prefix = " ".repeat(indent);
1045
+ for (const element of elements) {
1046
+ const parts = [];
1047
+ // [ClassName] "text" or "content-desc"
1048
+ parts.push(`[${element.class}]`);
1049
+ // Add label (text or content-desc)
1050
+ const label = element.text || element.contentDesc;
1051
+ if (label) {
1052
+ parts.push(`"${label}"`);
1053
+ }
1054
+ // Add frame and tap coordinates
1055
+ const f = element.frame;
1056
+ parts.push(`frame=(${f.x}, ${f.y}, ${f.width}x${f.height}) tap=(${element.tap.x}, ${element.tap.y})`);
1057
+ lines.push(`${prefix}${parts.join(" ")}`);
1058
+ // Recurse into children
1059
+ if (element.children.length > 0) {
1060
+ lines.push(formatAndroidAccessibilityTree(element.children, indent + 1));
1061
+ }
1062
+ }
1063
+ return lines.join("\n");
1064
+ }
1065
+ /**
1066
+ * Flatten element tree to array for searching
1067
+ */
1068
+ function flattenElements(elements) {
1069
+ const result = [];
1070
+ for (const element of elements) {
1071
+ result.push(element);
1072
+ if (element.children.length > 0) {
1073
+ result.push(...flattenElements(element.children));
1074
+ }
1075
+ }
1076
+ return result;
1077
+ }
1078
+ /**
1079
+ * Get the UI hierarchy from the connected Android device using uiautomator dump
1080
+ */
1081
+ export async function androidDescribeAll(deviceId) {
1082
+ try {
1083
+ const adbAvailable = await isAdbAvailable();
1084
+ if (!adbAvailable) {
1085
+ return {
1086
+ success: false,
1087
+ error: "ADB is not installed or not in PATH. Install Android SDK Platform Tools."
1088
+ };
1089
+ }
1090
+ const deviceArg = buildDeviceArg(deviceId);
1091
+ const device = deviceId || (await getDefaultAndroidDevice());
1092
+ if (!device) {
1093
+ return {
1094
+ success: false,
1095
+ error: "No Android device connected. Connect a device or start an emulator."
1096
+ };
1097
+ }
1098
+ // Use file-based approach (most reliable across devices)
1099
+ // /dev/tty doesn't work on most emulators/devices
1100
+ const remotePath = "/sdcard/ui_dump.xml";
1101
+ await execAsync(`adb ${deviceArg} shell uiautomator dump ${remotePath}`, {
1102
+ timeout: ADB_TIMEOUT
1103
+ });
1104
+ const { stdout } = await execAsync(`adb ${deviceArg} shell cat ${remotePath}`, {
1105
+ timeout: ADB_TIMEOUT,
1106
+ maxBuffer: 10 * 1024 * 1024
1107
+ });
1108
+ const xmlContent = stdout.trim();
1109
+ // Clean up
1110
+ await execAsync(`adb ${deviceArg} shell rm ${remotePath}`, {
1111
+ timeout: 5000
1112
+ }).catch(() => { });
1113
+ if (!xmlContent || !xmlContent.includes("<hierarchy")) {
1114
+ return {
1115
+ success: false,
1116
+ error: "Failed to get UI hierarchy. Make sure the device screen is unlocked and the app is in foreground."
1117
+ };
1118
+ }
1119
+ // Parse XML
1120
+ const parser = new XMLParser({
1121
+ ignoreAttributes: false,
1122
+ attributeNamePrefix: "@_"
1123
+ });
1124
+ const parsed = parser.parse(xmlContent);
1125
+ if (!parsed.hierarchy) {
1126
+ return {
1127
+ success: false,
1128
+ error: "Invalid UI hierarchy XML structure"
1129
+ };
1130
+ }
1131
+ const elements = parseHierarchy(parsed.hierarchy);
1132
+ const formatted = formatAndroidAccessibilityTree(elements);
1133
+ return {
1134
+ success: true,
1135
+ elements,
1136
+ formatted
1137
+ };
1138
+ }
1139
+ catch (error) {
1140
+ return {
1141
+ success: false,
1142
+ error: `Failed to get UI hierarchy: ${error instanceof Error ? error.message : String(error)}`
1143
+ };
1144
+ }
1145
+ }
1146
+ /**
1147
+ * Get accessibility info for the UI element at specific coordinates
1148
+ */
1149
+ export async function androidDescribePoint(x, y, deviceId) {
1150
+ try {
1151
+ // First get the full hierarchy
1152
+ const result = await androidDescribeAll(deviceId);
1153
+ if (!result.success || !result.elements) {
1154
+ return result;
1155
+ }
1156
+ // Flatten and find elements containing the point
1157
+ const allElements = flattenElements(result.elements);
1158
+ // Find all elements whose bounds contain the point
1159
+ const matchingElements = allElements.filter((el) => {
1160
+ const b = el.bounds;
1161
+ return x >= b.left && x <= b.right && y >= b.top && y <= b.bottom;
1162
+ });
1163
+ if (matchingElements.length === 0) {
1164
+ return {
1165
+ success: true,
1166
+ formatted: `No element found at (${x}, ${y})`
1167
+ };
1168
+ }
1169
+ // Return the deepest (smallest) element that contains the point
1170
+ // Sort by area (smallest first) to get the most specific element
1171
+ matchingElements.sort((a, b) => {
1172
+ const areaA = a.frame.width * a.frame.height;
1173
+ const areaB = b.frame.width * b.frame.height;
1174
+ return areaA - areaB;
1175
+ });
1176
+ const element = matchingElements[0];
1177
+ // Format detailed output
1178
+ const lines = [];
1179
+ const label = element.text || element.contentDesc;
1180
+ 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})`);
1181
+ if (element.resourceId) {
1182
+ lines.push(` resource-id: ${element.resourceId}`);
1183
+ }
1184
+ if (element.contentDesc && element.text) {
1185
+ // Show content-desc separately if we showed text as label
1186
+ lines.push(` content-desc: ${element.contentDesc}`);
1187
+ }
1188
+ if (element.text && element.contentDesc) {
1189
+ // Show text separately if we showed content-desc as label
1190
+ lines.push(` text: ${element.text}`);
1191
+ }
1192
+ // Show state flags
1193
+ const flags = [];
1194
+ if (element.clickable)
1195
+ flags.push("clickable");
1196
+ if (element.enabled)
1197
+ flags.push("enabled");
1198
+ if (element.focusable)
1199
+ flags.push("focusable");
1200
+ if (element.focused)
1201
+ flags.push("focused");
1202
+ if (element.scrollable)
1203
+ flags.push("scrollable");
1204
+ if (element.selected)
1205
+ flags.push("selected");
1206
+ if (element.checked)
1207
+ flags.push("checked");
1208
+ if (flags.length > 0) {
1209
+ lines.push(` state: ${flags.join(", ")}`);
1210
+ }
1211
+ return {
1212
+ success: true,
1213
+ elements: [element],
1214
+ formatted: lines.join("\n")
1215
+ };
1216
+ }
1217
+ catch (error) {
1218
+ return {
1219
+ success: false,
1220
+ error: `Failed to describe point: ${error instanceof Error ? error.message : String(error)}`
1221
+ };
1222
+ }
1223
+ }
1224
+ /**
1225
+ * Tap an element by its text, content-description, or resource-id
1226
+ */
1227
+ export async function androidTapElement(options) {
1228
+ try {
1229
+ const { text, textContains, contentDesc, contentDescContains, resourceId, index = 0, deviceId } = options;
1230
+ // Validate that at least one search criterion is provided
1231
+ if (!text && !textContains && !contentDesc && !contentDescContains && !resourceId) {
1232
+ return {
1233
+ success: false,
1234
+ error: "At least one of text, textContains, contentDesc, contentDescContains, or resourceId must be provided"
1235
+ };
1236
+ }
1237
+ // Get the UI hierarchy
1238
+ const result = await androidDescribeAll(deviceId);
1239
+ if (!result.success || !result.elements) {
1240
+ return {
1241
+ success: false,
1242
+ error: result.error || "Failed to get UI hierarchy"
1243
+ };
1244
+ }
1245
+ // Flatten and search
1246
+ const allElements = flattenElements(result.elements);
1247
+ // Filter elements based on search criteria
1248
+ const matchingElements = allElements.filter((el) => {
1249
+ if (text && el.text !== text)
1250
+ return false;
1251
+ if (textContains && (!el.text || !el.text.toLowerCase().includes(textContains.toLowerCase())))
1252
+ return false;
1253
+ if (contentDesc && el.contentDesc !== contentDesc)
1254
+ return false;
1255
+ if (contentDescContains && (!el.contentDesc || !el.contentDesc.toLowerCase().includes(contentDescContains.toLowerCase())))
1256
+ return false;
1257
+ if (resourceId) {
1258
+ // Support both full resource-id and short form
1259
+ if (!el.resourceId)
1260
+ return false;
1261
+ if (el.resourceId !== resourceId && !el.resourceId.endsWith(`:id/${resourceId}`))
1262
+ return false;
1263
+ }
1264
+ return true;
1265
+ });
1266
+ if (matchingElements.length === 0) {
1267
+ const criteria = [];
1268
+ if (text)
1269
+ criteria.push(`text="${text}"`);
1270
+ if (textContains)
1271
+ criteria.push(`textContains="${textContains}"`);
1272
+ if (contentDesc)
1273
+ criteria.push(`contentDesc="${contentDesc}"`);
1274
+ if (contentDescContains)
1275
+ criteria.push(`contentDescContains="${contentDescContains}"`);
1276
+ if (resourceId)
1277
+ criteria.push(`resourceId="${resourceId}"`);
1278
+ return {
1279
+ success: false,
1280
+ error: `Element not found: ${criteria.join(", ")}`
1281
+ };
1282
+ }
1283
+ if (index >= matchingElements.length) {
1284
+ return {
1285
+ success: false,
1286
+ error: `Index ${index} out of range. Found ${matchingElements.length} matching element(s).`
1287
+ };
1288
+ }
1289
+ const element = matchingElements[index];
1290
+ const label = element.text || element.contentDesc || element.resourceId || element.class;
1291
+ // Log if multiple matches
1292
+ let resultMessage;
1293
+ if (matchingElements.length > 1) {
1294
+ resultMessage = `Found ${matchingElements.length} elements, tapping "${label}" (index ${index}) at (${element.tap.x}, ${element.tap.y})`;
1295
+ }
1296
+ else {
1297
+ resultMessage = `Tapped "${label}" at (${element.tap.x}, ${element.tap.y})`;
1298
+ }
1299
+ // Perform the tap
1300
+ const tapResult = await androidTap(element.tap.x, element.tap.y, deviceId);
1301
+ if (!tapResult.success) {
1302
+ return tapResult;
1303
+ }
1304
+ return {
1305
+ success: true,
1306
+ result: resultMessage
1307
+ };
1308
+ }
1309
+ catch (error) {
1310
+ return {
1311
+ success: false,
1312
+ error: `Failed to tap element: ${error instanceof Error ? error.message : String(error)}`
1313
+ };
1314
+ }
1315
+ }
686
1316
  //# sourceMappingURL=android.js.map