react-native-ai-devtools 1.4.30 → 1.4.32

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.
@@ -1523,9 +1523,28 @@ export async function getPressableElements(options = {}) {
1523
1523
  return null;
1524
1524
  }
1525
1525
 
1526
- function collectText(fiber, d) {
1526
+ function findHostsInSubtree(fiber, depth, hosts, limit) {
1527
+ if (!fiber || depth > 20 || hosts.length >= limit) return;
1528
+ if (typeof fiber.type === 'string' && getMeasurable(fiber)) {
1529
+ hosts.push(fiber);
1530
+ return;
1531
+ }
1532
+ var child = fiber.child;
1533
+ while (child && hosts.length < limit) {
1534
+ findHostsInSubtree(child, depth + 1, hosts, limit);
1535
+ child = child.sibling;
1536
+ }
1537
+ }
1538
+
1539
+ function collectText(fiber, d, isRoot) {
1527
1540
  if (!fiber || d > 30) return '';
1528
1541
  var props = fiber.memoizedProps;
1542
+ // Stop descent at nested pressable/input boundaries — their text belongs to them, not to the outer wrapper.
1543
+ if (!isRoot && props && (typeof props.onPress === 'function' ||
1544
+ typeof props.onChangeText === 'function' ||
1545
+ typeof props.onFocus === 'function')) {
1546
+ return '';
1547
+ }
1529
1548
  if (props) {
1530
1549
  var ch = props.children;
1531
1550
  if (typeof ch === 'string') return ch;
@@ -1542,7 +1561,7 @@ export async function getPressableElements(options = {}) {
1542
1561
  var parts = [];
1543
1562
  var child = fiber.child;
1544
1563
  while (child) {
1545
- var t = collectText(child, d + 1);
1564
+ var t = collectText(child, d + 1, false);
1546
1565
  if (t) parts.push(t);
1547
1566
  child = child.sibling;
1548
1567
  }
@@ -1610,22 +1629,28 @@ export async function getPressableElements(options = {}) {
1610
1629
  var isInput = !isPressable && props && (typeof props.onChangeText === 'function' || typeof props.onFocus === 'function');
1611
1630
 
1612
1631
  if (isPressable || isInput) {
1613
- var host = findFirstHost(fiber, 0);
1614
- if (host) {
1615
- var text = collectText(fiber, 0);
1632
+ var hostsForThis = [];
1633
+ findHostsInSubtree(fiber, 0, hostsForThis, 16);
1634
+ if (hostsForThis.length > 0) {
1635
+ var text = collectText(fiber, 0, true);
1616
1636
  var componentName = findMeaningfulAncestorName(fiber) || name || 'Unknown';
1617
1637
  var path = buildPath(fiber);
1618
1638
  var testID = (props && (props.testID || props.nativeID)) || null;
1619
1639
  var accessibilityLabel = (props && props.accessibilityLabel) || null;
1620
1640
 
1621
- hostFibers.push(host);
1641
+ var hostIndices = [];
1642
+ for (var hi = 0; hi < hostsForThis.length; hi++) {
1643
+ hostIndices.push(hostFibers.length);
1644
+ hostFibers.push(hostsForThis[hi]);
1645
+ }
1622
1646
  fiberMeta.push({
1623
1647
  component: componentName,
1624
1648
  path: path,
1625
1649
  text: text ? text.slice(0, 100) : '',
1626
1650
  testID: testID,
1627
1651
  accessibilityLabel: accessibilityLabel,
1628
- isInput: !!isInput
1652
+ isInput: !!isInput,
1653
+ hostIndices: hostIndices
1629
1654
  });
1630
1655
  }
1631
1656
  }
@@ -1641,16 +1666,18 @@ export async function getPressableElements(options = {}) {
1641
1666
 
1642
1667
  if (hostFibers.length === 0) return { error: 'No pressable elements found on screen.' };
1643
1668
 
1644
- // Also measure the root view for viewport detection
1669
+ // Also measure the root view for viewport detection (appended; tracked by explicit index to preserve hostIndices)
1645
1670
  var rootHost = findFirstHost(roots[0].current, 0);
1671
+ var rootIdx = -1;
1646
1672
  if (rootHost) {
1647
- hostFibers.unshift(rootHost);
1648
- fiberMeta.unshift({ component: '__root__', path: '', text: '', testID: null, accessibilityLabel: null, isInput: false });
1673
+ rootIdx = hostFibers.length;
1674
+ hostFibers.push(rootHost);
1649
1675
  }
1650
1676
 
1651
1677
  globalThis.__pressableFibers = hostFibers;
1652
1678
  globalThis.__pressableMeta = fiberMeta;
1653
1679
  globalThis.__pressableMeasurements = new Array(hostFibers.length).fill(null);
1680
+ globalThis.__pressableRootIdx = rootIdx;
1654
1681
 
1655
1682
  for (var i = 0; i < hostFibers.length; i++) {
1656
1683
  try {
@@ -1684,34 +1711,53 @@ export async function getPressableElements(options = {}) {
1684
1711
  var fibers = globalThis.__pressableFibers;
1685
1712
  var meta = globalThis.__pressableMeta;
1686
1713
  var measurements = globalThis.__pressableMeasurements;
1714
+ var rootIdx = globalThis.__pressableRootIdx;
1687
1715
  globalThis.__pressableFibers = null;
1688
1716
  globalThis.__pressableMeta = null;
1689
1717
  globalThis.__pressableMeasurements = null;
1718
+ globalThis.__pressableRootIdx = null;
1690
1719
 
1691
1720
  if (!fibers || !measurements || !meta) {
1692
1721
  return { error: 'No measurement data. Run get_pressable_elements again.' };
1693
1722
  }
1694
1723
 
1695
- // Get viewport dimensions
1724
+ // Get viewport dimensions from the explicit root measurement (fallback to scanning)
1696
1725
  var viewportW = 9999, viewportH = 9999;
1697
- for (var v = 0; v < measurements.length; v++) {
1698
- if (measurements[v] && measurements[v].x === 0 && measurements[v].y <= 0 &&
1699
- measurements[v].width > 0 && measurements[v].height > 0) {
1700
- viewportW = measurements[v].width;
1701
- viewportH = measurements[v].height + measurements[v].y;
1702
- break;
1726
+ var rootM = (rootIdx != null && rootIdx >= 0) ? measurements[rootIdx] : null;
1727
+ if (rootM && rootM.width > 0 && rootM.height > 0) {
1728
+ viewportW = rootM.width;
1729
+ viewportH = rootM.height + (rootM.y > 0 ? rootM.y : 0);
1730
+ } else {
1731
+ for (var v = 0; v < measurements.length; v++) {
1732
+ if (measurements[v] && measurements[v].x === 0 && measurements[v].y <= 0 &&
1733
+ measurements[v].width > 0 && measurements[v].height > 0) {
1734
+ viewportW = measurements[v].width;
1735
+ viewportH = measurements[v].height + measurements[v].y;
1736
+ break;
1737
+ }
1703
1738
  }
1704
1739
  }
1705
1740
 
1706
1741
  var elements = [];
1707
1742
 
1708
- for (var i = 0; i < measurements.length; i++) {
1709
- var m = measurements[i];
1710
- if (!m) continue;
1711
-
1743
+ for (var i = 0; i < meta.length; i++) {
1712
1744
  var info = meta[i];
1713
- // Skip the root viewport measurement entry
1714
- if (info.component === '__root__') continue;
1745
+
1746
+ // Union all host measurements for this pressable to get its true bounds
1747
+ var uMinX = Infinity, uMinY = Infinity, uMaxX = -Infinity, uMaxY = -Infinity;
1748
+ var hasValid = false;
1749
+ var indices = info.hostIndices || [];
1750
+ for (var hi2 = 0; hi2 < indices.length; hi2++) {
1751
+ var mm = measurements[indices[hi2]];
1752
+ if (!mm || mm.width <= 0 || mm.height <= 0) continue;
1753
+ hasValid = true;
1754
+ if (mm.x < uMinX) uMinX = mm.x;
1755
+ if (mm.y < uMinY) uMinY = mm.y;
1756
+ if (mm.x + mm.width > uMaxX) uMaxX = mm.x + mm.width;
1757
+ if (mm.y + mm.height > uMaxY) uMaxY = mm.y + mm.height;
1758
+ }
1759
+ if (!hasValid) continue;
1760
+ var m = { x: uMinX, y: uMinY, width: uMaxX - uMinX, height: uMaxY - uMinY };
1715
1761
 
1716
1762
  // Filter: only visible within viewport
1717
1763
  if (m.width <= 0 || m.height <= 0) continue;
@@ -1742,8 +1788,19 @@ export async function getPressableElements(options = {}) {
1742
1788
  }
1743
1789
 
1744
1790
  // Deduplicate: multiple nested pressables (View > TouchableOpacity > TouchableOpacity)
1745
- // often share the same frame. Keep the one with the most meaningful component name.
1791
+ // often share the same frame. Keep the one with the most meaningful component name,
1792
+ // but merge text/testID/accessibilityLabel from the loser so stopping collectText
1793
+ // at nested-pressable boundaries does not drop labels across the merge.
1746
1794
  var HOST_NAMES = /^(View|TouchableOpacity|TouchableHighlight|TouchableWithoutFeedback|Pressable|TouchableNativeFeedback|Text|RCTView|RCTText)$/;
1795
+ function mergeFields(winner, loser) {
1796
+ if (!winner.text && loser.text) {
1797
+ winner.text = loser.text;
1798
+ winner.hasLabel = winner.text.length > 0;
1799
+ }
1800
+ if (!winner.testID && loser.testID) winner.testID = loser.testID;
1801
+ if (!winner.accessibilityLabel && loser.accessibilityLabel) winner.accessibilityLabel = loser.accessibilityLabel;
1802
+ return winner;
1803
+ }
1747
1804
  var deduped = {};
1748
1805
  for (var di = 0; di < elements.length; di++) {
1749
1806
  var el = elements[di];
@@ -1752,16 +1809,20 @@ export async function getPressableElements(options = {}) {
1752
1809
  if (!existing) {
1753
1810
  deduped[key] = el;
1754
1811
  } else {
1755
- // Prefer non-host/non-primitive name
1756
1812
  var existingIsGeneric = HOST_NAMES.test(existing.component);
1757
1813
  var newIsGeneric = HOST_NAMES.test(el.component);
1814
+ var winner, loser;
1758
1815
  if (existingIsGeneric && !newIsGeneric) {
1759
- deduped[key] = el;
1760
- } else if (existingIsGeneric && newIsGeneric) {
1761
- // Both generic — prefer the one with more identifiers
1762
- if (!existing.testID && el.testID) deduped[key] = el;
1763
- else if (!existing.accessibilityLabel && el.accessibilityLabel) deduped[key] = el;
1816
+ winner = el; loser = existing;
1817
+ } else if (!existingIsGeneric && newIsGeneric) {
1818
+ winner = existing; loser = el;
1819
+ } else {
1820
+ // Both generic or both meaningful — prefer the one with more identifiers.
1821
+ if (!existing.testID && el.testID) { winner = el; loser = existing; }
1822
+ else if (!existing.accessibilityLabel && el.accessibilityLabel) { winner = el; loser = existing; }
1823
+ else { winner = existing; loser = el; }
1764
1824
  }
1825
+ deduped[key] = mergeFields(winner, loser);
1765
1826
  }
1766
1827
  }
1767
1828
  elements = [];
@@ -1769,6 +1830,30 @@ export async function getPressableElements(options = {}) {
1769
1830
  elements.push(deduped[dk]);
1770
1831
  }
1771
1832
 
1833
+ // Tag wrappers: pressables that cover >=50% of viewport AND geometrically contain another pressable.
1834
+ // These are typically keyboard-dismiss/full-screen Touchable wrappers — agents should skip them.
1835
+ var viewportArea = (viewportW > 0 && viewportH > 0 && viewportW < 9999 && viewportH < 9999)
1836
+ ? viewportW * viewportH : 0;
1837
+ if (viewportArea > 0) {
1838
+ for (var wi = 0; wi < elements.length; wi++) {
1839
+ var we = elements[wi];
1840
+ var weArea = we.frame.width * we.frame.height;
1841
+ if (weArea < viewportArea * 0.5) continue;
1842
+ for (var wj = 0; wj < elements.length; wj++) {
1843
+ if (wj === wi) continue;
1844
+ var other = elements[wj];
1845
+ if (other.frame.x >= we.frame.x &&
1846
+ other.frame.y >= we.frame.y &&
1847
+ other.frame.x + other.frame.width <= we.frame.x + we.frame.width &&
1848
+ other.frame.y + other.frame.height <= we.frame.y + we.frame.height &&
1849
+ other.frame.width * other.frame.height < weArea) {
1850
+ we.isWrapper = true;
1851
+ break;
1852
+ }
1853
+ }
1854
+ }
1855
+ }
1856
+
1772
1857
  // Sort top-to-bottom, left-to-right
1773
1858
  elements.sort(function(a, b) {
1774
1859
  if (a.center.y !== b.center.y) return a.center.y - b.center.y;
@@ -1808,7 +1893,8 @@ export async function getPressableElements(options = {}) {
1808
1893
  ids.push(`a11y="${el.accessibilityLabel}"`);
1809
1894
  const idStr = ids.length > 0 ? ` [${ids.join(", ")}]` : "";
1810
1895
  const inputStr = el.isInput ? " (input)" : "";
1811
- 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}`);
1896
+ 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}`);
1812
1898
  if (el.path)
1813
1899
  lines.push(` path: ${el.path}`);
1814
1900
  }
@@ -2360,6 +2446,51 @@ export async function pressElement(options) {
2360
2446
  return null;
2361
2447
  }
2362
2448
 
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)$/;
2450
+
2451
+ function isScreenHidden(name, props) {
2452
+ if (!props) return false;
2453
+ if (name === 'RNSScreen' && props['aria-hidden'] === true) return true;
2454
+ if (name === 'MaybeScreen' && props.active === 0) return true;
2455
+ if (name === 'SceneView' && props.focused === false) return true;
2456
+ return false;
2457
+ }
2458
+
2459
+ function findMeaningfulAncestorName(fiber) {
2460
+ var cur = fiber.return;
2461
+ var depth = 0;
2462
+ var fallbackName = null;
2463
+ while (cur && depth < 20) {
2464
+ var aname = getComponentName(cur);
2465
+ if (aname && typeof cur.type !== 'string') {
2466
+ if (!fallbackName) fallbackName = aname;
2467
+ if (!RN_PRIMITIVES.test(aname)) return aname;
2468
+ }
2469
+ cur = cur.return;
2470
+ depth++;
2471
+ }
2472
+ return fallbackName;
2473
+ }
2474
+
2475
+ // Walk UP collecting testID/nativeID from ancestors. Stop at screen boundaries.
2476
+ function collectAncestorTestIDs(fiber, maxUp) {
2477
+ var ids = [];
2478
+ var cur = fiber.return;
2479
+ var d = 0;
2480
+ while (cur && d < maxUp) {
2481
+ var cname = getComponentName(cur);
2482
+ if (cname === 'RNSScreen' || cname === 'MaybeScreen' || cname === 'SceneView') break;
2483
+ var cp = cur.memoizedProps;
2484
+ if (cp) {
2485
+ if (typeof cp.testID === 'string' && cp.testID) ids.push(cp.testID);
2486
+ if (typeof cp.nativeID === 'string' && cp.nativeID) ids.push(cp.nativeID);
2487
+ }
2488
+ cur = cur.return;
2489
+ d++;
2490
+ }
2491
+ return ids;
2492
+ }
2493
+
2363
2494
  // Find the first measurable host descendant of a fiber.
2364
2495
  // For inputs, prefer TextInput-specific hosts over generic RCTView.
2365
2496
  function findFirstHost(fiber, depth, isInput) {
@@ -2402,10 +2533,7 @@ export async function pressElement(options) {
2402
2533
  var name = getComponentName(fiber);
2403
2534
  var props = fiber.memoizedProps;
2404
2535
 
2405
- // Skip inactive/hidden screens
2406
- if (name === 'RNSScreen' && props && props['aria-hidden'] === true) return;
2407
- if (name === 'MaybeScreen' && props && props.active === 0) return;
2408
- if (name === 'SceneView' && props && props.focused === false) return;
2536
+ if (isScreenHidden(name, props)) return;
2409
2537
 
2410
2538
  var isPressable = props && typeof props.onPress === 'function';
2411
2539
  var isInput = !isPressable && props && (typeof props.onChangeText === 'function' || typeof props.onFocus === 'function');
@@ -2421,14 +2549,18 @@ export async function pressElement(options) {
2421
2549
  text = extractText(fiber, 0) || val || defVal || ph;
2422
2550
  }
2423
2551
  var tid = props.testID || props.nativeID || null;
2552
+ var meaningful = findMeaningfulAncestorName(fiber);
2553
+ var ancestorIDs = collectAncestorTestIDs(fiber, maxTraversalUp);
2424
2554
 
2425
2555
  var host = findFirstHost(fiber, 0, isInput);
2426
2556
  if (host) {
2427
2557
  hostFibers.push(host);
2428
2558
  tapMeta.push({
2429
2559
  name: name || '(anonymous)',
2560
+ meaningfulComponentName: meaningful || null,
2430
2561
  text: text.substring(0, 100),
2431
2562
  testID: tid,
2563
+ ancestorTestIDs: ancestorIDs,
2432
2564
  path: path.join(' > '),
2433
2565
  isInput: isInput,
2434
2566
  isPressable: isPressable,
@@ -2449,21 +2581,40 @@ export async function pressElement(options) {
2449
2581
  walkFiber(roots[ri].current, 0, []);
2450
2582
  }
2451
2583
 
2452
- // Phase 2a: testID on non-pressable wrapper — walk UP to pressable/input parent
2584
+ // Phase 2a: testID on non-pressable wrapper — walk UP or DOWN to pressable/input.
2585
+ // Skipped if Phase 1 already matched via own testID or ancestor testID.
2453
2586
  if (searchTestID !== null) {
2454
- var hasDirectTestIDMatch = false;
2587
+ var hasEnrichedTestIDMatch = false;
2455
2588
  for (var di = 0; di < tapMeta.length; di++) {
2456
- if (tapMeta[di].testID === searchTestID) { hasDirectTestIDMatch = true; break; }
2457
- }
2589
+ if (tapMeta[di].testID === searchTestID) { hasEnrichedTestIDMatch = true; break; }
2590
+ var aids = tapMeta[di].ancestorTestIDs || [];
2591
+ for (var ai = 0; ai < aids.length; ai++) {
2592
+ if (aids[ai] === searchTestID) { hasEnrichedTestIDMatch = true; break; }
2593
+ }
2594
+ if (hasEnrichedTestIDMatch) break;
2595
+ }
2596
+
2597
+ if (!hasEnrichedTestIDMatch) {
2598
+ function findDescendantPressable(fiber, d) {
2599
+ if (!fiber || d > 10) return null;
2600
+ var fp = fiber.memoizedProps;
2601
+ var dIsPressable = fp && typeof fp.onPress === 'function';
2602
+ var dIsInput = !dIsPressable && fp && (typeof fp.onChangeText === 'function' || typeof fp.onFocus === 'function');
2603
+ if (dIsPressable || dIsInput) return { fiber: fiber, isPressable: dIsPressable, isInput: dIsInput };
2604
+ var c = fiber.child;
2605
+ while (c) {
2606
+ var r = findDescendantPressable(c, d + 1);
2607
+ if (r) return r;
2608
+ c = c.sibling;
2609
+ }
2610
+ return null;
2611
+ }
2458
2612
 
2459
- if (!hasDirectTestIDMatch) {
2460
2613
  function findByTestID2a(fiber, path) {
2461
2614
  if (!fiber) return;
2462
2615
  var name = getComponentName(fiber);
2463
2616
  var props = fiber.memoizedProps;
2464
- if (name === 'RNSScreen' && props && props['aria-hidden'] === true) return;
2465
- if (name === 'MaybeScreen' && props && props.active === 0) return;
2466
- if (name === 'SceneView' && props && props.focused === false) return;
2617
+ if (isScreenHidden(name, props)) return;
2467
2618
 
2468
2619
  var tid = props && (props.testID || props.nativeID || null);
2469
2620
  if (tid === searchTestID) {
@@ -2477,8 +2628,10 @@ export async function pressElement(options) {
2477
2628
  hostFibers.push(host);
2478
2629
  tapMeta.push({
2479
2630
  name: name || '(anonymous)',
2631
+ meaningfulComponentName: findMeaningfulAncestorName(fiber) || null,
2480
2632
  text: text.substring(0, 100),
2481
2633
  testID: searchTestID,
2634
+ ancestorTestIDs: [],
2482
2635
  path: path.join(' > '),
2483
2636
  isInput: nIsInput,
2484
2637
  isPressable: nIsPressable,
@@ -2486,6 +2639,7 @@ export async function pressElement(options) {
2486
2639
  });
2487
2640
  }
2488
2641
  } else {
2642
+ var foundAncestor = false;
2489
2643
  var parent = fiber.return;
2490
2644
  var d = 0;
2491
2645
  while (parent && d < maxTraversalUp) {
@@ -2499,19 +2653,45 @@ export async function pressElement(options) {
2499
2653
  hostFibers.push(host);
2500
2654
  tapMeta.push({
2501
2655
  name: name || '(anonymous)',
2656
+ meaningfulComponentName: findMeaningfulAncestorName(parent) || null,
2502
2657
  text: pText.substring(0, 100),
2503
2658
  testID: pp.testID || pp.nativeID || searchTestID,
2659
+ ancestorTestIDs: [],
2504
2660
  path: path.join(' > '),
2505
2661
  isInput: pIsInput,
2506
2662
  isPressable: pIsPressable,
2507
2663
  source: 'testID-ancestor'
2508
2664
  });
2665
+ foundAncestor = true;
2509
2666
  }
2510
2667
  break;
2511
2668
  }
2512
2669
  parent = parent.return;
2513
2670
  d++;
2514
2671
  }
2672
+
2673
+ if (!foundAncestor) {
2674
+ var desc = findDescendantPressable(fiber, 0);
2675
+ if (desc) {
2676
+ var dp = desc.fiber.memoizedProps;
2677
+ 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 : ''));
2678
+ var dhost = findFirstHost(desc.fiber, 0, desc.isInput);
2679
+ if (dhost) {
2680
+ hostFibers.push(dhost);
2681
+ tapMeta.push({
2682
+ name: getComponentName(desc.fiber) || '(anonymous)',
2683
+ meaningfulComponentName: findMeaningfulAncestorName(desc.fiber) || null,
2684
+ text: dText.substring(0, 100),
2685
+ testID: dp.testID || dp.nativeID || searchTestID,
2686
+ ancestorTestIDs: [searchTestID],
2687
+ path: path.join(' > '),
2688
+ isInput: desc.isInput,
2689
+ isPressable: desc.isPressable,
2690
+ source: 'testID-descendant'
2691
+ });
2692
+ }
2693
+ }
2694
+ }
2515
2695
  }
2516
2696
  }
2517
2697
  var child = fiber.child;
@@ -2527,25 +2707,41 @@ export async function pressElement(options) {
2527
2707
  }
2528
2708
  }
2529
2709
 
2530
- // Phase 2b: component name on non-pressable node — walk UP to pressable parent
2710
+ // Phase 2b: component name on non-pressable node — walk UP or DOWN to pressable parent.
2711
+ // Skipped if Phase 1 already matched via own name or meaningfulComponentName.
2531
2712
  if (searchComponent !== null) {
2532
- var hasDirectComponentMatch = false;
2713
+ var scLower = searchComponent.toLowerCase();
2714
+ var hasEnrichedComponentMatch = false;
2533
2715
  for (var ci = 0; ci < tapMeta.length; ci++) {
2534
- if (tapMeta[ci].name.toLowerCase().indexOf(searchComponent.toLowerCase()) !== -1 && tapMeta[ci].source === 'direct') {
2535
- hasDirectComponentMatch = true; break;
2716
+ var cn = (tapMeta[ci].name || '').toLowerCase();
2717
+ var cm = (tapMeta[ci].meaningfulComponentName || '').toLowerCase();
2718
+ if (cn.indexOf(scLower) !== -1 || cm.indexOf(scLower) !== -1) {
2719
+ hasEnrichedComponentMatch = true; break;
2536
2720
  }
2537
2721
  }
2538
2722
 
2539
- if (!hasDirectComponentMatch) {
2723
+ if (!hasEnrichedComponentMatch) {
2724
+ function findDescendantPressableOnly(fiber, d) {
2725
+ if (!fiber || d > 10) return null;
2726
+ var fp = fiber.memoizedProps;
2727
+ if (fp && typeof fp.onPress === 'function') return fiber;
2728
+ var c = fiber.child;
2729
+ while (c) {
2730
+ var r = findDescendantPressableOnly(c, d + 1);
2731
+ if (r) return r;
2732
+ c = c.sibling;
2733
+ }
2734
+ return null;
2735
+ }
2736
+
2540
2737
  function findByName2b(fiber, path) {
2541
2738
  if (!fiber) return;
2542
2739
  var name = getComponentName(fiber);
2543
2740
  var props = fiber.memoizedProps;
2544
- if (name === 'RNSScreen' && props && props['aria-hidden'] === true) return;
2545
- if (name === 'MaybeScreen' && props && props.active === 0) return;
2546
- if (name === 'SceneView' && props && props.focused === false) return;
2741
+ if (isScreenHidden(name, props)) return;
2547
2742
 
2548
- if (name && name.toLowerCase().indexOf(searchComponent.toLowerCase()) !== -1) {
2743
+ if (name && name.toLowerCase().indexOf(scLower) !== -1) {
2744
+ var foundAncestor = false;
2549
2745
  var parent = fiber.return;
2550
2746
  var d = 0;
2551
2747
  while (parent && d < maxTraversalUp) {
@@ -2557,19 +2753,45 @@ export async function pressElement(options) {
2557
2753
  hostFibers.push(host);
2558
2754
  tapMeta.push({
2559
2755
  name: name,
2756
+ meaningfulComponentName: findMeaningfulAncestorName(parent) || null,
2560
2757
  text: text.substring(0, 100),
2561
2758
  testID: pp.testID || pp.nativeID || null,
2759
+ ancestorTestIDs: [],
2562
2760
  path: path.join(' > '),
2563
2761
  isInput: false,
2564
2762
  isPressable: true,
2565
2763
  source: 'component-ancestor'
2566
2764
  });
2765
+ foundAncestor = true;
2567
2766
  }
2568
2767
  break;
2569
2768
  }
2570
2769
  parent = parent.return;
2571
2770
  d++;
2572
2771
  }
2772
+
2773
+ if (!foundAncestor) {
2774
+ var descFiber = findDescendantPressableOnly(fiber, 0);
2775
+ if (descFiber) {
2776
+ var dp = descFiber.memoizedProps;
2777
+ var dText = extractText(descFiber, 0);
2778
+ var dhost = findFirstHost(descFiber, 0, false);
2779
+ if (dhost) {
2780
+ hostFibers.push(dhost);
2781
+ tapMeta.push({
2782
+ name: getComponentName(descFiber) || '(anonymous)',
2783
+ meaningfulComponentName: name,
2784
+ text: dText.substring(0, 100),
2785
+ testID: dp.testID || dp.nativeID || null,
2786
+ ancestorTestIDs: [],
2787
+ path: path.join(' > '),
2788
+ isInput: false,
2789
+ isPressable: true,
2790
+ source: 'component-descendant'
2791
+ });
2792
+ }
2793
+ }
2794
+ }
2573
2795
  }
2574
2796
  var child = fiber.child;
2575
2797
  while (child) {
@@ -2666,16 +2888,25 @@ export async function pressElement(options) {
2666
2888
 
2667
2889
  var info = meta[i];
2668
2890
 
2669
- // Match by query
2891
+ // Match by query — OR across own and enriched identifiers
2670
2892
  var matched = true;
2671
2893
  if (searchText !== null) {
2672
2894
  matched = matched && info.text.toLowerCase().indexOf(searchText.toLowerCase()) !== -1;
2673
2895
  }
2674
2896
  if (searchTestID !== null) {
2675
- matched = matched && (info.testID === searchTestID);
2897
+ var ownTidMatch = info.testID === searchTestID;
2898
+ var aTids = info.ancestorTestIDs || [];
2899
+ var ancestorTidMatch = false;
2900
+ for (var ti = 0; ti < aTids.length; ti++) {
2901
+ if (aTids[ti] === searchTestID) { ancestorTidMatch = true; break; }
2902
+ }
2903
+ matched = matched && (ownTidMatch || ancestorTidMatch);
2676
2904
  }
2677
2905
  if (searchComponent !== null) {
2678
- matched = matched && (info.name || '').toLowerCase().indexOf(searchComponent.toLowerCase()) !== -1;
2906
+ var scq = searchComponent.toLowerCase();
2907
+ var ownNameMatch = (info.name || '').toLowerCase().indexOf(scq) !== -1;
2908
+ var meaningfulMatch = (info.meaningfulComponentName || '').toLowerCase().indexOf(scq) !== -1;
2909
+ matched = matched && (ownNameMatch || meaningfulMatch);
2679
2910
  }
2680
2911
 
2681
2912
  if (matched) {