react-native-ai-debugger 1.8.0 → 1.8.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.
Files changed (107) hide show
  1. package/build/core/appDetection.d.ts +7 -0
  2. package/build/core/appDetection.d.ts.map +1 -1
  3. package/build/core/appDetection.js +38 -17
  4. package/build/core/appDetection.js.map +1 -1
  5. package/build/core/componentSearch.d.ts +38 -0
  6. package/build/core/componentSearch.d.ts.map +1 -0
  7. package/build/core/componentSearch.js +419 -0
  8. package/build/core/componentSearch.js.map +1 -0
  9. package/build/core/componentTree.d.ts +12 -0
  10. package/build/core/componentTree.d.ts.map +1 -0
  11. package/build/core/componentTree.js +275 -0
  12. package/build/core/componentTree.js.map +1 -0
  13. package/build/core/connection.d.ts +1 -0
  14. package/build/core/connection.d.ts.map +1 -1
  15. package/build/core/connection.js +97 -88
  16. package/build/core/connection.js.map +1 -1
  17. package/build/core/debugGlobals.d.ts +5 -0
  18. package/build/core/debugGlobals.d.ts.map +1 -0
  19. package/build/core/debugGlobals.js +300 -0
  20. package/build/core/debugGlobals.js.map +1 -0
  21. package/build/core/executor.d.ts +7 -206
  22. package/build/core/executor.d.ts.map +1 -1
  23. package/build/core/executor.js +10 -4333
  24. package/build/core/executor.js.map +1 -1
  25. package/build/core/index.d.ts +3 -2
  26. package/build/core/index.d.ts.map +1 -1
  27. package/build/core/index.js +3 -2
  28. package/build/core/index.js.map +1 -1
  29. package/build/core/inspector.d.ts +50 -0
  30. package/build/core/inspector.d.ts.map +1 -0
  31. package/build/core/inspector.js +604 -0
  32. package/build/core/inspector.js.map +1 -0
  33. package/build/core/jsExecute.d.ts +27 -0
  34. package/build/core/jsExecute.d.ts.map +1 -0
  35. package/build/core/jsExecute.js +453 -0
  36. package/build/core/jsExecute.js.map +1 -0
  37. package/build/core/pressables.d.ts +44 -0
  38. package/build/core/pressables.d.ts.map +1 -0
  39. package/build/core/pressables.js +1480 -0
  40. package/build/core/pressables.js.map +1 -0
  41. package/build/core/register.d.ts +8 -0
  42. package/build/core/register.d.ts.map +1 -0
  43. package/build/core/register.js +216 -0
  44. package/build/core/register.js.map +1 -0
  45. package/build/core/screenLayout.d.ts +52 -0
  46. package/build/core/screenLayout.d.ts.map +1 -0
  47. package/build/core/screenLayout.js +828 -0
  48. package/build/core/screenLayout.js.map +1 -0
  49. package/build/core/toolHelpers.d.ts +24 -0
  50. package/build/core/toolHelpers.d.ts.map +1 -0
  51. package/build/core/toolHelpers.js +118 -0
  52. package/build/core/toolHelpers.js.map +1 -0
  53. package/build/core/types.d.ts +11 -6
  54. package/build/core/types.d.ts.map +1 -1
  55. package/build/index.d.ts +2 -4
  56. package/build/index.d.ts.map +1 -1
  57. package/build/index.js +37 -4440
  58. package/build/index.js.map +1 -1
  59. package/build/tools/accountTools.d.ts +2 -0
  60. package/build/tools/accountTools.d.ts.map +1 -1
  61. package/build/tools/accountTools.js +5 -0
  62. package/build/tools/accountTools.js.map +1 -1
  63. package/build/tools/bundleTools.d.ts +3 -0
  64. package/build/tools/bundleTools.d.ts.map +1 -0
  65. package/build/tools/bundleTools.js +368 -0
  66. package/build/tools/bundleTools.js.map +1 -0
  67. package/build/tools/componentTools.d.ts +3 -0
  68. package/build/tools/componentTools.d.ts.map +1 -0
  69. package/build/tools/componentTools.js +639 -0
  70. package/build/tools/componentTools.js.map +1 -0
  71. package/build/tools/connectionTools.d.ts +3 -0
  72. package/build/tools/connectionTools.d.ts.map +1 -0
  73. package/build/tools/connectionTools.js +564 -0
  74. package/build/tools/connectionTools.js.map +1 -0
  75. package/build/tools/deviceTools.d.ts +3 -0
  76. package/build/tools/deviceTools.d.ts.map +1 -0
  77. package/build/tools/deviceTools.js +296 -0
  78. package/build/tools/deviceTools.js.map +1 -0
  79. package/build/tools/executionTools.d.ts +3 -0
  80. package/build/tools/executionTools.d.ts.map +1 -0
  81. package/build/tools/executionTools.js +173 -0
  82. package/build/tools/executionTools.js.map +1 -0
  83. package/build/tools/interactionTools.d.ts +3 -0
  84. package/build/tools/interactionTools.d.ts.map +1 -0
  85. package/build/tools/interactionTools.js +414 -0
  86. package/build/tools/interactionTools.js.map +1 -0
  87. package/build/tools/logTools.d.ts +3 -0
  88. package/build/tools/logTools.d.ts.map +1 -0
  89. package/build/tools/logTools.js +371 -0
  90. package/build/tools/logTools.js.map +1 -0
  91. package/build/tools/metaTools.d.ts +7 -0
  92. package/build/tools/metaTools.d.ts.map +1 -0
  93. package/build/tools/metaTools.js +205 -0
  94. package/build/tools/metaTools.js.map +1 -0
  95. package/build/tools/networkTools.d.ts +3 -0
  96. package/build/tools/networkTools.d.ts.map +1 -0
  97. package/build/tools/networkTools.js +478 -0
  98. package/build/tools/networkTools.js.map +1 -0
  99. package/build/tools/reduxTools.d.ts +3 -0
  100. package/build/tools/reduxTools.d.ts.map +1 -0
  101. package/build/tools/reduxTools.js +83 -0
  102. package/build/tools/reduxTools.js.map +1 -0
  103. package/build/tools/screenshotTools.d.ts +3 -0
  104. package/build/tools/screenshotTools.d.ts.map +1 -0
  105. package/build/tools/screenshotTools.js +606 -0
  106. package/build/tools/screenshotTools.js.map +1 -0
  107. package/package.json +1 -1
@@ -0,0 +1,1480 @@
1
+ import { executeInApp, delay } from "./jsExecute.js";
2
+ import { getConnectedAppByDevice, getFirstConnectedApp, getConnectedAppBySimulatorUdid, getConnectedAppByAndroidDeviceId } from "./connection.js";
3
+ /**
4
+ * E1 / Step 5 (2026-05-15 plan): build a pressables list from the platform
5
+ * accessibility tree when fiber isn't reachable. Used when metro is offline
6
+ * (connectedApps.size === 0) so the screenshot tools can still hand the agent
7
+ * labelled coordinates instead of forcing it to guess pixels — the dominant
8
+ * cause of unmeaningful coordinate-strategy taps in the 7-day failure data.
9
+ */
10
+ async function getAccessibilityPressables(platform, udid) {
11
+ try {
12
+ if (platform === "ios") {
13
+ const { iosGetUITree } = await import("./ios.js");
14
+ const tree = await iosGetUITree(udid);
15
+ if (!tree.success || !tree.elements)
16
+ return [];
17
+ const interactiveTraits = new Set(["button", "link", "searchfield", "tab", "image"]);
18
+ return tree.elements
19
+ .filter(el => {
20
+ // iOS: rely on a11y traits + element type. Buttons, links,
21
+ // tabs, and any element with a non-empty label/identifier
22
+ // that isn't a static text container.
23
+ const traitsLower = (el.traits || []).map(t => t.toLowerCase());
24
+ if (traitsLower.some(t => interactiveTraits.has(t)))
25
+ return true;
26
+ const type = (el.type || "").toLowerCase();
27
+ if (type.includes("button") || type.includes("textfield") || type.includes("searchfield"))
28
+ return true;
29
+ return false;
30
+ })
31
+ .map(el => {
32
+ const type = (el.type || "").toLowerCase();
33
+ return {
34
+ component: el.type || "Element",
35
+ path: "",
36
+ center: el.center,
37
+ frame: el.frame,
38
+ text: el.label || "",
39
+ testID: el.identifier || null,
40
+ accessibilityLabel: el.label || null,
41
+ hasLabel: !!el.label,
42
+ isInput: type.includes("textfield") || type.includes("searchfield")
43
+ };
44
+ });
45
+ }
46
+ else {
47
+ const { androidGetUITree } = await import("./android.js");
48
+ const tree = await androidGetUITree();
49
+ if (!tree.success || !tree.elements)
50
+ return [];
51
+ return tree.elements
52
+ .filter(el => el.clickable || el.focused || (el.className && el.className.includes("EditText")))
53
+ .map(el => ({
54
+ component: (el.className || "View").split(".").pop() || "View",
55
+ path: "",
56
+ center: el.center,
57
+ frame: { x: el.bounds.left, y: el.bounds.top, width: el.bounds.width, height: el.bounds.height },
58
+ text: el.text || "",
59
+ testID: el.resourceId || null,
60
+ accessibilityLabel: el.contentDesc || null,
61
+ hasLabel: !!(el.text || el.contentDesc),
62
+ isInput: !!(el.className && el.className.includes("EditText"))
63
+ }));
64
+ }
65
+ }
66
+ catch {
67
+ return [];
68
+ }
69
+ }
70
+ export async function getPressableElements(options = {}) {
71
+ const { device, platform, udid } = options;
72
+ // Resolve which connected RN app corresponds to the requested device/udid.
73
+ // The E1 fallback below must fire when no app matches THIS device, even if
74
+ // unrelated apps remain connected to Metro. Without this filter the fiber
75
+ // path would silently fall back to the first-connected app and return data
76
+ // for the wrong device (Bug 1, 2026-05-16).
77
+ let targetApp = null;
78
+ if (udid && platform === "ios") {
79
+ targetApp = getConnectedAppBySimulatorUdid(udid);
80
+ }
81
+ else if (device) {
82
+ const matched = getConnectedAppByDevice(device);
83
+ if (matched && (!platform || matched.platform === platform))
84
+ targetApp = matched;
85
+ }
86
+ else if (platform === "android") {
87
+ targetApp = getConnectedAppByAndroidDeviceId(undefined);
88
+ }
89
+ else if (platform === "ios") {
90
+ const first = getFirstConnectedApp();
91
+ targetApp = first && first.platform === "ios" ? first : null;
92
+ }
93
+ else {
94
+ targetApp = getFirstConnectedApp();
95
+ }
96
+ // When the caller didn't pin a platform, adopt the resolved app's platform so
97
+ // downstream branches (Android coordinate reconciliation below) run uniformly
98
+ // regardless of entry point. The standalone get_pressable_elements MCP tool
99
+ // omits `platform`; without this derivation it would skip reconciliation and
100
+ // return raw fiber DP that downstream consumers misinterpret as device pixels.
101
+ const effectivePlatform = platform ?? targetApp?.platform;
102
+ // E1 fallback: no app resolved for THIS device → use platform accessibility tree.
103
+ // Only kicks in when caller passed `platform` so we don't break older
104
+ // metro-required call paths that didn't opt into the fallback.
105
+ if (!targetApp && platform) {
106
+ const elements = await getAccessibilityPressables(platform, udid);
107
+ return {
108
+ success: true,
109
+ result: elements.length > 0
110
+ ? `Pressable elements (from accessibility tree, ${elements.length} found):\n` +
111
+ elements.map((el, i) => {
112
+ const label = el.accessibilityLabel || el.text || el.testID || el.component;
113
+ const idStr = el.testID ? ` testID="${el.testID}"` : "";
114
+ const inputStr = el.isInput ? " [input]" : "";
115
+ return `${i + 1}. ${el.component} "${label}"${idStr}${inputStr} — center:(${el.center.x},${el.center.y}) frame:(${el.frame.x},${el.frame.y} ${el.frame.width}x${el.frame.height})`;
116
+ }).join("\n")
117
+ : "No interactive elements found via accessibility.",
118
+ parsedElements: elements
119
+ };
120
+ }
121
+ // --- Step 1: walk fiber tree, find pressable/input elements, dispatch measureInWindow ---
122
+ const dispatchExpression = `
123
+ (function() {
124
+ var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
125
+ if (!hook) return { error: 'React DevTools hook not found.' };
126
+
127
+ var roots = [];
128
+ if (hook.getFiberRoots) {
129
+ roots = Array.from(hook.getFiberRoots(1) || []);
130
+ }
131
+ if (roots.length === 0 && hook.renderers) {
132
+ for (var entry of hook.renderers) {
133
+ var r = Array.from(hook.getFiberRoots ? (hook.getFiberRoots(entry[0]) || []) : []);
134
+ if (r.length > 0) { roots = r; break; }
135
+ }
136
+ }
137
+ if (roots.length === 0) return { error: 'No fiber roots found.' };
138
+
139
+ function getMeasurable(fiber) {
140
+ var sn = fiber.stateNode;
141
+ if (!sn) return null;
142
+ if (typeof sn.measureInWindow === 'function') return sn;
143
+ if (sn.canonical && sn.canonical.publicInstance &&
144
+ typeof sn.canonical.publicInstance.measureInWindow === 'function') {
145
+ return sn.canonical.publicInstance;
146
+ }
147
+ // Fabric leaf nodes like RCTText have no publicInstance. Measure via
148
+ // the native Fabric UIManager using the shadow node instead — needed
149
+ // for text bounds that are tight around the glyphs (not the scroll
150
+ // container the text happens to live inside).
151
+ if (sn.node && globalThis.nativeFabricUIManager &&
152
+ typeof globalThis.nativeFabricUIManager.measureInWindow === 'function') {
153
+ var node = sn.node;
154
+ return {
155
+ measureInWindow: function(cb) {
156
+ try { globalThis.nativeFabricUIManager.measureInWindow(node, cb); } catch(e) {}
157
+ }
158
+ };
159
+ }
160
+ return null;
161
+ }
162
+
163
+ function findFirstHost(fiber, depth) {
164
+ if (!fiber || depth > 20) return null;
165
+ if (typeof fiber.type === 'string' && getMeasurable(fiber)) return fiber;
166
+ var child = fiber.child;
167
+ while (child) {
168
+ var found = findFirstHost(child, depth + 1);
169
+ if (found) return found;
170
+ child = child.sibling;
171
+ }
172
+ return null;
173
+ }
174
+
175
+ function findHostsInSubtree(fiber, depth, hosts, limit) {
176
+ if (!fiber || depth > 20 || hosts.length >= limit) return;
177
+ if (typeof fiber.type === 'string' && getMeasurable(fiber)) {
178
+ hosts.push(fiber);
179
+ return;
180
+ }
181
+ var child = fiber.child;
182
+ while (child && hosts.length < limit) {
183
+ findHostsInSubtree(child, depth + 1, hosts, limit);
184
+ child = child.sibling;
185
+ }
186
+ }
187
+
188
+ function collectText(fiber, d, isRoot) {
189
+ if (!fiber || d > 30) return '';
190
+ var props = fiber.memoizedProps;
191
+ // Stop descent at nested pressable/input boundaries — their text belongs to them, not to the outer wrapper.
192
+ if (!isRoot && props && (typeof props.onPress === 'function' ||
193
+ typeof props.onChangeText === 'function' ||
194
+ typeof props.onFocus === 'function')) {
195
+ return '';
196
+ }
197
+ if (props) {
198
+ var ch = props.children;
199
+ if (typeof ch === 'string') return ch;
200
+ if (typeof ch === 'number') return String(ch);
201
+ if (Array.isArray(ch)) {
202
+ var inline = [];
203
+ for (var ci = 0; ci < ch.length; ci++) {
204
+ if (typeof ch[ci] === 'string') inline.push(ch[ci]);
205
+ else if (typeof ch[ci] === 'number') inline.push(String(ch[ci]));
206
+ }
207
+ if (inline.length > 0) return inline.join('');
208
+ }
209
+ }
210
+ var parts = [];
211
+ var child = fiber.child;
212
+ while (child) {
213
+ var t = collectText(child, d + 1, false);
214
+ if (t) parts.push(t);
215
+ child = child.sibling;
216
+ }
217
+ return parts.join(' ').trim();
218
+ }
219
+
220
+ function getComponentName(fiber) {
221
+ if (!fiber || !fiber.type) return null;
222
+ if (typeof fiber.type === 'string') return fiber.type;
223
+ return fiber.type.displayName || fiber.type.name || null;
224
+ }
225
+
226
+ var RN_PRIMITIVES = /^(Animated\\(.*|withAnimated.*|AnimatedComponent.*|ForwardRef.*|memo\\(.*|Context\\.Consumer|Context\\.Provider|ScrollViewContext(Base)?|VirtualizedListContext(Resetter)?|TextInputContext|KeyboardAvoidingViewContext|RCT.*|RNS.*|RNC.*|ViewManagerAdapter_.*|VirtualizedList.*|CellRenderer.*|FrameSizeProvider.*|MaybeScreenContainer|MaybeScreen|Navigation.*|Screen$|ScreenStack|ScreenContainer|ScreenContentWrapper|SceneView|DelayedFreeze|Freeze|Suspender|DebugContainer|StaticContainer|SafeAreaProvider.*|SafeAreaFrameContext|SafeAreaInsetsContext|ExpoRoot|ExpoRootComponent|GestureHandler.*|NativeViewGestureHandler|GestureDetector|PanGestureHandler|Reanimated.*|BottomTabNavigator|TabLayout|RouteNode|Route$|KeyboardProvider|PortalProviderComponent|BottomSheetModalProviderWrapper|ThemeContext|ThemeProvider|TextAncestorContext|PressabilityDebugView|TouchableHighlightImpl|StatusBarOverlay|BottomSheetHostingContainerComponent|BottomSheetGestureHandlersProvider|BottomSheetBackdropContainerComponent|BottomSheetContainerComponent|BottomSheetDraggableViewComponent|BottomSheetHandleContainerComponent|BottomSheetBackgroundContainerComponent|DebuggingOverlay|InspectorDeferred|Inspector|InspectorOverlay|InspectorPanel|StyleInspector|BoxInspector|BoxContainer|ElementBox|BorderBox|InspectorPanelButton)$/;
227
+
228
+ var hostFibers = [];
229
+ var fiberMeta = [];
230
+ var textFibers = [];
231
+ var textContents = [];
232
+
233
+ function findMeaningfulAncestorName(fiber) {
234
+ var cur = fiber.return;
235
+ var depth = 0;
236
+ var fallbackName = null;
237
+ while (cur && depth < 20) {
238
+ var name = getComponentName(cur);
239
+ if (name && typeof cur.type !== 'string') {
240
+ if (!fallbackName) fallbackName = name;
241
+ if (!RN_PRIMITIVES.test(name)) return name;
242
+ }
243
+ cur = cur.return;
244
+ depth++;
245
+ }
246
+ return fallbackName;
247
+ }
248
+
249
+ // Layout-only and touch-wrapper components that should be skipped when scanning
250
+ // children for a meaningful icon/content component name.
251
+ var SKIP_IN_CHILD_SCAN = /^(View|Text|Image|ImageBackground|ScrollView|FlatList|SectionList|KeyboardAvoidingView|SafeAreaView|TouchableOpacity|TouchableHighlight|TouchableWithoutFeedback|TouchableNativeFeedback|Pressable|TextInput|ActivityIndicator|Switch|Modal|StatusBar|VirtualizedList|RefreshControl|Animated\\(.*|withAnimated.*|AnimatedComponent.*)$/;
252
+
253
+ // Scan children of a pressable for a meaningful component name (e.g. SvgChevronBack inside a View).
254
+ // Skips generic layout/touch wrappers to find the actual icon or content component.
255
+ function findMeaningfulChildName(fiber) {
256
+ function scan(f, d) {
257
+ if (!f || d > 12) return null;
258
+ var n = getComponentName(f);
259
+ if (n && typeof f.type !== 'string' && !RN_PRIMITIVES.test(n) && !SKIP_IN_CHILD_SCAN.test(n)) return n;
260
+ var c = f.child;
261
+ while (c) {
262
+ var r = scan(c, d + 1);
263
+ if (r) return r;
264
+ c = c.sibling;
265
+ }
266
+ return null;
267
+ }
268
+ return scan(fiber.child, 0);
269
+ }
270
+
271
+ var GENERIC_COMPONENT = /^(View|TouchableOpacity|TouchableHighlight|TouchableWithoutFeedback|Pressable|TouchableNativeFeedback|Text|RCTView|RCTText|Unknown)$/;
272
+
273
+ function buildPath(fiber) {
274
+ var parts = [];
275
+ var cur = fiber;
276
+ var depth = 0;
277
+ while (cur && depth < 30) {
278
+ var name = getComponentName(cur);
279
+ if (name && typeof cur.type !== 'string' && !RN_PRIMITIVES.test(name)) {
280
+ parts.unshift(name);
281
+ }
282
+ cur = cur.return;
283
+ depth++;
284
+ }
285
+ // Keep last 3 segments
286
+ if (parts.length > 3) {
287
+ parts = parts.slice(-3);
288
+ return '... > ' + parts.join(' > ');
289
+ }
290
+ return parts.join(' > ');
291
+ }
292
+
293
+ function walkPressables(fiber, depth) {
294
+ if (!fiber || depth > 5000) return;
295
+ var name = getComponentName(fiber);
296
+ var props = fiber.memoizedProps;
297
+
298
+ // Skip inactive/unfocused screens
299
+ if (name === 'MaybeScreen' && props && props.active === 0) return;
300
+ if (name === 'SceneView' && props && props.focused === false) return;
301
+ if (name === 'RNSScreen' && props && props['aria-hidden'] === true) return;
302
+
303
+ var isPressable = props && typeof props.onPress === 'function';
304
+ var isInput = !isPressable && props && (typeof props.onChangeText === 'function' || typeof props.onFocus === 'function');
305
+
306
+ if (isPressable || isInput) {
307
+ var hostsForThis = [];
308
+ findHostsInSubtree(fiber, 0, hostsForThis, 16);
309
+ if (hostsForThis.length > 0) {
310
+ var text = collectText(fiber, 0, true);
311
+ var componentName = findMeaningfulAncestorName(fiber) || name || 'Unknown';
312
+ // If name is generic (e.g. View, TouchableOpacity), scan children for a
313
+ // meaningful name like SvgChevronBack so icon-only buttons are identifiable.
314
+ if (GENERIC_COMPONENT.test(componentName)) {
315
+ var childName = findMeaningfulChildName(fiber);
316
+ if (childName) componentName = childName;
317
+ }
318
+ var path = buildPath(fiber);
319
+ var testID = (props && (props.testID || props.nativeID)) || null;
320
+ var accessibilityLabel = (props && props.accessibilityLabel) || null;
321
+
322
+ var hostIndices = [];
323
+ for (var hi = 0; hi < hostsForThis.length; hi++) {
324
+ hostIndices.push(hostFibers.length);
325
+ hostFibers.push(hostsForThis[hi]);
326
+ }
327
+ fiberMeta.push({
328
+ component: componentName,
329
+ path: path,
330
+ text: text ? text.slice(0, 100) : '',
331
+ testID: testID,
332
+ accessibilityLabel: accessibilityLabel,
333
+ isInput: !!isInput,
334
+ hostIndices: hostIndices
335
+ });
336
+ }
337
+ }
338
+
339
+ var child = fiber.child;
340
+ while (child) {
341
+ walkPressables(child, depth + 1);
342
+ child = child.sibling;
343
+ }
344
+ }
345
+
346
+ walkPressables(roots[0].current, 0);
347
+
348
+ // Collect text fibers that are NOT inside a pressable — used later to attach
349
+ // spatial 'nearbyText' hints to icon-only pressables.
350
+ function extractTextString(fiber) {
351
+ var p = fiber.memoizedProps;
352
+ if (!p) return '';
353
+ var ch = p.children;
354
+ if (typeof ch === 'string') return ch;
355
+ if (typeof ch === 'number') return String(ch);
356
+ if (Array.isArray(ch)) {
357
+ var parts = [];
358
+ for (var k = 0; k < ch.length; k++) {
359
+ if (typeof ch[k] === 'string') parts.push(ch[k]);
360
+ else if (typeof ch[k] === 'number') parts.push(String(ch[k]));
361
+ }
362
+ if (parts.length > 0) return parts.join('');
363
+ }
364
+ return '';
365
+ }
366
+
367
+ function walkTexts(fiber, depth, insidePressable, inHidden) {
368
+ if (!fiber || depth > 5000) return;
369
+ var name = getComponentName(fiber);
370
+ var props = fiber.memoizedProps;
371
+
372
+ // Track hidden-screen ancestry without early return — a MaybeScreen wrapper
373
+ // can enclose BOTH the active and inactive siblings, so we must keep walking
374
+ // to find the active one while still skipping the inactive subtree's texts.
375
+ var nextHidden = inHidden;
376
+ if (name === 'MaybeScreen' && props && props.active === 0) nextHidden = true;
377
+ if (name === 'SceneView' && props && props.focused === false) nextHidden = true;
378
+ if (name === 'RNSScreen' && props && props['aria-hidden'] === true) nextHidden = true;
379
+
380
+ var hasOnPress = props && typeof props.onPress === 'function';
381
+ var isInputHere = props && (typeof props.onChangeText === 'function' || typeof props.onFocus === 'function');
382
+ var nextInside = insidePressable || !!hasOnPress || !!isInputHere;
383
+
384
+ // Record standalone text when outside any pressable. Detects both stock
385
+ // Text and custom wrappers (CustomText, ThemedText, etc.) by checking for
386
+ // direct string/number children. Fabric RCTText has no publicInstance, so
387
+ // we walk up the fiber chain to the nearest measurable host (the View
388
+ // enclosing the text) and use its frame as the text's proxy bounds.
389
+ if (!insidePressable && !nextHidden && name !== 'RCTText' && typeof fiber.type !== 'string') {
390
+ var str = extractTextString(fiber);
391
+ if (str && str.length > 0 && str.length <= 120) {
392
+ var up = fiber;
393
+ var upDepth = 0;
394
+ var measurable = null;
395
+ while (up && upDepth < 20) {
396
+ if (typeof up.type === 'string' && getMeasurable(up)) {
397
+ measurable = up;
398
+ break;
399
+ }
400
+ up = up.return;
401
+ upDepth++;
402
+ }
403
+ if (measurable) {
404
+ textFibers.push(measurable);
405
+ textContents.push(str);
406
+ }
407
+ }
408
+ }
409
+
410
+ var child = fiber.child;
411
+ while (child) {
412
+ walkTexts(child, depth + 1, nextInside, nextHidden);
413
+ child = child.sibling;
414
+ }
415
+ }
416
+ walkTexts(roots[0].current, 0, false, false);
417
+
418
+ if (hostFibers.length === 0) return { error: 'No pressable elements found on screen.' };
419
+
420
+ // Also measure the root view for viewport detection (appended; tracked by explicit index to preserve hostIndices)
421
+ var rootHost = findFirstHost(roots[0].current, 0);
422
+ var rootIdx = -1;
423
+ if (rootHost) {
424
+ rootIdx = hostFibers.length;
425
+ hostFibers.push(rootHost);
426
+ }
427
+
428
+ globalThis.__pressableFibers = hostFibers;
429
+ globalThis.__pressableMeta = fiberMeta;
430
+ globalThis.__pressableMeasurements = new Array(hostFibers.length).fill(null);
431
+ globalThis.__pressableRootIdx = rootIdx;
432
+
433
+ for (var i = 0; i < hostFibers.length; i++) {
434
+ try {
435
+ (function(idx) {
436
+ getMeasurable(hostFibers[idx]).measureInWindow(function(fx, fy, fw, fh) {
437
+ globalThis.__pressableMeasurements[idx] = { x: fx, y: fy, width: fw, height: fh };
438
+ });
439
+ })(i);
440
+ } catch(e) {}
441
+ }
442
+
443
+ globalThis.__pressableTextContents = textContents;
444
+ globalThis.__pressableTextMeasurements = new Array(textFibers.length).fill(null);
445
+ for (var ti = 0; ti < textFibers.length; ti++) {
446
+ try {
447
+ (function(idx) {
448
+ getMeasurable(textFibers[idx]).measureInWindow(function(fx, fy, fw, fh) {
449
+ globalThis.__pressableTextMeasurements[idx] = { x: fx, y: fy, width: fw, height: fh };
450
+ });
451
+ })(ti);
452
+ } catch(e) {}
453
+ }
454
+
455
+ return { count: hostFibers.length, textCount: textFibers.length };
456
+ })()
457
+ `;
458
+ const dispatchResult = await executeInApp(dispatchExpression, false, { timeoutMs: 30000 }, device);
459
+ if (!dispatchResult.success)
460
+ return dispatchResult;
461
+ try {
462
+ const parsed = JSON.parse(dispatchResult.result || "{}");
463
+ if (parsed.error)
464
+ return { success: false, error: parsed.error };
465
+ }
466
+ catch {
467
+ /* ignore */
468
+ }
469
+ // Wait for measureInWindow callbacks
470
+ await delay(300);
471
+ // --- Step 2: read measurements, filter visible, build results ---
472
+ const resolveExpression = `
473
+ (function() {
474
+ var fibers = globalThis.__pressableFibers;
475
+ var meta = globalThis.__pressableMeta;
476
+ var measurements = globalThis.__pressableMeasurements;
477
+ var rootIdx = globalThis.__pressableRootIdx;
478
+ var textContents = globalThis.__pressableTextContents || [];
479
+ var textMeasurements = globalThis.__pressableTextMeasurements || [];
480
+ globalThis.__pressableFibers = null;
481
+ globalThis.__pressableMeta = null;
482
+ globalThis.__pressableMeasurements = null;
483
+ globalThis.__pressableRootIdx = null;
484
+ globalThis.__pressableTextContents = null;
485
+ globalThis.__pressableTextMeasurements = null;
486
+
487
+ if (!fibers || !measurements || !meta) {
488
+ return { error: 'No measurement data. Run get_pressable_elements again.' };
489
+ }
490
+
491
+ // Get viewport dimensions from the explicit root measurement (fallback to scanning)
492
+ var viewportW = 9999, viewportH = 9999;
493
+ var rootM = (rootIdx != null && rootIdx >= 0) ? measurements[rootIdx] : null;
494
+ if (rootM && rootM.width > 0 && rootM.height > 0) {
495
+ viewportW = rootM.width;
496
+ viewportH = rootM.height + (rootM.y > 0 ? rootM.y : 0);
497
+ } else {
498
+ for (var v = 0; v < measurements.length; v++) {
499
+ if (measurements[v] && measurements[v].x === 0 && measurements[v].y <= 0 &&
500
+ measurements[v].width > 0 && measurements[v].height > 0) {
501
+ viewportW = measurements[v].width;
502
+ viewportH = measurements[v].height + measurements[v].y;
503
+ break;
504
+ }
505
+ }
506
+ }
507
+
508
+ var elements = [];
509
+
510
+ for (var i = 0; i < meta.length; i++) {
511
+ var info = meta[i];
512
+
513
+ // Union all host measurements for this pressable to get its true bounds
514
+ var uMinX = Infinity, uMinY = Infinity, uMaxX = -Infinity, uMaxY = -Infinity;
515
+ var hasValid = false;
516
+ var indices = info.hostIndices || [];
517
+ for (var hi2 = 0; hi2 < indices.length; hi2++) {
518
+ var mm = measurements[indices[hi2]];
519
+ if (!mm || mm.width <= 0 || mm.height <= 0) continue;
520
+ hasValid = true;
521
+ if (mm.x < uMinX) uMinX = mm.x;
522
+ if (mm.y < uMinY) uMinY = mm.y;
523
+ if (mm.x + mm.width > uMaxX) uMaxX = mm.x + mm.width;
524
+ if (mm.y + mm.height > uMaxY) uMaxY = mm.y + mm.height;
525
+ }
526
+ if (!hasValid) continue;
527
+ var m = { x: uMinX, y: uMinY, width: uMaxX - uMinX, height: uMaxY - uMinY };
528
+
529
+ // Filter: only visible within viewport
530
+ if (m.width <= 0 || m.height <= 0) continue;
531
+ if (m.x + m.width < 0 || m.y + m.height < 0) continue;
532
+ if (m.x > viewportW || m.y > viewportH) continue;
533
+
534
+ var text = info.text || '';
535
+
536
+ elements.push({
537
+ component: info.component,
538
+ path: info.path,
539
+ center: {
540
+ x: Math.round(m.x + m.width / 2),
541
+ y: Math.round(m.y + m.height / 2)
542
+ },
543
+ frame: {
544
+ x: Math.round(m.x),
545
+ y: Math.round(m.y),
546
+ width: Math.round(m.width),
547
+ height: Math.round(m.height)
548
+ },
549
+ text: text,
550
+ testID: info.testID,
551
+ accessibilityLabel: info.accessibilityLabel,
552
+ hasLabel: text.length > 0,
553
+ isInput: info.isInput
554
+ });
555
+ }
556
+
557
+ // Deduplicate: multiple nested pressables (View > TouchableOpacity > TouchableOpacity)
558
+ // often share the same frame. Keep the one with the most meaningful component name,
559
+ // but merge text/testID/accessibilityLabel from the loser so stopping collectText
560
+ // at nested-pressable boundaries does not drop labels across the merge.
561
+ var HOST_NAMES = /^(View|TouchableOpacity|TouchableHighlight|TouchableWithoutFeedback|Pressable|TouchableNativeFeedback|Text|RCTView|RCTText)$/;
562
+ function mergeFields(winner, loser) {
563
+ if (!winner.text && loser.text) {
564
+ winner.text = loser.text;
565
+ winner.hasLabel = winner.text.length > 0;
566
+ }
567
+ if (!winner.testID && loser.testID) winner.testID = loser.testID;
568
+ if (!winner.accessibilityLabel && loser.accessibilityLabel) winner.accessibilityLabel = loser.accessibilityLabel;
569
+ return winner;
570
+ }
571
+ var deduped = {};
572
+ for (var di = 0; di < elements.length; di++) {
573
+ var el = elements[di];
574
+ var key = el.frame.x + ',' + el.frame.y + ',' + el.frame.width + ',' + el.frame.height;
575
+ var existing = deduped[key];
576
+ if (!existing) {
577
+ deduped[key] = el;
578
+ } else {
579
+ var existingIsGeneric = HOST_NAMES.test(existing.component);
580
+ var newIsGeneric = HOST_NAMES.test(el.component);
581
+ var winner, loser;
582
+ if (existingIsGeneric && !newIsGeneric) {
583
+ winner = el; loser = existing;
584
+ } else if (!existingIsGeneric && newIsGeneric) {
585
+ winner = existing; loser = el;
586
+ } else {
587
+ // Both generic or both meaningful — prefer the one with more identifiers.
588
+ if (!existing.testID && el.testID) { winner = el; loser = existing; }
589
+ else if (!existing.accessibilityLabel && el.accessibilityLabel) { winner = el; loser = existing; }
590
+ else { winner = existing; loser = el; }
591
+ }
592
+ deduped[key] = mergeFields(winner, loser);
593
+ }
594
+ }
595
+ elements = [];
596
+ for (var dk in deduped) {
597
+ elements.push(deduped[dk]);
598
+ }
599
+
600
+ // Tag wrappers: pressables that cover >=50% of viewport AND geometrically contain another pressable.
601
+ // These are typically keyboard-dismiss/full-screen Touchable wrappers — agents should skip them.
602
+ var viewportArea = (viewportW > 0 && viewportH > 0 && viewportW < 9999 && viewportH < 9999)
603
+ ? viewportW * viewportH : 0;
604
+ if (viewportArea > 0) {
605
+ for (var wi = 0; wi < elements.length; wi++) {
606
+ var we = elements[wi];
607
+ var weArea = we.frame.width * we.frame.height;
608
+ if (weArea < viewportArea * 0.5) continue;
609
+ for (var wj = 0; wj < elements.length; wj++) {
610
+ if (wj === wi) continue;
611
+ var other = elements[wj];
612
+ if (other.frame.x >= we.frame.x &&
613
+ other.frame.y >= we.frame.y &&
614
+ other.frame.x + other.frame.width <= we.frame.x + we.frame.width &&
615
+ other.frame.y + other.frame.height <= we.frame.y + we.frame.height &&
616
+ other.frame.width * other.frame.height < weArea) {
617
+ we.isWrapper = true;
618
+ break;
619
+ }
620
+ }
621
+ }
622
+ }
623
+
624
+ // Build a normalized list of visible standalone texts (content + frame)
625
+ var textBoxes = [];
626
+ for (var tmi = 0; tmi < textMeasurements.length; tmi++) {
627
+ var tm = textMeasurements[tmi];
628
+ var tc = textContents[tmi];
629
+ if (!tm || !tc) continue;
630
+ if (tm.width <= 0 || tm.height <= 0) continue;
631
+ if (tm.x + tm.width < 0 || tm.y + tm.height < 0) continue;
632
+ if (tm.x > viewportW || tm.y > viewportH) continue;
633
+ textBoxes.push({
634
+ text: tc,
635
+ x: tm.x, y: tm.y, width: tm.width, height: tm.height,
636
+ cx: tm.x + tm.width / 2, cy: tm.y + tm.height / 2
637
+ });
638
+ }
639
+
640
+ // Humanize component name → intent (strip Svg/Icon prefixes, split camelCase)
641
+ function humanize(name) {
642
+ if (!name) return '';
643
+ var n = String(name);
644
+ n = n.replace(/^(Svg|Icon)/, '').replace(/(Svg|Icon)$/, '');
645
+ n = n.replace(/([a-z0-9])([A-Z])/g, '$1 $2');
646
+ n = n.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2');
647
+ return n.trim().toLowerCase();
648
+ }
649
+
650
+ // Attach nearbyText for pressables that lack their own text/label.
651
+ // Nearest text by center-to-center distance, within a reasonable radius.
652
+ var NEARBY_RADIUS = 120; // points
653
+ for (var ei = 0; ei < elements.length; ei++) {
654
+ var pe = elements[ei];
655
+ pe.intent = humanize(pe.component);
656
+ // Skip when the pressable already has its own human-readable text or a11y label.
657
+ // testID alone doesn't suppress nearbyText — the testID may be cryptic and the
658
+ // surrounding visible text is still a useful handle for an agent.
659
+ if (pe.hasLabel || pe.accessibilityLabel) continue;
660
+ if (textBoxes.length === 0) continue;
661
+
662
+ var pcx = pe.frame.x + pe.frame.width / 2;
663
+ var pcy = pe.frame.y + pe.frame.height / 2;
664
+ var best = null;
665
+ var bestDist = Infinity;
666
+ for (var tbi = 0; tbi < textBoxes.length; tbi++) {
667
+ var tb = textBoxes[tbi];
668
+ // Skip texts contained inside this pressable (shouldn't happen — walkTexts excludes them — but defensive)
669
+ if (tb.x >= pe.frame.x && tb.y >= pe.frame.y &&
670
+ tb.x + tb.width <= pe.frame.x + pe.frame.width &&
671
+ tb.y + tb.height <= pe.frame.y + pe.frame.height) continue;
672
+ var dx = tb.cx - pcx;
673
+ var dy = tb.cy - pcy;
674
+ var d = Math.sqrt(dx * dx + dy * dy);
675
+ if (d < bestDist && d <= NEARBY_RADIUS) {
676
+ bestDist = d;
677
+ best = tb;
678
+ }
679
+ }
680
+ if (best) pe.nearbyText = best.text;
681
+ }
682
+
683
+ // Sort top-to-bottom, left-to-right
684
+ elements.sort(function(a, b) {
685
+ if (a.center.y !== b.center.y) return a.center.y - b.center.y;
686
+ return a.center.x - b.center.x;
687
+ });
688
+
689
+ var iconCount = 0;
690
+ var labeledCount = 0;
691
+ for (var j = 0; j < elements.length; j++) {
692
+ if (elements[j].hasLabel) labeledCount++;
693
+ else iconCount++;
694
+ }
695
+
696
+ return {
697
+ pressableElements: elements,
698
+ summary: 'Found ' + elements.length + ' pressable elements (' + iconCount + ' icon-only, ' + labeledCount + ' with text labels)'
699
+ };
700
+ })()
701
+ `;
702
+ const result = await executeInApp(resolveExpression, false, { timeoutMs: 30000 }, device);
703
+ if (result.success && result.result) {
704
+ try {
705
+ const parsed = JSON.parse(result.result);
706
+ if (parsed.error)
707
+ return { success: false, error: parsed.error };
708
+ const pressableElements = parsed.pressableElements || [];
709
+ // Android coordinate reconciliation (2026-05-17).
710
+ //
711
+ // The fiber walker produces rich metadata (component names, testID, intent,
712
+ // wrapper flags, nearbyText) but its measureInWindow output on Bridgeless/Fabric
713
+ // is in app-window DP space — off from the actual screen-pixel hit area by the
714
+ // status bar height (varies per device) AND an additional unaccounted offset
715
+ // we couldn't characterize across devices.
716
+ //
717
+ // uiautomator's bounds, on the other hand, are in absolute display pixels and
718
+ // match what tap(x, y) → adb input dispatches — they are touch-accurate.
719
+ //
720
+ // Strategy: keep fiber's metadata, but replace coords with uiautomator's bounds
721
+ // when we can match a fiber element to a uiautomator node by text / contentDesc /
722
+ // resourceId. For icon-only pressables that have no text-match candidate,
723
+ // fall back to fiber's DP coords scaled by density — they're imperfect but
724
+ // they keep the same gross layout that an agent could use as a "near this
725
+ // region" hint, and they're better than nothing.
726
+ //
727
+ // iOS is unaffected — fiber returns points and AXe returns points; uniform.
728
+ if (effectivePlatform === "android" && pressableElements.length > 0) {
729
+ let densityScale = 2.625;
730
+ let statusBarPixels = 0;
731
+ try {
732
+ const { androidGetDensity, androidGetStatusBarHeight } = await import("./android.js");
733
+ const [densityResult, statusBarResult] = await Promise.all([
734
+ androidGetDensity(),
735
+ androidGetStatusBarHeight().catch(() => ({ success: false, heightPixels: 0 })),
736
+ ]);
737
+ densityScale = (densityResult.density || 420) / 160;
738
+ if (statusBarResult.success && statusBarResult.heightPixels) {
739
+ statusBarPixels = statusBarResult.heightPixels;
740
+ }
741
+ }
742
+ catch { /* default */ }
743
+ let uiNodes;
744
+ try {
745
+ const { androidGetUITree } = await import("./android.js");
746
+ const tree = await androidGetUITree();
747
+ if (tree.success && tree.elements)
748
+ uiNodes = tree.elements;
749
+ }
750
+ catch { /* uiautomator unavailable */ }
751
+ const norm = (s) => (s ?? "").toLowerCase().replace(/\s+/g, " ").trim();
752
+ const findUiMatch = (el) => {
753
+ if (!uiNodes || uiNodes.length === 0)
754
+ return null;
755
+ const targetTestID = norm(el.testID);
756
+ const targetText = norm(el.text);
757
+ const targetA11y = norm(el.accessibilityLabel);
758
+ if (!targetTestID && !targetText && !targetA11y)
759
+ return null;
760
+ // Prefer testID → resourceId exact match (most stable).
761
+ if (targetTestID) {
762
+ const m = uiNodes.find(u => norm(u.resourceId).endsWith("/" + targetTestID) || norm(u.resourceId) === targetTestID);
763
+ if (m)
764
+ return m;
765
+ }
766
+ // Then exact text/contentDesc match.
767
+ for (const query of [targetText, targetA11y]) {
768
+ if (!query)
769
+ continue;
770
+ const m = uiNodes.find(u => norm(u.text) === query || norm(u.contentDesc) === query);
771
+ if (m)
772
+ return m;
773
+ }
774
+ // Then substring — handles fiber's collected text being a superset of one node's label.
775
+ for (const query of [targetText, targetA11y]) {
776
+ if (!query)
777
+ continue;
778
+ const m = uiNodes.find(u => {
779
+ const ut = norm(u.text);
780
+ const ud = norm(u.contentDesc);
781
+ return (ut && (ut.includes(query) || query.includes(ut))) ||
782
+ (ud && (ud.includes(query) || query.includes(ud)));
783
+ });
784
+ if (m)
785
+ return m;
786
+ }
787
+ return null;
788
+ };
789
+ for (const el of pressableElements) {
790
+ const match = findUiMatch(el);
791
+ if (match) {
792
+ el.center = { x: match.center.x, y: match.center.y };
793
+ el.frame = {
794
+ x: match.bounds.left,
795
+ y: match.bounds.top,
796
+ width: match.bounds.width,
797
+ height: match.bounds.height,
798
+ };
799
+ }
800
+ else {
801
+ // Fall back to fiber's DP → device-pixel scaling, plus the dynamic
802
+ // status-bar pixel offset so the y origin lines up with display
803
+ // coordinates (uiautomator's space). measureInWindow on Bridgeless
804
+ // returns coordinates relative to the React window which sits
805
+ // below the system status bar on non-edge-to-edge apps; without
806
+ // this shift, fallback elements report y-values that are
807
+ // ~status-bar-height too small.
808
+ //
809
+ // Note: this is a best-effort correction. Edge-to-edge apps where
810
+ // the React window starts at the display origin will see a
811
+ // small over-correction here. The proper fix would query
812
+ // WindowInsets, but the data isn't exposed via adb in a way
813
+ // we can reach today, and the icon-only-no-text fallback is
814
+ // already a "region hint" path — agents should prefer
815
+ // tap(component=...) / tap(testID=...) for precision.
816
+ el.center = {
817
+ x: el.center.x * densityScale,
818
+ y: el.center.y * densityScale + statusBarPixels,
819
+ };
820
+ el.frame = {
821
+ x: el.frame.x * densityScale,
822
+ y: el.frame.y * densityScale + statusBarPixels,
823
+ width: el.frame.width * densityScale,
824
+ height: el.frame.height * densityScale,
825
+ };
826
+ }
827
+ }
828
+ }
829
+ // Format as readable text
830
+ const lines = [parsed.summary, ""];
831
+ for (let i = 0; i < pressableElements.length; i++) {
832
+ const el = pressableElements[i];
833
+ const num = i + 1;
834
+ const label = el.hasLabel
835
+ ? `"${el.text}"`
836
+ : el.intent
837
+ ? `(${el.intent} icon)`
838
+ : "(icon/image)";
839
+ const ids = [];
840
+ if (el.testID)
841
+ ids.push(`testID="${el.testID}"`);
842
+ if (el.accessibilityLabel)
843
+ ids.push(`a11y="${el.accessibilityLabel}"`);
844
+ const idStr = ids.length > 0 ? ` [${ids.join(", ")}]` : "";
845
+ const inputStr = el.isInput ? " (input)" : "";
846
+ const wrapperStr = el.isWrapper ? " [wrapper — skip unless dismissing keyboard]" : "";
847
+ const nearPart = el.nearbyText ? ` near "${el.nearbyText}"` : "";
848
+ lines.push(`${num}. ${el.component} ${label}${nearPart} — center:(${el.center.x},${el.center.y}) frame:(${el.frame.x},${el.frame.y} ${el.frame.width}x${el.frame.height})${idStr}${inputStr}${wrapperStr}`);
849
+ if (el.path)
850
+ lines.push(` path: ${el.path}`);
851
+ }
852
+ return {
853
+ success: true,
854
+ result: lines.join("\n"),
855
+ parsedElements: pressableElements
856
+ };
857
+ }
858
+ catch {
859
+ // If parsing fails, return original result
860
+ }
861
+ }
862
+ return result;
863
+ }
864
+ // ============================================================================
865
+ // Press Element (invoke onPress via React Fiber Tree)
866
+ // ============================================================================
867
+ /**
868
+ * Find a pressable element in the React fiber tree and invoke its onPress handler.
869
+ * Matches by text content, testID, or component name.
870
+ */
871
+ export async function pressElement(options) {
872
+ const { text, testID, component, index = 0, maxTraversalDepth = 15 } = options;
873
+ if (!text && !testID && !component) {
874
+ return { success: false, error: "At least one of text, testID, or component must be provided." };
875
+ }
876
+ const esc = (s) => s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
877
+ const textParam = text ? `'${esc(text)}'` : "null";
878
+ const testIDParam = testID ? `'${esc(testID)}'` : "null";
879
+ const componentParam = component ? `'${esc(component)}'` : "null";
880
+ // --- Step 1: Walk fiber tree, collect pressable/input elements, dispatch measureInWindow ---
881
+ const dispatchExpression = `
882
+ (function() {
883
+ var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
884
+ if (!hook) return { error: 'React DevTools hook not found. Ensure app is running in __DEV__ mode.' };
885
+
886
+ var roots = [];
887
+ if (hook.getFiberRoots) {
888
+ roots = Array.from(hook.getFiberRoots(1) || []);
889
+ }
890
+ if (roots.length === 0 && hook.renderers) {
891
+ for (var entry of hook.renderers) {
892
+ var r = Array.from(hook.getFiberRoots ? (hook.getFiberRoots(entry[0]) || []) : []);
893
+ if (r.length > 0) { roots = r; break; }
894
+ }
895
+ }
896
+ if (roots.length === 0) return { error: 'No fiber roots found. Is a React Native app mounted?' };
897
+
898
+ var searchText = ${textParam};
899
+ var searchTestID = ${testIDParam};
900
+ var searchComponent = ${componentParam};
901
+ var targetIndex = ${index};
902
+ var maxTraversalUp = ${maxTraversalDepth};
903
+
904
+ function getComponentName(fiber) {
905
+ if (!fiber || !fiber.type) return null;
906
+ if (typeof fiber.type === 'string') return fiber.type;
907
+ return fiber.type.displayName || fiber.type.name || null;
908
+ }
909
+
910
+ // When a fiber holds a string/number child via memoizedProps.children, return it
911
+ // without recursing — Text > RCTText > NativeText all carry the same string,
912
+ // and walking through every layer duplicates it (e.g. "CircularsCircularsCirculars").
913
+ function extractText(fiber, depth) {
914
+ if (!fiber || depth > 5000) return '';
915
+ var props = fiber.memoizedProps;
916
+ if (props) {
917
+ var ch = props.children;
918
+ if (typeof ch === 'string') return ch;
919
+ if (typeof ch === 'number') return String(ch);
920
+ if (Array.isArray(ch)) {
921
+ var allPrimitive = ch.length > 0;
922
+ var inline = [];
923
+ for (var i = 0; i < ch.length; i++) {
924
+ if (typeof ch[i] === 'string') inline.push(ch[i]);
925
+ else if (typeof ch[i] === 'number') inline.push(String(ch[i]));
926
+ else { allPrimitive = false; }
927
+ }
928
+ if (allPrimitive && inline.length > 0) return inline.join('');
929
+ }
930
+ }
931
+ var parts = [];
932
+ var child = fiber.child;
933
+ while (child) {
934
+ var t = extractText(child, depth + 1);
935
+ if (t) parts.push(t);
936
+ child = child.sibling;
937
+ }
938
+ return parts.join(' ');
939
+ }
940
+
941
+ function getMeasurable(fiber) {
942
+ var sn = fiber.stateNode;
943
+ if (!sn) return null;
944
+ if (typeof sn.measureInWindow === 'function') return sn;
945
+ if (sn.canonical && sn.canonical.publicInstance &&
946
+ typeof sn.canonical.publicInstance.measureInWindow === 'function') {
947
+ return sn.canonical.publicInstance;
948
+ }
949
+ // Fabric leaf nodes like RCTText have no publicInstance. Measure via
950
+ // the native Fabric UIManager using the shadow node instead — needed
951
+ // for text bounds that are tight around the glyphs (not the scroll
952
+ // container the text happens to live inside).
953
+ if (sn.node && globalThis.nativeFabricUIManager &&
954
+ typeof globalThis.nativeFabricUIManager.measureInWindow === 'function') {
955
+ var node = sn.node;
956
+ return {
957
+ measureInWindow: function(cb) {
958
+ try { globalThis.nativeFabricUIManager.measureInWindow(node, cb); } catch(e) {}
959
+ }
960
+ };
961
+ }
962
+ return null;
963
+ }
964
+
965
+ var RN_PRIMITIVES = /^(Animated\\(.*|withAnimated.*|AnimatedComponent.*|ForwardRef.*|memo\\(.*|Context\\.Consumer|Context\\.Provider|ScrollViewContext(Base)?|VirtualizedListContext(Resetter)?|TextInputContext|KeyboardAvoidingViewContext|RCT.*|RNS.*|RNC.*|ViewManagerAdapter_.*|VirtualizedList.*|CellRenderer.*|FrameSizeProvider.*|MaybeScreenContainer|MaybeScreen|Navigation.*|Screen$|ScreenStack|ScreenContainer|ScreenContentWrapper|SceneView|DelayedFreeze|Freeze|Suspender|DebugContainer|StaticContainer|SafeAreaProvider.*|SafeAreaFrameContext|SafeAreaInsetsContext|ExpoRoot|ExpoRootComponent|GestureHandler.*|NativeViewGestureHandler|GestureDetector|PanGestureHandler|Reanimated.*|BottomTabNavigator|TabLayout|RouteNode|Route$|KeyboardProvider|PortalProviderComponent|BottomSheetModalProviderWrapper|ThemeContext|ThemeProvider|TextAncestorContext|PressabilityDebugView|TouchableHighlightImpl|StatusBarOverlay|BottomSheetHostingContainerComponent|BottomSheetGestureHandlersProvider|BottomSheetBackdropContainerComponent|BottomSheetContainerComponent|BottomSheetDraggableViewComponent|BottomSheetHandleContainerComponent|BottomSheetBackgroundContainerComponent|DebuggingOverlay|InspectorDeferred|Inspector|InspectorOverlay|InspectorPanel|StyleInspector|BoxInspector|BoxContainer|ElementBox|BorderBox|InspectorPanelButton)$/;
966
+
967
+ function isScreenHidden(name, props) {
968
+ if (!props) return false;
969
+ if (name === 'RNSScreen' && props['aria-hidden'] === true) return true;
970
+ if (name === 'MaybeScreen' && props.active === 0) return true;
971
+ if (name === 'SceneView' && props.focused === false) return true;
972
+ return false;
973
+ }
974
+
975
+ function findMeaningfulAncestorName(fiber) {
976
+ var cur = fiber.return;
977
+ var depth = 0;
978
+ var fallbackName = null;
979
+ while (cur && depth < 20) {
980
+ var aname = getComponentName(cur);
981
+ if (aname && typeof cur.type !== 'string') {
982
+ if (!fallbackName) fallbackName = aname;
983
+ if (!RN_PRIMITIVES.test(aname)) return aname;
984
+ }
985
+ cur = cur.return;
986
+ depth++;
987
+ }
988
+ return fallbackName;
989
+ }
990
+
991
+ // Walk UP collecting testID/nativeID from ancestors. Stop at screen boundaries.
992
+ function collectAncestorTestIDs(fiber, maxUp) {
993
+ var ids = [];
994
+ var cur = fiber.return;
995
+ var d = 0;
996
+ while (cur && d < maxUp) {
997
+ var cname = getComponentName(cur);
998
+ if (cname === 'RNSScreen' || cname === 'MaybeScreen' || cname === 'SceneView') break;
999
+ var cp = cur.memoizedProps;
1000
+ if (cp) {
1001
+ if (typeof cp.testID === 'string' && cp.testID) ids.push(cp.testID);
1002
+ if (typeof cp.nativeID === 'string' && cp.nativeID) ids.push(cp.nativeID);
1003
+ }
1004
+ cur = cur.return;
1005
+ d++;
1006
+ }
1007
+ return ids;
1008
+ }
1009
+
1010
+ // Find the first measurable host descendant of a fiber.
1011
+ // For inputs, prefer TextInput-specific hosts over generic RCTView.
1012
+ function findFirstHost(fiber, depth, isInput) {
1013
+ if (!fiber || depth > 20) return null;
1014
+ if (typeof fiber.type === 'string' && getMeasurable(fiber)) {
1015
+ if (isInput) {
1016
+ var hostType = typeof fiber.type === 'string' ? fiber.type : '';
1017
+ if (hostType.indexOf('TextInput') !== -1 || hostType.indexOf('textinput') !== -1) {
1018
+ return fiber;
1019
+ }
1020
+ }
1021
+ return fiber;
1022
+ }
1023
+ var child = fiber.child;
1024
+ var fallback = null;
1025
+ while (child) {
1026
+ var found = findFirstHost(child, depth + 1, isInput);
1027
+ if (found) {
1028
+ if (isInput) {
1029
+ var ft = typeof found.type === 'string' ? found.type : '';
1030
+ if (ft.indexOf('TextInput') !== -1 || ft.indexOf('textinput') !== -1) {
1031
+ return found;
1032
+ }
1033
+ if (!fallback) fallback = found;
1034
+ } else {
1035
+ return found;
1036
+ }
1037
+ }
1038
+ child = child.sibling;
1039
+ }
1040
+ return fallback;
1041
+ }
1042
+
1043
+ var hostFibers = [];
1044
+ var tapMeta = [];
1045
+
1046
+ // Phase 1: Walk the entire tree, collect all pressable/input elements
1047
+ function walkFiber(fiber, depth, path) {
1048
+ if (!fiber || depth > 5000) return;
1049
+ var name = getComponentName(fiber);
1050
+ var props = fiber.memoizedProps;
1051
+
1052
+ if (isScreenHidden(name, props)) return;
1053
+
1054
+ var isPressable = props && typeof props.onPress === 'function';
1055
+ var isInput = !isPressable && props && (typeof props.onChangeText === 'function' || typeof props.onFocus === 'function');
1056
+
1057
+ if (isPressable || isInput) {
1058
+ var text = '';
1059
+ if (isPressable) {
1060
+ text = extractText(fiber, 0);
1061
+ } else {
1062
+ var val = typeof props.value === 'string' ? props.value : '';
1063
+ var defVal = typeof props.defaultValue === 'string' ? props.defaultValue : '';
1064
+ var ph = typeof props.placeholder === 'string' ? props.placeholder : '';
1065
+ text = extractText(fiber, 0) || val || defVal || ph;
1066
+ }
1067
+ var tid = props.testID || props.nativeID || null;
1068
+ var meaningful = findMeaningfulAncestorName(fiber);
1069
+ var ancestorIDs = collectAncestorTestIDs(fiber, maxTraversalUp);
1070
+
1071
+ var host = findFirstHost(fiber, 0, isInput);
1072
+ if (host) {
1073
+ hostFibers.push(host);
1074
+ tapMeta.push({
1075
+ name: name || '(anonymous)',
1076
+ meaningfulComponentName: meaningful || null,
1077
+ text: text.substring(0, 100),
1078
+ testID: tid,
1079
+ ancestorTestIDs: ancestorIDs,
1080
+ path: path.join(' > '),
1081
+ isInput: isInput,
1082
+ isPressable: isPressable,
1083
+ source: 'direct'
1084
+ });
1085
+ }
1086
+ }
1087
+
1088
+ var child = fiber.child;
1089
+ while (child) {
1090
+ var childName = getComponentName(child);
1091
+ walkFiber(child, depth + 1, childName ? path.concat([childName]) : path);
1092
+ child = child.sibling;
1093
+ }
1094
+ }
1095
+
1096
+ for (var ri = 0; ri < roots.length; ri++) {
1097
+ walkFiber(roots[ri].current, 0, []);
1098
+ }
1099
+
1100
+ // Phase 2a: testID on non-pressable wrapper — walk UP or DOWN to pressable/input.
1101
+ // Skipped if Phase 1 already matched via own testID or ancestor testID.
1102
+ if (searchTestID !== null) {
1103
+ var hasEnrichedTestIDMatch = false;
1104
+ for (var di = 0; di < tapMeta.length; di++) {
1105
+ if (tapMeta[di].testID === searchTestID) { hasEnrichedTestIDMatch = true; break; }
1106
+ var aids = tapMeta[di].ancestorTestIDs || [];
1107
+ for (var ai = 0; ai < aids.length; ai++) {
1108
+ if (aids[ai] === searchTestID) { hasEnrichedTestIDMatch = true; break; }
1109
+ }
1110
+ if (hasEnrichedTestIDMatch) break;
1111
+ }
1112
+
1113
+ if (!hasEnrichedTestIDMatch) {
1114
+ function findDescendantPressable(fiber, d) {
1115
+ if (!fiber || d > 10) return null;
1116
+ var fp = fiber.memoizedProps;
1117
+ var dIsPressable = fp && typeof fp.onPress === 'function';
1118
+ var dIsInput = !dIsPressable && fp && (typeof fp.onChangeText === 'function' || typeof fp.onFocus === 'function');
1119
+ if (dIsPressable || dIsInput) return { fiber: fiber, isPressable: dIsPressable, isInput: dIsInput };
1120
+ var c = fiber.child;
1121
+ while (c) {
1122
+ var r = findDescendantPressable(c, d + 1);
1123
+ if (r) return r;
1124
+ c = c.sibling;
1125
+ }
1126
+ return null;
1127
+ }
1128
+
1129
+ function findByTestID2a(fiber, path) {
1130
+ if (!fiber) return;
1131
+ var name = getComponentName(fiber);
1132
+ var props = fiber.memoizedProps;
1133
+ if (isScreenHidden(name, props)) return;
1134
+
1135
+ var tid = props && (props.testID || props.nativeID || null);
1136
+ if (tid === searchTestID) {
1137
+ var nIsPressable = props && typeof props.onPress === 'function';
1138
+ var nIsInput = !nIsPressable && props && (typeof props.onChangeText === 'function' || typeof props.onFocus === 'function');
1139
+
1140
+ if (nIsPressable || nIsInput) {
1141
+ var text = nIsPressable ? extractText(fiber, 0) : (extractText(fiber, 0) || (typeof props.value === 'string' ? props.value : '') || (typeof props.defaultValue === 'string' ? props.defaultValue : '') || (typeof props.placeholder === 'string' ? props.placeholder : ''));
1142
+ var host = findFirstHost(fiber, 0, nIsInput);
1143
+ if (host) {
1144
+ hostFibers.push(host);
1145
+ tapMeta.push({
1146
+ name: name || '(anonymous)',
1147
+ meaningfulComponentName: findMeaningfulAncestorName(fiber) || null,
1148
+ text: text.substring(0, 100),
1149
+ testID: searchTestID,
1150
+ ancestorTestIDs: [],
1151
+ path: path.join(' > '),
1152
+ isInput: nIsInput,
1153
+ isPressable: nIsPressable,
1154
+ source: 'testID-direct'
1155
+ });
1156
+ }
1157
+ } else {
1158
+ var foundAncestor = false;
1159
+ var parent = fiber.return;
1160
+ var d = 0;
1161
+ while (parent && d < maxTraversalUp) {
1162
+ var pp = parent.memoizedProps;
1163
+ var pIsPressable = pp && typeof pp.onPress === 'function';
1164
+ var pIsInput = !pIsPressable && pp && (typeof pp.onChangeText === 'function' || typeof pp.onFocus === 'function');
1165
+ if (pIsPressable || pIsInput) {
1166
+ var pText = pIsPressable ? extractText(parent, 0) : (extractText(parent, 0) || (typeof pp.value === 'string' ? pp.value : '') || (typeof pp.defaultValue === 'string' ? pp.defaultValue : '') || (typeof pp.placeholder === 'string' ? pp.placeholder : ''));
1167
+ var host = findFirstHost(parent, 0, pIsInput);
1168
+ if (host) {
1169
+ hostFibers.push(host);
1170
+ tapMeta.push({
1171
+ name: name || '(anonymous)',
1172
+ meaningfulComponentName: findMeaningfulAncestorName(parent) || null,
1173
+ text: pText.substring(0, 100),
1174
+ testID: pp.testID || pp.nativeID || searchTestID,
1175
+ ancestorTestIDs: [],
1176
+ path: path.join(' > '),
1177
+ isInput: pIsInput,
1178
+ isPressable: pIsPressable,
1179
+ source: 'testID-ancestor'
1180
+ });
1181
+ foundAncestor = true;
1182
+ }
1183
+ break;
1184
+ }
1185
+ parent = parent.return;
1186
+ d++;
1187
+ }
1188
+
1189
+ if (!foundAncestor) {
1190
+ var desc = findDescendantPressable(fiber, 0);
1191
+ if (desc) {
1192
+ var dp = desc.fiber.memoizedProps;
1193
+ var dText = desc.isPressable ? extractText(desc.fiber, 0) : (extractText(desc.fiber, 0) || (typeof dp.value === 'string' ? dp.value : '') || (typeof dp.defaultValue === 'string' ? dp.defaultValue : '') || (typeof dp.placeholder === 'string' ? dp.placeholder : ''));
1194
+ var dhost = findFirstHost(desc.fiber, 0, desc.isInput);
1195
+ if (dhost) {
1196
+ hostFibers.push(dhost);
1197
+ tapMeta.push({
1198
+ name: getComponentName(desc.fiber) || '(anonymous)',
1199
+ meaningfulComponentName: findMeaningfulAncestorName(desc.fiber) || null,
1200
+ text: dText.substring(0, 100),
1201
+ testID: dp.testID || dp.nativeID || searchTestID,
1202
+ ancestorTestIDs: [searchTestID],
1203
+ path: path.join(' > '),
1204
+ isInput: desc.isInput,
1205
+ isPressable: desc.isPressable,
1206
+ source: 'testID-descendant'
1207
+ });
1208
+ }
1209
+ }
1210
+ }
1211
+ }
1212
+ }
1213
+ var child = fiber.child;
1214
+ while (child) {
1215
+ var childName = getComponentName(child);
1216
+ findByTestID2a(child, childName ? path.concat([childName]) : path);
1217
+ child = child.sibling;
1218
+ }
1219
+ }
1220
+ for (var ri2a = 0; ri2a < roots.length; ri2a++) {
1221
+ findByTestID2a(roots[ri2a].current, []);
1222
+ }
1223
+ }
1224
+ }
1225
+
1226
+ // Phase 2b: component name on non-pressable node — walk UP or DOWN to pressable parent.
1227
+ // Skipped if Phase 1 already matched via own name or meaningfulComponentName.
1228
+ if (searchComponent !== null) {
1229
+ var scLower = searchComponent.toLowerCase();
1230
+ var hasEnrichedComponentMatch = false;
1231
+ for (var ci = 0; ci < tapMeta.length; ci++) {
1232
+ var cn = (tapMeta[ci].name || '').toLowerCase();
1233
+ var cm = (tapMeta[ci].meaningfulComponentName || '').toLowerCase();
1234
+ if (cn.indexOf(scLower) !== -1 || cm.indexOf(scLower) !== -1) {
1235
+ hasEnrichedComponentMatch = true; break;
1236
+ }
1237
+ }
1238
+
1239
+ if (!hasEnrichedComponentMatch) {
1240
+ function findDescendantPressableOnly(fiber, d) {
1241
+ if (!fiber || d > 10) return null;
1242
+ var fp = fiber.memoizedProps;
1243
+ if (fp && typeof fp.onPress === 'function') return fiber;
1244
+ var c = fiber.child;
1245
+ while (c) {
1246
+ var r = findDescendantPressableOnly(c, d + 1);
1247
+ if (r) return r;
1248
+ c = c.sibling;
1249
+ }
1250
+ return null;
1251
+ }
1252
+
1253
+ function findByName2b(fiber, path) {
1254
+ if (!fiber) return;
1255
+ var name = getComponentName(fiber);
1256
+ var props = fiber.memoizedProps;
1257
+ if (isScreenHidden(name, props)) return;
1258
+
1259
+ if (name && name.toLowerCase().indexOf(scLower) !== -1) {
1260
+ var foundAncestor = false;
1261
+ var parent = fiber.return;
1262
+ var d = 0;
1263
+ while (parent && d < maxTraversalUp) {
1264
+ var pp = parent.memoizedProps;
1265
+ if (pp && typeof pp.onPress === 'function') {
1266
+ var text = extractText(parent, 0);
1267
+ var host = findFirstHost(parent, 0, false);
1268
+ if (host) {
1269
+ hostFibers.push(host);
1270
+ tapMeta.push({
1271
+ name: name,
1272
+ meaningfulComponentName: findMeaningfulAncestorName(parent) || null,
1273
+ text: text.substring(0, 100),
1274
+ testID: pp.testID || pp.nativeID || null,
1275
+ ancestorTestIDs: [],
1276
+ path: path.join(' > '),
1277
+ isInput: false,
1278
+ isPressable: true,
1279
+ source: 'component-ancestor'
1280
+ });
1281
+ foundAncestor = true;
1282
+ }
1283
+ break;
1284
+ }
1285
+ parent = parent.return;
1286
+ d++;
1287
+ }
1288
+
1289
+ if (!foundAncestor) {
1290
+ var descFiber = findDescendantPressableOnly(fiber, 0);
1291
+ if (descFiber) {
1292
+ var dp = descFiber.memoizedProps;
1293
+ var dText = extractText(descFiber, 0);
1294
+ var dhost = findFirstHost(descFiber, 0, false);
1295
+ if (dhost) {
1296
+ hostFibers.push(dhost);
1297
+ tapMeta.push({
1298
+ name: getComponentName(descFiber) || '(anonymous)',
1299
+ meaningfulComponentName: name,
1300
+ text: dText.substring(0, 100),
1301
+ testID: dp.testID || dp.nativeID || null,
1302
+ ancestorTestIDs: [],
1303
+ path: path.join(' > '),
1304
+ isInput: false,
1305
+ isPressable: true,
1306
+ source: 'component-descendant'
1307
+ });
1308
+ }
1309
+ }
1310
+ }
1311
+ }
1312
+ var child = fiber.child;
1313
+ while (child) {
1314
+ var childName = getComponentName(child);
1315
+ findByName2b(child, childName ? path.concat([childName]) : path);
1316
+ child = child.sibling;
1317
+ }
1318
+ }
1319
+ for (var ri2b = 0; ri2b < roots.length; ri2b++) {
1320
+ findByName2b(roots[ri2b].current, []);
1321
+ }
1322
+ }
1323
+ }
1324
+
1325
+ if (hostFibers.length === 0) {
1326
+ var criteria = [];
1327
+ if (searchText !== null) criteria.push('text="' + searchText + '"');
1328
+ if (searchTestID !== null) criteria.push('testID="' + searchTestID + '"');
1329
+ if (searchComponent !== null) criteria.push('component="' + searchComponent + '"');
1330
+ return { error: 'No pressable or focusable elements found. Searched for: ' + criteria.join(', ') };
1331
+ }
1332
+
1333
+ // Store host fibers and metadata globally for step 2, dispatch measureInWindow
1334
+ globalThis.__tapHostFibers = hostFibers;
1335
+ globalThis.__tapMeta = tapMeta;
1336
+ globalThis.__tapMeasurements = new Array(hostFibers.length).fill(null);
1337
+
1338
+ for (var mi = 0; mi < hostFibers.length; mi++) {
1339
+ try {
1340
+ (function(idx) {
1341
+ getMeasurable(hostFibers[idx]).measureInWindow(function(fx, fy, fw, fh) {
1342
+ globalThis.__tapMeasurements[idx] = { x: fx, y: fy, width: fw, height: fh };
1343
+ });
1344
+ })(mi);
1345
+ } catch(e) {}
1346
+ }
1347
+
1348
+ return { count: hostFibers.length };
1349
+ })()
1350
+ `;
1351
+ const dispatchResult = await executeInApp(dispatchExpression, false, { timeoutMs: 30000 }, options.device);
1352
+ if (!dispatchResult.success)
1353
+ return dispatchResult;
1354
+ try {
1355
+ const parsed = JSON.parse(dispatchResult.result || "{}");
1356
+ if (parsed.error)
1357
+ return { success: false, error: parsed.error };
1358
+ }
1359
+ catch {
1360
+ /* ignore */
1361
+ }
1362
+ // Wait for measureInWindow callbacks
1363
+ await delay(300);
1364
+ // --- Step 2: Read measurements, filter visible, match by query ---
1365
+ const resolveExpression = `
1366
+ (function() {
1367
+ var hostFibers = globalThis.__tapHostFibers;
1368
+ var meta = globalThis.__tapMeta;
1369
+ var measurements = globalThis.__tapMeasurements;
1370
+ globalThis.__tapHostFibers = null;
1371
+ globalThis.__tapMeta = null;
1372
+ globalThis.__tapMeasurements = null;
1373
+
1374
+ if (!hostFibers || !measurements || !meta) {
1375
+ return { error: 'No measurement data. Dispatch step may have failed.' };
1376
+ }
1377
+
1378
+ var searchText = ${textParam};
1379
+ var searchTestID = ${testIDParam};
1380
+ var searchComponent = ${componentParam};
1381
+ var targetIndex = ${index};
1382
+
1383
+ // Determine viewport bounds
1384
+ var viewportW = 9999, viewportH = 9999;
1385
+ for (var v = 0; v < measurements.length; v++) {
1386
+ if (measurements[v] && measurements[v].x === 0 && measurements[v].y <= 0 &&
1387
+ measurements[v].width > 0 && measurements[v].height > 0) {
1388
+ viewportW = measurements[v].width;
1389
+ viewportH = measurements[v].height + measurements[v].y;
1390
+ break;
1391
+ }
1392
+ }
1393
+
1394
+ // Filter visible and match
1395
+ var matches = [];
1396
+ for (var i = 0; i < measurements.length; i++) {
1397
+ var m = measurements[i];
1398
+ if (!m) continue;
1399
+
1400
+ // Visibility filter: positive dimensions, within viewport
1401
+ if (m.width <= 0 || m.height <= 0) continue;
1402
+ if (m.x + m.width < 0 || m.y + m.height < 0) continue;
1403
+ if (m.x > viewportW || m.y > viewportH) continue;
1404
+
1405
+ var info = meta[i];
1406
+
1407
+ // Match by query — OR across own and enriched identifiers
1408
+ var matched = true;
1409
+ if (searchText !== null) {
1410
+ matched = matched && info.text.toLowerCase().indexOf(searchText.toLowerCase()) !== -1;
1411
+ }
1412
+ if (searchTestID !== null) {
1413
+ var ownTidMatch = info.testID === searchTestID;
1414
+ var aTids = info.ancestorTestIDs || [];
1415
+ var ancestorTidMatch = false;
1416
+ for (var ti = 0; ti < aTids.length; ti++) {
1417
+ if (aTids[ti] === searchTestID) { ancestorTidMatch = true; break; }
1418
+ }
1419
+ matched = matched && (ownTidMatch || ancestorTidMatch);
1420
+ }
1421
+ if (searchComponent !== null) {
1422
+ var scq = searchComponent.toLowerCase();
1423
+ var ownNameMatch = (info.name || '').toLowerCase().indexOf(scq) !== -1;
1424
+ var meaningfulMatch = (info.meaningfulComponentName || '').toLowerCase().indexOf(scq) !== -1;
1425
+ matched = matched && (ownNameMatch || meaningfulMatch);
1426
+ }
1427
+
1428
+ if (matched) {
1429
+ matches.push({
1430
+ name: info.name,
1431
+ text: info.text,
1432
+ testID: info.testID,
1433
+ path: info.path,
1434
+ isInput: info.isInput,
1435
+ x: Math.round(m.x + m.width / 2),
1436
+ y: Math.round(m.y + m.height / 2)
1437
+ });
1438
+ }
1439
+ }
1440
+
1441
+ if (matches.length === 0) {
1442
+ var criteria = [];
1443
+ if (searchText !== null) criteria.push('text="' + searchText + '"');
1444
+ if (searchTestID !== null) criteria.push('testID="' + searchTestID + '"');
1445
+ if (searchComponent !== null) criteria.push('component="' + searchComponent + '"');
1446
+ return { error: 'No visible pressable or focusable elements found matching: ' + criteria.join(', ') };
1447
+ }
1448
+
1449
+ if (targetIndex >= matches.length) {
1450
+ return {
1451
+ error: 'Found ' + matches.length + ' visible match(es) but index ' + targetIndex + ' requested (0-based). Use index 0-' + (matches.length - 1) + '.',
1452
+ matches: matches.map(function(m, i) {
1453
+ return { index: i, component: m.name, text: m.text, testID: m.testID };
1454
+ })
1455
+ };
1456
+ }
1457
+
1458
+ var target = matches[targetIndex];
1459
+ var result = {
1460
+ needsNativeTap: true,
1461
+ nativeTapTarget: { x: target.x, y: target.y, unit: 'points' },
1462
+ pressed: target.name,
1463
+ matchIndex: targetIndex,
1464
+ totalMatches: matches.length,
1465
+ text: target.text,
1466
+ testID: target.testID,
1467
+ path: target.path,
1468
+ isInput: target.isInput
1469
+ };
1470
+ if (matches.length > 1) {
1471
+ result.allMatches = matches.map(function(m, i) {
1472
+ return { index: i, component: m.name, text: m.text, testID: m.testID, x: m.x, y: m.y };
1473
+ });
1474
+ }
1475
+ return result;
1476
+ })()
1477
+ `;
1478
+ return executeInApp(resolveExpression, false, { timeoutMs: 10000 }, options.device);
1479
+ }
1480
+ //# sourceMappingURL=pressables.js.map