execbro 2.2.0 → 2.3.0

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 (53) hide show
  1. package/README.md +4 -3
  2. package/build/__tests__/unit/screenSpace.test.d.ts +2 -0
  3. package/build/__tests__/unit/screenSpace.test.d.ts.map +1 -0
  4. package/build/__tests__/unit/screenSpace.test.js +58 -0
  5. package/build/__tests__/unit/screenSpace.test.js.map +1 -0
  6. package/build/__tests__/unit/visibility.test.js +21 -0
  7. package/build/__tests__/unit/visibility.test.js.map +1 -1
  8. package/build/core/guides.d.ts.map +1 -1
  9. package/build/core/guides.js +18 -4
  10. package/build/core/guides.js.map +1 -1
  11. package/build/core/index.d.ts +1 -1
  12. package/build/core/index.d.ts.map +1 -1
  13. package/build/core/index.js +1 -1
  14. package/build/core/index.js.map +1 -1
  15. package/build/core/injected/visibility.d.ts +14 -1
  16. package/build/core/injected/visibility.d.ts.map +1 -1
  17. package/build/core/injected/visibility.js +28 -4
  18. package/build/core/injected/visibility.js.map +1 -1
  19. package/build/core/injectedFilters.d.ts +10 -2
  20. package/build/core/injectedFilters.d.ts.map +1 -1
  21. package/build/core/injectedFilters.js +10 -2
  22. package/build/core/injectedFilters.js.map +1 -1
  23. package/build/core/inspector.d.ts +6 -0
  24. package/build/core/inspector.d.ts.map +1 -1
  25. package/build/core/inspector.js +200 -36
  26. package/build/core/inspector.js.map +1 -1
  27. package/build/core/pressables.d.ts +0 -32
  28. package/build/core/pressables.d.ts.map +1 -1
  29. package/build/core/pressables.js +5 -1061
  30. package/build/core/pressables.js.map +1 -1
  31. package/build/core/screenLayout.d.ts +3 -0
  32. package/build/core/screenLayout.d.ts.map +1 -1
  33. package/build/core/screenLayout.js +32 -0
  34. package/build/core/screenLayout.js.map +1 -1
  35. package/build/core/screenSpace.d.ts +75 -0
  36. package/build/core/screenSpace.d.ts.map +1 -0
  37. package/build/core/screenSpace.js +84 -0
  38. package/build/core/screenSpace.js.map +1 -0
  39. package/build/core/screenSpaceDevice.d.ts +15 -0
  40. package/build/core/screenSpaceDevice.d.ts.map +1 -0
  41. package/build/core/screenSpaceDevice.js +24 -0
  42. package/build/core/screenSpaceDevice.js.map +1 -0
  43. package/build/core/screenState.d.ts.map +1 -1
  44. package/build/core/screenState.js +107 -8
  45. package/build/core/screenState.js.map +1 -1
  46. package/build/tools/componentTools.d.ts.map +1 -1
  47. package/build/tools/componentTools.js +54 -186
  48. package/build/tools/componentTools.js.map +1 -1
  49. package/build/tools/screenshotTools.d.ts.map +1 -1
  50. package/build/tools/screenshotTools.js +37 -72
  51. package/build/tools/screenshotTools.js.map +1 -1
  52. package/package.json +1 -1
  53. package/tools.json +1 -5
@@ -1,7 +1,6 @@
1
1
  import { executeInApp, delay } from "./jsExecute.js";
2
- import { iconLabel } from "./iconSemantics.js";
3
- import { getConnectedAppByDevice, getFirstConnectedApp, getConnectedAppBySimulatorUdid, getConnectedAppByAndroidDeviceId } from "./connection.js";
4
2
  import { VISIBILITY_HELPERS_JS } from "./injected/visibility.js";
3
+ import { RN_PRIMITIVES_SRC } from "./injectedFilters.js";
5
4
  // ============================================================================
6
5
  // Pressable Elements & onPress invocation
7
6
  // ============================================================================
@@ -71,1064 +70,6 @@ async function awaitMeasurements(globalNames, toolName, device) {
71
70
  }
72
71
  });
73
72
  }
74
- /**
75
- * E1 / Step 5 (2026-05-15 plan): build a pressables list from the platform
76
- * accessibility tree when fiber isn't reachable. Used when metro is offline
77
- * (connectedApps.size === 0) so the screenshot tools can still hand the agent
78
- * labelled coordinates instead of forcing it to guess pixels — the dominant
79
- * cause of unmeaningful coordinate-strategy taps in the 7-day failure data.
80
- */
81
- async function getAccessibilityPressables(platform, udid) {
82
- try {
83
- if (platform === "ios") {
84
- const { iosGetUITree } = await import("./ios.js");
85
- const tree = await iosGetUITree(udid);
86
- if (!tree.success || !tree.elements)
87
- return [];
88
- const interactiveTraits = new Set(["button", "link", "searchfield", "tab", "image"]);
89
- return tree.elements
90
- .filter(el => {
91
- // iOS: rely on a11y traits + element type. Buttons, links,
92
- // tabs, and any element with a non-empty label/identifier
93
- // that isn't a static text container.
94
- const traitsLower = (el.traits || []).map(t => t.toLowerCase());
95
- if (traitsLower.some(t => interactiveTraits.has(t)))
96
- return true;
97
- const type = (el.type || "").toLowerCase();
98
- if (type.includes("button") || type.includes("textfield") || type.includes("searchfield"))
99
- return true;
100
- return false;
101
- })
102
- .map(el => {
103
- const type = (el.type || "").toLowerCase();
104
- return {
105
- component: el.type || "Element",
106
- path: "",
107
- center: el.center,
108
- frame: el.frame,
109
- text: el.label || "",
110
- testID: el.identifier || null,
111
- accessibilityLabel: el.label || null,
112
- hasLabel: !!el.label,
113
- isInput: type.includes("textfield") || type.includes("searchfield")
114
- };
115
- });
116
- }
117
- else {
118
- const { androidGetUITree } = await import("./android.js");
119
- const tree = await androidGetUITree();
120
- if (!tree.success || !tree.elements)
121
- return [];
122
- return tree.elements
123
- .filter(el => el.clickable || el.focused || (el.className && el.className.includes("EditText")))
124
- .map(el => ({
125
- component: (el.className || "View").split(".").pop() || "View",
126
- path: "",
127
- center: el.center,
128
- frame: { x: el.bounds.left, y: el.bounds.top, width: el.bounds.width, height: el.bounds.height },
129
- text: el.text || "",
130
- testID: el.resourceId || null,
131
- accessibilityLabel: el.contentDesc || null,
132
- hasLabel: !!(el.text || el.contentDesc),
133
- isInput: !!(el.className && el.className.includes("EditText"))
134
- }));
135
- }
136
- }
137
- catch {
138
- return [];
139
- }
140
- }
141
- export async function getPressableElements(options = {}) {
142
- const { device, platform, udid } = options;
143
- // Resolve which connected RN app corresponds to the requested device/udid.
144
- // The E1 fallback below must fire when no app matches THIS device, even if
145
- // unrelated apps remain connected to Metro. Without this filter the fiber
146
- // path would silently fall back to the first-connected app and return data
147
- // for the wrong device (Bug 1, 2026-05-16).
148
- let targetApp = null;
149
- if (udid && platform === "ios") {
150
- targetApp = getConnectedAppBySimulatorUdid(udid);
151
- // scan_metro links simulatorUdid asynchronously after the CDP connect; in
152
- // that window the udid lookup misses even though the right app is already
153
- // connected. Fall back to the device-name match so callers (ios_screenshot)
154
- // don't silently degrade to the accessibility-tree pressables list.
155
- if (!targetApp && device) {
156
- const matched = getConnectedAppByDevice(device);
157
- if (matched && matched.platform === "ios")
158
- targetApp = matched;
159
- }
160
- }
161
- else if (device) {
162
- const matched = getConnectedAppByDevice(device);
163
- if (matched && (!platform || matched.platform === platform))
164
- targetApp = matched;
165
- }
166
- else if (platform === "android") {
167
- targetApp = getConnectedAppByAndroidDeviceId(undefined);
168
- }
169
- else if (platform === "ios") {
170
- const first = getFirstConnectedApp();
171
- targetApp = first && first.platform === "ios" ? first : null;
172
- }
173
- else {
174
- targetApp = getFirstConnectedApp();
175
- }
176
- // When the caller didn't pin a platform, adopt the resolved app's platform so
177
- // downstream branches (Android coordinate reconciliation below) run uniformly
178
- // regardless of entry point. The standalone get_pressable_elements MCP tool
179
- // omits `platform`; without this derivation it would skip reconciliation and
180
- // return raw fiber DP that downstream consumers misinterpret as device pixels.
181
- const effectivePlatform = platform ?? targetApp?.platform;
182
- // E1 fallback: no app resolved for THIS device → use platform accessibility tree.
183
- // Only kicks in when caller passed `platform` so we don't break older
184
- // metro-required call paths that didn't opt into the fallback.
185
- if (!targetApp && platform) {
186
- const elements = await getAccessibilityPressables(platform, udid);
187
- return {
188
- success: true,
189
- result: elements.length > 0
190
- ? `Pressable elements (from accessibility tree, ${elements.length} found):\n` +
191
- elements.map((el, i) => {
192
- const label = el.accessibilityLabel || el.text || el.testID || el.component;
193
- const idStr = el.testID ? ` testID="${el.testID}"` : "";
194
- const inputStr = el.isInput ? " [input]" : "";
195
- 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})`;
196
- }).join("\n")
197
- : "No interactive elements found via accessibility.",
198
- parsedElements: elements
199
- };
200
- }
201
- // --- Step 1: walk fiber tree, find pressable/input elements, dispatch measureInWindow ---
202
- const dispatchExpression = `
203
- (function() {
204
- var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
205
- if (!hook) return { error: 'React DevTools hook not found.' };
206
-
207
- var roots = [];
208
- if (hook.getFiberRoots) {
209
- roots = Array.from(hook.getFiberRoots(1) || []);
210
- }
211
- if (roots.length === 0 && hook.renderers) {
212
- for (var entry of hook.renderers) {
213
- var r = Array.from(hook.getFiberRoots ? (hook.getFiberRoots(entry[0]) || []) : []);
214
- if (r.length > 0) { roots = r; break; }
215
- }
216
- }
217
- if (roots.length === 0) return { error: 'No fiber roots found.' };
218
-
219
- function getMeasurable(fiber) {
220
- var sn = fiber.stateNode;
221
- if (!sn) return null;
222
- if (typeof sn.measureInWindow === 'function') return sn;
223
- if (sn.canonical && sn.canonical.publicInstance &&
224
- typeof sn.canonical.publicInstance.measureInWindow === 'function') {
225
- return sn.canonical.publicInstance;
226
- }
227
- // Fabric leaf nodes like RCTText have no publicInstance. Measure via
228
- // the native Fabric UIManager using the shadow node instead — needed
229
- // for text bounds that are tight around the glyphs (not the scroll
230
- // container the text happens to live inside).
231
- if (sn.node && globalThis.nativeFabricUIManager &&
232
- typeof globalThis.nativeFabricUIManager.measureInWindow === 'function') {
233
- var node = sn.node;
234
- return {
235
- measureInWindow: function(cb) {
236
- try { globalThis.nativeFabricUIManager.measureInWindow(node, cb); } catch(e) {}
237
- }
238
- };
239
- }
240
- return null;
241
- }
242
-
243
- function findFirstHost(fiber, depth) {
244
- if (!fiber || depth > 20) return null;
245
- if (typeof fiber.type === 'string' && getMeasurable(fiber)) return fiber;
246
- var child = fiber.child;
247
- while (child) {
248
- var found = findFirstHost(child, depth + 1);
249
- if (found) return found;
250
- child = child.sibling;
251
- }
252
- return null;
253
- }
254
-
255
- function findHostsInSubtree(fiber, depth, hosts, limit) {
256
- if (!fiber || depth > 20 || hosts.length >= limit) return;
257
- if (typeof fiber.type === 'string' && getMeasurable(fiber)) {
258
- hosts.push(fiber);
259
- return;
260
- }
261
- var child = fiber.child;
262
- while (child && hosts.length < limit) {
263
- findHostsInSubtree(child, depth + 1, hosts, limit);
264
- child = child.sibling;
265
- }
266
- }
267
-
268
- function collectText(fiber, d, isRoot) {
269
- if (!fiber || d > 30) return '';
270
- var props = fiber.memoizedProps;
271
- // Stop descent at nested pressable/input boundaries — their text belongs to them, not to the outer wrapper.
272
- if (!isRoot && props && (typeof props.onPress === 'function' ||
273
- typeof props.onChangeText === 'function' ||
274
- typeof props.onFocus === 'function')) {
275
- return '';
276
- }
277
- // Host text fibers carry the raw string as memoizedProps — needed when a
278
- // mixed-children Text falls through to the fiber-child walk below.
279
- if (typeof props === 'string') return props;
280
- if (typeof props === 'number') return String(props);
281
- if (props) {
282
- var ch = props.children;
283
- if (typeof ch === 'string') return ch;
284
- if (typeof ch === 'number') return String(ch);
285
- if (Array.isArray(ch)) {
286
- var inline = [];
287
- var hasElement = false;
288
- for (var ci = 0; ci < ch.length; ci++) {
289
- if (typeof ch[ci] === 'string') inline.push(ch[ci]);
290
- else if (typeof ch[ci] === 'number') inline.push(String(ch[ci]));
291
- else if (ch[ci] != null && ch[ci] !== false) hasElement = true;
292
- }
293
- // Mixed content ("Create " + <Text>2 digital items</Text>): fall through
294
- // to the fiber-child walk so nested Text elements are included; the host
295
- // text fibers re-supply the inline strings, so nothing is lost or doubled.
296
- if (inline.length > 0 && !hasElement) return inline.join('');
297
- }
298
- }
299
- var parts = [];
300
- var child = fiber.child;
301
- while (child) {
302
- var t = collectText(child, d + 1, false);
303
- if (t) parts.push(t);
304
- child = child.sibling;
305
- }
306
- return parts.join(' ').replace(/\\s+/g, ' ').trim();
307
- }
308
-
309
- function getComponentName(fiber) {
310
- if (!fiber || !fiber.type) return null;
311
- if (typeof fiber.type === 'string') return fiber.type;
312
- return fiber.type.displayName || fiber.type.name || null;
313
- }
314
-
315
- ${VISIBILITY_HELPERS_JS}
316
-
317
- 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)$/;
318
-
319
- var hostFibers = [];
320
- var fiberMeta = [];
321
- var textFibers = [];
322
- var textContents = [];
323
-
324
- function findMeaningfulAncestorName(fiber) {
325
- var cur = fiber.return;
326
- var depth = 0;
327
- var fallbackName = null;
328
- while (cur && depth < 20) {
329
- var name = getComponentName(cur);
330
- if (name && typeof cur.type !== 'string') {
331
- if (!fallbackName) fallbackName = name;
332
- if (!RN_PRIMITIVES.test(name)) return name;
333
- }
334
- cur = cur.return;
335
- depth++;
336
- }
337
- return fallbackName;
338
- }
339
-
340
- // Layout-only and touch-wrapper components that should be skipped when scanning
341
- // children for a meaningful icon/content component name.
342
- 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.*)$/;
343
-
344
- // Scan children of a pressable for a meaningful component name (e.g. SvgChevronBack inside a View).
345
- // Skips generic layout/touch wrappers to find the actual icon or content component.
346
- function findMeaningfulChildName(fiber) {
347
- function scan(f, d) {
348
- if (!f || d > 12) return null;
349
- var n = getComponentName(f);
350
- if (n && typeof f.type !== 'string' && !RN_PRIMITIVES.test(n) && !SKIP_IN_CHILD_SCAN.test(n)) return n;
351
- var c = f.child;
352
- while (c) {
353
- var r = scan(c, d + 1);
354
- if (r) return r;
355
- c = c.sibling;
356
- }
357
- return null;
358
- }
359
- return scan(fiber.child, 0);
360
- }
361
-
362
- var GENERIC_COMPONENT = /^(View|TouchableOpacity|TouchableHighlight|TouchableWithoutFeedback|Pressable|TouchableNativeFeedback|Text|RCTView|RCTText|Unknown)$/;
363
-
364
- // The components that render PressabilityDebugView in DEV builds. Used to walk
365
- // up from a PDV's host view to the touchable that owns it — the composite chain
366
- // in between is generic wrappers (View ForwardRef, Animated(View)) whose props
367
- // carry no onPress, so they are useless as a collectText / identifier root.
368
- var PDV_OWNER_COMPONENT = /^(Pressable|Touchable(Opacity|Highlight|WithoutFeedback|NativeFeedback|Bounce))$/;
369
-
370
- function buildPath(fiber) {
371
- var parts = [];
372
- var cur = fiber;
373
- var depth = 0;
374
- while (cur && depth < 30) {
375
- var name = getComponentName(cur);
376
- if (name && typeof cur.type !== 'string' && !RN_PRIMITIVES.test(name)) {
377
- parts.unshift(name);
378
- }
379
- cur = cur.return;
380
- depth++;
381
- }
382
- // Keep last 3 segments
383
- if (parts.length > 3) {
384
- parts = parts.slice(-3);
385
- return '... > ' + parts.join(' > ');
386
- }
387
- return parts.join(' > ');
388
- }
389
-
390
- function walkPressables(fiber, depth) {
391
- if (!fiber || depth > 5000) return;
392
- var name = getComponentName(fiber);
393
- var props = fiber.memoizedProps;
394
-
395
- // Skip inactive/unfocused screens
396
- if (isHiddenNavigationScene(name, props)) return;
397
-
398
- var isPressable = props && typeof props.onPress === 'function';
399
- var isInput = !isPressable && props && (typeof props.onChangeText === 'function' || typeof props.onFocus === 'function');
400
-
401
- if (isPressable || isInput) {
402
- var hostsForThis = [];
403
- findHostsInSubtree(fiber, 0, hostsForThis, 16);
404
- if (hostsForThis.length > 0) {
405
- var text = collectText(fiber, 0, true);
406
- var componentName = findMeaningfulAncestorName(fiber) || name || 'Unknown';
407
- // Scan children for a meaningful name like SvgChevronBack: it replaces a
408
- // generic name (View, TouchableOpacity) and is also kept as the icon hint
409
- // for text-less buttons even when the ancestor name (FloatingHeader) wins.
410
- var childName = (!text || GENERIC_COMPONENT.test(componentName)) ? findMeaningfulChildName(fiber) : null;
411
- if (childName && GENERIC_COMPONENT.test(componentName)) componentName = childName;
412
- var path = buildPath(fiber);
413
- var testID = (props && (props.testID || props.nativeID)) || null;
414
- var accessibilityLabel = (props && props.accessibilityLabel) || null;
415
-
416
- var hostIndices = [];
417
- for (var hi = 0; hi < hostsForThis.length; hi++) {
418
- hostIndices.push(hostFibers.length);
419
- hostFibers.push(hostsForThis[hi]);
420
- }
421
- fiberMeta.push({
422
- component: componentName,
423
- path: path,
424
- text: text ? text.slice(0, 100) : '',
425
- testID: testID,
426
- accessibilityLabel: accessibilityLabel,
427
- isInput: !!isInput,
428
- icon: childName || null,
429
- hostIndices: hostIndices
430
- });
431
- }
432
- }
433
-
434
- var child = fiber.child;
435
- while (child) {
436
- walkPressables(child, depth + 1);
437
- child = child.sibling;
438
- }
439
- }
440
-
441
- // PRIMARY detection: PressabilityDebugView-based walk — mirrors RN Inspector "Touchables" exactly.
442
- // In DEV mode, every Pressable / TouchableOpacity / TouchableHighlight /
443
- // TouchableWithoutFeedback / TouchableNativeFeedback renders a PressabilityDebugView
444
- // as a child. Its immediate parent (fiber.return) is the HOST view that carries
445
- // the event handlers — we measure that.
446
- // FALLBACK: plain onPress detection for production builds (PDV not rendered).
447
-
448
- var anyPDV = false;
449
- (function quickCheckPDV(f, d) {
450
- if (!f || d > 200 || anyPDV) return;
451
- var n = getComponentName(f);
452
- if (n === 'PressabilityDebugView') { anyPDV = true; return; }
453
- quickCheckPDV(f.child, d + 1);
454
- if (!anyPDV) quickCheckPDV(f.sibling, d);
455
- })(roots[0].current, 0);
456
-
457
- // Walk for TextInput elements (not covered by PressabilityDebugView in either mode).
458
- function walkInputs(fiber, depth) {
459
- if (!fiber || depth > 5000) return;
460
- var name = getComponentName(fiber);
461
- var props = fiber.memoizedProps;
462
- if (isHiddenNavigationScene(name, props)) return;
463
- var isInput = props && !props.onPress &&
464
- (typeof props.onChangeText === 'function' || typeof props.onFocus === 'function');
465
- if (isInput) {
466
- var hostsForThis = [];
467
- findHostsInSubtree(fiber, 0, hostsForThis, 16);
468
- if (hostsForThis.length > 0) {
469
- var text = collectText(fiber, 0, true);
470
- var componentName = findMeaningfulAncestorName(fiber) || name || 'Unknown';
471
- if (GENERIC_COMPONENT.test(componentName)) {
472
- var childName = findMeaningfulChildName(fiber);
473
- if (childName) componentName = childName;
474
- }
475
- var props2 = fiber.memoizedProps || {};
476
- var testID = (props2.testID || props2.nativeID) || null;
477
- var accessibilityLabel = props2.accessibilityLabel || null;
478
- var hostIndices = [];
479
- for (var hi3 = 0; hi3 < hostsForThis.length; hi3++) {
480
- hostIndices.push(hostFibers.length);
481
- hostFibers.push(hostsForThis[hi3]);
482
- }
483
- fiberMeta.push({
484
- component: componentName,
485
- path: buildPath(fiber),
486
- text: text ? text.slice(0, 100) : '',
487
- testID: testID,
488
- accessibilityLabel: accessibilityLabel,
489
- isInput: true,
490
- hostIndices: hostIndices
491
- });
492
- }
493
- }
494
- var child = fiber.child;
495
- while (child) { walkInputs(child, depth + 1); child = child.sibling; }
496
- }
497
-
498
- function walkPressabilityDebugViews(fiber, depth, hidden) {
499
- if (!fiber || depth > 5000) return;
500
- var name = getComponentName(fiber);
501
- var props = fiber.memoizedProps;
502
- var nextHidden = hidden;
503
- if (isHiddenNavigationScene(name, props)) nextHidden = true;
504
-
505
- if (!nextHidden && name === 'PressabilityDebugView') {
506
- // fiber.return = the HOST view that wraps the pressable's children
507
- var hostFiber = fiber.return;
508
- if (hostFiber && getMeasurable(hostFiber)) {
509
- // Walk up from the host fiber to the touchable component that owns this
510
- // PDV. Match by name (PDV_OWNER_COMPONENT) — stopping at the first
511
- // composite ancestor is wrong because that's a generic wrapper
512
- // (ForwardRef displayName 'View', then Animated(View) for Touchables).
513
- var pressableFiber = hostFiber;
514
- var firstComposite = null;
515
- var cur = hostFiber.return;
516
- var upDepth = 0;
517
- while (cur && upDepth < 10) {
518
- if (typeof cur.type !== 'string' && cur.type !== null) {
519
- if (!firstComposite) firstComposite = cur;
520
- var curName = getComponentName(cur);
521
- if (curName && PDV_OWNER_COMPONENT.test(curName)) {
522
- pressableFiber = cur;
523
- break;
524
- }
525
- }
526
- cur = cur.return;
527
- upDepth++;
528
- }
529
- if (pressableFiber === hostFiber && firstComposite) pressableFiber = firstComposite;
530
-
531
- var pressableProps = pressableFiber.memoizedProps || {};
532
- var text = collectText(pressableFiber, 0, true);
533
- // Prefer the nearest semantic ancestor (FilterRow, RoleButton, ...) over
534
- // generic Touchable/Pressable/View wrappers. The climb is capped at a few
535
- // composite levels so a bare Pressable isn't named after its screen container.
536
- var componentName = null;
537
- var an = pressableFiber.return;
538
- var anComposites = 0;
539
- var anDepth = 0;
540
- while (an && anDepth < 12 && anComposites < 4 && !componentName) {
541
- if (typeof an.type !== 'string') {
542
- var anName = getComponentName(an);
543
- if (anName) {
544
- anComposites++;
545
- if (!RN_PRIMITIVES.test(anName) && !GENERIC_COMPONENT.test(anName)) {
546
- componentName = anName;
547
- }
548
- }
549
- }
550
- an = an.return;
551
- anDepth++;
552
- }
553
- if (!componentName) {
554
- componentName = findMeaningfulAncestorName(pressableFiber) || getComponentName(pressableFiber) || 'Unknown';
555
- }
556
- // Keep the icon child name even when the ancestor name wins — icon-only
557
- // buttons get their semantics from it (SvgChevronBack → back button).
558
- var childName = (!text || GENERIC_COMPONENT.test(componentName)) ? findMeaningfulChildName(pressableFiber) : null;
559
- if (childName && GENERIC_COMPONENT.test(componentName)) componentName = childName;
560
- var path = buildPath(pressableFiber);
561
-
562
- // testID/accessibilityLabel are spread onto the host view by TouchableOpacity etc.
563
- var hostProps = hostFiber.memoizedProps || {};
564
- var testID = hostProps.testID || hostProps.nativeID || pressableProps.testID || pressableProps.nativeID || null;
565
- var accessibilityLabel = hostProps.accessibilityLabel || pressableProps.accessibilityLabel || null;
566
-
567
- var hostIdx = hostFibers.length;
568
- hostFibers.push(hostFiber);
569
- fiberMeta.push({
570
- component: componentName,
571
- path: path,
572
- text: text ? text.slice(0, 100) : '',
573
- testID: testID,
574
- accessibilityLabel: accessibilityLabel,
575
- isInput: false,
576
- icon: childName || null,
577
- hostIndices: [hostIdx]
578
- });
579
- }
580
- // Do not recurse into PressabilityDebugView's own children (debug overlay)
581
- return;
582
- }
583
-
584
- var child = fiber.child;
585
- while (child) {
586
- walkPressabilityDebugViews(child, depth + 1, nextHidden);
587
- child = child.sibling;
588
- }
589
- }
590
-
591
- if (anyPDV) {
592
- walkPressabilityDebugViews(roots[0].current, 0, false);
593
- walkInputs(roots[0].current, 0);
594
- } else {
595
- // Fallback: original onPress-based detection for production builds
596
- walkPressables(roots[0].current, 0);
597
- }
598
-
599
- // Collect text fibers that are NOT inside a pressable — used later to attach
600
- // spatial 'nearbyText' hints to icon-only pressables.
601
- function extractTextString(fiber) {
602
- var p = fiber.memoizedProps;
603
- if (!p) return '';
604
- var ch = p.children;
605
- if (typeof ch === 'string') return ch;
606
- if (typeof ch === 'number') return String(ch);
607
- if (Array.isArray(ch)) {
608
- var parts = [];
609
- for (var k = 0; k < ch.length; k++) {
610
- if (typeof ch[k] === 'string') parts.push(ch[k]);
611
- else if (typeof ch[k] === 'number') parts.push(String(ch[k]));
612
- }
613
- if (parts.length > 0) return parts.join('');
614
- }
615
- return '';
616
- }
617
-
618
- function walkTexts(fiber, depth, insidePressable, inHidden) {
619
- if (!fiber || depth > 5000) return;
620
- var name = getComponentName(fiber);
621
- var props = fiber.memoizedProps;
622
-
623
- // Track hidden-screen ancestry without early return — a MaybeScreen wrapper
624
- // can enclose BOTH the active and inactive siblings, so we must keep walking
625
- // to find the active one while still skipping the inactive subtree's texts.
626
- var nextHidden = inHidden;
627
- if (isHiddenNavigationScene(name, props)) nextHidden = true;
628
-
629
- var hasOnPress = props && typeof props.onPress === 'function';
630
- var isInputHere = props && (typeof props.onChangeText === 'function' || typeof props.onFocus === 'function');
631
- var nextInside = insidePressable || !!hasOnPress || !!isInputHere;
632
-
633
- // Record standalone text when outside any pressable. Detects both stock
634
- // Text and custom wrappers (CustomText, ThemedText, etc.) by checking for
635
- // direct string/number children. Fabric RCTText has no publicInstance, so
636
- // we walk up the fiber chain to the nearest measurable host (the View
637
- // enclosing the text) and use its frame as the text's proxy bounds.
638
- if (!insidePressable && !nextHidden && name !== 'RCTText' && typeof fiber.type !== 'string') {
639
- var str = extractTextString(fiber);
640
- if (str && str.length > 0 && str.length <= 120) {
641
- var up = fiber;
642
- var upDepth = 0;
643
- var measurable = null;
644
- while (up && upDepth < 20) {
645
- if (typeof up.type === 'string' && getMeasurable(up)) {
646
- measurable = up;
647
- break;
648
- }
649
- up = up.return;
650
- upDepth++;
651
- }
652
- if (measurable) {
653
- textFibers.push(measurable);
654
- textContents.push(str);
655
- }
656
- }
657
- }
658
-
659
- var child = fiber.child;
660
- while (child) {
661
- walkTexts(child, depth + 1, nextInside, nextHidden);
662
- child = child.sibling;
663
- }
664
- }
665
- walkTexts(roots[0].current, 0, false, false);
666
-
667
- if (hostFibers.length === 0) return { error: 'No pressable elements found on screen.' };
668
-
669
- // Also measure the root view for viewport detection (appended; tracked by explicit index to preserve hostIndices)
670
- var rootHost = findFirstHost(roots[0].current, 0);
671
- var rootIdx = -1;
672
- if (rootHost) {
673
- rootIdx = hostFibers.length;
674
- hostFibers.push(rootHost);
675
- }
676
-
677
- globalThis.__pressableFibers = hostFibers;
678
- globalThis.__pressableMeta = fiberMeta;
679
- globalThis.__pressableMeasurements = new Array(hostFibers.length).fill(null);
680
- globalThis.__pressableRootIdx = rootIdx;
681
-
682
- for (var i = 0; i < hostFibers.length; i++) {
683
- try {
684
- (function(idx) {
685
- getMeasurable(hostFibers[idx]).measureInWindow(function(fx, fy, fw, fh) {
686
- globalThis.__pressableMeasurements[idx] = { x: fx, y: fy, width: fw, height: fh };
687
- });
688
- })(i);
689
- } catch(e) {}
690
- }
691
-
692
- globalThis.__pressableTextContents = textContents;
693
- globalThis.__pressableTextMeasurements = new Array(textFibers.length).fill(null);
694
- for (var ti = 0; ti < textFibers.length; ti++) {
695
- try {
696
- (function(idx) {
697
- getMeasurable(textFibers[idx]).measureInWindow(function(fx, fy, fw, fh) {
698
- globalThis.__pressableTextMeasurements[idx] = { x: fx, y: fy, width: fw, height: fh };
699
- });
700
- })(ti);
701
- } catch(e) {}
702
- }
703
-
704
- return { count: hostFibers.length, textCount: textFibers.length };
705
- })()
706
- `;
707
- const dispatchResult = await executeInApp(dispatchExpression, false, { timeoutMs: 30000, originatingToolName: "get_pressable_elements" }, device);
708
- if (!dispatchResult.success)
709
- return dispatchResult;
710
- try {
711
- const parsed = JSON.parse(dispatchResult.result || "{}");
712
- if (parsed.error)
713
- return { success: false, error: parsed.error };
714
- }
715
- catch {
716
- /* ignore */
717
- }
718
- // Wait for measureInWindow callbacks (adaptive; see waitForMeasurements)
719
- await awaitMeasurements(["__pressableMeasurements", "__pressableTextMeasurements"], "get_pressable_elements", device);
720
- // --- Step 2: read measurements, filter visible, build results ---
721
- const resolveExpression = `
722
- (function() {
723
- var fibers = globalThis.__pressableFibers;
724
- var meta = globalThis.__pressableMeta;
725
- var measurements = globalThis.__pressableMeasurements;
726
- var rootIdx = globalThis.__pressableRootIdx;
727
- var textContents = globalThis.__pressableTextContents || [];
728
- var textMeasurements = globalThis.__pressableTextMeasurements || [];
729
- globalThis.__pressableFibers = null;
730
- globalThis.__pressableMeta = null;
731
- globalThis.__pressableMeasurements = null;
732
- globalThis.__pressableRootIdx = null;
733
- globalThis.__pressableTextContents = null;
734
- globalThis.__pressableTextMeasurements = null;
735
-
736
- if (!fibers || !measurements || !meta) {
737
- return { error: 'No measurement data. Run get_pressable_elements again.' };
738
- }
739
-
740
- // Get viewport dimensions from the explicit root measurement (fallback to scanning)
741
- var viewportW = 9999, viewportH = 9999;
742
- var rootM = (rootIdx != null && rootIdx >= 0) ? measurements[rootIdx] : null;
743
- if (rootM && rootM.width > 0 && rootM.height > 0) {
744
- viewportW = rootM.width;
745
- viewportH = rootM.height + (rootM.y > 0 ? rootM.y : 0);
746
- } else {
747
- for (var v = 0; v < measurements.length; v++) {
748
- if (measurements[v] && measurements[v].x === 0 && measurements[v].y <= 0 &&
749
- measurements[v].width > 0 && measurements[v].height > 0) {
750
- viewportW = measurements[v].width;
751
- viewportH = measurements[v].height + measurements[v].y;
752
- break;
753
- }
754
- }
755
- }
756
-
757
- var elements = [];
758
-
759
- for (var i = 0; i < meta.length; i++) {
760
- var info = meta[i];
761
-
762
- // Union all host measurements for this pressable to get its true bounds
763
- var uMinX = Infinity, uMinY = Infinity, uMaxX = -Infinity, uMaxY = -Infinity;
764
- var hasValid = false;
765
- var indices = info.hostIndices || [];
766
- for (var hi2 = 0; hi2 < indices.length; hi2++) {
767
- var mm = measurements[indices[hi2]];
768
- if (!mm || mm.width <= 0 || mm.height <= 0) continue;
769
- hasValid = true;
770
- if (mm.x < uMinX) uMinX = mm.x;
771
- if (mm.y < uMinY) uMinY = mm.y;
772
- if (mm.x + mm.width > uMaxX) uMaxX = mm.x + mm.width;
773
- if (mm.y + mm.height > uMaxY) uMaxY = mm.y + mm.height;
774
- }
775
- if (!hasValid) continue;
776
- var m = { x: uMinX, y: uMinY, width: uMaxX - uMinX, height: uMaxY - uMinY };
777
-
778
- // Filter: only visible within viewport
779
- if (m.width <= 0 || m.height <= 0) continue;
780
- if (m.x + m.width < 0 || m.y + m.height < 0) continue;
781
- if (m.x > viewportW || m.y > viewportH) continue;
782
-
783
- var text = info.text || '';
784
-
785
- elements.push({
786
- component: info.component,
787
- path: info.path,
788
- center: {
789
- x: Math.round(m.x + m.width / 2),
790
- y: Math.round(m.y + m.height / 2)
791
- },
792
- frame: {
793
- x: Math.round(m.x),
794
- y: Math.round(m.y),
795
- width: Math.round(m.width),
796
- height: Math.round(m.height)
797
- },
798
- text: text,
799
- testID: info.testID,
800
- accessibilityLabel: info.accessibilityLabel,
801
- hasLabel: text.length > 0,
802
- isInput: info.isInput,
803
- icon: info.icon || null
804
- });
805
- }
806
-
807
- // Deduplicate: multiple nested pressables (View > TouchableOpacity > TouchableOpacity)
808
- // often share the same frame. Keep the one with the most meaningful component name,
809
- // but merge text/testID/accessibilityLabel from the loser so stopping collectText
810
- // at nested-pressable boundaries does not drop labels across the merge.
811
- var HOST_NAMES = /^(View|TouchableOpacity|TouchableHighlight|TouchableWithoutFeedback|Pressable|TouchableNativeFeedback|Text|RCTView|RCTText)$/;
812
- function mergeFields(winner, loser) {
813
- if (!winner.text && loser.text) {
814
- winner.text = loser.text;
815
- winner.hasLabel = winner.text.length > 0;
816
- }
817
- if (!winner.testID && loser.testID) winner.testID = loser.testID;
818
- if (!winner.accessibilityLabel && loser.accessibilityLabel) winner.accessibilityLabel = loser.accessibilityLabel;
819
- if (!winner.icon && loser.icon) winner.icon = loser.icon;
820
- return winner;
821
- }
822
- var deduped = {};
823
- for (var di = 0; di < elements.length; di++) {
824
- var el = elements[di];
825
- var key = el.frame.x + ',' + el.frame.y + ',' + el.frame.width + ',' + el.frame.height;
826
- var existing = deduped[key];
827
- if (!existing) {
828
- deduped[key] = el;
829
- } else {
830
- var existingIsGeneric = HOST_NAMES.test(existing.component);
831
- var newIsGeneric = HOST_NAMES.test(el.component);
832
- var winner, loser;
833
- if (existingIsGeneric && !newIsGeneric) {
834
- winner = el; loser = existing;
835
- } else if (!existingIsGeneric && newIsGeneric) {
836
- winner = existing; loser = el;
837
- } else {
838
- // Both generic or both meaningful — prefer the one with more identifiers.
839
- if (!existing.testID && el.testID) { winner = el; loser = existing; }
840
- else if (!existing.accessibilityLabel && el.accessibilityLabel) { winner = el; loser = existing; }
841
- else { winner = existing; loser = el; }
842
- }
843
- deduped[key] = mergeFields(winner, loser);
844
- }
845
- }
846
- elements = [];
847
- for (var dk in deduped) {
848
- elements.push(deduped[dk]);
849
- }
850
-
851
- // Tag wrappers: pressables that cover >=50% of viewport AND geometrically contain another pressable.
852
- // These are typically keyboard-dismiss/full-screen Touchable wrappers — agents should skip them.
853
- var viewportArea = (viewportW > 0 && viewportH > 0 && viewportW < 9999 && viewportH < 9999)
854
- ? viewportW * viewportH : 0;
855
- if (viewportArea > 0) {
856
- for (var wi = 0; wi < elements.length; wi++) {
857
- var we = elements[wi];
858
- var weArea = we.frame.width * we.frame.height;
859
- if (weArea < viewportArea * 0.5) continue;
860
- for (var wj = 0; wj < elements.length; wj++) {
861
- if (wj === wi) continue;
862
- var other = elements[wj];
863
- if (other.frame.x >= we.frame.x &&
864
- other.frame.y >= we.frame.y &&
865
- other.frame.x + other.frame.width <= we.frame.x + we.frame.width &&
866
- other.frame.y + other.frame.height <= we.frame.y + we.frame.height &&
867
- other.frame.width * other.frame.height < weArea) {
868
- we.isWrapper = true;
869
- break;
870
- }
871
- }
872
- }
873
- }
874
-
875
- // Build a normalized list of visible standalone texts (content + frame)
876
- var textBoxes = [];
877
- for (var tmi = 0; tmi < textMeasurements.length; tmi++) {
878
- var tm = textMeasurements[tmi];
879
- var tc = textContents[tmi];
880
- if (!tm || !tc) continue;
881
- if (tm.width <= 0 || tm.height <= 0) continue;
882
- if (tm.x + tm.width < 0 || tm.y + tm.height < 0) continue;
883
- if (tm.x > viewportW || tm.y > viewportH) continue;
884
- textBoxes.push({
885
- text: tc,
886
- x: tm.x, y: tm.y, width: tm.width, height: tm.height,
887
- cx: tm.x + tm.width / 2, cy: tm.y + tm.height / 2
888
- });
889
- }
890
-
891
- // Humanize component name → intent (strip Svg/Icon prefixes, split camelCase)
892
- function humanize(name) {
893
- if (!name) return '';
894
- var n = String(name);
895
- n = n.replace(/^(Svg|Icon)/, '').replace(/(Svg|Icon)$/, '');
896
- n = n.replace(/([a-z0-9])([A-Z])/g, '$1 $2');
897
- n = n.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2');
898
- return n.trim().toLowerCase();
899
- }
900
-
901
- // Attach nearbyText for pressables that lack their own text/label.
902
- // Nearest text by center-to-center distance, within a reasonable radius.
903
- var NEARBY_RADIUS = 120; // points
904
- for (var ei = 0; ei < elements.length; ei++) {
905
- var pe = elements[ei];
906
- pe.intent = humanize(pe.icon || pe.component);
907
- // Skip when the pressable already has its own human-readable text or a11y label.
908
- // testID alone doesn't suppress nearbyText — the testID may be cryptic and the
909
- // surrounding visible text is still a useful handle for an agent.
910
- if (pe.hasLabel || pe.accessibilityLabel) continue;
911
- if (textBoxes.length === 0) continue;
912
-
913
- var pcx = pe.frame.x + pe.frame.width / 2;
914
- var pcy = pe.frame.y + pe.frame.height / 2;
915
- var best = null;
916
- var bestDist = Infinity;
917
- for (var tbi = 0; tbi < textBoxes.length; tbi++) {
918
- var tb = textBoxes[tbi];
919
- // Skip texts contained inside this pressable (shouldn't happen — walkTexts excludes them — but defensive)
920
- if (tb.x >= pe.frame.x && tb.y >= pe.frame.y &&
921
- tb.x + tb.width <= pe.frame.x + pe.frame.width &&
922
- tb.y + tb.height <= pe.frame.y + pe.frame.height) continue;
923
- // Row siblings first: a checkbox's label sits in the same row but its
924
- // long text's CENTER is far away, so center distance alone misses it.
925
- // Row-aligned + small horizontal edge gap = strongest signal.
926
- var rowAligned = Math.abs(tb.cy - pcy) <= Math.max(24, pe.frame.height / 2);
927
- var gapL = pe.frame.x - (tb.x + tb.width);
928
- var gapR = tb.x - (pe.frame.x + pe.frame.width);
929
- var hGap = Math.max(gapL, gapR);
930
- if (hGap < 0) hGap = 0;
931
- var d;
932
- if (rowAligned && hGap <= 80) {
933
- d = hGap;
934
- } else {
935
- var dx = tb.cx - pcx;
936
- var dy = tb.cy - pcy;
937
- d = Math.sqrt(dx * dx + dy * dy);
938
- if (d > NEARBY_RADIUS) continue;
939
- }
940
- if (d < bestDist) {
941
- bestDist = d;
942
- best = tb;
943
- }
944
- }
945
- if (best) pe.nearbyText = best.text;
946
- }
947
-
948
- // Sort top-to-bottom, left-to-right
949
- elements.sort(function(a, b) {
950
- if (a.center.y !== b.center.y) return a.center.y - b.center.y;
951
- return a.center.x - b.center.x;
952
- });
953
-
954
- var iconCount = 0;
955
- var labeledCount = 0;
956
- for (var j = 0; j < elements.length; j++) {
957
- if (elements[j].hasLabel) labeledCount++;
958
- else iconCount++;
959
- }
960
-
961
- return {
962
- pressableElements: elements,
963
- summary: 'Found ' + elements.length + ' pressable elements (' + iconCount + ' icon-only, ' + labeledCount + ' with text labels)'
964
- };
965
- })()
966
- `;
967
- const result = await executeInApp(resolveExpression, false, { timeoutMs: 30000, originatingToolName: "get_pressable_elements" }, device);
968
- if (result.success && result.result) {
969
- try {
970
- const parsed = JSON.parse(result.result);
971
- if (parsed.error)
972
- return { success: false, error: parsed.error };
973
- const pressableElements = parsed.pressableElements || [];
974
- // Android coordinate reconciliation (2026-05-17).
975
- //
976
- // The fiber walker produces rich metadata (component names, testID, intent,
977
- // wrapper flags, nearbyText) but its measureInWindow output on Bridgeless/Fabric
978
- // is in app-window DP space — off from the actual screen-pixel hit area by the
979
- // status bar height (varies per device) AND an additional unaccounted offset
980
- // we couldn't characterize across devices.
981
- //
982
- // uiautomator's bounds, on the other hand, are in absolute display pixels and
983
- // match what tap(x, y) → adb input dispatches — they are touch-accurate.
984
- //
985
- // Strategy: keep fiber's metadata, but replace coords with uiautomator's bounds
986
- // when we can match a fiber element to a uiautomator node by text / contentDesc /
987
- // resourceId. For icon-only pressables that have no text-match candidate,
988
- // fall back to fiber's DP coords scaled by density — they're imperfect but
989
- // they keep the same gross layout that an agent could use as a "near this
990
- // region" hint, and they're better than nothing.
991
- //
992
- // iOS is unaffected — fiber returns points and AXe returns points; uniform.
993
- if (effectivePlatform === "android" && pressableElements.length > 0) {
994
- let densityScale = 2.625;
995
- let statusBarPixels = 0;
996
- try {
997
- const { androidGetDensity, androidGetStatusBarHeight } = await import("./android.js");
998
- const [densityResult, statusBarResult] = await Promise.all([
999
- androidGetDensity(),
1000
- androidGetStatusBarHeight().catch(() => ({ success: false, heightPixels: 0 })),
1001
- ]);
1002
- densityScale = (densityResult.density || 420) / 160;
1003
- if (statusBarResult.success && statusBarResult.heightPixels) {
1004
- statusBarPixels = statusBarResult.heightPixels;
1005
- }
1006
- }
1007
- catch { /* default */ }
1008
- let uiNodes;
1009
- try {
1010
- const { androidGetUITree } = await import("./android.js");
1011
- const tree = await androidGetUITree();
1012
- if (tree.success && tree.elements)
1013
- uiNodes = tree.elements;
1014
- }
1015
- catch { /* uiautomator unavailable */ }
1016
- const norm = (s) => (s ?? "").toLowerCase().replace(/\s+/g, " ").trim();
1017
- const findUiMatch = (el) => {
1018
- if (!uiNodes || uiNodes.length === 0)
1019
- return null;
1020
- const targetTestID = norm(el.testID);
1021
- const targetText = norm(el.text);
1022
- const targetA11y = norm(el.accessibilityLabel);
1023
- if (!targetTestID && !targetText && !targetA11y)
1024
- return null;
1025
- // Prefer testID → resourceId exact match (most stable).
1026
- if (targetTestID) {
1027
- const m = uiNodes.find(u => norm(u.resourceId).endsWith("/" + targetTestID) || norm(u.resourceId) === targetTestID);
1028
- if (m)
1029
- return m;
1030
- }
1031
- // Then exact text/contentDesc match.
1032
- for (const query of [targetText, targetA11y]) {
1033
- if (!query)
1034
- continue;
1035
- const m = uiNodes.find(u => norm(u.text) === query || norm(u.contentDesc) === query);
1036
- if (m)
1037
- return m;
1038
- }
1039
- // Then substring — handles fiber's collected text being a superset of one node's label.
1040
- for (const query of [targetText, targetA11y]) {
1041
- if (!query)
1042
- continue;
1043
- const m = uiNodes.find(u => {
1044
- const ut = norm(u.text);
1045
- const ud = norm(u.contentDesc);
1046
- return (ut && (ut.includes(query) || query.includes(ut))) ||
1047
- (ud && (ud.includes(query) || query.includes(ud)));
1048
- });
1049
- if (m)
1050
- return m;
1051
- }
1052
- return null;
1053
- };
1054
- for (const el of pressableElements) {
1055
- const match = findUiMatch(el);
1056
- if (match) {
1057
- el.center = { x: match.center.x, y: match.center.y };
1058
- el.frame = {
1059
- x: match.bounds.left,
1060
- y: match.bounds.top,
1061
- width: match.bounds.width,
1062
- height: match.bounds.height,
1063
- };
1064
- }
1065
- else {
1066
- // Fall back to fiber's DP → device-pixel scaling, plus the dynamic
1067
- // status-bar pixel offset so the y origin lines up with display
1068
- // coordinates (uiautomator's space). measureInWindow on Bridgeless
1069
- // returns coordinates relative to the React window which sits
1070
- // below the system status bar on non-edge-to-edge apps; without
1071
- // this shift, fallback elements report y-values that are
1072
- // ~status-bar-height too small.
1073
- //
1074
- // Note: this is a best-effort correction. Edge-to-edge apps where
1075
- // the React window starts at the display origin will see a
1076
- // small over-correction here. The proper fix would query
1077
- // WindowInsets, but the data isn't exposed via adb in a way
1078
- // we can reach today, and the icon-only-no-text fallback is
1079
- // already a "region hint" path — agents should prefer
1080
- // tap(component=...) / tap(testID=...) for precision.
1081
- el.center = {
1082
- x: el.center.x * densityScale,
1083
- y: el.center.y * densityScale + statusBarPixels,
1084
- };
1085
- el.frame = {
1086
- x: el.frame.x * densityScale,
1087
- y: el.frame.y * densityScale + statusBarPixels,
1088
- width: el.frame.width * densityScale,
1089
- height: el.frame.height * densityScale,
1090
- };
1091
- }
1092
- }
1093
- }
1094
- // Format as readable text
1095
- const lines = [parsed.summary, ""];
1096
- for (let i = 0; i < pressableElements.length; i++) {
1097
- const el = pressableElements[i];
1098
- const num = i + 1;
1099
- const iconHint = !el.hasLabel ? iconLabel(el.component, el.icon) : null;
1100
- const label = el.hasLabel
1101
- ? `"${el.text}"`
1102
- : iconHint
1103
- ? `(${iconHint})`
1104
- : el.intent
1105
- ? `(${el.intent} icon)`
1106
- : "(icon/image)";
1107
- const ids = [];
1108
- if (el.testID)
1109
- ids.push(`testID="${el.testID}"`);
1110
- if (el.accessibilityLabel)
1111
- ids.push(`a11y="${el.accessibilityLabel}"`);
1112
- const idStr = ids.length > 0 ? ` [${ids.join(", ")}]` : "";
1113
- const inputStr = el.isInput ? " (input)" : "";
1114
- const wrapperStr = el.isWrapper ? " [wrapper — skip unless dismissing keyboard]" : "";
1115
- const nearPart = el.nearbyText ? ` near "${el.nearbyText}"` : "";
1116
- 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}`);
1117
- if (el.path)
1118
- lines.push(` path: ${el.path}`);
1119
- }
1120
- return {
1121
- success: true,
1122
- result: lines.join("\n"),
1123
- parsedElements: pressableElements
1124
- };
1125
- }
1126
- catch {
1127
- // If parsing fails, return original result
1128
- }
1129
- }
1130
- return result;
1131
- }
1132
73
  // ============================================================================
1133
74
  // Press Element (invoke onPress via React Fiber Tree)
1134
75
  // ============================================================================
@@ -1232,7 +173,10 @@ export async function pressElement(options) {
1232
173
  return null;
1233
174
  }
1234
175
 
1235
- 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)$/;
176
+ // Shared with screenState/inputTarget — see injectedFilters.ts. This file used to
177
+ // carry two hand-pasted copies of this pattern; divergence between copies is what
178
+ // let inspect_at_point report a context provider as the touched element.
179
+ var RN_PRIMITIVES = ${RN_PRIMITIVES_SRC};
1236
180
 
1237
181
  function isScreenHidden(name, props) {
1238
182
  return isHiddenNavigationScene(name, props);