react-native-ai-debugger 1.4.40 → 1.5.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.
@@ -761,8 +761,8 @@ function formatLayoutTree(elements, renderLine) {
761
761
  }
762
762
  return lines.join("\n");
763
763
  }
764
- function formatScreenLayoutTree(elements, extended = false) {
765
- return formatLayoutTree(elements, (el, indent, isLeaf) => {
764
+ export function formatScreenLayoutTree(elements, extended = false, offScreen) {
765
+ const tree = formatLayoutTree(elements, (el, indent, isLeaf) => {
766
766
  const prefix = " ".repeat(indent);
767
767
  const frame = el.frame
768
768
  ? ` (${Math.round(el.frame.x)},${Math.round(el.frame.y)} ${Math.round(el.frame.width)}x${Math.round(el.frame.height)})`
@@ -775,6 +775,23 @@ function formatScreenLayoutTree(elements, extended = false) {
775
775
  : "";
776
776
  return `${prefix}${el.component}${frame}${idStr}${textStr}${layoutStr}`;
777
777
  });
778
+ const suffix = [];
779
+ if (offScreen?.offScreenAbove?.length) {
780
+ suffix.push(formatOffScreenLine(offScreen.offScreenAbove, "above fold"));
781
+ }
782
+ if (offScreen?.offScreenBelow?.length) {
783
+ suffix.push(formatOffScreenLine(offScreen.offScreenBelow, "below fold"));
784
+ }
785
+ return suffix.length > 0 ? `${tree}\n\n${suffix.join("\n")}` : tree;
786
+ }
787
+ function formatOffScreenLine(names, position) {
788
+ const total = names.length;
789
+ const CAP = 10;
790
+ if (total <= CAP) {
791
+ return `[... ${total} component${total === 1 ? "" : "s"} ${position}: ${names.join(", ")}]`;
792
+ }
793
+ const shown = names.slice(0, CAP).join(", ");
794
+ return `[... ${total} components ${position}: ${shown}, ... +${total - CAP} more]`;
778
795
  }
779
796
  function formatFoundComponentsToTonl(components) {
780
797
  const lines = ["#found{component,path,depth,key,layout}"];
@@ -1075,6 +1092,19 @@ export async function getScreenLayout(options = {}) {
1075
1092
  typeof sn.canonical.publicInstance.measureInWindow === 'function') {
1076
1093
  return sn.canonical.publicInstance;
1077
1094
  }
1095
+ // Fabric leaf nodes like RCTText have no publicInstance. Measure via
1096
+ // the native Fabric UIManager using the shadow node instead — needed
1097
+ // for text bounds that are tight around the glyphs (not the scroll
1098
+ // container the text happens to live inside).
1099
+ if (sn.node && globalThis.nativeFabricUIManager &&
1100
+ typeof globalThis.nativeFabricUIManager.measureInWindow === 'function') {
1101
+ var node = sn.node;
1102
+ return {
1103
+ measureInWindow: function(cb) {
1104
+ try { globalThis.nativeFabricUIManager.measureInWindow(node, cb); } catch(e) {}
1105
+ }
1106
+ };
1107
+ }
1078
1108
  return null;
1079
1109
  }
1080
1110
 
@@ -1086,7 +1116,7 @@ export async function getScreenLayout(options = {}) {
1086
1116
  // RN internals and primitives — skip these when looking for meaningful component names
1087
1117
  // Only filter internal components that can't be written as JSX.
1088
1118
  // Keep all user-facing components (View, Text, ScrollView, Modal, etc.)
1089
- var RN_PRIMITIVES = /^(Animated\\(.*|withAnimated.*|AnimatedComponent.*|ForwardRef.*|memo\\(.*|Context\\.Consumer|Context\\.Provider|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)$/;
1119
+ 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)$/;
1090
1120
 
1091
1121
  function getComponentName(fiber) {
1092
1122
  if (!fiber || !fiber.type) return null;
@@ -1141,7 +1171,7 @@ export async function getScreenLayout(options = {}) {
1141
1171
  if (componentsOnlyMode) {
1142
1172
  // componentsOnly: walk tree looking for meaningful custom components,
1143
1173
  // measure their first host child, track parent for tree output
1144
- function walkComponents(fiber, depth, path, parentIdx) {
1174
+ function walkComponents(fiber, depth, path, parentIdx, ancestors) {
1145
1175
  if (!fiber || depth > ${maxDepth}) return;
1146
1176
  var name = getComponentName(fiber);
1147
1177
  var isHost = typeof fiber.type === 'string';
@@ -1156,6 +1186,7 @@ export async function getScreenLayout(options = {}) {
1156
1186
  var isMeaningful = name && !isHost && !RN_PRIMITIVES.test(name);
1157
1187
 
1158
1188
  var myIdx = parentIdx;
1189
+ var nextAncestors = ancestors;
1159
1190
  if (isMeaningful) {
1160
1191
  var host = findFirstHost(fiber, 0);
1161
1192
  if (host) {
@@ -1169,19 +1200,24 @@ export async function getScreenLayout(options = {}) {
1169
1200
  depth: depth,
1170
1201
  path: path.concat([name]),
1171
1202
  parentIndex: parentIdx,
1203
+ ancestorIndices: ancestors.slice(),
1172
1204
  text: text ? text.slice(0, 80) : null
1173
1205
  });
1206
+ // Build the ancestor chain for descendants: [myIdx, ...previous ancestors]
1207
+ nextAncestors = [myIdx].concat(ancestors);
1174
1208
  }
1175
1209
  }
1176
1210
 
1177
1211
  var child = fiber.child;
1178
1212
  while (child) {
1179
1213
  var childName = getComponentName(child);
1180
- walkComponents(child, depth + 1, childName ? path.concat([childName]) : path, myIdx);
1214
+ walkComponents(child, depth + 1, childName ? path.concat([childName]) : path, myIdx, nextAncestors);
1181
1215
  child = child.sibling;
1182
1216
  }
1183
1217
  }
1184
- walkComponents(roots[0].current, 0, [], -1);
1218
+ for (var ri = 0; ri < roots.length; ri++) {
1219
+ walkComponents(roots[ri].current, 0, [], -1, []);
1220
+ }
1185
1221
  } else {
1186
1222
  // Default mode: collect all host fibers with ancestor info
1187
1223
  function walkFibers(fiber, depth, path) {
@@ -1231,7 +1267,9 @@ export async function getScreenLayout(options = {}) {
1231
1267
  child = child.sibling;
1232
1268
  }
1233
1269
  }
1234
- walkFibers(roots[0].current, 0, []);
1270
+ for (var ri = 0; ri < roots.length; ri++) {
1271
+ walkFibers(roots[ri].current, 0, []);
1272
+ }
1235
1273
  }
1236
1274
 
1237
1275
  if (hostFibers.length === 0) return { error: 'No measurable host components found.' };
@@ -1343,15 +1381,29 @@ export async function getScreenLayout(options = {}) {
1343
1381
  }
1344
1382
 
1345
1383
  var elements = [];
1384
+ var offScreenBelow = [];
1385
+ var offScreenAbove = [];
1346
1386
 
1347
1387
  for (var i = 0; i < measurements.length; i++) {
1348
1388
  var m = measurements[i];
1349
1389
  if (!m) continue;
1350
1390
 
1351
- // Filter: only visible within viewport (with some tolerance for partial visibility)
1391
+ // Filter: zero-size
1352
1392
  if (m.width <= 0 || m.height <= 0) continue;
1353
- if (m.x + m.width < 0 || m.y + m.height < 0) continue;
1354
- if (m.x > viewportW || m.y > viewportH) continue;
1393
+
1394
+ // Vertical: track off-screen above/below for the summary line
1395
+ if (m.y + m.height < 0) {
1396
+ if (meta[i] && meta[i].customName) offScreenAbove.push({ idx: i, y: m.y });
1397
+ continue;
1398
+ }
1399
+ if (m.y > viewportH) {
1400
+ if (meta[i] && meta[i].customName) offScreenBelow.push({ idx: i, y: m.y });
1401
+ continue;
1402
+ }
1403
+
1404
+ // Horizontal: center must be inside the viewport (drops phantom slides at x=-411 etc.)
1405
+ var centerX = m.x + m.width / 2;
1406
+ if (centerX < 0 || centerX > viewportW) continue;
1355
1407
 
1356
1408
  var fiber = fibers[i];
1357
1409
  var info = meta[i];
@@ -1427,6 +1479,26 @@ export async function getScreenLayout(options = {}) {
1427
1479
  }
1428
1480
  }
1429
1481
  elements = filtered;
1482
+
1483
+ // Second pass: rewrite orphans whose parentIndex points at a non-surviving element.
1484
+ // This happens when an intermediate ancestor was dropped by the viewport filter
1485
+ // (not the full-screen wrapper path). Walk ancestorIndices to the nearest survivor.
1486
+ var surviving = {};
1487
+ for (var si = 0; si < elements.length; si++) {
1488
+ surviving[elements[si].originalIndex] = true;
1489
+ }
1490
+ for (var oi = 0; oi < elements.length; oi++) {
1491
+ var e2 = elements[oi];
1492
+ if (e2.parentIndex === -1 || e2.parentIndex === undefined) continue;
1493
+ if (surviving[e2.parentIndex]) continue;
1494
+ var anc = meta[e2.originalIndex] && meta[e2.originalIndex].ancestorIndices;
1495
+ if (!anc) { e2.parentIndex = -1; continue; }
1496
+ var found = -1;
1497
+ for (var ai = 0; ai < anc.length; ai++) {
1498
+ if (surviving[anc[ai]]) { found = anc[ai]; break; }
1499
+ }
1500
+ e2.parentIndex = found;
1501
+ }
1430
1502
  }
1431
1503
 
1432
1504
  if (summaryMode) {
@@ -1447,7 +1519,25 @@ export async function getScreenLayout(options = {}) {
1447
1519
  return {
1448
1520
  viewport: { width: viewportW, height: viewportH },
1449
1521
  totalElements: elements.length,
1450
- elements: elements
1522
+ elements: elements,
1523
+ offScreenBelow: (function() {
1524
+ var seen = {}, out = [];
1525
+ offScreenBelow.sort(function(a, b) { return a.y - b.y; });
1526
+ for (var k = 0; k < offScreenBelow.length; k++) {
1527
+ var n = meta[offScreenBelow[k].idx].customName;
1528
+ if (n && !seen[n]) { seen[n] = true; out.push(n); }
1529
+ }
1530
+ return out;
1531
+ })(),
1532
+ offScreenAbove: (function() {
1533
+ var seen = {}, out = [];
1534
+ offScreenAbove.sort(function(a, b) { return b.y - a.y; });
1535
+ for (var k = 0; k < offScreenAbove.length; k++) {
1536
+ var n = meta[offScreenAbove[k].idx].customName;
1537
+ if (n && !seen[n]) { seen[n] = true; out.push(n); }
1538
+ }
1539
+ return out;
1540
+ })()
1451
1541
  };
1452
1542
  })()
1453
1543
  `;
@@ -1467,10 +1557,15 @@ export async function getScreenLayout(options = {}) {
1467
1557
  success: true,
1468
1558
  result: result.result,
1469
1559
  parsedElements: parsed.elements,
1470
- viewport: parsed.viewport
1560
+ viewport: parsed.viewport,
1561
+ offScreenBelow: Array.isArray(parsed.offScreenBelow) ? parsed.offScreenBelow : [],
1562
+ offScreenAbove: Array.isArray(parsed.offScreenAbove) ? parsed.offScreenAbove : []
1471
1563
  };
1472
1564
  }
1473
- const tree = formatScreenLayoutTree(parsed.elements, extended);
1565
+ const tree = formatScreenLayoutTree(parsed.elements, extended, {
1566
+ offScreenBelow: Array.isArray(parsed.offScreenBelow) ? parsed.offScreenBelow : [],
1567
+ offScreenAbove: Array.isArray(parsed.offScreenAbove) ? parsed.offScreenAbove : []
1568
+ });
1474
1569
  return { success: true, result: tree };
1475
1570
  }
1476
1571
  }
@@ -1508,6 +1603,19 @@ export async function getPressableElements(options = {}) {
1508
1603
  typeof sn.canonical.publicInstance.measureInWindow === 'function') {
1509
1604
  return sn.canonical.publicInstance;
1510
1605
  }
1606
+ // Fabric leaf nodes like RCTText have no publicInstance. Measure via
1607
+ // the native Fabric UIManager using the shadow node instead — needed
1608
+ // for text bounds that are tight around the glyphs (not the scroll
1609
+ // container the text happens to live inside).
1610
+ if (sn.node && globalThis.nativeFabricUIManager &&
1611
+ typeof globalThis.nativeFabricUIManager.measureInWindow === 'function') {
1612
+ var node = sn.node;
1613
+ return {
1614
+ measureInWindow: function(cb) {
1615
+ try { globalThis.nativeFabricUIManager.measureInWindow(node, cb); } catch(e) {}
1616
+ }
1617
+ };
1618
+ }
1511
1619
  return null;
1512
1620
  }
1513
1621
 
@@ -1574,10 +1682,12 @@ export async function getPressableElements(options = {}) {
1574
1682
  return fiber.type.displayName || fiber.type.name || null;
1575
1683
  }
1576
1684
 
1577
- var RN_PRIMITIVES = /^(Animated\\(.*|withAnimated.*|AnimatedComponent.*|ForwardRef.*|memo\\(.*|Context\\.Consumer|Context\\.Provider|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)$/;
1685
+ 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)$/;
1578
1686
 
1579
1687
  var hostFibers = [];
1580
1688
  var fiberMeta = [];
1689
+ var textFibers = [];
1690
+ var textContents = [];
1581
1691
 
1582
1692
  function findMeaningfulAncestorName(fiber) {
1583
1693
  var cur = fiber.return;
@@ -1595,6 +1705,30 @@ export async function getPressableElements(options = {}) {
1595
1705
  return fallbackName;
1596
1706
  }
1597
1707
 
1708
+ // Layout-only and touch-wrapper components that should be skipped when scanning
1709
+ // children for a meaningful icon/content component name.
1710
+ 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.*)$/;
1711
+
1712
+ // Scan children of a pressable for a meaningful component name (e.g. SvgChevronBack inside a View).
1713
+ // Skips generic layout/touch wrappers to find the actual icon or content component.
1714
+ function findMeaningfulChildName(fiber) {
1715
+ function scan(f, d) {
1716
+ if (!f || d > 12) return null;
1717
+ var n = getComponentName(f);
1718
+ if (n && typeof f.type !== 'string' && !RN_PRIMITIVES.test(n) && !SKIP_IN_CHILD_SCAN.test(n)) return n;
1719
+ var c = f.child;
1720
+ while (c) {
1721
+ var r = scan(c, d + 1);
1722
+ if (r) return r;
1723
+ c = c.sibling;
1724
+ }
1725
+ return null;
1726
+ }
1727
+ return scan(fiber.child, 0);
1728
+ }
1729
+
1730
+ var GENERIC_COMPONENT = /^(View|TouchableOpacity|TouchableHighlight|TouchableWithoutFeedback|Pressable|TouchableNativeFeedback|Text|RCTView|RCTText|Unknown)$/;
1731
+
1598
1732
  function buildPath(fiber) {
1599
1733
  var parts = [];
1600
1734
  var cur = fiber;
@@ -1634,6 +1768,12 @@ export async function getPressableElements(options = {}) {
1634
1768
  if (hostsForThis.length > 0) {
1635
1769
  var text = collectText(fiber, 0, true);
1636
1770
  var componentName = findMeaningfulAncestorName(fiber) || name || 'Unknown';
1771
+ // If name is generic (e.g. View, TouchableOpacity), scan children for a
1772
+ // meaningful name like SvgChevronBack so icon-only buttons are identifiable.
1773
+ if (GENERIC_COMPONENT.test(componentName)) {
1774
+ var childName = findMeaningfulChildName(fiber);
1775
+ if (childName) componentName = childName;
1776
+ }
1637
1777
  var path = buildPath(fiber);
1638
1778
  var testID = (props && (props.testID || props.nativeID)) || null;
1639
1779
  var accessibilityLabel = (props && props.accessibilityLabel) || null;
@@ -1664,6 +1804,76 @@ export async function getPressableElements(options = {}) {
1664
1804
 
1665
1805
  walkPressables(roots[0].current, 0);
1666
1806
 
1807
+ // Collect text fibers that are NOT inside a pressable — used later to attach
1808
+ // spatial 'nearbyText' hints to icon-only pressables.
1809
+ function extractTextString(fiber) {
1810
+ var p = fiber.memoizedProps;
1811
+ if (!p) return '';
1812
+ var ch = p.children;
1813
+ if (typeof ch === 'string') return ch;
1814
+ if (typeof ch === 'number') return String(ch);
1815
+ if (Array.isArray(ch)) {
1816
+ var parts = [];
1817
+ for (var k = 0; k < ch.length; k++) {
1818
+ if (typeof ch[k] === 'string') parts.push(ch[k]);
1819
+ else if (typeof ch[k] === 'number') parts.push(String(ch[k]));
1820
+ }
1821
+ if (parts.length > 0) return parts.join('');
1822
+ }
1823
+ return '';
1824
+ }
1825
+
1826
+ function walkTexts(fiber, depth, insidePressable, inHidden) {
1827
+ if (!fiber || depth > 5000) return;
1828
+ var name = getComponentName(fiber);
1829
+ var props = fiber.memoizedProps;
1830
+
1831
+ // Track hidden-screen ancestry without early return — a MaybeScreen wrapper
1832
+ // can enclose BOTH the active and inactive siblings, so we must keep walking
1833
+ // to find the active one while still skipping the inactive subtree's texts.
1834
+ var nextHidden = inHidden;
1835
+ if (name === 'MaybeScreen' && props && props.active === 0) nextHidden = true;
1836
+ if (name === 'SceneView' && props && props.focused === false) nextHidden = true;
1837
+ if (name === 'RNSScreen' && props && props['aria-hidden'] === true) nextHidden = true;
1838
+
1839
+ var hasOnPress = props && typeof props.onPress === 'function';
1840
+ var isInputHere = props && (typeof props.onChangeText === 'function' || typeof props.onFocus === 'function');
1841
+ var nextInside = insidePressable || !!hasOnPress || !!isInputHere;
1842
+
1843
+ // Record standalone text when outside any pressable. Detects both stock
1844
+ // Text and custom wrappers (CustomText, ThemedText, etc.) by checking for
1845
+ // direct string/number children. Fabric RCTText has no publicInstance, so
1846
+ // we walk up the fiber chain to the nearest measurable host (the View
1847
+ // enclosing the text) and use its frame as the text's proxy bounds.
1848
+ if (!insidePressable && !nextHidden && name !== 'RCTText' && typeof fiber.type !== 'string') {
1849
+ var str = extractTextString(fiber);
1850
+ if (str && str.length > 0 && str.length <= 120) {
1851
+ var up = fiber;
1852
+ var upDepth = 0;
1853
+ var measurable = null;
1854
+ while (up && upDepth < 20) {
1855
+ if (typeof up.type === 'string' && getMeasurable(up)) {
1856
+ measurable = up;
1857
+ break;
1858
+ }
1859
+ up = up.return;
1860
+ upDepth++;
1861
+ }
1862
+ if (measurable) {
1863
+ textFibers.push(measurable);
1864
+ textContents.push(str);
1865
+ }
1866
+ }
1867
+ }
1868
+
1869
+ var child = fiber.child;
1870
+ while (child) {
1871
+ walkTexts(child, depth + 1, nextInside, nextHidden);
1872
+ child = child.sibling;
1873
+ }
1874
+ }
1875
+ walkTexts(roots[0].current, 0, false, false);
1876
+
1667
1877
  if (hostFibers.length === 0) return { error: 'No pressable elements found on screen.' };
1668
1878
 
1669
1879
  // Also measure the root view for viewport detection (appended; tracked by explicit index to preserve hostIndices)
@@ -1689,7 +1899,19 @@ export async function getPressableElements(options = {}) {
1689
1899
  } catch(e) {}
1690
1900
  }
1691
1901
 
1692
- return { count: hostFibers.length };
1902
+ globalThis.__pressableTextContents = textContents;
1903
+ globalThis.__pressableTextMeasurements = new Array(textFibers.length).fill(null);
1904
+ for (var ti = 0; ti < textFibers.length; ti++) {
1905
+ try {
1906
+ (function(idx) {
1907
+ getMeasurable(textFibers[idx]).measureInWindow(function(fx, fy, fw, fh) {
1908
+ globalThis.__pressableTextMeasurements[idx] = { x: fx, y: fy, width: fw, height: fh };
1909
+ });
1910
+ })(ti);
1911
+ } catch(e) {}
1912
+ }
1913
+
1914
+ return { count: hostFibers.length, textCount: textFibers.length };
1693
1915
  })()
1694
1916
  `;
1695
1917
  const dispatchResult = await executeInApp(dispatchExpression, false, { timeoutMs: 30000 }, device);
@@ -1712,10 +1934,14 @@ export async function getPressableElements(options = {}) {
1712
1934
  var meta = globalThis.__pressableMeta;
1713
1935
  var measurements = globalThis.__pressableMeasurements;
1714
1936
  var rootIdx = globalThis.__pressableRootIdx;
1937
+ var textContents = globalThis.__pressableTextContents || [];
1938
+ var textMeasurements = globalThis.__pressableTextMeasurements || [];
1715
1939
  globalThis.__pressableFibers = null;
1716
1940
  globalThis.__pressableMeta = null;
1717
1941
  globalThis.__pressableMeasurements = null;
1718
1942
  globalThis.__pressableRootIdx = null;
1943
+ globalThis.__pressableTextContents = null;
1944
+ globalThis.__pressableTextMeasurements = null;
1719
1945
 
1720
1946
  if (!fibers || !measurements || !meta) {
1721
1947
  return { error: 'No measurement data. Run get_pressable_elements again.' };
@@ -1854,6 +2080,65 @@ export async function getPressableElements(options = {}) {
1854
2080
  }
1855
2081
  }
1856
2082
 
2083
+ // Build a normalized list of visible standalone texts (content + frame)
2084
+ var textBoxes = [];
2085
+ for (var tmi = 0; tmi < textMeasurements.length; tmi++) {
2086
+ var tm = textMeasurements[tmi];
2087
+ var tc = textContents[tmi];
2088
+ if (!tm || !tc) continue;
2089
+ if (tm.width <= 0 || tm.height <= 0) continue;
2090
+ if (tm.x + tm.width < 0 || tm.y + tm.height < 0) continue;
2091
+ if (tm.x > viewportW || tm.y > viewportH) continue;
2092
+ textBoxes.push({
2093
+ text: tc,
2094
+ x: tm.x, y: tm.y, width: tm.width, height: tm.height,
2095
+ cx: tm.x + tm.width / 2, cy: tm.y + tm.height / 2
2096
+ });
2097
+ }
2098
+
2099
+ // Humanize component name → intent (strip Svg/Icon prefixes, split camelCase)
2100
+ function humanize(name) {
2101
+ if (!name) return '';
2102
+ var n = String(name);
2103
+ n = n.replace(/^(Svg|Icon)/, '').replace(/(Svg|Icon)$/, '');
2104
+ n = n.replace(/([a-z0-9])([A-Z])/g, '$1 $2');
2105
+ n = n.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2');
2106
+ return n.trim().toLowerCase();
2107
+ }
2108
+
2109
+ // Attach nearbyText for pressables that lack their own text/label.
2110
+ // Nearest text by center-to-center distance, within a reasonable radius.
2111
+ var NEARBY_RADIUS = 120; // points
2112
+ for (var ei = 0; ei < elements.length; ei++) {
2113
+ var pe = elements[ei];
2114
+ pe.intent = humanize(pe.component);
2115
+ // Skip when the pressable already has its own human-readable text or a11y label.
2116
+ // testID alone doesn't suppress nearbyText — the testID may be cryptic and the
2117
+ // surrounding visible text is still a useful handle for an agent.
2118
+ if (pe.hasLabel || pe.accessibilityLabel) continue;
2119
+ if (textBoxes.length === 0) continue;
2120
+
2121
+ var pcx = pe.frame.x + pe.frame.width / 2;
2122
+ var pcy = pe.frame.y + pe.frame.height / 2;
2123
+ var best = null;
2124
+ var bestDist = Infinity;
2125
+ for (var tbi = 0; tbi < textBoxes.length; tbi++) {
2126
+ var tb = textBoxes[tbi];
2127
+ // Skip texts contained inside this pressable (shouldn't happen — walkTexts excludes them — but defensive)
2128
+ if (tb.x >= pe.frame.x && tb.y >= pe.frame.y &&
2129
+ tb.x + tb.width <= pe.frame.x + pe.frame.width &&
2130
+ tb.y + tb.height <= pe.frame.y + pe.frame.height) continue;
2131
+ var dx = tb.cx - pcx;
2132
+ var dy = tb.cy - pcy;
2133
+ var d = Math.sqrt(dx * dx + dy * dy);
2134
+ if (d < bestDist && d <= NEARBY_RADIUS) {
2135
+ bestDist = d;
2136
+ best = tb;
2137
+ }
2138
+ }
2139
+ if (best) pe.nearbyText = best.text;
2140
+ }
2141
+
1857
2142
  // Sort top-to-bottom, left-to-right
1858
2143
  elements.sort(function(a, b) {
1859
2144
  if (a.center.y !== b.center.y) return a.center.y - b.center.y;
@@ -1885,7 +2170,11 @@ export async function getPressableElements(options = {}) {
1885
2170
  for (let i = 0; i < pressableElements.length; i++) {
1886
2171
  const el = pressableElements[i];
1887
2172
  const num = i + 1;
1888
- const label = el.hasLabel ? `"${el.text}"` : "(icon/image)";
2173
+ const label = el.hasLabel
2174
+ ? `"${el.text}"`
2175
+ : el.intent
2176
+ ? `(${el.intent} icon)`
2177
+ : "(icon/image)";
1889
2178
  const ids = [];
1890
2179
  if (el.testID)
1891
2180
  ids.push(`testID="${el.testID}"`);
@@ -1894,7 +2183,8 @@ export async function getPressableElements(options = {}) {
1894
2183
  const idStr = ids.length > 0 ? ` [${ids.join(", ")}]` : "";
1895
2184
  const inputStr = el.isInput ? " (input)" : "";
1896
2185
  const wrapperStr = el.isWrapper ? " [wrapper — skip unless dismissing keyboard]" : "";
1897
- lines.push(`${num}. ${el.component} ${label} — center:(${el.center.x},${el.center.y}) frame:(${el.frame.x},${el.frame.y} ${el.frame.width}x${el.frame.height})${idStr}${inputStr}${wrapperStr}`);
2186
+ const nearPart = el.nearbyText ? ` near "${el.nearbyText}"` : "";
2187
+ 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}`);
1898
2188
  if (el.path)
1899
2189
  lines.push(` path: ${el.path}`);
1900
2190
  }
@@ -1914,8 +2204,8 @@ export async function getPressableElements(options = {}) {
1914
2204
  * Format enriched elements as an indented tree with tap coordinates in pixels.
1915
2205
  * Same tree structure as get_screen_layout but with tap:(x,y) per node.
1916
2206
  */
1917
- function formatEnrichedLayoutTree(elements) {
1918
- return formatLayoutTree(elements, (el, indent, isLeaf) => {
2207
+ function formatEnrichedLayoutTree(elements, offScreen) {
2208
+ const tree = formatLayoutTree(elements, (el, indent, isLeaf) => {
1919
2209
  const prefix = " ".repeat(indent);
1920
2210
  const frame = `(${Math.round(el.frame.x)},${Math.round(el.frame.y)} ${Math.round(el.frame.width)}x${Math.round(el.frame.height)})`;
1921
2211
  const tap = ` tap:(${el.tapX},${el.tapY})`;
@@ -1924,6 +2214,14 @@ function formatEnrichedLayoutTree(elements) {
1924
2214
  const textStr = el.text && isLeaf ? ` "${el.text}"` : "";
1925
2215
  return `${prefix}${el.component} ${frame}${tap}${idStr}${textStr}`;
1926
2216
  });
2217
+ const suffix = [];
2218
+ if (offScreen?.offScreenAbove?.length) {
2219
+ suffix.push(formatOffScreenLine(offScreen.offScreenAbove, "above fold"));
2220
+ }
2221
+ if (offScreen?.offScreenBelow?.length) {
2222
+ suffix.push(formatOffScreenLine(offScreen.offScreenBelow, "below fold"));
2223
+ }
2224
+ return suffix.length > 0 ? `${tree}\n\n${suffix.join("\n")}` : tree;
1927
2225
  }
1928
2226
  /**
1929
2227
  * Enrich screen layout data with tap-ready pixel coordinates for bundling with screenshots.
@@ -1935,24 +2233,29 @@ function formatEnrichedLayoutTree(elements) {
1935
2233
  * @param device - optional target device name
1936
2234
  * @returns formatted tree string with pixel coordinates, or null if unavailable
1937
2235
  */
1938
- export async function enrichScreenshotWithLayout(pixelRatio, screenshotScaleFactor, device) {
2236
+ export async function enrichScreenshotWithLayout(pixelRatio, screenshotScaleFactor, device, safeAreaTopPoints = 0) {
1939
2237
  try {
1940
2238
  const result = await getScreenLayout({ extended: false, summary: false, device, raw: true });
1941
2239
  if (!result.success || !result.parsedElements || result.parsedElements.length === 0)
1942
2240
  return null;
1943
2241
  const elements = result.parsedElements.map((el) => {
1944
2242
  const frame = el.frame || { x: 0, y: 0, width: 0, height: 0 };
1945
- // Convert center point from points/dp to screenshot pixels
1946
- // Points -> device pixels: multiply by pixelRatio
1947
- // Device pixels -> screenshot pixels: divide by screenshotScaleFactor (if image was resized)
2243
+ // React-native-screens modal/sheet presentations on iOS cause measureInWindow to return y
2244
+ // relative to the screen's content origin (below safe-area inset), not the window origin.
2245
+ // An element whose center y sits inside the safe-area band is physically impossible for a
2246
+ // visible interactive target — treat that as a sign of the shifted space and add the inset.
1948
2247
  const centerXPoints = frame.x + frame.width / 2;
1949
- const centerYPoints = frame.y + frame.height / 2;
2248
+ let centerYPoints = frame.y + frame.height / 2;
2249
+ let yPoints = frame.y;
2250
+ if (safeAreaTopPoints > 0 && centerYPoints < safeAreaTopPoints) {
2251
+ centerYPoints += safeAreaTopPoints;
2252
+ yPoints += safeAreaTopPoints;
2253
+ }
1950
2254
  const tapX = Math.round((centerXPoints * pixelRatio) / screenshotScaleFactor);
1951
2255
  const tapY = Math.round((centerYPoints * pixelRatio) / screenshotScaleFactor);
1952
- // Convert frame to pixels too
1953
2256
  const pixelFrame = {
1954
2257
  x: Math.round((frame.x * pixelRatio) / screenshotScaleFactor),
1955
- y: Math.round((frame.y * pixelRatio) / screenshotScaleFactor),
2258
+ y: Math.round((yPoints * pixelRatio) / screenshotScaleFactor),
1956
2259
  width: Math.round((frame.width * pixelRatio) / screenshotScaleFactor),
1957
2260
  height: Math.round((frame.height * pixelRatio) / screenshotScaleFactor),
1958
2261
  };
@@ -1969,7 +2272,10 @@ export async function enrichScreenshotWithLayout(pixelRatio, screenshotScaleFact
1969
2272
  path: el.path,
1970
2273
  };
1971
2274
  });
1972
- return formatEnrichedLayoutTree(elements);
2275
+ return formatEnrichedLayoutTree(elements, {
2276
+ offScreenBelow: result.offScreenBelow,
2277
+ offScreenAbove: result.offScreenAbove,
2278
+ });
1973
2279
  }
1974
2280
  catch {
1975
2281
  return null; // Non-fatal: screenshot works without layout
@@ -2412,27 +2718,35 @@ export async function pressElement(options) {
2412
2718
  return fiber.type.displayName || fiber.type.name || null;
2413
2719
  }
2414
2720
 
2721
+ // When a fiber holds a string/number child via memoizedProps.children, return it
2722
+ // without recursing — Text > RCTText > NativeText all carry the same string,
2723
+ // and walking through every layer duplicates it (e.g. "CircularsCircularsCirculars").
2415
2724
  function extractText(fiber, depth) {
2416
2725
  if (!fiber || depth > 5000) return '';
2417
- var parts = [];
2418
2726
  var props = fiber.memoizedProps;
2419
2727
  if (props) {
2420
2728
  var ch = props.children;
2421
- if (typeof ch === 'string') parts.push(ch);
2422
- else if (typeof ch === 'number') parts.push(String(ch));
2423
- else if (Array.isArray(ch)) {
2729
+ if (typeof ch === 'string') return ch;
2730
+ if (typeof ch === 'number') return String(ch);
2731
+ if (Array.isArray(ch)) {
2732
+ var allPrimitive = ch.length > 0;
2733
+ var inline = [];
2424
2734
  for (var i = 0; i < ch.length; i++) {
2425
- if (typeof ch[i] === 'string') parts.push(ch[i]);
2426
- else if (typeof ch[i] === 'number') parts.push(String(ch[i]));
2735
+ if (typeof ch[i] === 'string') inline.push(ch[i]);
2736
+ else if (typeof ch[i] === 'number') inline.push(String(ch[i]));
2737
+ else { allPrimitive = false; }
2427
2738
  }
2739
+ if (allPrimitive && inline.length > 0) return inline.join('');
2428
2740
  }
2429
2741
  }
2742
+ var parts = [];
2430
2743
  var child = fiber.child;
2431
2744
  while (child) {
2432
- parts.push(extractText(child, depth + 1));
2745
+ var t = extractText(child, depth + 1);
2746
+ if (t) parts.push(t);
2433
2747
  child = child.sibling;
2434
2748
  }
2435
- return parts.join('');
2749
+ return parts.join(' ');
2436
2750
  }
2437
2751
 
2438
2752
  function getMeasurable(fiber) {
@@ -2443,10 +2757,23 @@ export async function pressElement(options) {
2443
2757
  typeof sn.canonical.publicInstance.measureInWindow === 'function') {
2444
2758
  return sn.canonical.publicInstance;
2445
2759
  }
2760
+ // Fabric leaf nodes like RCTText have no publicInstance. Measure via
2761
+ // the native Fabric UIManager using the shadow node instead — needed
2762
+ // for text bounds that are tight around the glyphs (not the scroll
2763
+ // container the text happens to live inside).
2764
+ if (sn.node && globalThis.nativeFabricUIManager &&
2765
+ typeof globalThis.nativeFabricUIManager.measureInWindow === 'function') {
2766
+ var node = sn.node;
2767
+ return {
2768
+ measureInWindow: function(cb) {
2769
+ try { globalThis.nativeFabricUIManager.measureInWindow(node, cb); } catch(e) {}
2770
+ }
2771
+ };
2772
+ }
2446
2773
  return null;
2447
2774
  }
2448
2775
 
2449
- var RN_PRIMITIVES = /^(Animated\\(.*|withAnimated.*|AnimatedComponent.*|ForwardRef.*|memo\\(.*|Context\\.Consumer|Context\\.Provider|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)$/;
2776
+ 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)$/;
2450
2777
 
2451
2778
  function isScreenHidden(name, props) {
2452
2779
  if (!props) return false;
@@ -2941,21 +3268,19 @@ export async function pressElement(options) {
2941
3268
 
2942
3269
  var target = matches[targetIndex];
2943
3270
  var result = {
2944
- success: true,
3271
+ needsNativeTap: true,
3272
+ nativeTapTarget: { x: target.x, y: target.y, unit: 'points' },
2945
3273
  pressed: target.name,
2946
3274
  matchIndex: targetIndex,
2947
3275
  totalMatches: matches.length,
2948
3276
  text: target.text,
2949
3277
  testID: target.testID,
2950
3278
  path: target.path,
2951
- isInput: target.isInput,
2952
- x: target.x,
2953
- y: target.y,
2954
- unit: 'points'
3279
+ isInput: target.isInput
2955
3280
  };
2956
3281
  if (matches.length > 1) {
2957
3282
  result.allMatches = matches.map(function(m, i) {
2958
- return { index: i, component: m.name, text: m.text, testID: m.testID };
3283
+ return { index: i, component: m.name, text: m.text, testID: m.testID, x: m.x, y: m.y };
2959
3284
  });
2960
3285
  }
2961
3286
  return result;
@@ -3156,6 +3481,19 @@ export async function inspectAtPoint(x, y, options = {}) {
3156
3481
  typeof sn.canonical.publicInstance.measureInWindow === 'function') {
3157
3482
  return sn.canonical.publicInstance;
3158
3483
  }
3484
+ // Fabric leaf nodes like RCTText have no publicInstance. Measure via
3485
+ // the native Fabric UIManager using the shadow node instead — needed
3486
+ // for text bounds that are tight around the glyphs (not the scroll
3487
+ // container the text happens to live inside).
3488
+ if (sn.node && globalThis.nativeFabricUIManager &&
3489
+ typeof globalThis.nativeFabricUIManager.measureInWindow === 'function') {
3490
+ var node = sn.node;
3491
+ return {
3492
+ measureInWindow: function(cb) {
3493
+ try { globalThis.nativeFabricUIManager.measureInWindow(node, cb); } catch(e) {}
3494
+ }
3495
+ };
3496
+ }
3159
3497
  return null;
3160
3498
  }
3161
3499